/*
 * 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.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);
        }
    }

    // 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
    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; // 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 = s2.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;
    };

    static Comparator<ZipEntry> ENTRY_COMPARATOR =
        Comparator.comparing(ZipEntry::getName, ENTRYNAME_COMPARATOR);

    /*
     *  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.equals(mdName))
                checkModuleDescriptor(entryName);
            return;
        }

        // figure out the version and basename from the JarEntry
        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) {
                // don't have a base module-info.class yet, try to see if
                // a versioned one exists
                checkModuleDescriptor(VERSIONS_DIR + versionStr + "/" + MODULE_INFO);
            }
        }

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

    
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.
/** * 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 je = jf.getEntry(miName); if (je != null) { try (InputStream jis = jf.getInputStream(je)) { 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 -> 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); } }