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