1/*
2 * Copyright (c) 1996, 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.*;
29import java.lang.module.Configuration;
30import java.lang.module.FindException;
31import java.lang.module.InvalidModuleDescriptorException;
32import java.lang.module.ModuleDescriptor;
33import java.lang.module.ModuleDescriptor.Exports;
34import java.lang.module.ModuleDescriptor.Provides;
35import java.lang.module.ModuleDescriptor.Opens;
36import java.lang.module.ModuleDescriptor.Requires;
37import java.lang.module.ModuleDescriptor.Version;
38import java.lang.module.ModuleFinder;
39import java.lang.module.ModuleReader;
40import java.lang.module.ModuleReference;
41import java.lang.module.ResolutionException;
42import java.lang.module.ResolvedModule;
43import java.net.URI;
44import java.nio.ByteBuffer;
45import java.nio.file.Path;
46import java.nio.file.Files;
47import java.nio.file.Paths;
48import java.nio.file.StandardCopyOption;
49import java.util.*;
50import java.util.function.Consumer;
51import java.util.function.Supplier;
52import java.util.regex.Pattern;
53import java.util.stream.Collectors;
54import java.util.stream.Stream;
55import java.util.zip.*;
56import java.util.jar.*;
57import java.util.jar.Pack200.*;
58import java.util.jar.Manifest;
59import java.text.MessageFormat;
60
61import jdk.internal.module.Checks;
62import jdk.internal.module.ModuleHashes;
63import jdk.internal.module.ModuleHashesBuilder;
64import jdk.internal.module.ModuleInfo;
65import jdk.internal.module.ModuleInfoExtender;
66import jdk.internal.module.ModuleResolution;
67import jdk.internal.module.ModuleTarget;
68import jdk.internal.util.jar.JarIndex;
69
70import static jdk.internal.util.jar.JarIndex.INDEX_NAME;
71import static java.util.jar.JarFile.MANIFEST_NAME;
72import static java.util.stream.Collectors.joining;
73import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
74import static sun.tools.jar.Validator.ENTRYNAME_COMPARATOR;
75
76/**
77 * This class implements a simple utility for creating files in the JAR
78 * (Java Archive) file format. The JAR format is based on the ZIP file
79 * format, with optional meta-information stored in a MANIFEST entry.
80 */
81public class Main {
82    String program;
83    PrintWriter out, err;
84    String fname, mname, ename;
85    String zname = "";
86    String rootjar = null;
87
88    private static final int BASE_VERSION = 0;
89
90    private static class Entry {
91        final String name;
92        final File file;
93        final boolean isDir;
94
95        Entry(File file, String name, boolean isDir) {
96            this.file = file;
97            this.isDir = isDir;
98            this.name = name;
99        }
100
101        @Override
102        public boolean equals(Object o) {
103            if (this == o) return true;
104            if (!(o instanceof Entry)) return false;
105            return this.file.equals(((Entry)o).file);
106        }
107
108        @Override
109        public int hashCode() {
110            return file.hashCode();
111        }
112    }
113
114    // An entryName(path)->Entry map generated during "expand", it helps to
115    // decide whether or not an existing entry in a jar file needs to be
116    // replaced, during the "update" operation.
117    Map<String, Entry> entryMap = new HashMap<>();
118
119    // All entries need to be added/updated.
120    Set<Entry> entries = new LinkedHashSet<>();
121
122    // module-info.class entries need to be added/updated.
123    Map<String,byte[]> moduleInfos = new HashMap<>();
124
125    // A paths Set for each version, where each Set contains directories
126    // specified by the "-C" operation.
127    Map<Integer,Set<String>> pathsMap = new HashMap<>();
128
129    // There's also a files array per version
130    Map<Integer,String[]> filesMap = new HashMap<>();
131
132    // Do we think this is a multi-release jar?  Set to true
133    // if --release option found followed by at least file
134    boolean isMultiRelease;
135
136    // The last parsed --release value, if any. Used in conjunction with
137    // "-d,--describe-module" to select the operative module descriptor.
138    int releaseValue = -1;
139
140    /*
141     * cflag: create
142     * uflag: update
143     * xflag: xtract
144     * tflag: table
145     * vflag: verbose
146     * flag0: no zip compression (store only)
147     * Mflag: DO NOT generate a manifest file (just ZIP)
148     * iflag: generate jar index
149     * nflag: Perform jar normalization at the end
150     * pflag: preserve/don't strip leading slash and .. component from file name
151     * dflag: print module descriptor
152     */
153    boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, nflag, pflag, dflag;
154
155    /* To support additional GNU Style informational options */
156    Consumer<PrintWriter> info;
157
158    /* Modular jar related options */
159    Version moduleVersion;
160    Pattern modulesToHash;
161    ModuleResolution moduleResolution = ModuleResolution.empty();
162    ModuleFinder moduleFinder = ModuleFinder.of();
163
164    static final String MODULE_INFO = "module-info.class";
165    static final String MANIFEST_DIR = "META-INF/";
166    static final String VERSIONS_DIR = MANIFEST_DIR + "versions/";
167    static final String VERSION = "1.0";
168    static final int VERSIONS_DIR_LENGTH = VERSIONS_DIR.length();
169    private static ResourceBundle rsrc;
170
171    /**
172     * If true, maintain compatibility with JDK releases prior to 6.0 by
173     * timestamping extracted files with the time at which they are extracted.
174     * Default is to use the time given in the archive.
175     */
176    private static final boolean useExtractionTime =
177        Boolean.getBoolean("sun.tools.jar.useExtractionTime");
178
179    /**
180     * Initialize ResourceBundle
181     */
182    static {
183        try {
184            rsrc = ResourceBundle.getBundle("sun.tools.jar.resources.jar");
185        } catch (MissingResourceException e) {
186            throw new Error("Fatal: Resource for jar is missing");
187        }
188    }
189
190    static String getMsg(String key) {
191        try {
192            return (rsrc.getString(key));
193        } catch (MissingResourceException e) {
194            throw new Error("Error in message file");
195        }
196    }
197
198    static String formatMsg(String key, String arg) {
199        String msg = getMsg(key);
200        String[] args = new String[1];
201        args[0] = arg;
202        return MessageFormat.format(msg, (Object[]) args);
203    }
204
205    static String formatMsg2(String key, String arg, String arg1) {
206        String msg = getMsg(key);
207        String[] args = new String[2];
208        args[0] = arg;
209        args[1] = arg1;
210        return MessageFormat.format(msg, (Object[]) args);
211    }
212
213    public Main(PrintStream out, PrintStream err, String program) {
214        this.out = new PrintWriter(out, true);
215        this.err = new PrintWriter(err, true);
216        this.program = program;
217    }
218
219    public Main(PrintWriter out, PrintWriter err, String program) {
220        this.out = out;
221        this.err = err;
222        this.program = program;
223    }
224
225    /**
226     * Creates a new empty temporary file in the same directory as the
227     * specified file.  A variant of File.createTempFile.
228     */
229    private static File createTempFileInSameDirectoryAs(File file)
230        throws IOException {
231        File dir = file.getParentFile();
232        if (dir == null)
233            dir = new File(".");
234        return File.createTempFile("jartmp", null, dir);
235    }
236
237    private boolean ok;
238
239    /**
240     * Starts main program with the specified arguments.
241     */
242    public synchronized boolean run(String args[]) {
243        ok = true;
244        if (!parseArgs(args)) {
245            return false;
246        }
247        File tmpFile = null;
248        try {
249            if (cflag || uflag) {
250                if (fname != null) {
251                    // The name of the zip file as it would appear as its own
252                    // zip file entry. We use this to make sure that we don't
253                    // add the zip file to itself.
254                    zname = fname.replace(File.separatorChar, '/');
255                    if (zname.startsWith("./")) {
256                        zname = zname.substring(2);
257                    }
258                }
259            }
260            if (cflag) {
261                Manifest manifest = null;
262                if (!Mflag) {
263                    if (mname != null) {
264                        try (InputStream in = new FileInputStream(mname)) {
265                            manifest = new Manifest(new BufferedInputStream(in));
266                        }
267                    } else {
268                        manifest = new Manifest();
269                    }
270                    addVersion(manifest);
271                    addCreatedBy(manifest);
272                    if (isAmbiguousMainClass(manifest)) {
273                        return false;
274                    }
275                    if (ename != null) {
276                        addMainClass(manifest, ename);
277                    }
278                    if (isMultiRelease) {
279                        addMultiRelease(manifest);
280                    }
281                }
282                expand();
283                if (!moduleInfos.isEmpty()) {
284                    // All actual file entries (excl manifest and module-info.class)
285                    Set<String> jentries = new HashSet<>();
286                    // all packages if it's a class or resource
287                    Set<String> packages = new HashSet<>();
288                    entries.stream()
289                           .filter(e -> !e.isDir)
290                           .forEach( e -> {
291                               addPackageIfNamed(packages, e.name);
292                               jentries.add(e.name);
293                    });
294                    addExtendedModuleAttributes(moduleInfos, packages);
295
296                    // Basic consistency checks for modular jars.
297                    if (!checkModuleInfo(moduleInfos.get(MODULE_INFO), jentries))
298                        return false;
299
300                } else if (moduleVersion != null || modulesToHash != null) {
301                    error(getMsg("error.module.options.without.info"));
302                    return false;
303                }
304                if (vflag && fname == null) {
305                    // Disable verbose output so that it does not appear
306                    // on stdout along with file data
307                    // error("Warning: -v option ignored");
308                    vflag = false;
309                }
310                final String tmpbase = (fname == null)
311                        ? "tmpjar"
312                        : fname.substring(fname.indexOf(File.separatorChar) + 1);
313
314                tmpFile = createTemporaryFile(tmpbase, ".jar");
315                try (OutputStream out = new FileOutputStream(tmpFile)) {
316                    create(new BufferedOutputStream(out, 4096), manifest);
317                }
318                if (nflag) {
319                    File packFile = createTemporaryFile(tmpbase, ".pack");
320                    try {
321                        Packer packer = Pack200.newPacker();
322                        Map<String, String> p = packer.properties();
323                        p.put(Packer.EFFORT, "1"); // Minimal effort to conserve CPU
324                        try (JarFile jarFile = new JarFile(tmpFile.getCanonicalPath());
325                             OutputStream pack = new FileOutputStream(packFile))
326                        {
327                            packer.pack(jarFile, pack);
328                        }
329                        if (tmpFile.exists()) {
330                            tmpFile.delete();
331                        }
332                        tmpFile = createTemporaryFile(tmpbase, ".jar");
333                        try (OutputStream out = new FileOutputStream(tmpFile);
334                             JarOutputStream jos = new JarOutputStream(out))
335                        {
336                            Unpacker unpacker = Pack200.newUnpacker();
337                            unpacker.unpack(packFile, jos);
338                        }
339                    } finally {
340                        Files.deleteIfExists(packFile.toPath());
341                    }
342                }
343                validateAndClose(tmpFile);
344            } else if (uflag) {
345                File inputFile = null;
346                if (fname != null) {
347                    inputFile = new File(fname);
348                    tmpFile = createTempFileInSameDirectoryAs(inputFile);
349                } else {
350                    vflag = false;
351                    tmpFile = createTemporaryFile("tmpjar", ".jar");
352                }
353                expand();
354                try (FileInputStream in = (fname != null) ? new FileInputStream(inputFile)
355                        : new FileInputStream(FileDescriptor.in);
356                     FileOutputStream out = new FileOutputStream(tmpFile);
357                     InputStream manifest = (!Mflag && (mname != null)) ?
358                            (new FileInputStream(mname)) : null;
359                ) {
360                    boolean updateOk = update(in, new BufferedOutputStream(out),
361                        manifest, moduleInfos, null);
362                    if (ok) {
363                        ok = updateOk;
364                    }
365                }
366                validateAndClose(tmpFile);
367            } else if (tflag) {
368                replaceFSC(filesMap);
369                // For the "list table contents" action, access using the
370                // ZipFile class is always most efficient since only a
371                // "one-finger" scan through the central directory is required.
372                String[] files = filesMapToFiles(filesMap);
373                if (fname != null) {
374                    list(fname, files);
375                } else {
376                    InputStream in = new FileInputStream(FileDescriptor.in);
377                    try {
378                        list(new BufferedInputStream(in), files);
379                    } finally {
380                        in.close();
381                    }
382                }
383            } else if (xflag) {
384                replaceFSC(filesMap);
385                // For the extract action, when extracting all the entries,
386                // access using the ZipInputStream class is most efficient,
387                // since only a single sequential scan through the zip file is
388                // required.  When using the ZipFile class, a "two-finger" scan
389                // is required, but this is likely to be more efficient when a
390                // partial extract is requested.  In case the zip file has
391                // "leading garbage", we fall back from the ZipInputStream
392                // implementation to the ZipFile implementation, since only the
393                // latter can handle it.
394
395                String[] files = filesMapToFiles(filesMap);
396                if (fname != null && files != null) {
397                    extract(fname, files);
398                } else {
399                    InputStream in = (fname == null)
400                        ? new FileInputStream(FileDescriptor.in)
401                        : new FileInputStream(fname);
402                    try {
403                        if (!extract(new BufferedInputStream(in), files) && fname != null) {
404                            extract(fname, files);
405                        }
406                    } finally {
407                        in.close();
408                    }
409                }
410            } else if (iflag) {
411                String[] files = filesMap.get(BASE_VERSION);  // base entries only, can be null
412                genIndex(rootjar, files);
413            } else if (dflag) {
414                boolean found;
415                if (fname != null) {
416                    try (ZipFile zf = new ZipFile(fname)) {
417                        found = describeModule(zf);
418                    }
419                } else {
420                    try (FileInputStream fin = new FileInputStream(FileDescriptor.in)) {
421                        found = describeModuleFromStream(fin);
422                    }
423                }
424                if (!found)
425                    error(getMsg("error.module.descriptor.not.found"));
426            }
427        } catch (IOException e) {
428            fatalError(e);
429            ok = false;
430        } catch (Error ee) {
431            ee.printStackTrace();
432            ok = false;
433        } catch (Throwable t) {
434            t.printStackTrace();
435            ok = false;
436        } finally {
437            if (tmpFile != null && tmpFile.exists())
438                tmpFile.delete();
439        }
440        out.flush();
441        err.flush();
442        return ok;
443    }
444
445    private void validateAndClose(File tmpfile) throws IOException {
446        if (ok && isMultiRelease) {
447            try (JarFile jf = new JarFile(tmpfile)) {
448                ok = Validator.validate(this, jf);
449                if (!ok) {
450                    error(formatMsg("error.validator.jarfile.invalid", fname));
451                }
452            } catch (IOException e) {
453                error(formatMsg2("error.validator.jarfile.exception", fname, e.getMessage()));
454            }
455        }
456        Path path = tmpfile.toPath();
457        try {
458            if (ok) {
459                if (fname != null) {
460                    Files.move(path, Paths.get(fname), StandardCopyOption.REPLACE_EXISTING);
461                } else {
462                    Files.copy(path, new FileOutputStream(FileDescriptor.out));
463                }
464            }
465        } finally {
466            Files.deleteIfExists(path);
467        }
468    }
469
470    private String[] filesMapToFiles(Map<Integer,String[]> filesMap) {
471        if (filesMap.isEmpty()) return null;
472        return filesMap.entrySet()
473                .stream()
474                .flatMap(this::filesToEntryNames)
475                .toArray(String[]::new);
476    }
477
478    Stream<String> filesToEntryNames(Map.Entry<Integer,String[]> fileEntries) {
479        int version = fileEntries.getKey();
480        Set<String> cpaths = pathsMap.get(version);
481        return Stream.of(fileEntries.getValue())
482            .map(f -> toVersionedName(toEntryName(f, cpaths, false), version));
483    }
484
485    /**
486     * Parses command line arguments.
487     */
488    boolean parseArgs(String args[]) {
489        /* Preprocess and expand @file arguments */
490        try {
491            args = CommandLine.parse(args);
492        } catch (FileNotFoundException e) {
493            fatalError(formatMsg("error.cant.open", e.getMessage()));
494            return false;
495        } catch (IOException e) {
496            fatalError(e);
497            return false;
498        }
499        /* parse flags */
500        int count = 1;
501        try {
502            String flags = args[0];
503
504            // Note: flags.length == 2 can be treated as the short version of
505            // the GNU option since the there cannot be any other options,
506            // excluding -C, as per the old way.
507            if (flags.startsWith("--") ||
508                (flags.startsWith("-") && flags.length() == 2)) {
509                try {
510                    count = GNUStyleOptions.parseOptions(this, args);
511                } catch (GNUStyleOptions.BadArgs x) {
512                    if (info == null) {
513                        if (x.showUsage) {
514                            usageError(x.getMessage());
515                        } else {
516                            error(x.getMessage());
517                        }
518                        return false;
519                    }
520                }
521                if (info != null) {
522                    info.accept(out);
523                    return true;
524                }
525            } else {
526                // Legacy/compatibility options
527                if (flags.startsWith("-")) {
528                    flags = flags.substring(1);
529                }
530                for (int i = 0; i < flags.length(); i++) {
531                    switch (flags.charAt(i)) {
532                        case 'c':
533                            if (xflag || tflag || uflag || iflag) {
534                                usageError(getMsg("error.multiple.main.operations"));
535                                return false;
536                            }
537                            cflag = true;
538                            break;
539                        case 'u':
540                            if (cflag || xflag || tflag || iflag) {
541                                usageError(getMsg("error.multiple.main.operations"));
542                                return false;
543                            }
544                            uflag = true;
545                            break;
546                        case 'x':
547                            if (cflag || uflag || tflag || iflag) {
548                                usageError(getMsg("error.multiple.main.operations"));
549                                return false;
550                            }
551                            xflag = true;
552                            break;
553                        case 't':
554                            if (cflag || uflag || xflag || iflag) {
555                                usageError(getMsg("error.multiple.main.operations"));
556                                return false;
557                            }
558                            tflag = true;
559                            break;
560                        case 'M':
561                            Mflag = true;
562                            break;
563                        case 'v':
564                            vflag = true;
565                            break;
566                        case 'f':
567                            fname = args[count++];
568                            break;
569                        case 'm':
570                            mname = args[count++];
571                            break;
572                        case '0':
573                            flag0 = true;
574                            break;
575                        case 'i':
576                            if (cflag || uflag || xflag || tflag) {
577                                usageError(getMsg("error.multiple.main.operations"));
578                                return false;
579                            }
580                            // do not increase the counter, files will contain rootjar
581                            rootjar = args[count++];
582                            iflag = true;
583                            break;
584                        case 'n':
585                            nflag = true;
586                            break;
587                        case 'e':
588                            ename = args[count++];
589                            break;
590                        case 'P':
591                            pflag = true;
592                            break;
593                        default:
594                            usageError(formatMsg("error.illegal.option",
595                                       String.valueOf(flags.charAt(i))));
596                            return false;
597                    }
598                }
599            }
600        } catch (ArrayIndexOutOfBoundsException e) {
601            usageError(getMsg("main.usage.summary"));
602            return false;
603        }
604        if (!cflag && !tflag && !xflag && !uflag && !iflag && !dflag) {
605            usageError(getMsg("error.bad.option"));
606            return false;
607        }
608
609        /* parse file arguments */
610        int n = args.length - count;
611        if (n > 0) {
612            int version = BASE_VERSION;
613            int k = 0;
614            String[] nameBuf = new String[n];
615            pathsMap.put(version, new HashSet<>());
616            try {
617                for (int i = count; i < args.length; i++) {
618                    if (args[i].equals("-C")) {
619                        if (dflag) {
620                            // "--describe-module/-d" does not require file argument(s),
621                            // but does accept --release
622                            usageError(getMsg("error.bad.dflag"));
623                            return false;
624                        }
625                        /* change the directory */
626                        String dir = args[++i];
627                        dir = (dir.endsWith(File.separator) ?
628                               dir : (dir + File.separator));
629                        dir = dir.replace(File.separatorChar, '/');
630                        while (dir.indexOf("//") > -1) {
631                            dir = dir.replace("//", "/");
632                        }
633                        pathsMap.get(version).add(dir.replace(File.separatorChar, '/'));
634                        nameBuf[k++] = dir + args[++i];
635                    } else if (args[i].startsWith("--release")) {
636                        int v = BASE_VERSION;
637                        try {
638                            v = Integer.valueOf(args[++i]);
639                        } catch (NumberFormatException x) {
640                            error(formatMsg("error.release.value.notnumber", args[i]));
641                            // this will fall into the next error, thus returning false
642                        }
643                        if (v < 9) {
644                            usageError(formatMsg("error.release.value.toosmall", String.valueOf(v)));
645                            return false;
646                        }
647                        // associate the files, if any, with the previous version number
648                        if (k > 0) {
649                            String[] files = new String[k];
650                            System.arraycopy(nameBuf, 0, files, 0, k);
651                            filesMap.put(version, files);
652                            isMultiRelease = version > BASE_VERSION;
653                        }
654                        // reset the counters and start with the new version number
655                        k = 0;
656                        nameBuf = new String[n];
657                        version = v;
658                        releaseValue = version;
659                        pathsMap.put(version, new HashSet<>());
660                    } else {
661                        if (dflag) {
662                            // "--describe-module/-d" does not require file argument(s),
663                            // but does accept --release
664                            usageError(getMsg("error.bad.dflag"));
665                            return false;
666                        }
667                        nameBuf[k++] = args[i];
668                    }
669                }
670            } catch (ArrayIndexOutOfBoundsException e) {
671                usageError(getMsg("error.bad.file.arg"));
672                return false;
673            }
674            // associate remaining files, if any, with a version
675            if (k > 0) {
676                String[] files = new String[k];
677                System.arraycopy(nameBuf, 0, files, 0, k);
678                filesMap.put(version, files);
679                isMultiRelease = version > BASE_VERSION;
680            }
681        } else if (cflag && (mname == null)) {
682            usageError(getMsg("error.bad.cflag"));
683            return false;
684        } else if (uflag) {
685            if ((mname != null) || (ename != null)) {
686                /* just want to update the manifest */
687                return true;
688            } else {
689                usageError(getMsg("error.bad.uflag"));
690                return false;
691            }
692        }
693        return true;
694    }
695
696    /*
697     * Add the package of the given resource name if it's a .class
698     * or a resource in a named package.
699     */
700    void addPackageIfNamed(Set<String> packages, String name) {
701        if (name.startsWith(VERSIONS_DIR)) {
702            // trim the version dir prefix
703            int i0 = VERSIONS_DIR_LENGTH;
704            int i = name.indexOf('/', i0);
705            if (i <= 0) {
706                warn(formatMsg("warn.release.unexpected.versioned.entry", name));
707                return;
708            }
709            while (i0 < i) {
710                char c = name.charAt(i0);
711                if (c < '0' || c > '9') {
712                    warn(formatMsg("warn.release.unexpected.versioned.entry", name));
713                    return;
714                }
715                i0++;
716            }
717            name = name.substring(i + 1, name.length());
718        }
719        String pn = toPackageName(name);
720        // add if this is a class or resource in a package
721        if (Checks.isPackageName(pn)) {
722            packages.add(pn);
723        }
724    }
725
726    private String toEntryName(String name, Set<String> cpaths, boolean isDir) {
727        name = name.replace(File.separatorChar, '/');
728        if (isDir) {
729            name = name.endsWith("/") ? name : name + "/";
730        }
731        String matchPath = "";
732        for (String path : cpaths) {
733            if (name.startsWith(path) && path.length() > matchPath.length()) {
734                matchPath = path;
735            }
736        }
737        name = safeName(name.substring(matchPath.length()));
738        // the old implementaton doesn't remove
739        // "./" if it was led by "/" (?)
740        if (name.startsWith("./")) {
741            name = name.substring(2);
742        }
743        return name;
744    }
745
746    private static String toVersionedName(String name, int version) {
747        return version > BASE_VERSION
748                ? VERSIONS_DIR + version + "/" + name : name;
749    }
750
751    private static String toPackageName(String path) {
752        int index = path.lastIndexOf('/');
753        if (index != -1) {
754            return path.substring(0, index).replace('/', '.');
755        } else {
756            return "";
757        }
758    }
759
760    private void expand() throws IOException {
761        for (int version : filesMap.keySet()) {
762            String[] files = filesMap.get(version);
763            expand(null, files, pathsMap.get(version), version);
764        }
765    }
766
767    /**
768     * Expands list of files to process into full list of all files that
769     * can be found by recursively descending directories.
770     *
771     * @param dir    parent directory
772     * @param files  list of files to expand
773     * @param cpaths set of directories specified by -C option for the files
774     * @throws IOException if an I/O error occurs
775     */
776    private void expand(File dir, String[] files, Set<String> cpaths, int version)
777        throws IOException
778    {
779        if (files == null)
780            return;
781
782        for (int i = 0; i < files.length; i++) {
783            File f;
784            if (dir == null)
785                f = new File(files[i]);
786            else
787                f = new File(dir, files[i]);
788
789            boolean isDir = f.isDirectory();
790            String name = toEntryName(f.getPath(), cpaths, isDir);
791
792            if (version != BASE_VERSION) {
793                if (name.startsWith(VERSIONS_DIR)) {
794                    // the entry starts with VERSIONS_DIR and version != BASE_VERSION,
795                    // which means the "[dirs|files]" in --release v [dirs|files]
796                    // includes VERSIONS_DIR-ed entries --> warning and skip (?)
797                    error(formatMsg2("error.release.unexpected.versioned.entry",
798                                     name, String.valueOf(version)));
799                    ok = false;
800                    return;
801                }
802                name = toVersionedName(name, version);
803            }
804
805            if (f.isFile()) {
806                Entry e = new Entry(f, name, false);
807                if (isModuleInfoEntry(name)) {
808                    moduleInfos.putIfAbsent(name, Files.readAllBytes(f.toPath()));
809                    if (uflag)
810                        entryMap.put(name, e);
811                } else if (entries.add(e)) {
812                    if (uflag)
813                        entryMap.put(name, e);
814                }
815            } else if (isDir) {
816                Entry e = new Entry(f, name, true);
817                if (entries.add(e)) {
818                    // utilize entryMap for the duplicate dir check even in
819                    // case of cflag == true.
820                    // dir name confilict/duplicate could happen with -C option.
821                    // just remove the last "e" from the "entries" (zos will fail
822                    // with "duplicated" entries), but continue expanding the
823                    // sub tree
824                    if (entryMap.containsKey(name)) {
825                        entries.remove(e);
826                    } else {
827                        entryMap.put(name, e);
828                    }
829                    expand(f, f.list(), cpaths, version);
830                }
831            } else {
832                error(formatMsg("error.nosuch.fileordir", String.valueOf(f)));
833                ok = false;
834            }
835        }
836    }
837
838    /**
839     * Creates a new JAR file.
840     */
841    void create(OutputStream out, Manifest manifest) throws IOException
842    {
843        try (ZipOutputStream zos = new JarOutputStream(out)) {
844            if (flag0) {
845                zos.setMethod(ZipOutputStream.STORED);
846            }
847            // TODO: check module-info attributes against manifest ??
848            if (manifest != null) {
849                if (vflag) {
850                    output(getMsg("out.added.manifest"));
851                }
852                ZipEntry e = new ZipEntry(MANIFEST_DIR);
853                e.setTime(System.currentTimeMillis());
854                e.setSize(0);
855                e.setCrc(0);
856                zos.putNextEntry(e);
857                e = new ZipEntry(MANIFEST_NAME);
858                e.setTime(System.currentTimeMillis());
859                if (flag0) {
860                    crc32Manifest(e, manifest);
861                }
862                zos.putNextEntry(e);
863                manifest.write(zos);
864                zos.closeEntry();
865            }
866            updateModuleInfo(moduleInfos, zos);
867            for (Entry entry : entries) {
868                addFile(zos, entry);
869            }
870        }
871    }
872
873    private char toUpperCaseASCII(char c) {
874        return (c < 'a' || c > 'z') ? c : (char) (c + 'A' - 'a');
875    }
876
877    /**
878     * Compares two strings for equality, ignoring case.  The second
879     * argument must contain only upper-case ASCII characters.
880     * We don't want case comparison to be locale-dependent (else we
881     * have the notorious "turkish i bug").
882     */
883    private boolean equalsIgnoreCase(String s, String upper) {
884        assert upper.toUpperCase(java.util.Locale.ENGLISH).equals(upper);
885        int len;
886        if ((len = s.length()) != upper.length())
887            return false;
888        for (int i = 0; i < len; i++) {
889            char c1 = s.charAt(i);
890            char c2 = upper.charAt(i);
891            if (c1 != c2 && toUpperCaseASCII(c1) != c2)
892                return false;
893        }
894        return true;
895    }
896
897    /**
898     * Updates an existing jar file.
899     */
900    boolean update(InputStream in, OutputStream out,
901                   InputStream newManifest,
902                   Map<String,byte[]> moduleInfos,
903                   JarIndex jarIndex) throws IOException
904    {
905        ZipInputStream zis = new ZipInputStream(in);
906        ZipOutputStream zos = new JarOutputStream(out);
907        ZipEntry e = null;
908        boolean foundManifest = false;
909        boolean updateOk = true;
910
911        // All actual entries added/updated/existing, in the jar file (excl manifest
912        // and module-info.class ).
913        Set<String> jentries = new HashSet<>();
914
915        if (jarIndex != null) {
916            addIndex(jarIndex, zos);
917        }
918
919        // put the old entries first, replace if necessary
920        while ((e = zis.getNextEntry()) != null) {
921            String name = e.getName();
922
923            boolean isManifestEntry = equalsIgnoreCase(name, MANIFEST_NAME);
924            boolean isModuleInfoEntry = isModuleInfoEntry(name);
925
926            if ((jarIndex != null && equalsIgnoreCase(name, INDEX_NAME))
927                || (Mflag && isManifestEntry)) {
928                continue;
929            } else if (isManifestEntry && ((newManifest != null) ||
930                        (ename != null) || isMultiRelease)) {
931                foundManifest = true;
932                if (newManifest != null) {
933                    // Don't read from the newManifest InputStream, as we
934                    // might need it below, and we can't re-read the same data
935                    // twice.
936                    FileInputStream fis = new FileInputStream(mname);
937                    boolean ambiguous = isAmbiguousMainClass(new Manifest(fis));
938                    fis.close();
939                    if (ambiguous) {
940                        return false;
941                    }
942                }
943                // Update the manifest.
944                Manifest old = new Manifest(zis);
945                if (newManifest != null) {
946                    old.read(newManifest);
947                }
948                if (!updateManifest(old, zos)) {
949                    return false;
950                }
951            } else if (moduleInfos != null && isModuleInfoEntry) {
952                moduleInfos.putIfAbsent(name, zis.readAllBytes());
953            } else {
954                boolean isDir = e.isDirectory();
955                if (!entryMap.containsKey(name)) { // copy the old stuff
956                    // do our own compression
957                    ZipEntry e2 = new ZipEntry(name);
958                    e2.setMethod(e.getMethod());
959                    e2.setTime(e.getTime());
960                    e2.setComment(e.getComment());
961                    e2.setExtra(e.getExtra());
962                    if (e.getMethod() == ZipEntry.STORED) {
963                        e2.setSize(e.getSize());
964                        e2.setCrc(e.getCrc());
965                    }
966                    zos.putNextEntry(e2);
967                    copy(zis, zos);
968                } else { // replace with the new files
969                    Entry ent = entryMap.get(name);
970                    addFile(zos, ent);
971                    entryMap.remove(name);
972                    entries.remove(ent);
973                    isDir = ent.isDir;
974                }
975                if (!isDir) {
976                    jentries.add(name);
977                }
978            }
979        }
980
981        // add the remaining new files
982        for (Entry entry : entries) {
983            addFile(zos, entry);
984            if (!entry.isDir) {
985                jentries.add(entry.name);
986            }
987        }
988        if (!foundManifest) {
989            if (newManifest != null) {
990                Manifest m = new Manifest(newManifest);
991                updateOk = !isAmbiguousMainClass(m);
992                if (updateOk) {
993                    if (!updateManifest(m, zos)) {
994                        updateOk = false;
995                    }
996                }
997            } else if (ename != null) {
998                if (!updateManifest(new Manifest(), zos)) {
999                    updateOk = false;
1000                }
1001            }
1002        }
1003        if (updateOk) {
1004            if (moduleInfos != null && !moduleInfos.isEmpty()) {
1005                Set<String> pkgs = new HashSet<>();
1006                jentries.forEach( je -> addPackageIfNamed(pkgs, je));
1007                addExtendedModuleAttributes(moduleInfos, pkgs);
1008                updateOk = checkModuleInfo(moduleInfos.get(MODULE_INFO), jentries);
1009                updateModuleInfo(moduleInfos, zos);
1010                // TODO: check manifest main classes, etc
1011            } else if (moduleVersion != null || modulesToHash != null) {
1012                error(getMsg("error.module.options.without.info"));
1013                updateOk = false;
1014            }
1015        }
1016        zis.close();
1017        zos.close();
1018        return updateOk;
1019    }
1020
1021    private void addIndex(JarIndex index, ZipOutputStream zos)
1022        throws IOException
1023    {
1024        ZipEntry e = new ZipEntry(INDEX_NAME);
1025        e.setTime(System.currentTimeMillis());
1026        if (flag0) {
1027            CRC32OutputStream os = new CRC32OutputStream();
1028            index.write(os);
1029            os.updateEntry(e);
1030        }
1031        zos.putNextEntry(e);
1032        index.write(zos);
1033        zos.closeEntry();
1034    }
1035
1036    private void updateModuleInfo(Map<String,byte[]> moduleInfos, ZipOutputStream zos)
1037        throws IOException
1038    {
1039        String fmt = uflag ? "out.update.module-info": "out.added.module-info";
1040        for (Map.Entry<String,byte[]> mi : moduleInfos.entrySet()) {
1041            String name = mi.getKey();
1042            byte[] bytes = mi.getValue();
1043            ZipEntry e = new ZipEntry(name);
1044            e.setTime(System.currentTimeMillis());
1045            if (flag0) {
1046                crc32ModuleInfo(e, bytes);
1047            }
1048            zos.putNextEntry(e);
1049            zos.write(bytes);
1050            zos.closeEntry();
1051            if (vflag) {
1052                output(formatMsg(fmt, name));
1053            }
1054        }
1055    }
1056
1057    private boolean updateManifest(Manifest m, ZipOutputStream zos)
1058        throws IOException
1059    {
1060        addVersion(m);
1061        addCreatedBy(m);
1062        if (ename != null) {
1063            addMainClass(m, ename);
1064        }
1065        if (isMultiRelease) {
1066            addMultiRelease(m);
1067        }
1068        ZipEntry e = new ZipEntry(MANIFEST_NAME);
1069        e.setTime(System.currentTimeMillis());
1070        if (flag0) {
1071            crc32Manifest(e, m);
1072        }
1073        zos.putNextEntry(e);
1074        m.write(zos);
1075        if (vflag) {
1076            output(getMsg("out.update.manifest"));
1077        }
1078        return true;
1079    }
1080
1081    private static final boolean isWinDriveLetter(char c) {
1082        return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z'));
1083    }
1084
1085    private String safeName(String name) {
1086        if (!pflag) {
1087            int len = name.length();
1088            int i = name.lastIndexOf("../");
1089            if (i == -1) {
1090                i = 0;
1091            } else {
1092                i += 3; // strip any dot-dot components
1093            }
1094            if (File.separatorChar == '\\') {
1095                // the spec requests no drive letter. skip if
1096                // the entry name has one.
1097                while (i < len) {
1098                    int off = i;
1099                    if (i + 1 < len &&
1100                        name.charAt(i + 1) == ':' &&
1101                        isWinDriveLetter(name.charAt(i))) {
1102                        i += 2;
1103                    }
1104                    while (i < len && name.charAt(i) == '/') {
1105                        i++;
1106                    }
1107                    if (i == off) {
1108                        break;
1109                    }
1110                }
1111            } else {
1112                while (i < len && name.charAt(i) == '/') {
1113                    i++;
1114                }
1115            }
1116            if (i != 0) {
1117                name = name.substring(i);
1118            }
1119        }
1120        return name;
1121    }
1122
1123    private void addVersion(Manifest m) {
1124        Attributes global = m.getMainAttributes();
1125        if (global.getValue(Attributes.Name.MANIFEST_VERSION) == null) {
1126            global.put(Attributes.Name.MANIFEST_VERSION, VERSION);
1127        }
1128    }
1129
1130    private void addCreatedBy(Manifest m) {
1131        Attributes global = m.getMainAttributes();
1132        if (global.getValue(new Attributes.Name("Created-By")) == null) {
1133            String javaVendor = System.getProperty("java.vendor");
1134            String jdkVersion = System.getProperty("java.version");
1135            global.put(new Attributes.Name("Created-By"), jdkVersion + " (" +
1136                        javaVendor + ")");
1137        }
1138    }
1139
1140    private void addMainClass(Manifest m, String mainApp) {
1141        Attributes global = m.getMainAttributes();
1142
1143        // overrides any existing Main-Class attribute
1144        global.put(Attributes.Name.MAIN_CLASS, mainApp);
1145    }
1146
1147    private void addMultiRelease(Manifest m) {
1148        Attributes global = m.getMainAttributes();
1149        global.put(Attributes.Name.MULTI_RELEASE, "true");
1150    }
1151
1152    private boolean isAmbiguousMainClass(Manifest m) {
1153        if (ename != null) {
1154            Attributes global = m.getMainAttributes();
1155            if ((global.get(Attributes.Name.MAIN_CLASS) != null)) {
1156                usageError(getMsg("error.bad.eflag"));
1157                return true;
1158            }
1159        }
1160        return false;
1161    }
1162
1163    /**
1164     * Adds a new file entry to the ZIP output stream.
1165     */
1166    void addFile(ZipOutputStream zos, Entry entry) throws IOException {
1167
1168        File file = entry.file;
1169        String name = entry.name;
1170        boolean isDir = entry.isDir;
1171
1172        if (name.equals("") || name.equals(".") || name.equals(zname)) {
1173            return;
1174        } else if ((name.equals(MANIFEST_DIR) || name.equals(MANIFEST_NAME))
1175                   && !Mflag) {
1176            if (vflag) {
1177                output(formatMsg("out.ignore.entry", name));
1178            }
1179            return;
1180        } else if (name.equals(MODULE_INFO)) {
1181            throw new Error("Unexpected module info: " + name);
1182        }
1183
1184        long size = isDir ? 0 : file.length();
1185
1186        if (vflag) {
1187            out.print(formatMsg("out.adding", name));
1188        }
1189        ZipEntry e = new ZipEntry(name);
1190        e.setTime(file.lastModified());
1191        if (size == 0) {
1192            e.setMethod(ZipEntry.STORED);
1193            e.setSize(0);
1194            e.setCrc(0);
1195        } else if (flag0) {
1196            crc32File(e, file);
1197        }
1198        zos.putNextEntry(e);
1199        if (!isDir) {
1200            copy(file, zos);
1201        }
1202        zos.closeEntry();
1203        /* report how much compression occurred. */
1204        if (vflag) {
1205            size = e.getSize();
1206            long csize = e.getCompressedSize();
1207            out.print(formatMsg2("out.size", String.valueOf(size),
1208                        String.valueOf(csize)));
1209            if (e.getMethod() == ZipEntry.DEFLATED) {
1210                long ratio = 0;
1211                if (size != 0) {
1212                    ratio = ((size - csize) * 100) / size;
1213                }
1214                output(formatMsg("out.deflated", String.valueOf(ratio)));
1215            } else {
1216                output(getMsg("out.stored"));
1217            }
1218        }
1219    }
1220
1221    /**
1222     * A buffer for use only by copy(InputStream, OutputStream).
1223     * Not as clean as allocating a new buffer as needed by copy,
1224     * but significantly more efficient.
1225     */
1226    private byte[] copyBuf = new byte[8192];
1227
1228    /**
1229     * Copies all bytes from the input stream to the output stream.
1230     * Does not close or flush either stream.
1231     *
1232     * @param from the input stream to read from
1233     * @param to the output stream to write to
1234     * @throws IOException if an I/O error occurs
1235     */
1236    private void copy(InputStream from, OutputStream to) throws IOException {
1237        int n;
1238        while ((n = from.read(copyBuf)) != -1)
1239            to.write(copyBuf, 0, n);
1240    }
1241
1242    /**
1243     * Copies all bytes from the input file to the output stream.
1244     * Does not close or flush the output stream.
1245     *
1246     * @param from the input file to read from
1247     * @param to the output stream to write to
1248     * @throws IOException if an I/O error occurs
1249     */
1250    private void copy(File from, OutputStream to) throws IOException {
1251        try (InputStream in = new FileInputStream(from)) {
1252            copy(in, to);
1253        }
1254    }
1255
1256    /**
1257     * Copies all bytes from the input stream to the output file.
1258     * Does not close the input stream.
1259     *
1260     * @param from the input stream to read from
1261     * @param to the output file to write to
1262     * @throws IOException if an I/O error occurs
1263     */
1264    private void copy(InputStream from, File to) throws IOException {
1265        try (OutputStream out = new FileOutputStream(to)) {
1266            copy(from, out);
1267        }
1268    }
1269
1270    /**
1271     * Computes the crc32 of a module-info.class.  This is necessary when the
1272     * ZipOutputStream is in STORED mode.
1273     */
1274    private void crc32ModuleInfo(ZipEntry e, byte[] bytes) throws IOException {
1275        CRC32OutputStream os = new CRC32OutputStream();
1276        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
1277        in.transferTo(os);
1278        os.updateEntry(e);
1279    }
1280
1281    /**
1282     * Computes the crc32 of a Manifest.  This is necessary when the
1283     * ZipOutputStream is in STORED mode.
1284     */
1285    private void crc32Manifest(ZipEntry e, Manifest m) throws IOException {
1286        CRC32OutputStream os = new CRC32OutputStream();
1287        m.write(os);
1288        os.updateEntry(e);
1289    }
1290
1291    /**
1292     * Computes the crc32 of a File.  This is necessary when the
1293     * ZipOutputStream is in STORED mode.
1294     */
1295    private void crc32File(ZipEntry e, File f) throws IOException {
1296        CRC32OutputStream os = new CRC32OutputStream();
1297        copy(f, os);
1298        if (os.n != f.length()) {
1299            throw new JarException(formatMsg(
1300                        "error.incorrect.length", f.getPath()));
1301        }
1302        os.updateEntry(e);
1303    }
1304
1305    void replaceFSC(Map<Integer, String []> filesMap) {
1306        filesMap.keySet().forEach(version -> {
1307            String[] files = filesMap.get(version);
1308            if (files != null) {
1309                for (int i = 0; i < files.length; i++) {
1310                    files[i] = files[i].replace(File.separatorChar, '/');
1311                }
1312            }
1313        });
1314    }
1315
1316    @SuppressWarnings("serial")
1317    Set<ZipEntry> newDirSet() {
1318        return new HashSet<ZipEntry>() {
1319            public boolean add(ZipEntry e) {
1320                return ((e == null || useExtractionTime) ? false : super.add(e));
1321            }};
1322    }
1323
1324    void updateLastModifiedTime(Set<ZipEntry> zes) throws IOException {
1325        for (ZipEntry ze : zes) {
1326            long lastModified = ze.getTime();
1327            if (lastModified != -1) {
1328                String name = safeName(ze.getName().replace(File.separatorChar, '/'));
1329                if (name.length() != 0) {
1330                    File f = new File(name.replace('/', File.separatorChar));
1331                    f.setLastModified(lastModified);
1332                }
1333            }
1334        }
1335    }
1336
1337    /**
1338     * Extracts specified entries from JAR file.
1339     *
1340     * @return whether entries were found and successfully extracted
1341     * (indicating this was a zip file without "leading garbage")
1342     */
1343    boolean extract(InputStream in, String files[]) throws IOException {
1344        ZipInputStream zis = new ZipInputStream(in);
1345        ZipEntry e;
1346        // Set of all directory entries specified in archive.  Disallows
1347        // null entries.  Disallows all entries if using pre-6.0 behavior.
1348        boolean entriesFound = false;
1349        Set<ZipEntry> dirs = newDirSet();
1350        while ((e = zis.getNextEntry()) != null) {
1351            entriesFound = true;
1352            if (files == null) {
1353                dirs.add(extractFile(zis, e));
1354            } else {
1355                String name = e.getName();
1356                for (String file : files) {
1357                    if (name.startsWith(file)) {
1358                        dirs.add(extractFile(zis, e));
1359                        break;
1360                    }
1361                }
1362            }
1363        }
1364
1365        // Update timestamps of directories specified in archive with their
1366        // timestamps as given in the archive.  We do this after extraction,
1367        // instead of during, because creating a file in a directory changes
1368        // that directory's timestamp.
1369        updateLastModifiedTime(dirs);
1370
1371        return entriesFound;
1372    }
1373
1374    /**
1375     * Extracts specified entries from JAR file, via ZipFile.
1376     */
1377    void extract(String fname, String files[]) throws IOException {
1378        ZipFile zf = new ZipFile(fname);
1379        Set<ZipEntry> dirs = newDirSet();
1380        Enumeration<? extends ZipEntry> zes = zf.entries();
1381        while (zes.hasMoreElements()) {
1382            ZipEntry e = zes.nextElement();
1383            if (files == null) {
1384                dirs.add(extractFile(zf.getInputStream(e), e));
1385            } else {
1386                String name = e.getName();
1387                for (String file : files) {
1388                    if (name.startsWith(file)) {
1389                        dirs.add(extractFile(zf.getInputStream(e), e));
1390                        break;
1391                    }
1392                }
1393            }
1394        }
1395        zf.close();
1396        updateLastModifiedTime(dirs);
1397    }
1398
1399    /**
1400     * Extracts next entry from JAR file, creating directories as needed.  If
1401     * the entry is for a directory which doesn't exist prior to this
1402     * invocation, returns that entry, otherwise returns null.
1403     */
1404    ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException {
1405        ZipEntry rc = null;
1406        // The spec requres all slashes MUST be forward '/', it is possible
1407        // an offending zip/jar entry may uses the backwards slash in its
1408        // name. It might cause problem on Windows platform as it skips
1409        // our "safe" check for leading slahs and dot-dot. So replace them
1410        // with '/'.
1411        String name = safeName(e.getName().replace(File.separatorChar, '/'));
1412        if (name.length() == 0) {
1413            return rc;    // leading '/' or 'dot-dot' only path
1414        }
1415        File f = new File(name.replace('/', File.separatorChar));
1416        if (e.isDirectory()) {
1417            if (f.exists()) {
1418                if (!f.isDirectory()) {
1419                    throw new IOException(formatMsg("error.create.dir",
1420                        f.getPath()));
1421                }
1422            } else {
1423                if (!f.mkdirs()) {
1424                    throw new IOException(formatMsg("error.create.dir",
1425                        f.getPath()));
1426                } else {
1427                    rc = e;
1428                }
1429            }
1430
1431            if (vflag) {
1432                output(formatMsg("out.create", name));
1433            }
1434        } else {
1435            if (f.getParent() != null) {
1436                File d = new File(f.getParent());
1437                if (!d.exists() && !d.mkdirs() || !d.isDirectory()) {
1438                    throw new IOException(formatMsg(
1439                        "error.create.dir", d.getPath()));
1440                }
1441            }
1442            try {
1443                copy(is, f);
1444            } finally {
1445                if (is instanceof ZipInputStream)
1446                    ((ZipInputStream)is).closeEntry();
1447                else
1448                    is.close();
1449            }
1450            if (vflag) {
1451                if (e.getMethod() == ZipEntry.DEFLATED) {
1452                    output(formatMsg("out.inflated", name));
1453                } else {
1454                    output(formatMsg("out.extracted", name));
1455                }
1456            }
1457        }
1458        if (!useExtractionTime) {
1459            long lastModified = e.getTime();
1460            if (lastModified != -1) {
1461                f.setLastModified(lastModified);
1462            }
1463        }
1464        return rc;
1465    }
1466
1467    /**
1468     * Lists contents of JAR file.
1469     */
1470    void list(InputStream in, String files[]) throws IOException {
1471        ZipInputStream zis = new ZipInputStream(in);
1472        ZipEntry e;
1473        while ((e = zis.getNextEntry()) != null) {
1474            /*
1475             * In the case of a compressed (deflated) entry, the entry size
1476             * is stored immediately following the entry data and cannot be
1477             * determined until the entry is fully read. Therefore, we close
1478             * the entry first before printing out its attributes.
1479             */
1480            zis.closeEntry();
1481            printEntry(e, files);
1482        }
1483    }
1484
1485    /**
1486     * Lists contents of JAR file, via ZipFile.
1487     */
1488    void list(String fname, String files[]) throws IOException {
1489        ZipFile zf = new ZipFile(fname);
1490        Enumeration<? extends ZipEntry> zes = zf.entries();
1491        while (zes.hasMoreElements()) {
1492            printEntry(zes.nextElement(), files);
1493        }
1494        zf.close();
1495    }
1496
1497    /**
1498     * Outputs the class index table to the INDEX.LIST file of the
1499     * root jar file.
1500     */
1501    void dumpIndex(String rootjar, JarIndex index) throws IOException {
1502        File jarFile = new File(rootjar);
1503        Path jarPath = jarFile.toPath();
1504        Path tmpPath = createTempFileInSameDirectoryAs(jarFile).toPath();
1505        try {
1506            if (update(Files.newInputStream(jarPath),
1507                       Files.newOutputStream(tmpPath),
1508                       null, null, index)) {
1509                try {
1510                    Files.move(tmpPath, jarPath, REPLACE_EXISTING);
1511                } catch (IOException e) {
1512                    throw new IOException(getMsg("error.write.file"), e);
1513                }
1514            }
1515        } finally {
1516            Files.deleteIfExists(tmpPath);
1517        }
1518    }
1519
1520    private HashSet<String> jarPaths = new HashSet<String>();
1521
1522    /**
1523     * Generates the transitive closure of the Class-Path attribute for
1524     * the specified jar file.
1525     */
1526    List<String> getJarPath(String jar) throws IOException {
1527        List<String> files = new ArrayList<String>();
1528        files.add(jar);
1529        jarPaths.add(jar);
1530
1531        // take out the current path
1532        String path = jar.substring(0, Math.max(0, jar.lastIndexOf('/') + 1));
1533
1534        // class path attribute will give us jar file name with
1535        // '/' as separators, so we need to change them to the
1536        // appropriate one before we open the jar file.
1537        JarFile rf = new JarFile(jar.replace('/', File.separatorChar));
1538
1539        if (rf != null) {
1540            Manifest man = rf.getManifest();
1541            if (man != null) {
1542                Attributes attr = man.getMainAttributes();
1543                if (attr != null) {
1544                    String value = attr.getValue(Attributes.Name.CLASS_PATH);
1545                    if (value != null) {
1546                        StringTokenizer st = new StringTokenizer(value);
1547                        while (st.hasMoreTokens()) {
1548                            String ajar = st.nextToken();
1549                            if (!ajar.endsWith("/")) {  // it is a jar file
1550                                ajar = path.concat(ajar);
1551                                /* check on cyclic dependency */
1552                                if (! jarPaths.contains(ajar)) {
1553                                    files.addAll(getJarPath(ajar));
1554                                }
1555                            }
1556                        }
1557                    }
1558                }
1559            }
1560        }
1561        rf.close();
1562        return files;
1563    }
1564
1565    /**
1566     * Generates class index file for the specified root jar file.
1567     */
1568    void genIndex(String rootjar, String[] files) throws IOException {
1569        List<String> jars = getJarPath(rootjar);
1570        int njars = jars.size();
1571        String[] jarfiles;
1572
1573        if (njars == 1 && files != null) {
1574            // no class-path attribute defined in rootjar, will
1575            // use command line specified list of jars
1576            for (int i = 0; i < files.length; i++) {
1577                jars.addAll(getJarPath(files[i]));
1578            }
1579            njars = jars.size();
1580        }
1581        jarfiles = jars.toArray(new String[njars]);
1582        JarIndex index = new JarIndex(jarfiles);
1583        dumpIndex(rootjar, index);
1584    }
1585
1586    /**
1587     * Prints entry information, if requested.
1588     */
1589    void printEntry(ZipEntry e, String[] files) throws IOException {
1590        if (files == null) {
1591            printEntry(e);
1592        } else {
1593            String name = e.getName();
1594            for (String file : files) {
1595                if (name.startsWith(file)) {
1596                    printEntry(e);
1597                    return;
1598                }
1599            }
1600        }
1601    }
1602
1603    /**
1604     * Prints entry information.
1605     */
1606    void printEntry(ZipEntry e) throws IOException {
1607        if (vflag) {
1608            StringBuilder sb = new StringBuilder();
1609            String s = Long.toString(e.getSize());
1610            for (int i = 6 - s.length(); i > 0; --i) {
1611                sb.append(' ');
1612            }
1613            sb.append(s).append(' ').append(new Date(e.getTime()).toString());
1614            sb.append(' ').append(e.getName());
1615            output(sb.toString());
1616        } else {
1617            output(e.getName());
1618        }
1619    }
1620
1621    /**
1622     * Prints usage message.
1623     */
1624    void usageError(String s) {
1625        err.println(s);
1626        err.println(getMsg("main.usage.summary.try"));
1627    }
1628
1629    /**
1630     * A fatal exception has been caught.  No recovery possible
1631     */
1632    void fatalError(Exception e) {
1633        e.printStackTrace();
1634    }
1635
1636    /**
1637     * A fatal condition has been detected; message is "s".
1638     * No recovery possible
1639     */
1640    void fatalError(String s) {
1641        error(program + ": " + s);
1642    }
1643
1644    /**
1645     * Print an output message; like verbose output and the like
1646     */
1647    protected void output(String s) {
1648        out.println(s);
1649    }
1650
1651    /**
1652     * Print an error message; like something is broken
1653     */
1654    void error(String s) {
1655        err.println(s);
1656    }
1657
1658    /**
1659     * Print a warning message
1660     */
1661    void warn(String s) {
1662        err.println(s);
1663    }
1664
1665    /**
1666     * Main routine to start program.
1667     */
1668    public static void main(String args[]) {
1669        Main jartool = new Main(System.out, System.err, "jar");
1670        System.exit(jartool.run(args) ? 0 : 1);
1671    }
1672
1673    /**
1674     * An OutputStream that doesn't send its output anywhere, (but could).
1675     * It's here to find the CRC32 of an input file, necessary for STORED
1676     * mode in ZIP.
1677     */
1678    private static class CRC32OutputStream extends java.io.OutputStream {
1679        final CRC32 crc = new CRC32();
1680        long n = 0;
1681
1682        CRC32OutputStream() {}
1683
1684        public void write(int r) throws IOException {
1685            crc.update(r);
1686            n++;
1687        }
1688
1689        public void write(byte[] b, int off, int len) throws IOException {
1690            crc.update(b, off, len);
1691            n += len;
1692        }
1693
1694        /**
1695         * Updates a ZipEntry which describes the data read by this
1696         * output stream, in STORED mode.
1697         */
1698        public void updateEntry(ZipEntry e) {
1699            e.setMethod(ZipEntry.STORED);
1700            e.setSize(n);
1701            e.setCrc(crc.getValue());
1702        }
1703    }
1704
1705    /**
1706     * Attempt to create temporary file in the system-provided temporary folder, if failed attempts
1707     * to create it in the same folder as the file in parameter (if any)
1708     */
1709    private File createTemporaryFile(String tmpbase, String suffix) {
1710        File tmpfile = null;
1711
1712        try {
1713            tmpfile = File.createTempFile(tmpbase, suffix);
1714        } catch (IOException | SecurityException e) {
1715            // Unable to create file due to permission violation or security exception
1716        }
1717        if (tmpfile == null) {
1718            // Were unable to create temporary file, fall back to temporary file in the same folder
1719            if (fname != null) {
1720                try {
1721                    File tmpfolder = new File(fname).getAbsoluteFile().getParentFile();
1722                    tmpfile = File.createTempFile(fname, ".tmp" + suffix, tmpfolder);
1723                } catch (IOException ioe) {
1724                    // Last option failed - fall gracefully
1725                    fatalError(ioe);
1726                }
1727            } else {
1728                // No options left - we can not compress to stdout without access to the temporary folder
1729                fatalError(new IOException(getMsg("error.create.tempfile")));
1730            }
1731        }
1732        return tmpfile;
1733    }
1734
1735    // Modular jar support
1736
1737    /**
1738     * Associates a module descriptor's zip entry name along with its
1739     * bytes and an optional URI. Used when describing modules.
1740     */
1741    interface ModuleInfoEntry {
1742       String name();
1743       Optional<String> uriString();
1744       InputStream bytes() throws IOException;
1745    }
1746
1747    static class ZipFileModuleInfoEntry implements ModuleInfoEntry {
1748        private final ZipFile zipFile;
1749        private final ZipEntry entry;
1750        ZipFileModuleInfoEntry(ZipFile zipFile, ZipEntry entry) {
1751            this.zipFile = zipFile;
1752            this.entry = entry;
1753        }
1754        @Override public String name() { return entry.getName(); }
1755        @Override public InputStream bytes() throws IOException {
1756            return zipFile.getInputStream(entry);
1757        }
1758        /** Returns an optional containing the effective URI. */
1759        @Override public Optional<String> uriString() {
1760            String uri = (Paths.get(zipFile.getName())).toUri().toString();
1761            uri = "jar:" + uri + "/!" + entry.getName();
1762            return Optional.of(uri);
1763        }
1764    }
1765
1766    static class StreamedModuleInfoEntry implements ModuleInfoEntry {
1767        private final String name;
1768        private final byte[] bytes;
1769        StreamedModuleInfoEntry(String name, byte[] bytes) {
1770            this.name = name;
1771            this.bytes = bytes;
1772        }
1773        @Override public String name() { return name; }
1774        @Override public InputStream bytes() throws IOException {
1775            return new ByteArrayInputStream(bytes);
1776        }
1777        /** Returns an empty optional. */
1778        @Override public Optional<String> uriString() {
1779            return Optional.empty();  // no URI can be derived
1780        }
1781    }
1782
1783    /** Describes a module from a given zip file. */
1784    private boolean describeModule(ZipFile zipFile) throws IOException {
1785        ZipFileModuleInfoEntry[] infos = zipFile.stream()
1786                .filter(e -> isModuleInfoEntry(e.getName()))
1787                .sorted(Validator.ENTRY_COMPARATOR)
1788                .map(e -> new ZipFileModuleInfoEntry(zipFile, e))
1789                .toArray(ZipFileModuleInfoEntry[]::new);
1790
1791        if (infos.length == 0) {
1792            // No module descriptor found, derive and describe the automatic module
1793            String fn = zipFile.getName();
1794            ModuleFinder mf = ModuleFinder.of(Paths.get(fn));
1795            try {
1796                Set<ModuleReference> mref = mf.findAll();
1797                if (mref.isEmpty()) {
1798                    output(formatMsg("error.unable.derive.automodule", fn));
1799                    return true;
1800                }
1801                ModuleDescriptor md = mref.iterator().next().descriptor();
1802                output(getMsg("out.automodule") + "\n");
1803                describeModule(md, null, null, "");
1804            } catch (FindException e) {
1805                String msg = formatMsg("error.unable.derive.automodule", fn);
1806                Throwable t = e.getCause();
1807                if (t != null)
1808                    msg = msg + "\n" + t.getMessage();
1809                output(msg);
1810            }
1811        } else {
1812            return describeModuleFromEntries(infos);
1813        }
1814        return true;
1815    }
1816
1817    private boolean describeModuleFromStream(FileInputStream fis)
1818        throws IOException
1819    {
1820        List<ModuleInfoEntry> infos = new LinkedList<>();
1821
1822        try (BufferedInputStream bis = new BufferedInputStream(fis);
1823             ZipInputStream zis = new ZipInputStream(bis)) {
1824            ZipEntry e;
1825            while ((e = zis.getNextEntry()) != null) {
1826                String ename = e.getName();
1827                if (isModuleInfoEntry(ename)) {
1828                    infos.add(new StreamedModuleInfoEntry(ename, zis.readAllBytes()));
1829                }
1830            }
1831        }
1832
1833        if (infos.size() == 0)
1834            return false;
1835
1836        ModuleInfoEntry[] sorted = infos.stream()
1837                .sorted(Comparator.comparing(ModuleInfoEntry::name, ENTRYNAME_COMPARATOR))
1838                .toArray(ModuleInfoEntry[]::new);
1839
1840        return describeModuleFromEntries(sorted);
1841    }
1842
1843    private boolean lessThanEqualReleaseValue(ModuleInfoEntry entry) {
1844        return intVersionFromEntry(entry) <= releaseValue ? true : false;
1845    }
1846
1847    private static String versionFromEntryName(String name) {
1848        String s = name.substring(VERSIONS_DIR_LENGTH);
1849        return s.substring(0, s.indexOf("/"));
1850    }
1851
1852    private static int intVersionFromEntry(ModuleInfoEntry entry) {
1853        String name = entry.name();
1854        if (!name.startsWith(VERSIONS_DIR))
1855            return BASE_VERSION;
1856
1857        String s = name.substring(VERSIONS_DIR_LENGTH);
1858        s = s.substring(0, s.indexOf('/'));
1859        return Integer.valueOf(s);
1860    }
1861
1862    /**
1863     * Describes a single module descriptor, determined by the specified
1864     * --release, if any, from the given ordered entries.
1865     * The given infos must be ordered as per ENTRY_COMPARATOR.
1866     */
1867    private boolean describeModuleFromEntries(ModuleInfoEntry[] infos)
1868        throws IOException
1869    {
1870        assert infos.length > 0;
1871
1872        // Informative: output all non-root descriptors, if any
1873        String releases = Arrays.stream(infos)
1874                .filter(e -> !e.name().equals(MODULE_INFO))
1875                .map(ModuleInfoEntry::name)
1876                .map(Main::versionFromEntryName)
1877                .collect(joining(" "));
1878        if (!releases.equals(""))
1879            output("releases: " + releases + "\n");
1880
1881        // Describe the operative descriptor for the specified --release, if any
1882        if (releaseValue != -1) {
1883            ModuleInfoEntry entry = null;
1884            int i = 0;
1885            while (i < infos.length && lessThanEqualReleaseValue(infos[i])) {
1886                entry = infos[i];
1887                i++;
1888            }
1889
1890            if (entry == null) {
1891                output(formatMsg("error.no.operative.descriptor",
1892                                 String.valueOf(releaseValue)));
1893                return false;
1894            }
1895
1896            String uriString = entry.uriString().orElse("");
1897            try (InputStream is = entry.bytes()) {
1898                describeModule(is, uriString);
1899            }
1900        } else {
1901            // no specific --release specified, output the root, if any
1902            if (infos[0].name().equals(MODULE_INFO)) {
1903                String uriString = infos[0].uriString().orElse("");
1904                try (InputStream is = infos[0].bytes()) {
1905                    describeModule(is, uriString);
1906                }
1907            } else {
1908                // no root, output message to specify --release
1909                output(getMsg("error.no.root.descriptor"));
1910            }
1911        }
1912        return true;
1913    }
1914
1915    static <T> String toString(Collection<T> set) {
1916        if (set.isEmpty()) { return ""; }
1917        return " " + set.stream().map(e -> e.toString().toLowerCase(Locale.ROOT))
1918                  .sorted().collect(joining(" "));
1919    }
1920
1921
1922    private void describeModule(InputStream entryInputStream, String uriString)
1923        throws IOException
1924    {
1925        ModuleInfo.Attributes attrs = ModuleInfo.read(entryInputStream, null);
1926        ModuleDescriptor md = attrs.descriptor();
1927        ModuleTarget target = attrs.target();
1928        ModuleHashes hashes = attrs.recordedHashes();
1929
1930        describeModule(md, target, hashes, uriString);
1931    }
1932
1933    private void describeModule(ModuleDescriptor md,
1934                                ModuleTarget target,
1935                                ModuleHashes hashes,
1936                                String uriString)
1937        throws IOException
1938    {
1939        StringBuilder sb = new StringBuilder();
1940
1941        sb.append(md.toNameAndVersion());
1942
1943        if (!uriString.equals(""))
1944            sb.append(" ").append(uriString);
1945        if (md.isOpen())
1946            sb.append(" open");
1947        if (md.isAutomatic())
1948            sb.append(" automatic");
1949        sb.append("\n");
1950
1951        // unqualified exports (sorted by package)
1952        md.exports().stream()
1953                .sorted(Comparator.comparing(Exports::source))
1954                .filter(e -> !e.isQualified())
1955                .forEach(e -> sb.append("exports ").append(e.source())
1956                                .append(toString(e.modifiers())).append("\n"));
1957
1958        // dependences
1959        md.requires().stream().sorted()
1960                .forEach(r -> sb.append("requires ").append(r.name())
1961                                .append(toString(r.modifiers())).append("\n"));
1962
1963        // service use and provides
1964        md.uses().stream().sorted()
1965                .forEach(s -> sb.append("uses ").append(s).append("\n"));
1966
1967        md.provides().stream()
1968                .sorted(Comparator.comparing(Provides::service))
1969                .forEach(p -> sb.append("provides ").append(p.service())
1970                                .append(" with")
1971                                .append(toString(p.providers()))
1972                                .append("\n"));
1973
1974        // qualified exports
1975        md.exports().stream()
1976                .sorted(Comparator.comparing(Exports::source))
1977                .filter(Exports::isQualified)
1978                .forEach(e -> sb.append("qualified exports ").append(e.source())
1979                                .append(" to").append(toString(e.targets()))
1980                                .append("\n"));
1981
1982        // open packages
1983        md.opens().stream()
1984                .sorted(Comparator.comparing(Opens::source))
1985                .filter(o -> !o.isQualified())
1986                .forEach(o -> sb.append("opens ").append(o.source())
1987                                 .append(toString(o.modifiers()))
1988                                 .append("\n"));
1989
1990        md.opens().stream()
1991                .sorted(Comparator.comparing(Opens::source))
1992                .filter(Opens::isQualified)
1993                .forEach(o -> sb.append("qualified opens ").append(o.source())
1994                                 .append(toString(o.modifiers()))
1995                                 .append(" to").append(toString(o.targets()))
1996                                 .append("\n"));
1997
1998        // non-exported/non-open packages
1999        Set<String> concealed = new TreeSet<>(md.packages());
2000        md.exports().stream().map(Exports::source).forEach(concealed::remove);
2001        md.opens().stream().map(Opens::source).forEach(concealed::remove);
2002        concealed.forEach(p -> sb.append("contains ").append(p).append("\n"));
2003
2004        md.mainClass().ifPresent(v -> sb.append("main-class ").append(v).append("\n"));
2005
2006        if (target != null) {
2007            String targetPlatform = target.targetPlatform();
2008            if (!targetPlatform.isEmpty())
2009                sb.append("platform ").append(targetPlatform).append("\n");
2010       }
2011
2012       if (hashes != null) {
2013           hashes.names().stream().sorted().forEach(
2014                   mod -> sb.append("hashes ").append(mod).append(" ")
2015                            .append(hashes.algorithm()).append(" ")
2016                            .append(toHex(hashes.hashFor(mod)))
2017                            .append("\n"));
2018        }
2019
2020        output(sb.toString());
2021    }
2022
2023    private static String toHex(byte[] ba) {
2024        StringBuilder sb = new StringBuilder(ba.length << 1);
2025        for (byte b: ba) {
2026            sb.append(String.format("%02x", b & 0xff));
2027        }
2028        return sb.toString();
2029    }
2030
2031    static String toBinaryName(String classname) {
2032        return (classname.replace('.', '/')) + ".class";
2033    }
2034
2035    private boolean checkModuleInfo(byte[] moduleInfoBytes, Set<String> entries)
2036        throws IOException
2037    {
2038        boolean ok = true;
2039        if (moduleInfoBytes != null) {  // no root module-info.class if null
2040            try {
2041                // ModuleDescriptor.read() checks open/exported pkgs vs packages
2042                ModuleDescriptor md = ModuleDescriptor.read(ByteBuffer.wrap(moduleInfoBytes));
2043                // A module must have the implementation class of the services it 'provides'.
2044                if (md.provides().stream().map(Provides::providers).flatMap(List::stream)
2045                      .filter(p -> !entries.contains(toBinaryName(p)))
2046                      .peek(p -> fatalError(formatMsg("error.missing.provider", p)))
2047                      .count() != 0) {
2048                    ok = false;
2049                }
2050            } catch (InvalidModuleDescriptorException x) {
2051                fatalError(x.getMessage());
2052                ok = false;
2053            }
2054        }
2055        return ok;
2056    }
2057
2058    /**
2059     * Adds extended modules attributes to the given module-info's.  The given
2060     * Map values are updated in-place. Returns false if an error occurs.
2061     */
2062    private void addExtendedModuleAttributes(Map<String,byte[]> moduleInfos,
2063                                                Set<String> packages)
2064        throws IOException
2065    {
2066        for (Map.Entry<String,byte[]> e: moduleInfos.entrySet()) {
2067            ModuleDescriptor md = ModuleDescriptor.read(ByteBuffer.wrap(e.getValue()));
2068            e.setValue(extendedInfoBytes(md, e.getValue(), packages));
2069        }
2070    }
2071
2072    static boolean isModuleInfoEntry(String name) {
2073        // root or versioned module-info.class
2074        if (name.endsWith(MODULE_INFO)) {
2075            int end = name.length() - MODULE_INFO.length();
2076            if (end == 0)
2077                return true;
2078            if (name.startsWith(VERSIONS_DIR)) {
2079                int off = VERSIONS_DIR_LENGTH;
2080                if (off == end)      // meta-inf/versions/module-info.class
2081                    return false;
2082                while (off < end - 1) {
2083                    char c = name.charAt(off++);
2084                    if (c < '0' || c > '9')
2085                        return false;
2086                }
2087                return name.charAt(off) == '/';
2088            }
2089        }
2090        return false;
2091    }
2092
2093    /**
2094     * Returns a byte array containing the given module-info.class plus any
2095     * extended attributes.
2096     *
2097     * If --module-version, --main-class, or other options were provided
2098     * then the corresponding class file attributes are added to the
2099     * module-info here.
2100     */
2101    private byte[] extendedInfoBytes(ModuleDescriptor md,
2102                                     byte[] miBytes,
2103                                     Set<String> packages)
2104        throws IOException
2105    {
2106        ByteArrayOutputStream baos = new ByteArrayOutputStream();
2107        InputStream is = new ByteArrayInputStream(miBytes);
2108        ModuleInfoExtender extender = ModuleInfoExtender.newExtender(is);
2109
2110        // Add (or replace) the Packages attribute
2111        extender.packages(packages);
2112
2113        // --main-class
2114        if (ename != null)
2115            extender.mainClass(ename);
2116
2117        // --module-version
2118        if (moduleVersion != null)
2119            extender.version(moduleVersion);
2120
2121        // --hash-modules
2122        if (modulesToHash != null) {
2123            String mn = md.name();
2124            Hasher hasher = new Hasher(md, fname);
2125            ModuleHashes moduleHashes = hasher.computeHashes(mn);
2126            if (moduleHashes != null) {
2127                extender.hashes(moduleHashes);
2128            } else {
2129                warn("warning: no module is recorded in hash in " + mn);
2130            }
2131        }
2132
2133        if (moduleResolution.value() != 0) {
2134            extender.moduleResolution(moduleResolution);
2135        }
2136
2137        extender.write(baos);
2138        return baos.toByteArray();
2139    }
2140
2141    /**
2142     * Compute and record hashes
2143     */
2144    private class Hasher {
2145        final ModuleHashesBuilder hashesBuilder;
2146        final ModuleFinder finder;
2147        final Set<String> modules;
2148        Hasher(ModuleDescriptor descriptor, String fname) throws IOException {
2149            // Create a module finder that finds the modular JAR
2150            // being created/updated
2151            URI uri = Paths.get(fname).toUri();
2152            ModuleReference mref = new ModuleReference(descriptor, uri) {
2153                @Override
2154                public ModuleReader open() {
2155                    throw new UnsupportedOperationException("should not reach here");
2156                }
2157            };
2158
2159            // Compose a module finder with the module path and
2160            // the modular JAR being created or updated
2161            this.finder = ModuleFinder.compose(moduleFinder,
2162                new ModuleFinder() {
2163                    @Override
2164                    public Optional<ModuleReference> find(String name) {
2165                        if (descriptor.name().equals(name))
2166                            return Optional.of(mref);
2167                        else
2168                            return Optional.empty();
2169                    }
2170
2171                    @Override
2172                    public Set<ModuleReference> findAll() {
2173                        return Collections.singleton(mref);
2174                    }
2175                });
2176
2177            // Determine the modules that matches the pattern {@code modulesToHash}
2178            Set<String> roots = finder.findAll().stream()
2179                .map(ref -> ref.descriptor().name())
2180                .filter(mn -> modulesToHash.matcher(mn).find())
2181                .collect(Collectors.toSet());
2182
2183            // use system module path unless it creates a modular JAR for
2184            // a module that is present in the system image e.g. upgradeable
2185            // module
2186            ModuleFinder system;
2187            String name = descriptor.name();
2188            if (name != null && ModuleFinder.ofSystem().find(name).isPresent()) {
2189                system = ModuleFinder.of();
2190            } else {
2191                system = ModuleFinder.ofSystem();
2192            }
2193            // get a resolved module graph
2194            Configuration config =
2195                Configuration.empty().resolve(system, finder, roots);
2196
2197            // filter modules resolved from the system module finder
2198            this.modules = config.modules().stream()
2199                .map(ResolvedModule::name)
2200                .filter(mn -> roots.contains(mn) && !system.find(mn).isPresent())
2201                .collect(Collectors.toSet());
2202
2203            this.hashesBuilder = new ModuleHashesBuilder(config, modules);
2204        }
2205
2206        /**
2207         * Compute hashes of the specified module.
2208         *
2209         * It records the hashing modules that depend upon the specified
2210         * module directly or indirectly.
2211         */
2212        ModuleHashes computeHashes(String name) {
2213            if (hashesBuilder == null)
2214                return null;
2215
2216            return hashesBuilder.computeHashes(Set.of(name)).get(name);
2217        }
2218    }
2219}
2220