jdk/src/jdk.jartool/share/classes/sun/tools/jar/Validator.java
author sherman
Thu, 12 Jan 2017 16:41:08 -0800
changeset 43095 336dfda4ae89
parent 41754 8b7c8d5e9a0d
child 43099 47f8baf1fcbd
permissions -rw-r--r--
8172432: jar cleanup/update for module and mrm jar 8171830: jar tool should validate if any exported or open package is missing Reviewed-by: mchung, psandoz, chegar

/*
 * 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.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.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 static final int vdlen = VERSIONS_DIR.length();
    private final Main main;
    private final JarFile jf;
    private int oldVersion = -1;
    private String currentTopLevelName;
    private boolean isValid = true;
    private Set<String> concealedPkgs;
    private ModuleDescriptor md;

    private Validator(Main main, JarFile jf) {
        this.main = main;
        this.jf = jf;
        loadModuleDescriptor();
    }

    static boolean validate(Main main, File f) throws IOException {
        return new Validator(main, new JarFile(f)).validate();
    }

    private boolean validate() {
        try {
            jf.stream()
              .filter(e -> !e.isDirectory() &&
                      !e.getName().equals(MANIFEST_NAME))
              .sorted(entryComparator)
              .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);
        }
    }

    // sort base entries before versioned entries, and sort entry classes with
    // nested classes so that the top level class appears before the associated
    // nested class
    private static Comparator<JarEntry> entryComparator = (je1, je2) ->  {
        String s1 = je1.getName();
        String s2 = je2.getName();
        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; // starting char for String compare
        if (b1 && b2) {
            // normally strings would be sorted so "10" goes before "9", but
            // version number strings need to be sorted numerically
            n = VERSIONS_DIR.length();   // skip the common prefix
            int i1 = s1.indexOf('/', n);
            int i2 = s1.indexOf('/', n);
            if (i1 == -1) throw new InvalidJarException(s1);
            if (i2 == -1) throw new InvalidJarException(s2);
            // shorter version numbers go first
            if (i1 != i2) return i1 - i2;
            // otherwise, handle equal length numbers below
        }
        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) {
                // change natural ordering so '.' comes before '$'
                // i.e. top level classes come before nested classes
                if (c1 == '$' && c2 == '.') return 1;
                if (c1 == '.' && c2 == '$') return -1;
                return c1 - c2;
            }
        }
        return l1 - l2;
    };

    /*
     *  Validator has state and assumes entries provided to accept are ordered
     *  from base entries first and then through the versioned entries in
     *  ascending version order.  Also, to find isolated nested classes,
     *  classes must be ordered so that the top level class is before the associated
     *  nested class(es).
    */
    public void validate(JarEntry je) {
        String entryName = je.getName();

        // directories are always accepted
        if (entryName.endsWith("/")) {
            debug("%s is a directory", entryName);
            return;
        }

        // validate the versioned module-info
        if (isModuleInfoEntry(entryName)) {
            if (entryName.length() != MODULE_INFO.length())
                checkModuleDescriptor(je);
            return;
        }

        // figure out the version and basename from the JarEntry
        int version;
        String basename;
        if (entryName.startsWith(VERSIONS_DIR)) {
            int n = entryName.indexOf("/", vdlen);
            if (n == -1) {
                error(formatMsg("error.validator.version.notnumber", entryName));
                isValid = false;
                return;
            }
            String v = entryName.substring(vdlen, n);
            try {
                version = Integer.parseInt(v);
            } 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;
        }

        // analyze the entry, keeping key attributes
        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();

        // process a base entry paying attention to nested classes
        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;
            }
            // top level class or resource entry
            if (fp.isClass()) {
                currentTopLevelName = fp.topLevelName();
                if (!checkInternalName(entryName, basename, internalName)) {
                    isValid = false;
                    return;
                }
            }
            fps.put(internalName, fp);
            return;
        }

        // process a versioned entry, look for previous entry with same name
        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");

        // are the two classes/resources identical?
        if (fp.isIdentical(matchFp)) {
            warn(formatMsg("warn.validator.identical.entry", entryName));
            return;  // it's okay, just takes up room
        }
        debug("sha1 not equal -- different bytes");

        // ok, not identical, check for compatible class version and api
        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 loadModuleDescriptor() {
        ZipEntry je = jf.getEntry(MODULE_INFO);
        if (je != null) {
            try (InputStream jis = jf.getInputStream(je)) {
                md = ModuleDescriptor.read(jis);
                concealedPkgs = new HashSet<>(md.packages());
                md.exports().stream().map(Exports::source).forEach(concealedPkgs::remove);
                md.opens().stream().map(Opens::source).forEach(concealedPkgs::remove);
                return;
            } catch (Exception x) {
                error(x.getMessage() + " : " + je.getName());
                this.isValid = false;
            }
        }
        md = null;
        concealedPkgs = Collections.emptySet();
    }

    private static boolean isPlatformModule(String name) {
        return name.startsWith("java.") || name.startsWith("jdk.");
    }

    /**
     * Checks whether or not the given versioned module descriptor's attributes
     * are valid when compared against the root module descriptor.
     *
     * A versioned module descriptor must be identical to the root 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(JarEntry je) {
        try (InputStream is = jf.getInputStream(je)) {
            ModuleDescriptor root = this.md;
            ModuleDescriptor md = null;
            try {
                md = ModuleDescriptor.read(is);
            } catch (InvalidModuleDescriptorException x) {
                error(x.getMessage());
                isValid = false;
                return;
            }
            if (root == null) {
                this.md = md;
            } else {
                if (!root.name().equals(md.name())) {
                    error(getMsg("error.versioned.info.name.notequal"));
                    isValid = false;
                }
                if (!root.requires().equals(md.requires())) {
                    Set<Requires> rootRequires = root.requires();
                    for (Requires r : md.requires()) {
                        if (rootRequires.contains(r))
                            continue;
                        if (r.modifiers().contains(Requires.Modifier.TRANSITIVE)) {
                            error(getMsg("error.versioned.info.requires.transitive"));
                            isValid = false;
                        } else if (!isPlatformModule(r.name())) {
                            error(getMsg("error.versioned.info.requires.added"));
                            isValid = false;
                        }
                    }
                    for (Requires r : rootRequires) {
                        Set<Requires> mdRequires = md.requires();
                        if (mdRequires.contains(r))
                            continue;
                        if (!isPlatformModule(r.name())) {
                            error(getMsg("error.versioned.info.requires.dropped"));
                            isValid = false;
                        }
                    }
                }
                if (!root.exports().equals(md.exports())) {
                    error(getMsg("error.versioned.info.exports.notequal"));
                    isValid = false;
                }
                if (!root.opens().equals(md.opens())) {
                    error(getMsg("error.versioned.info.opens.notequal"));
                    isValid = false;
                }
                if (!root.provides().equals(md.provides())) {
                    error(getMsg("error.versioned.info.provides.notequal"));
                    isValid = false;
                }
                if (!root.mainClass().equals(md.mainClass())) {
                    error(formatMsg("error.validator.info.manclass.notequal", je.getName()));
                    isValid = false;
                }
                if (!root.version().equals(md.version())) {
                    error(formatMsg("error.validator.info.version.notequal", je.getName()));
                    isValid = false;
                }
            }
        } catch (IOException x) {
            error(x.getMessage());
            isValid = false;
        }
    }

    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);
    }

}