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