Skip to content

Commit cd052c7

Browse files
committed
8345431: Improve jar --validate to detect duplicate or invalid entries
Reviewed-by: lancea, jpai
1 parent b2a61a9 commit cd052c7

File tree

5 files changed

+596
-15
lines changed

5 files changed

+596
-15
lines changed

src/jdk.jartool/share/classes/sun/tools/jar/Main.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,8 @@ public synchronized boolean run(String[] args) {
447447
}
448448

449449
private boolean validateJar(File file) throws IOException {
450-
try (ZipFile zf = new ZipFile(file)) {
451-
return Validator.validate(this, zf);
450+
try {
451+
return Validator.validate(this, file);
452452
} catch (IOException e) {
453453
error(formatMsg("error.validator.jarfile.exception", fname, e.getMessage()));
454454
return true;

src/jdk.jartool/share/classes/sun/tools/jar/Validator.java

Lines changed: 217 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
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.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -25,7 +25,9 @@
2525

2626
package sun.tools.jar;
2727

28+
import java.io.BufferedInputStream;
2829
import java.io.File;
30+
import java.io.FileInputStream;
2931
import java.io.IOException;
3032
import java.io.InputStream;
3133
import java.lang.module.ModuleDescriptor;
@@ -36,16 +38,19 @@
3638
import java.util.Collections;
3739
import java.util.HashMap;
3840
import java.util.HashSet;
41+
import java.util.LinkedHashMap;
3942
import java.util.List;
4043
import java.util.Map;
4144
import java.util.Set;
4245
import java.util.TreeMap;
4346
import java.util.function.Function;
47+
import java.util.function.IntSupplier;
48+
import java.util.regex.Pattern;
4449
import java.util.stream.Collectors;
4550
import java.util.zip.ZipEntry;
4651
import java.util.zip.ZipFile;
52+
import java.util.zip.ZipInputStream;
4753

48-
import static java.util.jar.JarFile.MANIFEST_NAME;
4954
import static sun.tools.jar.Main.VERSIONS_DIR;
5055
import static sun.tools.jar.Main.VERSIONS_DIR_LENGTH;
5156
import static sun.tools.jar.Main.MODULE_INFO;
@@ -54,6 +59,32 @@
5459
import static sun.tools.jar.Main.toBinaryName;
5560

5661
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+
);
5788

5889
private final Map<String,FingerPrint> classes = new HashMap<>();
5990
private final Main main;
@@ -62,20 +93,189 @@ final class Validator {
6293
private Set<String> concealedPkgs = Collections.emptySet();
6394
private ModuleDescriptor md;
6495
private String mdName;
96+
private final ZipInputStream zis;
6597

66-
private Validator(Main main, ZipFile zf) {
98+
private Validator(Main main, ZipFile zf, ZipInputStream zis) {
6799
this.main = main;
68100
this.zf = zf;
101+
this.zis = zis;
69102
checkModuleDescriptor(MODULE_INFO);
70103
}
71104

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+
}
74271
}
75272

273+
76274
private boolean validate() {
77275
try {
276+
var entryValidator = new EntryValidator();
78277
zf.stream()
278+
.peek(entryValidator::addCenEntry)
79279
.filter(e -> e.getName().endsWith(".class"))
80280
.map(this::getFingerPrint)
81281
.filter(FingerPrint::isClass) // skip any non-class entry
@@ -91,7 +291,18 @@ private boolean validate() {
91291
else
92292
validateVersioned(entries);
93293
});
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) {
95306
errorAndInvalid(e.getMessage());
96307
}
97308
return isValid;

src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved.
2+
# Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved.
33
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
#
55
# This code is free software; you can redistribute it and/or modify it
@@ -143,6 +143,18 @@ warn.validator.concealed.public.class=\
143143
Warning: entry {0} is a public class\n\
144144
in a concealed package, placing this jar on the class path will result\n\
145145
in incompatible public interfaces
146+
warn.validator.duplicate.cen.entry=\
147+
Warning: There were {0} central directory entries found for {1}
148+
warn.validator.duplicate.loc.entry=\
149+
Warning: There were {0} local file headers found for {1}
150+
warn.validator.invalid.entry.name=\
151+
Warning: entry name {0} is not valid
152+
warn.validator.cen.only.entry=\
153+
Warning: An equivalent for the central directory entry {0} was not found in the local file headers
154+
warn.validator.loc.only.entry=\
155+
Warning: An equivalent entry for the local file header {0} was not found in the central directory
156+
warn.validator.order.mismatch=\
157+
Warning: Central directory and local file header entries are not in the same order
146158
warn.release.unexpected.versioned.entry=\
147159
unexpected versioned entry {0}
148160
warn.index.is.ignored=\
@@ -265,10 +277,13 @@ main.help.opt.main.extract=\
265277
main.help.opt.main.describe-module=\
266278
\ -d, --describe-module Print the module descriptor, or automatic module name
267279
main.help.opt.main.validate=\
268-
\ --validate Validate the contents of the jar archive. This option\n\
269-
\ will validate that the API exported by a multi-release\n\
280+
\ --validate Validate the contents of the jar archive. This option:\n\
281+
\ - Validates that the API exported by a multi-release\n\
270282
\ jar archive is consistent across all different release\n\
271-
\ versions.
283+
\ versions.\n\
284+
\ - Issues a warning if there are invalid or duplicate file names
285+
286+
272287
main.help.opt.any=\
273288
\ Operation modifiers valid in any mode:\n\
274289
\n\
@@ -346,7 +361,5 @@ main.help.postopt=\
346361
\n\
347362
\ Mandatory or optional arguments to long options are also mandatory or optional\n\
348363
\ for any corresponding short options.
349-
main.help.opt.extract=\
350-
\ Operation modifiers valid only in extract mode:\n
351364
main.help.opt.extract.dir=\
352365
\ --dir Directory into which the jar will be extracted

src/jdk.jartool/share/man/jar.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
# Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
2+
# Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved.
33
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
#
55
# This code is free software; you can redistribute it and/or modify it
@@ -106,6 +106,10 @@ argument is the first argument specified on the command line.
106106
`-d` or `--describe-module`
107107
: Prints the module descriptor or automatic module name.
108108

109+
`--validate`
110+
: Validate the contents of the JAR file.
111+
See `Integrity of a JAR File` section below for more details.
112+
109113
## Operation Modifiers Valid in Any Mode
110114

111115
You can use the following options to customize the actions of any operation
@@ -213,6 +217,27 @@ operation modes:
213217
`--version`
214218
: Prints the program version.
215219

220+
## Integrity of a JAR File
221+
As a JAR file is based on ZIP format, it is possible to create a JAR file using tools
222+
other than the `jar` command. The --validate option may be used to perform the following
223+
integrity checks against a JAR file:
224+
225+
- That there are no duplicate Zip entry file names
226+
- Verify that the Zip entry file name:
227+
- is not an absolute path
228+
- the file name is not '.' or '..'
229+
- does not contain a backslash, '\\'
230+
- does not contain a drive letter
231+
- path element does not include '.' or '..
232+
- The API exported by a multi-release jar archive is consistent across all different release
233+
versions.
234+
235+
The jar tool exits with a status of 0 if there were no integrity issues encountered and >0 if an
236+
error/warning occurred.
237+
238+
When an integrity issue is reported, it will often require that the JAR file is re-created by the
239+
original source of the JAR file.
240+
216241
## Examples of jar Command Syntax
217242

218243
- Create an archive, `classes.jar`, that contains two class files,

0 commit comments

Comments
 (0)