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