1/*
2 * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package sun.tools.jar;
27
28import java.io.File;
29import java.io.IOException;
30import java.io.InputStream;
31import java.lang.module.InvalidModuleDescriptorException;
32import java.lang.module.ModuleDescriptor;
33import java.lang.module.ModuleDescriptor.Exports;
34import java.lang.module.InvalidModuleDescriptorException;
35import java.lang.module.ModuleDescriptor.Opens;
36import java.lang.module.ModuleDescriptor.Provides;
37import java.lang.module.ModuleDescriptor.Requires;
38import java.util.Collections;
39import java.util.Comparator;
40import java.util.HashMap;
41import java.util.HashSet;
42import java.util.List;
43import java.util.Map;
44import java.util.Set;
45import java.util.function.Consumer;
46import java.util.jar.JarEntry;
47import java.util.jar.JarFile;
48import java.util.zip.ZipEntry;
49
50import static java.util.jar.JarFile.MANIFEST_NAME;
51import static sun.tools.jar.Main.VERSIONS_DIR;
52import static sun.tools.jar.Main.VERSIONS_DIR_LENGTH;
53import static sun.tools.jar.Main.MODULE_INFO;
54import static sun.tools.jar.Main.getMsg;
55import static sun.tools.jar.Main.formatMsg;
56import static sun.tools.jar.Main.formatMsg2;
57import static sun.tools.jar.Main.toBinaryName;
58import static sun.tools.jar.Main.isModuleInfoEntry;
59
60final class Validator {
61    private final static boolean DEBUG = Boolean.getBoolean("jar.debug");
62    private final  Map<String,FingerPrint> fps = new HashMap<>();
63    private final Main main;
64    private final JarFile jf;
65    private int oldVersion = -1;
66    private String currentTopLevelName;
67    private boolean isValid = true;
68    private Set<String> concealedPkgs = Collections.emptySet();
69    private ModuleDescriptor md;
70    private String mdName;
71
72    private Validator(Main main, JarFile jf) {
73        this.main = main;
74        this.jf = jf;
75        checkModuleDescriptor(MODULE_INFO);
76    }
77
78    static boolean validate(Main main, JarFile jf) throws IOException {
79        return new Validator(main, jf).validate();
80    }
81
82    private boolean validate() {
83        try {
84            jf.stream()
85              .filter(e -> !e.isDirectory() &&
86                      !e.getName().equals(MANIFEST_NAME))
87              .sorted(ENTRY_COMPARATOR)
88              .forEachOrdered(e -> validate(e));
89            return isValid;
90        } catch (InvalidJarException e) {
91            error(formatMsg("error.validator.bad.entry.name", e.getMessage()));
92        }
93        return false;
94    }
95
96    private static class InvalidJarException extends RuntimeException {
97        private static final long serialVersionUID = -3642329147299217726L;
98        InvalidJarException(String msg) {
99            super(msg);
100        }
101    }
102
103    // sort base entries before versioned entries, and sort entry classes with
104    // nested classes so that the top level class appears before the associated
105    // nested class
106    static Comparator<String> ENTRYNAME_COMPARATOR = (s1, s2) ->  {
107
108        if (s1.equals(s2)) return 0;
109        boolean b1 = s1.startsWith(VERSIONS_DIR);
110        boolean b2 = s2.startsWith(VERSIONS_DIR);
111        if (b1 && !b2) return 1;
112        if (!b1 && b2) return -1;
113        int n = 0; // starting char for String compare
114        if (b1 && b2) {
115            // normally strings would be sorted so "10" goes before "9", but
116            // version number strings need to be sorted numerically
117            n = VERSIONS_DIR.length();   // skip the common prefix
118            int i1 = s1.indexOf('/', n);
119            int i2 = s2.indexOf('/', n);
120            if (i1 == -1) throw new InvalidJarException(s1);
121            if (i2 == -1) throw new InvalidJarException(s2);
122            // shorter version numbers go first
123            if (i1 != i2) return i1 - i2;
124            // otherwise, handle equal length numbers below
125        }
126        int l1 = s1.length();
127        int l2 = s2.length();
128        int lim = Math.min(l1, l2);
129        for (int k = n; k < lim; k++) {
130            char c1 = s1.charAt(k);
131            char c2 = s2.charAt(k);
132            if (c1 != c2) {
133                // change natural ordering so '.' comes before '$'
134                // i.e. top level classes come before nested classes
135                if (c1 == '$' && c2 == '.') return 1;
136                if (c1 == '.' && c2 == '$') return -1;
137                return c1 - c2;
138            }
139        }
140        return l1 - l2;
141    };
142
143    static Comparator<ZipEntry> ENTRY_COMPARATOR =
144        Comparator.comparing(ZipEntry::getName, ENTRYNAME_COMPARATOR);
145
146    /*
147     *  Validator has state and assumes entries provided to accept are ordered
148     *  from base entries first and then through the versioned entries in
149     *  ascending version order.  Also, to find isolated nested classes,
150     *  classes must be ordered so that the top level class is before the associated
151     *  nested class(es).
152    */
153    public void validate(JarEntry je) {
154        String entryName = je.getName();
155
156        // directories are always accepted
157        if (entryName.endsWith("/")) {
158            debug("%s is a directory", entryName);
159            return;
160        }
161
162        // validate the versioned module-info
163        if (isModuleInfoEntry(entryName)) {
164            if (!entryName.equals(mdName))
165                checkModuleDescriptor(entryName);
166            return;
167        }
168
169        // figure out the version and basename from the JarEntry
170        int version;
171        String basename;
172        String versionStr = null;;
173        if (entryName.startsWith(VERSIONS_DIR)) {
174            int n = entryName.indexOf("/", VERSIONS_DIR_LENGTH);
175            if (n == -1) {
176                error(formatMsg("error.validator.version.notnumber", entryName));
177                isValid = false;
178                return;
179            }
180            versionStr = entryName.substring(VERSIONS_DIR_LENGTH, n);
181            try {
182                version = Integer.parseInt(versionStr);
183            } catch (NumberFormatException x) {
184                error(formatMsg("error.validator.version.notnumber", entryName));
185                isValid = false;
186                return;
187            }
188            if (n == entryName.length()) {
189                error(formatMsg("error.validator.entryname.tooshort", entryName));
190                isValid = false;
191                return;
192            }
193            basename = entryName.substring(n + 1);
194        } else {
195            version = 0;
196            basename = entryName;
197        }
198        debug("\n===================\nversion %d %s", version, entryName);
199
200        if (oldVersion != version) {
201            oldVersion = version;
202            currentTopLevelName = null;
203            if (md == null && versionStr != null) {
204                // don't have a base module-info.class yet, try to see if
205                // a versioned one exists
206                checkModuleDescriptor(VERSIONS_DIR + versionStr + "/" + MODULE_INFO);
207            }
208        }
209
210        // analyze the entry, keeping key attributes
211        FingerPrint fp;
212        try (InputStream is = jf.getInputStream(je)) {
213            fp = new FingerPrint(basename, is.readAllBytes());
214        } catch (IOException x) {
215            error(x.getMessage());
216            isValid = false;
217            return;
218        }
219        String internalName = fp.name();
220
221        // process a base entry paying attention to nested classes
222        if (version == 0) {
223            debug("base entry found");
224            if (fp.isNestedClass()) {
225                debug("nested class found");
226                if (fp.topLevelName().equals(currentTopLevelName)) {
227                    fps.put(internalName, fp);
228                    return;
229                }
230                error(formatMsg("error.validator.isolated.nested.class", entryName));
231                isValid = false;
232                return;
233            }
234            // top level class or resource entry
235            if (fp.isClass()) {
236                currentTopLevelName = fp.topLevelName();
237                if (!checkInternalName(entryName, basename, internalName)) {
238                    isValid = false;
239                    return;
240                }
241            }
242            fps.put(internalName, fp);
243            return;
244        }
245
246        // process a versioned entry, look for previous entry with same name
247        FingerPrint matchFp = fps.get(internalName);
248        debug("looking for match");
249        if (matchFp == null) {
250            debug("no match found");
251            if (fp.isClass()) {
252                if (fp.isNestedClass()) {
253                    if (!checkNestedClass(version, entryName, internalName, fp)) {
254                        isValid = false;
255                    }
256                    return;
257                }
258                if (fp.isPublicClass()) {
259                    if (!isConcealed(internalName)) {
260                        error(Main.formatMsg("error.validator.new.public.class", entryName));
261                        isValid = false;
262                        return;
263                    }
264                    warn(formatMsg("warn.validator.concealed.public.class", entryName));
265                    debug("%s is a public class entry in a concealed package", entryName);
266                }
267                debug("%s is a non-public class entry", entryName);
268                fps.put(internalName, fp);
269                currentTopLevelName = fp.topLevelName();
270                return;
271            }
272            debug("%s is a resource entry");
273            fps.put(internalName, fp);
274            return;
275        }
276        debug("match found");
277
278        // are the two classes/resources identical?
279        if (fp.isIdentical(matchFp)) {
280            warn(formatMsg("warn.validator.identical.entry", entryName));
281            return;  // it's okay, just takes up room
282        }
283        debug("sha1 not equal -- different bytes");
284
285        // ok, not identical, check for compatible class version and api
286        if (fp.isClass()) {
287            if (fp.isNestedClass()) {
288                if (!checkNestedClass(version, entryName, internalName, fp)) {
289                    isValid = false;
290                }
291                return;
292            }
293            debug("%s is a class entry", entryName);
294            if (!fp.isCompatibleVersion(matchFp)) {
295                error(formatMsg("error.validator.incompatible.class.version", entryName));
296                isValid = false;
297                return;
298            }
299            if (!fp.isSameAPI(matchFp)) {
300                error(formatMsg("error.validator.different.api", entryName));
301                isValid = false;
302                return;
303            }
304            if (!checkInternalName(entryName, basename, internalName)) {
305                isValid = false;
306                return;
307            }
308            debug("fingerprints same -- same api");
309            fps.put(internalName, fp);
310            currentTopLevelName = fp.topLevelName();
311            return;
312        }
313        debug("%s is a resource", entryName);
314
315        warn(formatMsg("warn.validator.resources.with.same.name", entryName));
316        fps.put(internalName, fp);
317        return;
318    }
319
320    /**
321     * Checks whether or not the given versioned module descriptor's attributes
322     * are valid when compared against the root/base module descriptor.
323     *
324     * A versioned module descriptor must be identical to the root/base module
325     * descriptor, with two exceptions:
326     *  - A versioned descriptor can have different non-public `requires`
327     *    clauses of platform ( `java.*` and `jdk.*` ) modules, and
328     *  - A versioned descriptor can have different `uses` clauses, even of
329     *    service types defined outside of the platform modules.
330     */
331    private void checkModuleDescriptor(String miName) {
332        ZipEntry je = jf.getEntry(miName);
333        if (je != null) {
334            try (InputStream jis = jf.getInputStream(je)) {
335                ModuleDescriptor md = ModuleDescriptor.read(jis);
336                // Initialize the base md if it's not yet. A "base" md can be either the
337                // root module-info.class or the first versioned module-info.class
338                ModuleDescriptor base = this.md;
339
340                if (base == null) {
341                    concealedPkgs = new HashSet<>(md.packages());
342                    md.exports().stream().map(Exports::source).forEach(concealedPkgs::remove);
343                    md.opens().stream().map(Opens::source).forEach(concealedPkgs::remove);
344                    // must have the implementation class of the services it 'provides'.
345                    if (md.provides().stream().map(Provides::providers)
346                          .flatMap(List::stream)
347                          .filter(p -> jf.getEntry(toBinaryName(p)) == null)
348                          .peek(p -> error(formatMsg("error.missing.provider", p)))
349                          .count() != 0) {
350                        isValid = false;
351                        return;
352                    }
353                    this.md = md;
354                    this.mdName = miName;
355                    return;
356                }
357
358                if (!base.name().equals(md.name())) {
359                    error(getMsg("error.validator.info.name.notequal"));
360                    isValid = false;
361                }
362                if (!base.requires().equals(md.requires())) {
363                    Set<Requires> baseRequires = base.requires();
364                    for (Requires r : md.requires()) {
365                        if (baseRequires.contains(r))
366                            continue;
367                        if (r.modifiers().contains(Requires.Modifier.TRANSITIVE)) {
368                            error(getMsg("error.validator.info.requires.transitive"));
369                            isValid = false;
370                        } else if (!isPlatformModule(r.name())) {
371                            error(getMsg("error.validator.info.requires.added"));
372                            isValid = false;
373                        }
374                    }
375                    for (Requires r : baseRequires) {
376                        Set<Requires> mdRequires = md.requires();
377                        if (mdRequires.contains(r))
378                            continue;
379                        if (!isPlatformModule(r.name())) {
380                            error(getMsg("error.validator.info.requires.dropped"));
381                            isValid = false;
382                        }
383                    }
384                }
385                if (!base.exports().equals(md.exports())) {
386                    error(getMsg("error.validator.info.exports.notequal"));
387                    isValid = false;
388                }
389                if (!base.opens().equals(md.opens())) {
390                    error(getMsg("error.validator.info.opens.notequal"));
391                    isValid = false;
392                }
393                if (!base.provides().equals(md.provides())) {
394                    error(getMsg("error.validator.info.provides.notequal"));
395                    isValid = false;
396                }
397                if (!base.mainClass().equals(md.mainClass())) {
398                    error(formatMsg("error.validator.info.manclass.notequal", je.getName()));
399                    isValid = false;
400                }
401                if (!base.version().equals(md.version())) {
402                    error(formatMsg("error.validator.info.version.notequal", je.getName()));
403                    isValid = false;
404                }
405            } catch (Exception x) {
406                error(x.getMessage() + " : " + miName);
407                this.isValid = false;
408            }
409        }
410    }
411
412    private static boolean isPlatformModule(String name) {
413        return name.startsWith("java.") || name.startsWith("jdk.");
414    }
415
416    private boolean checkInternalName(String entryName, String basename, String internalName) {
417        String className = className(basename);
418        if (internalName.equals(className)) {
419            return true;
420        }
421        error(formatMsg2("error.validator.names.mismatch",
422                entryName, internalName.replace("/", ".")));
423        return false;
424    }
425
426    private boolean checkNestedClass(int version, String entryName, String internalName, FingerPrint fp) {
427        debug("%s is a nested class entry in top level class %s", entryName, fp.topLevelName());
428        if (fp.topLevelName().equals(currentTopLevelName)) {
429            debug("%s (top level class) was accepted", fp.topLevelName());
430            fps.put(internalName, fp);
431            return true;
432        }
433        debug("top level class was not accepted");
434        error(formatMsg("error.validator.isolated.nested.class", entryName));
435        return false;
436    }
437
438    private String className(String entryName) {
439        return entryName.endsWith(".class") ? entryName.substring(0, entryName.length() - 6) : null;
440    }
441
442    private boolean isConcealed(String internalName) {
443        if (concealedPkgs.isEmpty()) {
444            return false;
445        }
446        int idx = internalName.lastIndexOf('/');
447        String pkgName = idx != -1 ? internalName.substring(0, idx).replace('/', '.') : "";
448        return concealedPkgs.contains(pkgName);
449    }
450
451    private void debug(String fmt, Object... args) {
452        if (DEBUG) System.err.format(fmt, args);
453    }
454
455    private void error(String msg) {
456        main.error(msg);
457    }
458
459    private void warn(String msg) {
460        main.warn(msg);
461    }
462
463}
464