package sun.tools.jar;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.module.InvalidModuleDescriptorException;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.Exports;
import java.lang.module.InvalidModuleDescriptorException;
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.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
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;
import static sun.tools.jar.Main.getMsg;
import static sun.tools.jar.Main.formatMsg;
import static sun.tools.jar.Main.formatMsg2;
import static sun.tools.jar.Main.toBinaryName;
import static sun.tools.jar.Main.isModuleInfoEntry;
final class Validator {
private final static boolean DEBUG = Boolean.getBoolean("jar.debug");
private final Map<String,FingerPrint> fps = new HashMap<>();
private final Main main;
private final JarFile jf;
private int oldVersion = -1;
private String currentTopLevelName;
private boolean isValid = true;
private Set<String> concealedPkgs = Collections.emptySet();
private ModuleDescriptor md;
private String mdName;
private Validator(Main main, JarFile jf) {
this.main = main;
this.jf = jf;
checkModuleDescriptor(MODULE_INFO);
}
static boolean validate(Main main, JarFile jf) throws IOException {
return new Validator(main, jf).validate();
}
private boolean validate() {
try {
jf.stream()
.filter(e -> !e.isDirectory() &&
!e.getName().equals(MANIFEST_NAME))
.sorted(ENTRY_COMPARATOR)
.forEachOrdered(e -> validate(e));
return isValid;
} catch (InvalidJarException e) {
error(formatMsg("error.validator.bad.entry.name", e.getMessage()));
}
return false;
}
private static class InvalidJarException extends RuntimeException {
private static final long serialVersionUID = -3642329147299217726L;
InvalidJarException(String msg) {
super(msg);
}
}
static Comparator<String> ENTRYNAME_COMPARATOR = (s1, s2) -> {
if (s1.equals(s2)) return 0;
boolean b1 = s1.startsWith(VERSIONS_DIR);
boolean b2 = s2.startsWith(VERSIONS_DIR);
if (b1 && !b2) return 1;
if (!b1 && b2) return -1;
int n = 0;
if (b1 && b2) {
n = VERSIONS_DIR.length();
int i1 = s1.indexOf('/', n);
int i2 = s2.indexOf('/', n);
if (i1 == -1) throw new InvalidJarException(s1);
if (i2 == -1) throw new InvalidJarException(s2);
if (i1 != i2) return i1 - i2;
}
int l1 = s1.length();
int l2 = s2.length();
int lim = Math.min(l1, l2);
for (int k = n; k < lim; k++) {
char c1 = s1.charAt(k);
char c2 = s2.charAt(k);
if (c1 != c2) {
if (c1 == '$' && c2 == '.') return 1;
if (c1 == '.' && c2 == '$') return -1;
return c1 - c2;
}
}
return l1 - l2;
};
static Comparator<ZipEntry> ENTRY_COMPARATOR =
Comparator.comparing(ZipEntry::getName, ENTRYNAME_COMPARATOR);
public void validate(JarEntry je) {
String entryName = je.getName();
if (entryName.endsWith("/")) {
debug("%s is a directory", entryName);
return;
}
if (isModuleInfoEntry(entryName)) {
if (!entryName.equals(mdName))
checkModuleDescriptor(entryName);
return;
}
int version;
String basename;
String versionStr = null;;
if (entryName.startsWith(VERSIONS_DIR)) {
int n = entryName.indexOf("/", VERSIONS_DIR_LENGTH);
if (n == -1) {
error(formatMsg("error.validator.version.notnumber", entryName));
isValid = false;
return;
}
versionStr = entryName.substring(VERSIONS_DIR_LENGTH, n);
try {
version = Integer.parseInt(versionStr);
} catch (NumberFormatException x) {
error(formatMsg("error.validator.version.notnumber", entryName));
isValid = false;
return;
}
if (n == entryName.length()) {
error(formatMsg("error.validator.entryname.tooshort", entryName));
isValid = false;
return;
}
basename = entryName.substring(n + 1);
} else {
version = 0;
basename = entryName;
}
debug("\n===================\nversion %d %s", version, entryName);
if (oldVersion != version) {
oldVersion = version;
currentTopLevelName = null;
if (md == null && versionStr != null) {
checkModuleDescriptor(VERSIONS_DIR + versionStr + "/" + MODULE_INFO);
}
}
FingerPrint fp;
try (InputStream is = jf.getInputStream(je)) {
fp = new FingerPrint(basename, is.readAllBytes());
} catch (IOException x) {
error(x.getMessage());
isValid = false;
return;
}
String internalName = fp.name();
if (version == 0) {
debug("base entry found");
if (fp.isNestedClass()) {
debug("nested class found");
if (fp.topLevelName().equals(currentTopLevelName)) {
fps.put(internalName, fp);
return;
}
error(formatMsg("error.validator.isolated.nested.class", entryName));
isValid = false;
return;
}
if (fp.isClass()) {
currentTopLevelName = fp.topLevelName();
if (!checkInternalName(entryName, basename, internalName)) {
isValid = false;
return;
}
}
fps.put(internalName, fp);
return;
}
FingerPrint matchFp = fps.get(internalName);
debug("looking for match");
if (matchFp == null) {
debug("no match found");
if (fp.isClass()) {
if (fp.isNestedClass()) {
if (!checkNestedClass(version, entryName, internalName, fp)) {
isValid = false;
}
return;
}
if (fp.isPublicClass()) {
if (!isConcealed(internalName)) {
error(Main.formatMsg("error.validator.new.public.class", entryName));
isValid = false;
return;
}
warn(formatMsg("warn.validator.concealed.public.class", entryName));
debug("%s is a public class entry in a concealed package", entryName);
}
debug("%s is a non-public class entry", entryName);
fps.put(internalName, fp);
currentTopLevelName = fp.topLevelName();
return;
}
debug("%s is a resource entry");
fps.put(internalName, fp);
return;
}
debug("match found");
if (fp.isIdentical(matchFp)) {
warn(formatMsg("warn.validator.identical.entry", entryName));
return;
}
debug("sha1 not equal -- different bytes");
if (fp.isClass()) {
if (fp.isNestedClass()) {
if (!checkNestedClass(version, entryName, internalName, fp)) {
isValid = false;
}
return;
}
debug("%s is a class entry", entryName);
if (!fp.isCompatibleVersion(matchFp)) {
error(formatMsg("error.validator.incompatible.class.version", entryName));
isValid = false;
return;
}
if (!fp.isSameAPI(matchFp)) {
error(formatMsg("error.validator.different.api", entryName));
isValid = false;
return;
}
if (!checkInternalName(entryName, basename, internalName)) {
isValid = false;
return;
}
debug("fingerprints same -- same api");
fps.put(internalName, fp);
currentTopLevelName = fp.topLevelName();
return;
}
debug("%s is a resource", entryName);
warn(formatMsg("warn.validator.resources.with.same.name", entryName));
fps.put(internalName, fp);
return;
}
private void checkModuleDescriptor(String miName) {
ZipEntry je = jf.getEntry(miName);
if (je != null) {
try (InputStream jis = jf.getInputStream(je)) {
ModuleDescriptor md = ModuleDescriptor.read(jis);
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);
if (md.provides().stream().map(Provides::providers)
.flatMap(List::stream)
.filter(p -> jf.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())) {
error(getMsg("error.validator.info.name.notequal"));
isValid = false;
}
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)) {
error(getMsg("error.validator.info.requires.transitive"));
isValid = false;
} else if (!isPlatformModule(r.name())) {
error(getMsg("error.validator.info.requires.added"));
isValid = false;
}
}
for (Requires r : baseRequires) {
Set<Requires> mdRequires = md.requires();
if (mdRequires.contains(r))
continue;
if (!isPlatformModule(r.name())) {
error(getMsg("error.validator.info.requires.dropped"));
isValid = false;
}
}
}
if (!base.exports().equals(md.exports())) {
error(getMsg("error.validator.info.exports.notequal"));
isValid = false;
}
if (!base.opens().equals(md.opens())) {
error(getMsg("error.validator.info.opens.notequal"));
isValid = false;
}
if (!base.provides().equals(md.provides())) {
error(getMsg("error.validator.info.provides.notequal"));
isValid = false;
}
if (!base.mainClass().equals(md.mainClass())) {
error(formatMsg("error.validator.info.manclass.notequal", je.getName()));
isValid = false;
}
if (!base.version().equals(md.version())) {
error(formatMsg("error.validator.info.version.notequal", je.getName()));
isValid = false;
}
} catch (Exception x) {
error(x.getMessage() + " : " + miName);
this.isValid = false;
}
}
}
private static boolean isPlatformModule(String name) {
return name.startsWith("java.") || name.startsWith("jdk.");
}
private boolean checkInternalName(String entryName, String basename, String internalName) {
String className = className(basename);
if (internalName.equals(className)) {
return true;
}
error(formatMsg2("error.validator.names.mismatch",
entryName, internalName.replace("/", ".")));
return false;
}
private boolean checkNestedClass(int version, String entryName, String internalName, FingerPrint fp) {
debug("%s is a nested class entry in top level class %s", entryName, fp.topLevelName());
if (fp.topLevelName().equals(currentTopLevelName)) {
debug("%s (top level class) was accepted", fp.topLevelName());
fps.put(internalName, fp);
return true;
}
debug("top level class was not accepted");
error(formatMsg("error.validator.isolated.nested.class", entryName));
return false;
}
private String className(String entryName) {
return entryName.endsWith(".class") ? entryName.substring(0, entryName.length() - 6) : null;
}
private boolean isConcealed(String internalName) {
if (concealedPkgs.isEmpty()) {
return false;
}
int idx = internalName.lastIndexOf('/');
String pkgName = idx != -1 ? internalName.substring(0, idx).replace('/', '.') : "";
return concealedPkgs.contains(pkgName);
}
private void debug(String fmt, Object... args) {
if (DEBUG) System.err.format(fmt, args);
}
private void error(String msg) {
main.error(msg);
}
private void warn(String msg) {
main.warn(msg);
}
}