blob: 1852b2cad9d04a519fbde1ef292a930fe8252338 [file]
/*
* 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
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
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;
import java.lang.module.ModuleDescriptor.Exports;
import java.lang.module.ModuleDescriptor.Opens;
import java.lang.module.ModuleDescriptor.Provides;
import java.lang.module.ModuleDescriptor.Requires;
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 sun.tools.jar.Main.VERSIONS_DIR;
import static sun.tools.jar.Main.VERSIONS_DIR_LENGTH;
import static sun.tools.jar.Main.MODULE_INFO;
import static sun.tools.jar.Main.getMsg;
import static sun.tools.jar.Main.formatMsg;
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;
private final ZipFile zf;
private boolean isValid = true;
private Set<String> concealedPkgs = Collections.emptySet();
private ModuleDescriptor md;
private String mdName;
private final ZipInputStream zis;
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, 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
.collect(Collectors.groupingBy(
FingerPrint::mrversion,
TreeMap::new,
Collectors.toMap(FingerPrint::className,
Function.identity(),
this::sameNameFingerPrint)))
.forEach((version, entries) -> {
if (version == 0)
validateBase(entries);
else
validateVersioned(entries);
});
/*
* 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;
}
static class InvalidJarException extends RuntimeException {
private static final long serialVersionUID = -3642329147299217726L;
InvalidJarException(String msg) {
super(msg);
}
}
private FingerPrint sameNameFingerPrint(FingerPrint fp1, FingerPrint fp2) {
checkClassName(fp1);
checkClassName(fp2);
// entries/classes with same name, return fp2 for now ?
return fp2;
}
private FingerPrint getFingerPrint(ZipEntry ze) {
// figure out the version and basename from the ZipEntry
String ename = ze.getName();
String bname = ename;
int version = 0;
if (ename.startsWith(VERSIONS_DIR)) {
int n = ename.indexOf("/", VERSIONS_DIR_LENGTH);
if (n == -1) {
throw new InvalidJarException(
formatMsg("error.validator.version.notnumber", ename));
}
try {
version = Integer.parseInt(ename, VERSIONS_DIR_LENGTH, n, 10);
} catch (NumberFormatException x) {
throw new InvalidJarException(
formatMsg("error.validator.version.notnumber", ename));
}
if (n == ename.length()) {
throw new InvalidJarException(
formatMsg("error.validator.entryname.tooshort", ename));
}
bname = ename.substring(n + 1);
}
// return the cooresponding fingerprint entry
try (InputStream is = zf.getInputStream(ze)) {
return new FingerPrint(bname, ename, version, is.readAllBytes());
} catch (IOException x) {
throw new InvalidJarException(x.getMessage());
}
}
/*
* Validates (a) if there is any isolated nested class, and (b) if the
* class name in class file (by asm) matches the entry's basename.
*/
public void validateBase(Map<String, FingerPrint> fps) {
fps.values().forEach( fp -> {
if (!checkClassName(fp)) {
return;
}
if (fp.isNestedClass()) {
checkNestedClass(fp, fps);
}
classes.put(fp.className(), fp);
});
}
public void validateVersioned(Map<String, FingerPrint> fps) {
fps.values().forEach( fp -> {
// all versioned entries must be compatible with their release target number
if (fp.mrversion() < fp.classReleaseVersion()) {
errorAndInvalid(formatMsg("error.release.value.toohigh.versioned.entry",
fp.entryName(), // META-INF/versions/9/com/foo/Bar.class has class file version
String.valueOf(fp.classMajorVersion()), // 69, but class file version
String.valueOf(fp.mrversion() + 44), // 53 or less is required to target release
String.valueOf(fp.mrversion()))); // 9 of the Java Platform
return;
}
// validate the versioned module-info
if (MODULE_INFO.equals(fp.basename())) {
checkModuleDescriptor(fp.entryName());
return;
}
// process a versioned entry, look for previous entry with same name
FingerPrint matchFp = classes.get(fp.className());
if (matchFp == null) {
// no match found
if (fp.isNestedClass()) {
checkNestedClass(fp, fps);
return;
}
if (fp.isPublicClass()) {
if (!isConcealed(fp.className())) {
errorAndInvalid(formatMsg("error.validator.new.public.class",
fp.entryName()));
return;
}
// entry is a public class entry in a concealed package
warn(formatMsg("warn.validator.concealed.public.class",
fp.entryName()));
}
classes.put(fp.className(), fp);
return;
}
// are the two classes/resources identical?
if (fp.isIdentical(matchFp)) {
warn(formatMsg("warn.validator.identical.entry", fp.entryName()));
return; // it's okay, just takes up room
}
// ok, not identical, check for compatible class version and api
if (fp.isNestedClass()) {
checkNestedClass(fp, fps);
return; // fall through, need check nested public class??
}
if (!fp.isCompatibleVersion(matchFp)) {
errorAndInvalid(formatMsg("error.validator.incompatible.class.version",
fp.entryName()));
return;
}
if (!fp.isSameAPI(matchFp)) {
errorAndInvalid(formatMsg("error.validator.different.api",
fp.entryName()));
return;
}
if (!checkClassName(fp)) {
return;
}
classes.put(fp.className(), fp);
return;
});
}
/*
* Checks whether or not the given versioned module descriptor's attributes
* are valid when compared against the root/base module descriptor.
*
* A versioned module descriptor must be identical to the root/base module
* descriptor, with two exceptions:
* - A versioned descriptor can have different non-public `requires`
* clauses of platform ( `java.*` and `jdk.*` ) modules, and
* - A versioned descriptor can have different `uses` clauses, even of
* service types defined outside of the platform modules.
*/
private void checkModuleDescriptor(String miName) {
ZipEntry ze = zf.getEntry(miName);
if (ze != null) {
try (InputStream jis = zf.getInputStream(ze)) {
ModuleDescriptor md = ModuleDescriptor.read(jis);
// Initialize the base md if it's not yet. A "base" md can be either the
// root module-info.class or the first versioned module-info.class
ModuleDescriptor base = this.md;
if (base == null) {
concealedPkgs = new HashSet<>(md.packages());
md.exports().stream().map(Exports::source).forEach(concealedPkgs::remove);
md.opens().stream().map(Opens::source).forEach(concealedPkgs::remove);
// must have the implementation class of the services it 'provides'.
if (md.provides().stream().map(Provides::providers)
.flatMap(List::stream)
.filter(p -> zf.getEntry(toBinaryName(p)) == null)
.peek(p -> error(formatMsg("error.missing.provider", p)))
.count() != 0) {
isValid = false;
return;
}
this.md = md;
this.mdName = miName;
return;
}
if (!base.name().equals(md.name())) {
errorAndInvalid(getMsg("error.validator.info.name.notequal"));
}
if (!base.requires().equals(md.requires())) {
Set<Requires> baseRequires = base.requires();
for (Requires r : md.requires()) {
if (baseRequires.contains(r))
continue;
if (r.modifiers().contains(Requires.Modifier.TRANSITIVE)) {
errorAndInvalid(getMsg("error.validator.info.requires.transitive"));
} else if (!isPlatformModule(r.name())) {
errorAndInvalid(getMsg("error.validator.info.requires.added"));
}
}
for (Requires r : baseRequires) {
Set<Requires> mdRequires = md.requires();
if (mdRequires.contains(r))
continue;
if (!isPlatformModule(r.name())) {
errorAndInvalid(getMsg("error.validator.info.requires.dropped"));
}
}
}
if (!base.exports().equals(md.exports())) {
errorAndInvalid(getMsg("error.validator.info.exports.notequal"));
}
if (!base.opens().equals(md.opens())) {
errorAndInvalid(getMsg("error.validator.info.opens.notequal"));
}
if (!base.provides().equals(md.provides())) {
errorAndInvalid(getMsg("error.validator.info.provides.notequal"));
}
if (!base.mainClass().equals(md.mainClass())) {
errorAndInvalid(formatMsg("error.validator.info.manclass.notequal",
ze.getName()));
}
if (!base.version().equals(md.version())) {
errorAndInvalid(formatMsg("error.validator.info.version.notequal",
ze.getName()));
}
} catch (Exception x) {
errorAndInvalid(x.getMessage() + " : " + miName);
}
}
}
private boolean checkClassName(FingerPrint fp) {
if (fp.className().equals(className(fp.basename()))) {
return true;
}
error(formatMsg("error.validator.names.mismatch",
fp.entryName(), fp.className().replace("/", ".")));
return isValid = false;
}
private boolean checkNestedClass(FingerPrint fp, Map<String, FingerPrint> outerClasses) {
if (outerClasses.containsKey(fp.outerClassName())) {
return true;
}
// outer class was not available
error(formatMsg("error.validator.isolated.nested.class", fp.entryName()));
return isValid = false;
}
private boolean isConcealed(String className) {
if (concealedPkgs.isEmpty()) {
return false;
}
int idx = className.lastIndexOf('/');
String pkgName = idx != -1 ? className.substring(0, idx).replace('/', '.') : "";
return concealedPkgs.contains(pkgName);
}
private static boolean isPlatformModule(String name) {
return name.startsWith("java.") || name.startsWith("jdk.");
}
private static String className(String entryName) {
return entryName.endsWith(".class") ? entryName.substring(0, entryName.length() - 6) : null;
}
private void error(String msg) {
main.error(msg);
}
private void errorAndInvalid(String msg) {
main.error(msg);
isValid = false;
}
private void warn(String msg) {
main.warn(msg);
}
}