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