/* * Copyright (c) 2017, 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.File; 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.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.function.Function; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; 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; final class Validator { private final Map classes = new HashMap<>(); private final Main main; private final ZipFile zf; private boolean isValid = true; private Set concealedPkgs = Collections.emptySet(); private ModuleDescriptor md; private String mdName; private Validator(Main main, ZipFile zf) { this.main = main; this.zf = zf; checkModuleDescriptor(MODULE_INFO); } static boolean validate(Main main, ZipFile zf) throws IOException { return new Validator(main, zf).validate(); } private boolean validate() { try { zf.stream() .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); }); } catch (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 fps) { fps.values().forEach( fp -> { if (!checkClassName(fp)) { isValid = false; return; } if (fp.isNestedClass()) { if (!checkNestedClass(fp, fps)) { isValid = false; } } classes.put(fp.className(), fp); }); } public void validateVersioned(Map fps) { fps.values().forEach( fp -> { // 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()) { if (!checkNestedClass(fp, fps)) { isValid = false; } 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()) { if (!checkNestedClass(fp, fps)) { isValid = false; } 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)) { isValid = false; 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 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 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(formatMsg2("error.validator.names.mismatch", fp.entryName(), fp.className().replace("/", "."))); return false; } private boolean checkNestedClass(FingerPrint fp, Map outerClasses) { if (outerClasses.containsKey(fp.outerClassName())) { return true; } // outer class was not available error(formatMsg("error.validator.isolated.nested.class", fp.entryName())); return 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); } }