Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/jdk.jartool/share/classes/sun/tools/jar/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,8 @@ public synchronized boolean run(String[] args) {
}

private boolean validateJar(File file) throws IOException {
try (ZipFile zf = new ZipFile(file)) {
return Validator.validate(this, zf);
try {
return Validator.validate(this, file);
} catch (IOException e) {
error(formatMsg("error.validator.jarfile.exception", fname, e.getMessage()));
return true;
Expand Down
223 changes: 217 additions & 6 deletions src/jdk.jartool/share/classes/sun/tools/jar/Validator.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
Expand All @@ -25,7 +25,9 @@

package sun.tools.jar;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.module.ModuleDescriptor;
Expand All @@ -36,16 +38,19 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.function.IntSupplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;

import static java.util.jar.JarFile.MANIFEST_NAME;
import static sun.tools.jar.Main.VERSIONS_DIR;
import static sun.tools.jar.Main.VERSIONS_DIR_LENGTH;
import static sun.tools.jar.Main.MODULE_INFO;
Expand All @@ -54,6 +59,32 @@
import static sun.tools.jar.Main.toBinaryName;

final class Validator {
/**
* Regex expression to verify that the Zip Entry file name:
* - is not an absolute path
* - the file name is not '.' or '..'
* - does not contain a backslash, '\'
* - does not contain a drive letter
* - path element does not include '.' or '..'
*/
private static final Pattern INVALID_ZIP_ENTRY_NAME_PATTERN = Pattern.compile(
// Don't allow a '..' in the path
"^(\\.|\\.\\.)$"
+ "|^\\.\\./"
+ "|/\\.\\.$"
+ "|/\\.\\./"
// Don't allow a '.' in the path
+ "|^\\./"
+ "|/\\.$"
+ "|/\\./"
// Don't allow absolute path
+ "|^/"
// Don't allow a backslash in the path
+ "|^\\\\"
+ "|.*\\\\.*"
// Don't allow a drive letter
+ "|.*[a-zA-Z]:.*"
);

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

private Validator(Main main, ZipFile zf) {
private Validator(Main main, ZipFile zf, ZipInputStream zis) {
this.main = main;
this.zf = zf;
this.zis = zis;
checkModuleDescriptor(MODULE_INFO);
}

static boolean validate(Main main, ZipFile zf) throws IOException {
return new Validator(main, zf).validate();
static boolean validate(Main main, File zipFile) throws IOException {
try (ZipFile zf = new ZipFile(zipFile);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(
new FileInputStream(zipFile)))) {
return new Validator(main, zf, zis).validate();
}
}

/**
* Validate that the CEN/LOC file name header field adheres to
* PKWARE APPNOTE-6.3.3.TXT:
*
* 4.4.17.1 The name of the file, with optional relative path.
* The path stored MUST not contain a drive or
* device letter, or a leading slash. All slashes
* MUST be forward slashes '/' as opposed to
* backwards slashes '\' for compatibility with Amiga
* and UNIX file systems etc.
* Also validate that the file name is not "." or "..", and that any name
* element is not equal to "." or ".."
*
* @param entryName ZIP entry name
* @return true if a valid Zip Entry file name; false otherwise
*/
public static boolean isZipEntryNameValid(String entryName) {
return !INVALID_ZIP_ENTRY_NAME_PATTERN.matcher(entryName).find();
}

/**
* Validate base on entries in CEN and LOC. To ensure
* - Valid entry name
* - No duplicate entries
* - CEN and LOC should have same entries, in the same order
*
* NOTE: In order to check the encounter order based on the CEN listing,
* this implementation assumes CEN entries are to be added before
* adding any LOC entries. That is, addCenEntry should be called before
* calls to addLocEntry to ensure encounter order can be compared
* properly.
*/
private class EntryValidator {
// A place holder when an entry is not yet seen in the directory
static final EntryEncounter PLACE_HOLDER = new EntryEncounter(0, 0);
// Flag to signal the CEN and LOC is not in the same order
boolean outOfOrder = false;
/**
* A record to keep the encounter order in the directory and count of the appearances
*/
record EntryEncounter(int order, int count) {
/**
* Add to the appearance count.
* @param encounterOrder The supplier for the encounter order in the directory
*/
EntryEncounter increase(IntSupplier encounterOrder) {
return isPlaceHolder() ?
// First encounter of the entry in this directory
new EntryEncounter(encounterOrder.getAsInt(), 1) :
// After first encounter, keep the order but add the count
new EntryEncounter(order, count + 1);
}

/**
* True if this entry is not in the directory.
*/
boolean isPlaceHolder() {
return this == PLACE_HOLDER;
}
}

/**
* Information used for validation for a entry in CEN and LOC.
*/
record EntryInfo(EntryEncounter cen, EntryEncounter loc) {}

/**
* Ordered deduplication set for entries
*/
LinkedHashMap<String, EntryInfo> entries = new LinkedHashMap<>();
// Encounter order in CEN, step by 1 on each new entry
int cenEncounterOrder = 0;
// Encounter order in LOC, step by 1 for new LOC entry that exists in CEN
// Order comparing is based on CEN listing, therefore we skip LOC only entries.
int locEncounterOrder = 0;

/**
* Record an entry apperance in CEN
*/
public void addCenEntry(ZipEntry cenEntry) {
var entryName = cenEntry.getName();
var entryInfo = entries.get(entryName);
if (entryInfo == null) {
entries.put(entryName, new EntryInfo(
new EntryEncounter(cenEncounterOrder++, 1),
PLACE_HOLDER));
} else {
assert entryInfo.loc().isPlaceHolder();
entries.put(entryName, new EntryInfo(
entryInfo.cen().increase(() -> cenEncounterOrder++),
entryInfo.loc()));
}
}

/**
* Record an entry apperance in LOC
* We compare entry order based on the CEN. Thus do not increase LOC
* encounter order if the entry is only in LOC.
* NOTE: This works because all CEN entries are added before adding LOC entries.
*/
public void addLocEntry(ZipEntry locEntry) {
var entryName = locEntry.getName();
var entryInfo = entries.get(entryName);
if (entryInfo == null) {
entries.put(entryName, new EntryInfo(
PLACE_HOLDER,
new EntryEncounter(locEncounterOrder, 1)));
} else {
entries.put(entryName, new EntryInfo(
entryInfo.cen(),
entryInfo.loc().increase(() -> entryInfo.cen().isPlaceHolder() ? locEncounterOrder : locEncounterOrder++)));
}
}

/**
* Issue warning for duplicate entries
*/
private void checkDuplicates(int count, String msg, String entryName) {
if (count > 1) {
warn(formatMsg(msg, Integer.toString(count), entryName));
isValid = false;
}
}

/**
* Validation per entry observed.
* Each entry must appear at least once in the CEN or LOC.
*/
private void validateEntry(String entryName, EntryInfo entryInfo) {
// Check invalid entry name
if (!isZipEntryNameValid(entryName)) {
warn(formatMsg("warn.validator.invalid.entry.name", entryName));
isValid = false;
}
// Check duplicate entries in CEN
checkDuplicates(entryInfo.cen().count(), "warn.validator.duplicate.cen.entry", entryName);
// Check duplicate entries in LOC
checkDuplicates(entryInfo.loc().count(), "warn.validator.duplicate.loc.entry", entryName);
// Check consistency between CEN and LOC
if (entryInfo.cen().isPlaceHolder()) {
warn(formatMsg("warn.validator.loc.only.entry", entryName));
isValid = false;
} else if (entryInfo.loc().isPlaceHolder()) {
warn(formatMsg("warn.validator.cen.only.entry", entryName));
isValid = false;
} else if (!outOfOrder && entryInfo.loc().order() != entryInfo.cen().order()) {
outOfOrder = true;
isValid = false;
warn(getMsg("warn.validator.order.mismatch"));
}
}

/**
* Validate the jar entries by checking each entry in encounter order
*/
public void validate() {
entries.sequencedEntrySet().forEach(e -> validateEntry(e.getKey(), e.getValue()));
}
}


private boolean validate() {
try {
var entryValidator = new EntryValidator();
zf.stream()
.peek(entryValidator::addCenEntry)
.filter(e -> e.getName().endsWith(".class"))
.map(this::getFingerPrint)
.filter(FingerPrint::isClass) // skip any non-class entry
Expand All @@ -91,7 +291,18 @@ private boolean validate() {
else
validateVersioned(entries);
});
} catch (InvalidJarException e) {

/*
* Retrieve entries from the ZipInputStream to verify local file headers(LOC)
* have same entries as the cental directory(CEN).
*/
ZipEntry e;
while ((e = zis.getNextEntry()) != null) {
entryValidator.addLocEntry(e);
}

entryValidator.validate();
} catch (IOException | InvalidJarException e) {
errorAndInvalid(e.getMessage());
}
return isValid;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Copyright (c) 1999, 2024, Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 1999, 2025, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -143,6 +143,18 @@ warn.validator.concealed.public.class=\
Warning: entry {0} is a public class\n\
in a concealed package, placing this jar on the class path will result\n\
in incompatible public interfaces
warn.validator.duplicate.cen.entry=\
Warning: There were {0} central directory entries found for {1}
warn.validator.duplicate.loc.entry=\
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as for warn.validator.duplicate.cen.entry

Warning: There were {0} local file headers found for {1}
warn.validator.invalid.entry.name=\
Warning: entry name {0} is not valid
warn.validator.cen.only.entry=\
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would change

Warning: Entry {0} in central directory is not in local file header
to
Warning: An equivalent local file header was not found for the central directory header for the file name: {0}

Warning: An equivalent for the central directory entry {0} was not found in the local file headers
warn.validator.loc.only.entry=\
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as for warn.validator.duplicate.loc.entry

Warning: An equivalent entry for the local file header {0} was not found in the central directory
warn.validator.order.mismatch=\
Warning: Central directory and local file header entries are not in the same order
warn.release.unexpected.versioned.entry=\
unexpected versioned entry {0}
warn.index.is.ignored=\
Expand Down Expand Up @@ -265,10 +277,13 @@ main.help.opt.main.extract=\
main.help.opt.main.describe-module=\
\ -d, --describe-module Print the module descriptor, or automatic module name
main.help.opt.main.validate=\
\ --validate Validate the contents of the jar archive. This option\n\
\ will validate that the API exported by a multi-release\n\
\ --validate Validate the contents of the jar archive. This option:\n\
\ - Validates that the API exported by a multi-release\n\
\ jar archive is consistent across all different release\n\
\ versions.
\ versions.\n\
\ - Issues a warning if there are invalid or duplicate file names


main.help.opt.any=\
\ Operation modifiers valid in any mode:\n\
\n\
Expand Down Expand Up @@ -346,7 +361,5 @@ main.help.postopt=\
\n\
\ Mandatory or optional arguments to long options are also mandatory or optional\n\
\ for any corresponding short options.
main.help.opt.extract=\
\ Operation modifiers valid only in extract mode:\n
main.help.opt.extract.dir=\
\ --dir Directory into which the jar will be extracted
27 changes: 26 additions & 1 deletion src/jdk.jartool/share/man/jar.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
# Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved.
# Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
Expand Down Expand Up @@ -106,6 +106,10 @@ argument is the first argument specified on the command line.
`-d` or `--describe-module`
: Prints the module descriptor or automatic module name.

`--validate`
: Validate the contents of the JAR file.
See `Integrity of a JAR File` section below for more details.

## Operation Modifiers Valid in Any Mode

You can use the following options to customize the actions of any operation
Expand Down Expand Up @@ -213,6 +217,27 @@ operation modes:
`--version`
: Prints the program version.

## Integrity of a JAR File
As a JAR file is based on ZIP format, it is possible to create a JAR file using tools
other than the `jar` command. The --validate option may be used to perform the following
integrity checks against a JAR file:

- That there are no duplicate Zip entry file names
- Verify that the Zip entry file name:
- is not an absolute path
- the file name is not '.' or '..'
- does not contain a backslash, '\\'
- does not contain a drive letter
- path element does not include '.' or '..
- The API exported by a multi-release jar archive is consistent across all different release
versions.

The jar tool exits with a status of 0 if there were no integrity issues encountered and >0 if an
error/warning occurred.

When an integrity issue is reported, it will often require that the JAR file is re-created by the
original source of the JAR file.

## Examples of jar Command Syntax

- Create an archive, `classes.jar`, that contains two class files,
Expand Down
Loading