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