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