1
1
/*
2
- * Copyright (c) 2017, 2024 , Oracle and/or its affiliates. All rights reserved.
2
+ * Copyright (c) 2017, 2025 , Oracle and/or its affiliates. All rights reserved.
3
3
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4
4
*
5
5
* This code is free software; you can redistribute it and/or modify it
25
25
26
26
package sun .tools .jar ;
27
27
28
+ import java .io .BufferedInputStream ;
28
29
import java .io .File ;
30
+ import java .io .FileInputStream ;
29
31
import java .io .IOException ;
30
32
import java .io .InputStream ;
31
33
import java .lang .module .ModuleDescriptor ;
36
38
import java .util .Collections ;
37
39
import java .util .HashMap ;
38
40
import java .util .HashSet ;
41
+ import java .util .LinkedHashMap ;
39
42
import java .util .List ;
40
43
import java .util .Map ;
41
44
import java .util .Set ;
42
45
import java .util .TreeMap ;
43
46
import java .util .function .Function ;
47
+ import java .util .function .IntSupplier ;
48
+ import java .util .regex .Pattern ;
44
49
import java .util .stream .Collectors ;
45
50
import java .util .zip .ZipEntry ;
46
51
import java .util .zip .ZipFile ;
52
+ import java .util .zip .ZipInputStream ;
47
53
48
- import static java .util .jar .JarFile .MANIFEST_NAME ;
49
54
import static sun .tools .jar .Main .VERSIONS_DIR ;
50
55
import static sun .tools .jar .Main .VERSIONS_DIR_LENGTH ;
51
56
import static sun .tools .jar .Main .MODULE_INFO ;
54
59
import static sun .tools .jar .Main .toBinaryName ;
55
60
56
61
final class Validator {
62
+ /**
63
+ * Regex expression to verify that the Zip Entry file name:
64
+ * - is not an absolute path
65
+ * - the file name is not '.' or '..'
66
+ * - does not contain a backslash, '\'
67
+ * - does not contain a drive letter
68
+ * - path element does not include '.' or '..'
69
+ */
70
+ private static final Pattern INVALID_ZIP_ENTRY_NAME_PATTERN = Pattern .compile (
71
+ // Don't allow a '..' in the path
72
+ "^(\\ .|\\ .\\ .)$"
73
+ + "|^\\ .\\ ./"
74
+ + "|/\\ .\\ .$"
75
+ + "|/\\ .\\ ./"
76
+ // Don't allow a '.' in the path
77
+ + "|^\\ ./"
78
+ + "|/\\ .$"
79
+ + "|/\\ ./"
80
+ // Don't allow absolute path
81
+ + "|^/"
82
+ // Don't allow a backslash in the path
83
+ + "|^\\ \\ "
84
+ + "|.*\\ \\ .*"
85
+ // Don't allow a drive letter
86
+ + "|.*[a-zA-Z]:.*"
87
+ );
57
88
58
89
private final Map <String ,FingerPrint > classes = new HashMap <>();
59
90
private final Main main ;
@@ -62,20 +93,189 @@ final class Validator {
62
93
private Set <String > concealedPkgs = Collections .emptySet ();
63
94
private ModuleDescriptor md ;
64
95
private String mdName ;
96
+ private final ZipInputStream zis ;
65
97
66
- private Validator (Main main , ZipFile zf ) {
98
+ private Validator (Main main , ZipFile zf , ZipInputStream zis ) {
67
99
this .main = main ;
68
100
this .zf = zf ;
101
+ this .zis = zis ;
69
102
checkModuleDescriptor (MODULE_INFO );
70
103
}
71
104
72
- static boolean validate (Main main , ZipFile zf ) throws IOException {
73
- return new Validator (main , zf ).validate ();
105
+ static boolean validate (Main main , File zipFile ) throws IOException {
106
+ try (ZipFile zf = new ZipFile (zipFile );
107
+ ZipInputStream zis = new ZipInputStream (new BufferedInputStream (
108
+ new FileInputStream (zipFile )))) {
109
+ return new Validator (main , zf , zis ).validate ();
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Validate that the CEN/LOC file name header field adheres to
115
+ * PKWARE APPNOTE-6.3.3.TXT:
116
+ *
117
+ * 4.4.17.1 The name of the file, with optional relative path.
118
+ * The path stored MUST not contain a drive or
119
+ * device letter, or a leading slash. All slashes
120
+ * MUST be forward slashes '/' as opposed to
121
+ * backwards slashes '\' for compatibility with Amiga
122
+ * and UNIX file systems etc.
123
+ * Also validate that the file name is not "." or "..", and that any name
124
+ * element is not equal to "." or ".."
125
+ *
126
+ * @param entryName ZIP entry name
127
+ * @return true if a valid Zip Entry file name; false otherwise
128
+ */
129
+ public static boolean isZipEntryNameValid (String entryName ) {
130
+ return !INVALID_ZIP_ENTRY_NAME_PATTERN .matcher (entryName ).find ();
131
+ }
132
+
133
+ /**
134
+ * Validate base on entries in CEN and LOC. To ensure
135
+ * - Valid entry name
136
+ * - No duplicate entries
137
+ * - CEN and LOC should have same entries, in the same order
138
+ *
139
+ * NOTE: In order to check the encounter order based on the CEN listing,
140
+ * this implementation assumes CEN entries are to be added before
141
+ * adding any LOC entries. That is, addCenEntry should be called before
142
+ * calls to addLocEntry to ensure encounter order can be compared
143
+ * properly.
144
+ */
145
+ private class EntryValidator {
146
+ // A place holder when an entry is not yet seen in the directory
147
+ static final EntryEncounter PLACE_HOLDER = new EntryEncounter (0 , 0 );
148
+ // Flag to signal the CEN and LOC is not in the same order
149
+ boolean outOfOrder = false ;
150
+ /**
151
+ * A record to keep the encounter order in the directory and count of the appearances
152
+ */
153
+ record EntryEncounter (int order , int count ) {
154
+ /**
155
+ * Add to the appearance count.
156
+ * @param encounterOrder The supplier for the encounter order in the directory
157
+ */
158
+ EntryEncounter increase (IntSupplier encounterOrder ) {
159
+ return isPlaceHolder () ?
160
+ // First encounter of the entry in this directory
161
+ new EntryEncounter (encounterOrder .getAsInt (), 1 ) :
162
+ // After first encounter, keep the order but add the count
163
+ new EntryEncounter (order , count + 1 );
164
+ }
165
+
166
+ /**
167
+ * True if this entry is not in the directory.
168
+ */
169
+ boolean isPlaceHolder () {
170
+ return this == PLACE_HOLDER ;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Information used for validation for a entry in CEN and LOC.
176
+ */
177
+ record EntryInfo (EntryEncounter cen , EntryEncounter loc ) {}
178
+
179
+ /**
180
+ * Ordered deduplication set for entries
181
+ */
182
+ LinkedHashMap <String , EntryInfo > entries = new LinkedHashMap <>();
183
+ // Encounter order in CEN, step by 1 on each new entry
184
+ int cenEncounterOrder = 0 ;
185
+ // Encounter order in LOC, step by 1 for new LOC entry that exists in CEN
186
+ // Order comparing is based on CEN listing, therefore we skip LOC only entries.
187
+ int locEncounterOrder = 0 ;
188
+
189
+ /**
190
+ * Record an entry apperance in CEN
191
+ */
192
+ public void addCenEntry (ZipEntry cenEntry ) {
193
+ var entryName = cenEntry .getName ();
194
+ var entryInfo = entries .get (entryName );
195
+ if (entryInfo == null ) {
196
+ entries .put (entryName , new EntryInfo (
197
+ new EntryEncounter (cenEncounterOrder ++, 1 ),
198
+ PLACE_HOLDER ));
199
+ } else {
200
+ assert entryInfo .loc ().isPlaceHolder ();
201
+ entries .put (entryName , new EntryInfo (
202
+ entryInfo .cen ().increase (() -> cenEncounterOrder ++),
203
+ entryInfo .loc ()));
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Record an entry apperance in LOC
209
+ * We compare entry order based on the CEN. Thus do not increase LOC
210
+ * encounter order if the entry is only in LOC.
211
+ * NOTE: This works because all CEN entries are added before adding LOC entries.
212
+ */
213
+ public void addLocEntry (ZipEntry locEntry ) {
214
+ var entryName = locEntry .getName ();
215
+ var entryInfo = entries .get (entryName );
216
+ if (entryInfo == null ) {
217
+ entries .put (entryName , new EntryInfo (
218
+ PLACE_HOLDER ,
219
+ new EntryEncounter (locEncounterOrder , 1 )));
220
+ } else {
221
+ entries .put (entryName , new EntryInfo (
222
+ entryInfo .cen (),
223
+ entryInfo .loc ().increase (() -> entryInfo .cen ().isPlaceHolder () ? locEncounterOrder : locEncounterOrder ++)));
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Issue warning for duplicate entries
229
+ */
230
+ private void checkDuplicates (int count , String msg , String entryName ) {
231
+ if (count > 1 ) {
232
+ warn (formatMsg (msg , Integer .toString (count ), entryName ));
233
+ isValid = false ;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Validation per entry observed.
239
+ * Each entry must appear at least once in the CEN or LOC.
240
+ */
241
+ private void validateEntry (String entryName , EntryInfo entryInfo ) {
242
+ // Check invalid entry name
243
+ if (!isZipEntryNameValid (entryName )) {
244
+ warn (formatMsg ("warn.validator.invalid.entry.name" , entryName ));
245
+ isValid = false ;
246
+ }
247
+ // Check duplicate entries in CEN
248
+ checkDuplicates (entryInfo .cen ().count (), "warn.validator.duplicate.cen.entry" , entryName );
249
+ // Check duplicate entries in LOC
250
+ checkDuplicates (entryInfo .loc ().count (), "warn.validator.duplicate.loc.entry" , entryName );
251
+ // Check consistency between CEN and LOC
252
+ if (entryInfo .cen ().isPlaceHolder ()) {
253
+ warn (formatMsg ("warn.validator.loc.only.entry" , entryName ));
254
+ isValid = false ;
255
+ } else if (entryInfo .loc ().isPlaceHolder ()) {
256
+ warn (formatMsg ("warn.validator.cen.only.entry" , entryName ));
257
+ isValid = false ;
258
+ } else if (!outOfOrder && entryInfo .loc ().order () != entryInfo .cen ().order ()) {
259
+ outOfOrder = true ;
260
+ isValid = false ;
261
+ warn (getMsg ("warn.validator.order.mismatch" ));
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Validate the jar entries by checking each entry in encounter order
267
+ */
268
+ public void validate () {
269
+ entries .sequencedEntrySet ().forEach (e -> validateEntry (e .getKey (), e .getValue ()));
270
+ }
74
271
}
75
272
273
+
76
274
private boolean validate () {
77
275
try {
276
+ var entryValidator = new EntryValidator ();
78
277
zf .stream ()
278
+ .peek (entryValidator ::addCenEntry )
79
279
.filter (e -> e .getName ().endsWith (".class" ))
80
280
.map (this ::getFingerPrint )
81
281
.filter (FingerPrint ::isClass ) // skip any non-class entry
@@ -91,7 +291,18 @@ private boolean validate() {
91
291
else
92
292
validateVersioned (entries );
93
293
});
94
- } catch (InvalidJarException e ) {
294
+
295
+ /*
296
+ * Retrieve entries from the ZipInputStream to verify local file headers(LOC)
297
+ * have same entries as the cental directory(CEN).
298
+ */
299
+ ZipEntry e ;
300
+ while ((e = zis .getNextEntry ()) != null ) {
301
+ entryValidator .addLocEntry (e );
302
+ }
303
+
304
+ entryValidator .validate ();
305
+ } catch (IOException | InvalidJarException e ) {
95
306
errorAndInvalid (e .getMessage ());
96
307
}
97
308
return isValid ;
0 commit comments