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