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