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