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