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