1 /* 2 * Copyright (c) 1996, 2015, 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.nio.file.Path; 30 import java.nio.file.Files; 31 import java.util.*; 32 import java.util.zip.*; 33 import java.util.jar.*; 34 import java.util.jar.Pack200.*; 35 import java.util.jar.Manifest; 36 import java.text.MessageFormat; 37 import sun.misc.JarIndex; 38 import static sun.misc.JarIndex.INDEX_NAME; 39 import static java.util.jar.JarFile.MANIFEST_NAME; 40 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 41 42 /** 43 * This class implements a simple utility for creating files in the JAR 44 * (Java Archive) file format. The JAR format is based on the ZIP file 45 * format, with optional meta-information stored in a MANIFEST entry. 46 */ 47 public 48 class Main { 49 String program; 50 PrintStream out, err; 51 String fname, mname, ename; 52 String zname = ""; 53 String[] files; 54 String rootjar = null; 55 56 // An entryName(path)->File map generated during "expand", it helps to 57 // decide whether or not an existing entry in a jar file needs to be 58 // replaced, during the "update" operation. 59 Map<String, File> entryMap = new HashMap<String, File>(); 60 61 // All files need to be added/updated. 62 Set<File> entries = new LinkedHashSet<File>(); 63 64 // Directories specified by "-C" operation. 65 Set<String> paths = new HashSet<String>(); 66 67 /* 68 * cflag: create 69 * uflag: update 70 * xflag: xtract 71 * tflag: table 72 * vflag: verbose 73 * flag0: no zip compression (store only) 74 * Mflag: DO NOT generate a manifest file (just ZIP) 75 * iflag: generate jar index 76 * nflag: Perform jar normalization at the end 77 * pflag: preserve/don't strip leading slash and .. component from file name 78 */ 79 boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, nflag, pflag; 80 81 static final String MANIFEST_DIR = "META-INF/"; 82 static final String VERSION = "1.0"; 83 84 private static ResourceBundle rsrc; 85 86 /** 87 * If true, maintain compatibility with JDK releases prior to 6.0 by 88 * timestamping extracted files with the time at which they are extracted. 89 * Default is to use the time given in the archive. 90 */ 91 private static final boolean useExtractionTime = 92 Boolean.getBoolean("sun.tools.jar.useExtractionTime"); 93 94 /** 95 * Initialize ResourceBundle 96 */ 97 static { 98 try { 99 rsrc = ResourceBundle.getBundle("sun.tools.jar.resources.jar"); 100 } catch (MissingResourceException e) { 101 throw new Error("Fatal: Resource for jar is missing"); 102 } 103 } 104 105 private String getMsg(String key) { 106 try { 107 return (rsrc.getString(key)); 108 } catch (MissingResourceException e) { 109 throw new Error("Error in message file"); 110 } 111 } 112 113 private String formatMsg(String key, String arg) { 114 String msg = getMsg(key); 115 String[] args = new String[1]; 116 args[0] = arg; 117 return MessageFormat.format(msg, (Object[]) args); 118 } 119 120 private String formatMsg2(String key, String arg, String arg1) { 121 String msg = getMsg(key); 122 String[] args = new String[2]; 123 args[0] = arg; 124 args[1] = arg1; 125 return MessageFormat.format(msg, (Object[]) args); 126 } 127 128 public Main(PrintStream out, PrintStream err, String program) { 129 this.out = out; 130 this.err = err; 131 this.program = program; 132 } 133 134 /** 135 * Creates a new empty temporary file in the same directory as the 136 * specified file. A variant of File.createTempFile. 137 */ 138 private static File createTempFileInSameDirectoryAs(File file) 139 throws IOException { 140 File dir = file.getParentFile(); 141 if (dir == null) 142 dir = new File("."); 143 return File.createTempFile("jartmp", null, dir); 144 } 145 146 private boolean ok; 147 148 /** 149 * Starts main program with the specified arguments. 150 */ 151 public synchronized boolean run(String args[]) { 152 ok = true; 153 if (!parseArgs(args)) { 154 return false; 155 } 156 try { 157 if (cflag || uflag) { 158 if (fname != null) { 159 // The name of the zip file as it would appear as its own 160 // zip file entry. We use this to make sure that we don't 161 // add the zip file to itself. 162 zname = fname.replace(File.separatorChar, '/'); 163 if (zname.startsWith("./")) { 164 zname = zname.substring(2); 165 } 166 } 167 } 168 if (cflag) { 169 Manifest manifest = null; 170 InputStream in = null; 171 172 if (!Mflag) { 173 if (mname != null) { 174 in = new FileInputStream(mname); 175 manifest = new Manifest(new BufferedInputStream(in)); 176 } else { 177 manifest = new Manifest(); 178 } 179 addVersion(manifest); 180 addCreatedBy(manifest); 181 if (isAmbiguousMainClass(manifest)) { 182 if (in != null) { 183 in.close(); 184 } 185 return false; 186 } 187 if (ename != null) { 188 addMainClass(manifest, ename); 189 } 190 } 191 expand(null, files, false); 192 OutputStream out; 193 if (fname != null) { 194 out = new FileOutputStream(fname); 195 } else { 196 out = new FileOutputStream(FileDescriptor.out); 197 if (vflag) { 198 // Disable verbose output so that it does not appear 199 // on stdout along with file data 200 // error("Warning: -v option ignored"); 201 vflag = false; 202 } 203 } 204 File tmpfile = null; 205 final OutputStream finalout = out; 206 final String tmpbase = (fname == null) 207 ? "tmpjar" 208 : fname.substring(fname.indexOf(File.separatorChar) + 1); 209 if (nflag) { 210 tmpfile = createTemporaryFile(tmpbase, ".jar"); 211 out = new FileOutputStream(tmpfile); 212 } 213 create(new BufferedOutputStream(out, 4096), manifest); 214 if (in != null) { 215 in.close(); 216 } 217 out.close(); 218 if (nflag) { 219 JarFile jarFile = null; 220 File packFile = null; 221 JarOutputStream jos = null; 222 try { 223 Packer packer = Pack200.newPacker(); 224 Map<String, String> p = packer.properties(); 225 p.put(Packer.EFFORT, "1"); // Minimal effort to conserve CPU 226 jarFile = new JarFile(tmpfile.getCanonicalPath()); 227 packFile = createTemporaryFile(tmpbase, ".pack"); 228 out = new FileOutputStream(packFile); 229 packer.pack(jarFile, out); 230 jos = new JarOutputStream(finalout); 231 Unpacker unpacker = Pack200.newUnpacker(); 232 unpacker.unpack(packFile, jos); 233 } catch (IOException ioe) { 234 fatalError(ioe); 235 } finally { 236 if (jarFile != null) { 237 jarFile.close(); 238 } 239 if (out != null) { 240 out.close(); 241 } 242 if (jos != null) { 243 jos.close(); 244 } 245 if (tmpfile != null && tmpfile.exists()) { 246 tmpfile.delete(); 247 } 248 if (packFile != null && packFile.exists()) { 249 packFile.delete(); 250 } 251 } 252 } 253 } else if (uflag) { 254 File inputFile = null, tmpFile = null; 255 FileInputStream in; 256 FileOutputStream out; 257 if (fname != null) { 258 inputFile = new File(fname); 259 tmpFile = createTempFileInSameDirectoryAs(inputFile); 260 in = new FileInputStream(inputFile); 261 out = new FileOutputStream(tmpFile); 262 } else { 263 in = new FileInputStream(FileDescriptor.in); 264 out = new FileOutputStream(FileDescriptor.out); 265 vflag = false; 266 } 267 InputStream manifest = (!Mflag && (mname != null)) ? 268 (new FileInputStream(mname)) : null; 269 expand(null, files, true); 270 boolean updateOk = update(in, new BufferedOutputStream(out), 271 manifest, null); 272 if (ok) { 273 ok = updateOk; 274 } 275 in.close(); 276 out.close(); 277 if (manifest != null) { 278 manifest.close(); 279 } 280 if (ok && fname != null) { 281 // on Win32, we need this delete 282 inputFile.delete(); 283 if (!tmpFile.renameTo(inputFile)) { 284 tmpFile.delete(); 285 throw new IOException(getMsg("error.write.file")); 286 } 287 tmpFile.delete(); 288 } 289 } else if (tflag) { 290 replaceFSC(files); 291 // For the "list table contents" action, access using the 292 // ZipFile class is always most efficient since only a 293 // "one-finger" scan through the central directory is required. 294 if (fname != null) { 295 list(fname, files); 296 } else { 297 InputStream in = new FileInputStream(FileDescriptor.in); 298 try { 299 list(new BufferedInputStream(in), files); 300 } finally { 301 in.close(); 302 } 303 } 304 } else if (xflag) { 305 replaceFSC(files); 306 // For the extract action, when extracting all the entries, 307 // access using the ZipInputStream class is most efficient, 308 // since only a single sequential scan through the zip file is 309 // required. When using the ZipFile class, a "two-finger" scan 310 // is required, but this is likely to be more efficient when a 311 // partial extract is requested. In case the zip file has 312 // "leading garbage", we fall back from the ZipInputStream 313 // implementation to the ZipFile implementation, since only the 314 // latter can handle it. 315 if (fname != null && files != null) { 316 extract(fname, files); 317 } else { 318 InputStream in = (fname == null) 319 ? new FileInputStream(FileDescriptor.in) 320 : new FileInputStream(fname); 321 try { 322 if (!extract(new BufferedInputStream(in), files) && fname != null) { 323 extract(fname, files); 324 } 325 } finally { 326 in.close(); 327 } 328 } 329 } else if (iflag) { 330 genIndex(rootjar, files); 331 } 332 } catch (IOException e) { 333 fatalError(e); 334 ok = false; 335 } catch (Error ee) { 336 ee.printStackTrace(); 337 ok = false; 338 } catch (Throwable t) { 339 t.printStackTrace(); 340 ok = false; 341 } 342 out.flush(); 343 err.flush(); 344 return ok; 345 } 346 347 /** 348 * Parses command line arguments. 349 */ 350 boolean parseArgs(String args[]) { 351 /* Preprocess and expand @file arguments */ 352 try { 353 args = CommandLine.parse(args); 354 } catch (FileNotFoundException e) { 355 fatalError(formatMsg("error.cant.open", e.getMessage())); 356 return false; 357 } catch (IOException e) { 358 fatalError(e); 359 return false; 360 } 361 /* parse flags */ 362 int count = 1; 363 try { 364 String flags = args[0]; 365 if (flags.startsWith("-")) { 366 flags = flags.substring(1); 367 } 368 for (int i = 0; i < flags.length(); i++) { 369 switch (flags.charAt(i)) { 370 case 'c': 371 if (xflag || tflag || uflag || iflag) { 372 usageError(); 373 return false; 374 } 375 cflag = true; 376 break; 377 case 'u': 378 if (cflag || xflag || tflag || iflag) { 379 usageError(); 380 return false; 381 } 382 uflag = true; 383 break; 384 case 'x': 385 if (cflag || uflag || tflag || iflag) { 386 usageError(); 387 return false; 388 } 389 xflag = true; 390 break; 391 case 't': 392 if (cflag || uflag || xflag || iflag) { 393 usageError(); 394 return false; 395 } 396 tflag = true; 397 break; 398 case 'M': 399 Mflag = true; 400 break; 401 case 'v': 402 vflag = true; 403 break; 404 case 'f': 405 fname = args[count++]; 406 break; 407 case 'm': 408 mname = args[count++]; 409 break; 410 case '0': 411 flag0 = true; 412 break; 413 case 'i': 414 if (cflag || uflag || xflag || tflag) { 415 usageError(); 416 return false; 417 } 418 // do not increase the counter, files will contain rootjar 419 rootjar = args[count++]; 420 iflag = true; 421 break; 422 case 'n': 423 nflag = true; 424 break; 425 case 'e': 426 ename = args[count++]; 427 break; 428 case 'P': 429 pflag = true; 430 break; 431 default: 432 error(formatMsg("error.illegal.option", 433 String.valueOf(flags.charAt(i)))); 434 usageError(); 435 return false; 436 } 437 } 438 } catch (ArrayIndexOutOfBoundsException e) { 439 usageError(); 440 return false; 441 } 442 if (!cflag && !tflag && !xflag && !uflag && !iflag) { 443 error(getMsg("error.bad.option")); 444 usageError(); 445 return false; 446 } 447 /* parse file arguments */ 448 int n = args.length - count; 449 if (n > 0) { 450 int k = 0; 451 String[] nameBuf = new String[n]; 452 try { 453 for (int i = count; i < args.length; i++) { 454 if (args[i].equals("-C")) { 455 /* change the directory */ 456 String dir = args[++i]; 457 dir = (dir.endsWith(File.separator) ? 458 dir : (dir + File.separator)); 459 dir = dir.replace(File.separatorChar, '/'); 460 while (dir.indexOf("//") > -1) { 461 dir = dir.replace("//", "/"); 462 } 463 paths.add(dir.replace(File.separatorChar, '/')); 464 nameBuf[k++] = dir + args[++i]; 465 } else { 466 nameBuf[k++] = args[i]; 467 } 468 } 469 } catch (ArrayIndexOutOfBoundsException e) { 470 usageError(); 471 return false; 472 } 473 files = new String[k]; 474 System.arraycopy(nameBuf, 0, files, 0, k); 475 } else if (cflag && (mname == null)) { 476 error(getMsg("error.bad.cflag")); 477 usageError(); 478 return false; 479 } else if (uflag) { 480 if ((mname != null) || (ename != null)) { 481 /* just want to update the manifest */ 482 return true; 483 } else { 484 error(getMsg("error.bad.uflag")); 485 usageError(); 486 return false; 487 } 488 } 489 return true; 490 } 491 492 /** 493 * Expands list of files to process into full list of all files that 494 * can be found by recursively descending directories. 495 */ 496 void expand(File dir, String[] files, boolean isUpdate) { 497 if (files == null) { 498 return; 499 } 500 for (int i = 0; i < files.length; i++) { 501 File f; 502 if (dir == null) { 503 f = new File(files[i]); 504 } else { 505 f = new File(dir, files[i]); 506 } 507 if (f.isFile()) { 508 if (entries.add(f)) { 509 if (isUpdate) 510 entryMap.put(entryName(f.getPath()), f); 511 } 512 } else if (f.isDirectory()) { 513 if (entries.add(f)) { 514 if (isUpdate) { 515 String dirPath = f.getPath(); 516 dirPath = (dirPath.endsWith(File.separator)) ? dirPath : 517 (dirPath + File.separator); 518 entryMap.put(entryName(dirPath), f); 519 } 520 expand(f, f.list(), isUpdate); 521 } 522 } else { 523 error(formatMsg("error.nosuch.fileordir", String.valueOf(f))); 524 ok = false; 525 } 526 } 527 } 528 529 /** 530 * Creates a new JAR file. 531 */ 532 void create(OutputStream out, Manifest manifest) 533 throws IOException 534 { 535 ZipOutputStream zos = new JarOutputStream(out); 536 if (flag0) { 537 zos.setMethod(ZipOutputStream.STORED); 538 } 539 if (manifest != null) { 540 if (vflag) { 541 output(getMsg("out.added.manifest")); 542 } 543 ZipEntry e = new ZipEntry(MANIFEST_DIR); 544 e.setTime(System.currentTimeMillis()); 545 e.setSize(0); 546 e.setCrc(0); 547 zos.putNextEntry(e); 548 e = new ZipEntry(MANIFEST_NAME); 549 e.setTime(System.currentTimeMillis()); 550 if (flag0) { 551 crc32Manifest(e, manifest); 552 } 553 zos.putNextEntry(e); 554 manifest.write(zos); 555 zos.closeEntry(); 556 } 557 for (File file: entries) { 558 addFile(zos, file); 559 } 560 zos.close(); 561 } 562 563 private char toUpperCaseASCII(char c) { 564 return (c < 'a' || c > 'z') ? c : (char) (c + 'A' - 'a'); 565 } 566 567 /** 568 * Compares two strings for equality, ignoring case. The second 569 * argument must contain only upper-case ASCII characters. 570 * We don't want case comparison to be locale-dependent (else we 571 * have the notorious "turkish i bug"). 572 */ 573 private boolean equalsIgnoreCase(String s, String upper) { 574 assert upper.toUpperCase(java.util.Locale.ENGLISH).equals(upper); 575 int len; 576 if ((len = s.length()) != upper.length()) 577 return false; 578 for (int i = 0; i < len; i++) { 579 char c1 = s.charAt(i); 580 char c2 = upper.charAt(i); 581 if (c1 != c2 && toUpperCaseASCII(c1) != c2) 582 return false; 583 } 584 return true; 585 } 586 587 /** 588 * Updates an existing jar file. 589 */ 590 boolean update(InputStream in, OutputStream out, 591 InputStream newManifest, 592 JarIndex jarIndex) throws IOException 593 { 594 ZipInputStream zis = new ZipInputStream(in); 595 ZipOutputStream zos = new JarOutputStream(out); 596 ZipEntry e = null; 597 boolean foundManifest = false; 598 boolean updateOk = true; 599 600 if (jarIndex != null) { 601 addIndex(jarIndex, zos); 602 } 603 604 // put the old entries first, replace if necessary 605 while ((e = zis.getNextEntry()) != null) { 606 String name = e.getName(); 607 608 boolean isManifestEntry = equalsIgnoreCase(name, MANIFEST_NAME); 609 610 if ((jarIndex != null && equalsIgnoreCase(name, INDEX_NAME)) 611 || (Mflag && isManifestEntry)) { 612 continue; 613 } else if (isManifestEntry && ((newManifest != null) || 614 (ename != null))) { 615 foundManifest = true; 616 if (newManifest != null) { 617 // Don't read from the newManifest InputStream, as we 618 // might need it below, and we can't re-read the same data 619 // twice. 620 FileInputStream fis = new FileInputStream(mname); 621 boolean ambiguous = isAmbiguousMainClass(new Manifest(fis)); 622 fis.close(); 623 if (ambiguous) { 624 return false; 625 } 626 } 627 628 // Update the manifest. 629 Manifest old = new Manifest(zis); 630 if (newManifest != null) { 631 old.read(newManifest); 632 } 633 if (!updateManifest(old, zos)) { 634 return false; 635 } 636 } else { 637 if (!entryMap.containsKey(name)) { // copy the old stuff 638 // do our own compression 639 ZipEntry e2 = new ZipEntry(name); 640 e2.setMethod(e.getMethod()); 641 e2.setTime(e.getTime()); 642 e2.setComment(e.getComment()); 643 e2.setExtra(e.getExtra()); 644 if (e.getMethod() == ZipEntry.STORED) { 645 e2.setSize(e.getSize()); 646 e2.setCrc(e.getCrc()); 647 } 648 zos.putNextEntry(e2); 649 copy(zis, zos); 650 } else { // replace with the new files 651 File f = entryMap.get(name); 652 addFile(zos, f); 653 entryMap.remove(name); 654 entries.remove(f); 655 } 656 } 657 } 658 659 // add the remaining new files 660 for (File f: entries) { 661 addFile(zos, f); 662 } 663 if (!foundManifest) { 664 if (newManifest != null) { 665 Manifest m = new Manifest(newManifest); 666 updateOk = !isAmbiguousMainClass(m); 667 if (updateOk) { 668 if (!updateManifest(m, zos)) { 669 updateOk = false; 670 } 671 } 672 } else if (ename != null) { 673 if (!updateManifest(new Manifest(), zos)) { 674 updateOk = false; 675 } 676 } 677 } 678 zis.close(); 679 zos.close(); 680 return updateOk; 681 } 682 683 684 private void addIndex(JarIndex index, ZipOutputStream zos) 685 throws IOException 686 { 687 ZipEntry e = new ZipEntry(INDEX_NAME); 688 e.setTime(System.currentTimeMillis()); 689 if (flag0) { 690 CRC32OutputStream os = new CRC32OutputStream(); 691 index.write(os); 692 os.updateEntry(e); 693 } 694 zos.putNextEntry(e); 695 index.write(zos); 696 zos.closeEntry(); 697 } 698 699 private boolean updateManifest(Manifest m, ZipOutputStream zos) 700 throws IOException 701 { 702 addVersion(m); 703 addCreatedBy(m); 704 if (ename != null) { 705 addMainClass(m, ename); 706 } 707 ZipEntry e = new ZipEntry(MANIFEST_NAME); 708 e.setTime(System.currentTimeMillis()); 709 if (flag0) { 710 crc32Manifest(e, m); 711 } 712 zos.putNextEntry(e); 713 m.write(zos); 714 if (vflag) { 715 output(getMsg("out.update.manifest")); 716 } 717 return true; 718 } 719 720 private static final boolean isWinDriveLetter(char c) { 721 return ((c >= 'a') && (c <= 'z')) || ((c >= 'A') && (c <= 'Z')); 722 } 723 724 private String safeName(String name) { 725 if (!pflag) { 726 int len = name.length(); 727 int i = name.lastIndexOf("../"); 728 if (i == -1) { 729 i = 0; 730 } else { 731 i += 3; // strip any dot-dot components 732 } 733 if (File.separatorChar == '\\') { 734 // the spec requests no drive letter. skip if 735 // the entry name has one. 736 while (i < len) { 737 int off = i; 738 if (i + 1 < len && 739 name.charAt(i + 1) == ':' && 740 isWinDriveLetter(name.charAt(i))) { 741 i += 2; 742 } 743 while (i < len && name.charAt(i) == '/') { 744 i++; 745 } 746 if (i == off) { 747 break; 748 } 749 } 750 } else { 751 while (i < len && name.charAt(i) == '/') { 752 i++; 753 } 754 } 755 if (i != 0) { 756 name = name.substring(i); 757 } 758 } 759 return name; 760 } 761 762 private String entryName(String name) { 763 name = name.replace(File.separatorChar, '/'); 764 String matchPath = ""; 765 for (String path : paths) { 766 if (name.startsWith(path) 767 && (path.length() > matchPath.length())) { 768 matchPath = path; 769 } 770 } 771 name = safeName(name.substring(matchPath.length())); 772 // the old implementaton doesn't remove 773 // "./" if it was led by "/" (?) 774 if (name.startsWith("./")) { 775 name = name.substring(2); 776 } 777 return name; 778 } 779 780 private void addVersion(Manifest m) { 781 Attributes global = m.getMainAttributes(); 782 if (global.getValue(Attributes.Name.MANIFEST_VERSION) == null) { 783 global.put(Attributes.Name.MANIFEST_VERSION, VERSION); 784 } 785 } 786 787 private void addCreatedBy(Manifest m) { 788 Attributes global = m.getMainAttributes(); 789 if (global.getValue(new Attributes.Name("Created-By")) == null) { 790 String javaVendor = System.getProperty("java.vendor"); 791 String jdkVersion = System.getProperty("java.version"); 792 global.put(new Attributes.Name("Created-By"), jdkVersion + " (" + 793 javaVendor + ")"); 794 } 795 } 796 797 private void addMainClass(Manifest m, String mainApp) { 798 Attributes global = m.getMainAttributes(); 799 800 // overrides any existing Main-Class attribute 801 global.put(Attributes.Name.MAIN_CLASS, mainApp); 802 } 803 804 private boolean isAmbiguousMainClass(Manifest m) { 805 if (ename != null) { 806 Attributes global = m.getMainAttributes(); 807 if ((global.get(Attributes.Name.MAIN_CLASS) != null)) { 808 error(getMsg("error.bad.eflag")); 809 usageError(); 810 return true; 811 } 812 } 813 return false; 814 } 815 816 /** 817 * Adds a new file entry to the ZIP output stream. 818 */ 819 void addFile(ZipOutputStream zos, File file) throws IOException { 820 String name = file.getPath(); 821 boolean isDir = file.isDirectory(); 822 if (isDir) { 823 name = name.endsWith(File.separator) ? name : 824 (name + File.separator); 825 } 826 name = entryName(name); 827 828 if (name.equals("") || name.equals(".") || name.equals(zname)) { 829 return; 830 } else if ((name.equals(MANIFEST_DIR) || name.equals(MANIFEST_NAME)) 831 && !Mflag) { 832 if (vflag) { 833 output(formatMsg("out.ignore.entry", name)); 834 } 835 return; 836 } 837 838 long size = isDir ? 0 : file.length(); 839 840 if (vflag) { 841 out.print(formatMsg("out.adding", name)); 842 } 843 ZipEntry e = new ZipEntry(name); 844 e.setTime(file.lastModified()); 845 if (size == 0) { 846 e.setMethod(ZipEntry.STORED); 847 e.setSize(0); 848 e.setCrc(0); 849 } else if (flag0) { 850 crc32File(e, file); 851 } 852 zos.putNextEntry(e); 853 if (!isDir) { 854 copy(file, zos); 855 } 856 zos.closeEntry(); 857 /* report how much compression occurred. */ 858 if (vflag) { 859 size = e.getSize(); 860 long csize = e.getCompressedSize(); 861 out.print(formatMsg2("out.size", String.valueOf(size), 862 String.valueOf(csize))); 863 if (e.getMethod() == ZipEntry.DEFLATED) { 864 long ratio = 0; 865 if (size != 0) { 866 ratio = ((size - csize) * 100) / size; 867 } 868 output(formatMsg("out.deflated", String.valueOf(ratio))); 869 } else { 870 output(getMsg("out.stored")); 871 } 872 } 873 } 874 875 /** 876 * A buffer for use only by copy(InputStream, OutputStream). 877 * Not as clean as allocating a new buffer as needed by copy, 878 * but significantly more efficient. 879 */ 880 private byte[] copyBuf = new byte[8192]; 881 882 /** 883 * Copies all bytes from the input stream to the output stream. 884 * Does not close or flush either stream. 885 * 886 * @param from the input stream to read from 887 * @param to the output stream to write to 888 * @throws IOException if an I/O error occurs 889 */ 890 private void copy(InputStream from, OutputStream to) throws IOException { 891 int n; 892 while ((n = from.read(copyBuf)) != -1) 893 to.write(copyBuf, 0, n); 894 } 895 896 /** 897 * Copies all bytes from the input file to the output stream. 898 * Does not close or flush the output stream. 899 * 900 * @param from the input file to read from 901 * @param to the output stream to write to 902 * @throws IOException if an I/O error occurs 903 */ 904 private void copy(File from, OutputStream to) throws IOException { 905 InputStream in = new FileInputStream(from); 906 try { 907 copy(in, to); 908 } finally { 909 in.close(); 910 } 911 } 912 913 /** 914 * Copies all bytes from the input stream to the output file. 915 * Does not close the input stream. 916 * 917 * @param from the input stream to read from 918 * @param to the output file to write to 919 * @throws IOException if an I/O error occurs 920 */ 921 private void copy(InputStream from, File to) throws IOException { 922 OutputStream out = new FileOutputStream(to); 923 try { 924 copy(from, out); 925 } finally { 926 out.close(); 927 } 928 } 929 930 /** 931 * Computes the crc32 of a Manifest. This is necessary when the 932 * ZipOutputStream is in STORED mode. 933 */ 934 private void crc32Manifest(ZipEntry e, Manifest m) throws IOException { 935 CRC32OutputStream os = new CRC32OutputStream(); 936 m.write(os); 937 os.updateEntry(e); 938 } 939 940 /** 941 * Computes the crc32 of a File. This is necessary when the 942 * ZipOutputStream is in STORED mode. 943 */ 944 private void crc32File(ZipEntry e, File f) throws IOException { 945 CRC32OutputStream os = new CRC32OutputStream(); 946 copy(f, os); 947 if (os.n != f.length()) { 948 throw new JarException(formatMsg( 949 "error.incorrect.length", f.getPath())); 950 } 951 os.updateEntry(e); 952 } 953 954 void replaceFSC(String files[]) { 955 if (files != null) { 956 for (int i = 0; i < files.length; i++) { 957 files[i] = files[i].replace(File.separatorChar, '/'); 958 } 959 } 960 } 961 962 @SuppressWarnings("serial") 963 Set<ZipEntry> newDirSet() { 964 return new HashSet<ZipEntry>() { 965 public boolean add(ZipEntry e) { 966 return ((e == null || useExtractionTime) ? false : super.add(e)); 967 }}; 968 } 969 970 void updateLastModifiedTime(Set<ZipEntry> zes) throws IOException { 971 for (ZipEntry ze : zes) { 972 long lastModified = ze.getTime(); 973 if (lastModified != -1) { 974 String name = safeName(ze.getName().replace(File.separatorChar, '/')); 975 if (name.length() != 0) { 976 File f = new File(name.replace('/', File.separatorChar)); 977 f.setLastModified(lastModified); 978 } 979 } 980 } 981 } 982 983 /** 984 * Extracts specified entries from JAR file. 985 * 986 * @return whether entries were found and successfully extracted 987 * (indicating this was a zip file without "leading garbage") 988 */ 989 boolean extract(InputStream in, String files[]) throws IOException { 990 ZipInputStream zis = new ZipInputStream(in); 991 ZipEntry e; 992 // Set of all directory entries specified in archive. Disallows 993 // null entries. Disallows all entries if using pre-6.0 behavior. 994 boolean entriesFound = false; 995 Set<ZipEntry> dirs = newDirSet(); 996 while ((e = zis.getNextEntry()) != null) { 997 entriesFound = true; 998 if (files == null) { 999 dirs.add(extractFile(zis, e)); 1000 } else { 1001 String name = e.getName(); 1002 for (String file : files) { 1003 if (name.startsWith(file)) { 1004 dirs.add(extractFile(zis, e)); 1005 break; 1006 } 1007 } 1008 } 1009 } 1010 1011 // Update timestamps of directories specified in archive with their 1012 // timestamps as given in the archive. We do this after extraction, 1013 // instead of during, because creating a file in a directory changes 1014 // that directory's timestamp. 1015 updateLastModifiedTime(dirs); 1016 1017 return entriesFound; 1018 } 1019 1020 /** 1021 * Extracts specified entries from JAR file, via ZipFile. 1022 */ 1023 void extract(String fname, String files[]) throws IOException { 1024 ZipFile zf = new ZipFile(fname); 1025 Set<ZipEntry> dirs = newDirSet(); 1026 Enumeration<? extends ZipEntry> zes = zf.entries(); 1027 while (zes.hasMoreElements()) { 1028 ZipEntry e = zes.nextElement(); 1029 if (files == null) { 1030 dirs.add(extractFile(zf.getInputStream(e), e)); 1031 } else { 1032 String name = e.getName(); 1033 for (String file : files) { 1034 if (name.startsWith(file)) { 1035 dirs.add(extractFile(zf.getInputStream(e), e)); 1036 break; 1037 } 1038 } 1039 } 1040 } 1041 zf.close(); 1042 updateLastModifiedTime(dirs); 1043 } 1044 1045 /** 1046 * Extracts next entry from JAR file, creating directories as needed. If 1047 * the entry is for a directory which doesn't exist prior to this 1048 * invocation, returns that entry, otherwise returns null. 1049 */ 1050 ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException { 1051 ZipEntry rc = null; 1052 // The spec requres all slashes MUST be forward '/', it is possible 1053 // an offending zip/jar entry may uses the backwards slash in its 1054 // name. It might cause problem on Windows platform as it skips 1055 // our "safe" check for leading slahs and dot-dot. So replace them 1056 // with '/'. 1057 String name = safeName(e.getName().replace(File.separatorChar, '/')); 1058 if (name.length() == 0) { 1059 return rc; // leading '/' or 'dot-dot' only path 1060 } 1061 File f = new File(name.replace('/', File.separatorChar)); 1062 if (e.isDirectory()) { 1063 if (f.exists()) { 1064 if (!f.isDirectory()) { 1065 throw new IOException(formatMsg("error.create.dir", 1066 f.getPath())); 1067 } 1068 } else { 1069 if (!f.mkdirs()) { 1070 throw new IOException(formatMsg("error.create.dir", 1071 f.getPath())); 1072 } else { 1073 rc = e; 1074 } 1075 } 1076 1077 if (vflag) { 1078 output(formatMsg("out.create", name)); 1079 } 1080 } else { 1081 if (f.getParent() != null) { 1082 File d = new File(f.getParent()); 1083 if (!d.exists() && !d.mkdirs() || !d.isDirectory()) { 1084 throw new IOException(formatMsg( 1085 "error.create.dir", d.getPath())); 1086 } 1087 } 1088 try { 1089 copy(is, f); 1090 } finally { 1091 if (is instanceof ZipInputStream) 1092 ((ZipInputStream)is).closeEntry(); 1093 else 1094 is.close(); 1095 } 1096 if (vflag) { 1097 if (e.getMethod() == ZipEntry.DEFLATED) { 1098 output(formatMsg("out.inflated", name)); 1099 } else { 1100 output(formatMsg("out.extracted", name)); 1101 } 1102 } 1103 } 1104 if (!useExtractionTime) { 1105 long lastModified = e.getTime(); 1106 if (lastModified != -1) { 1107 f.setLastModified(lastModified); 1108 } 1109 } 1110 return rc; 1111 } 1112 1113 /** 1114 * Lists contents of JAR file. 1115 */ 1116 void list(InputStream in, String files[]) throws IOException { 1117 ZipInputStream zis = new ZipInputStream(in); 1118 ZipEntry e; 1119 while ((e = zis.getNextEntry()) != null) { 1120 /* 1121 * In the case of a compressed (deflated) entry, the entry size 1122 * is stored immediately following the entry data and cannot be 1123 * determined until the entry is fully read. Therefore, we close 1124 * the entry first before printing out its attributes. 1125 */ 1126 zis.closeEntry(); 1127 printEntry(e, files); 1128 } 1129 } 1130 1131 /** 1132 * Lists contents of JAR file, via ZipFile. 1133 */ 1134 void list(String fname, String files[]) throws IOException { 1135 ZipFile zf = new ZipFile(fname); 1136 Enumeration<? extends ZipEntry> zes = zf.entries(); 1137 while (zes.hasMoreElements()) { 1138 printEntry(zes.nextElement(), files); 1139 } 1140 zf.close(); 1141 } 1142 1143 /** 1144 * Outputs the class index table to the INDEX.LIST file of the 1145 * root jar file. 1146 */ 1147 void dumpIndex(String rootjar, JarIndex index) throws IOException { 1148 File jarFile = new File(rootjar); 1149 Path jarPath = jarFile.toPath(); 1150 Path tmpPath = createTempFileInSameDirectoryAs(jarFile).toPath(); 1151 try { 1152 if (update(Files.newInputStream(jarPath), 1153 Files.newOutputStream(tmpPath), 1154 null, index)) { 1155 try { 1156 Files.move(tmpPath, jarPath, REPLACE_EXISTING); 1157 } catch (IOException e) { 1158 throw new IOException(getMsg("error.write.file"), e); 1159 } 1160 } 1161 } finally { 1162 Files.deleteIfExists(tmpPath); 1163 } 1164 } 1165 1166 private HashSet<String> jarPaths = new HashSet<String>(); 1167 1168 /** 1169 * Generates the transitive closure of the Class-Path attribute for 1170 * the specified jar file. 1171 */ 1172 List<String> getJarPath(String jar) throws IOException { 1173 List<String> files = new ArrayList<String>(); 1174 files.add(jar); 1175 jarPaths.add(jar); 1176 1177 // take out the current path 1178 String path = jar.substring(0, Math.max(0, jar.lastIndexOf('/') + 1)); 1179 1180 // class path attribute will give us jar file name with 1181 // '/' as separators, so we need to change them to the 1182 // appropriate one before we open the jar file. 1183 JarFile rf = new JarFile(jar.replace('/', File.separatorChar)); 1184 1185 if (rf != null) { 1186 Manifest man = rf.getManifest(); 1187 if (man != null) { 1188 Attributes attr = man.getMainAttributes(); 1189 if (attr != null) { 1190 String value = attr.getValue(Attributes.Name.CLASS_PATH); 1191 if (value != null) { 1192 StringTokenizer st = new StringTokenizer(value); 1193 while (st.hasMoreTokens()) { 1194 String ajar = st.nextToken(); 1195 if (!ajar.endsWith("/")) { // it is a jar file 1196 ajar = path.concat(ajar); 1197 /* check on cyclic dependency */ 1198 if (! jarPaths.contains(ajar)) { 1199 files.addAll(getJarPath(ajar)); 1200 } 1201 } 1202 } 1203 } 1204 } 1205 } 1206 } 1207 rf.close(); 1208 return files; 1209 } 1210 1211 /** 1212 * Generates class index file for the specified root jar file. 1213 */ 1214 void genIndex(String rootjar, String[] files) throws IOException { 1215 List<String> jars = getJarPath(rootjar); 1216 int njars = jars.size(); 1217 String[] jarfiles; 1218 1219 if (njars == 1 && files != null) { 1220 // no class-path attribute defined in rootjar, will 1221 // use command line specified list of jars 1222 for (int i = 0; i < files.length; i++) { 1223 jars.addAll(getJarPath(files[i])); 1224 } 1225 njars = jars.size(); 1226 } 1227 jarfiles = jars.toArray(new String[njars]); 1228 JarIndex index = new JarIndex(jarfiles); 1229 dumpIndex(rootjar, index); 1230 } 1231 1232 /** 1233 * Prints entry information, if requested. 1234 */ 1235 void printEntry(ZipEntry e, String[] files) throws IOException { 1236 if (files == null) { 1237 printEntry(e); 1238 } else { 1239 String name = e.getName(); 1240 for (String file : files) { 1241 if (name.startsWith(file)) { 1242 printEntry(e); 1243 return; 1244 } 1245 } 1246 } 1247 } 1248 1249 /** 1250 * Prints entry information. 1251 */ 1252 void printEntry(ZipEntry e) throws IOException { 1253 if (vflag) { 1254 StringBuilder sb = new StringBuilder(); 1255 String s = Long.toString(e.getSize()); 1256 for (int i = 6 - s.length(); i > 0; --i) { 1257 sb.append(' '); 1258 } 1259 sb.append(s).append(' ').append(new Date(e.getTime()).toString()); 1260 sb.append(' ').append(e.getName()); 1261 output(sb.toString()); 1262 } else { 1263 output(e.getName()); 1264 } 1265 } 1266 1267 /** 1268 * Prints usage message. 1269 */ 1270 void usageError() { 1271 error(getMsg("usage")); 1272 } 1273 1274 /** 1275 * A fatal exception has been caught. No recovery possible 1276 */ 1277 void fatalError(Exception e) { 1278 e.printStackTrace(); 1279 } 1280 1281 /** 1282 * A fatal condition has been detected; message is "s". 1283 * No recovery possible 1284 */ 1285 void fatalError(String s) { 1286 error(program + ": " + s); 1287 } 1288 1289 /** 1290 * Print an output message; like verbose output and the like 1291 */ 1292 protected void output(String s) { 1293 out.println(s); 1294 } 1295 1296 /** 1297 * Print an error message; like something is broken 1298 */ 1299 protected void error(String s) { 1300 err.println(s); 1301 } 1302 1303 /** 1304 * Main routine to start program. 1305 */ 1306 public static void main(String args[]) { 1307 Main jartool = new Main(System.out, System.err, "jar"); 1308 System.exit(jartool.run(args) ? 0 : 1); 1309 } 1310 1311 /** 1312 * An OutputStream that doesn't send its output anywhere, (but could). 1313 * It's here to find the CRC32 of an input file, necessary for STORED 1314 * mode in ZIP. 1315 */ 1316 private static class CRC32OutputStream extends java.io.OutputStream { 1317 final CRC32 crc = new CRC32(); 1318 long n = 0; 1319 1320 CRC32OutputStream() {} 1321 1322 public void write(int r) throws IOException { 1323 crc.update(r); 1324 n++; 1325 } 1326 1327 public void write(byte[] b, int off, int len) throws IOException { 1328 crc.update(b, off, len); 1329 n += len; 1330 } 1331 1332 /** 1333 * Updates a ZipEntry which describes the data read by this 1334 * output stream, in STORED mode. 1335 */ 1336 public void updateEntry(ZipEntry e) { 1337 e.setMethod(ZipEntry.STORED); 1338 e.setSize(n); 1339 e.setCrc(crc.getValue()); 1340 } 1341 } 1342 1343 /** 1344 * Attempt to create temporary file in the system-provided temporary folder, if failed attempts 1345 * to create it in the same folder as the file in parameter (if any) 1346 */ 1347 private File createTemporaryFile(String tmpbase, String suffix) { 1348 File tmpfile = null; 1349 1350 try { 1351 tmpfile = File.createTempFile(tmpbase, suffix); 1352 } catch (IOException | SecurityException e) { 1353 // Unable to create file due to permission violation or security exception 1354 } 1355 if (tmpfile == null) { 1356 // Were unable to create temporary file, fall back to temporary file in the same folder 1357 if (fname != null) { 1358 try { 1359 File tmpfolder = new File(fname).getAbsoluteFile().getParentFile(); 1360 tmpfile = File.createTempFile(fname, ".tmp" + suffix, tmpfolder); 1361 } catch (IOException ioe) { 1362 // Last option failed - fall gracefully 1363 fatalError(ioe); 1364 } 1365 } else { 1366 // No options left - we can not compress to stdout without access to the temporary folder 1367 fatalError(new IOException(getMsg("error.create.tempfile"))); 1368 } 1369 } 1370 return tmpfile; 1371 } 1372 }