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