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