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