1 /* 2 * Copyright (c) 2003, 2013, 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 com.sun.java.util.jar.pack; 27 28 import com.sun.java.util.jar.pack.Attribute.Layout; 29 import java.beans.PropertyChangeListener; 30 import java.io.BufferedInputStream; 31 import java.io.ByteArrayInputStream; 32 import java.io.ByteArrayOutputStream; 33 import java.io.File; 34 import java.io.FileInputStream; 35 import java.io.IOException; 36 import java.io.InputStream; 37 import java.io.OutputStream; 38 import java.util.ArrayList; 39 import java.util.Collections; 40 import java.util.HashMap; 41 import java.util.List; 42 import java.util.ListIterator; 43 import java.util.Map; 44 import java.util.SortedMap; 45 import java.util.TimeZone; 46 import java.util.jar.JarEntry; 47 import java.util.jar.JarFile; 48 import java.util.jar.JarInputStream; 49 import java.util.jar.Pack200; 50 51 52 /* 53 * Implementation of the Pack provider. 54 * </pre></blockquote> 55 * @author John Rose 56 * @author Kumar Srinivasan 57 */ 58 59 60 public class PackerImpl extends TLGlobals implements Pack200.Packer { 61 62 /** 63 * Constructs a Packer object and sets the initial state of 64 * the packer engines. 65 */ 66 public PackerImpl() {} 67 68 /** 69 * Get the set of options for the pack and unpack engines. 70 * @return A sorted association of option key strings to option values. 71 */ 72 public SortedMap<String, String> properties() { 73 return props; 74 } 75 76 //Driver routines 77 78 /** 79 * Takes a JarFile and converts into a pack-stream. 80 * <p> 81 * Closes its input but not its output. (Pack200 archives are appendable.) 82 * @param in a JarFile 83 * @param out an OutputStream 84 * @exception IOException if an error is encountered. 85 */ 86 public synchronized void pack(JarFile in, OutputStream out) throws IOException { 87 assert(Utils.currentInstance.get() == null); 88 TimeZone tz = (props.getBoolean(Utils.PACK_DEFAULT_TIMEZONE)) 89 ? null 90 : TimeZone.getDefault(); 91 try { 92 Utils.currentInstance.set(this); 93 if (tz != null) TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 94 95 if ("0".equals(props.getProperty(Pack200.Packer.EFFORT))) { 96 Utils.copyJarFile(in, out); 97 } else { 98 (new DoPack()).run(in, out); 99 } 100 } finally { 101 Utils.currentInstance.set(null); 102 if (tz != null) TimeZone.setDefault(tz); 103 in.close(); 104 } 105 } 106 107 /** 108 * Takes a JarInputStream and converts into a pack-stream. 109 * <p> 110 * Closes its input but not its output. (Pack200 archives are appendable.) 111 * <p> 112 * The modification time and deflation hint attributes are not available, 113 * for the jar-manifest file and the directory containing the file. 114 * 115 * @see #MODIFICATION_TIME 116 * @see #DEFLATION_HINT 117 * @param in a JarInputStream 118 * @param out an OutputStream 119 * @exception IOException if an error is encountered. 120 */ 121 public synchronized void pack(JarInputStream in, OutputStream out) throws IOException { 122 assert(Utils.currentInstance.get() == null); 123 TimeZone tz = (props.getBoolean(Utils.PACK_DEFAULT_TIMEZONE)) ? null : 124 TimeZone.getDefault(); 125 try { 126 Utils.currentInstance.set(this); 127 if (tz != null) TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 128 if ("0".equals(props.getProperty(Pack200.Packer.EFFORT))) { 129 Utils.copyJarFile(in, out); 130 } else { 131 (new DoPack()).run(in, out); 132 } 133 } finally { 134 Utils.currentInstance.set(null); 135 if (tz != null) TimeZone.setDefault(tz); 136 in.close(); 137 } 138 } 139 /** 140 * Register a listener for changes to options. 141 * @param listener An object to be invoked when a property is changed. 142 */ 143 public void addPropertyChangeListener(PropertyChangeListener listener) { 144 props.addListener(listener); 145 } 146 147 /** 148 * Remove a listener for the PropertyChange event. 149 * @param listener The PropertyChange listener to be removed. 150 */ 151 public void removePropertyChangeListener(PropertyChangeListener listener) { 152 props.removeListener(listener); 153 } 154 155 156 157 // All the worker bees..... 158 // The packer worker. 159 private class DoPack { 160 final int verbose = props.getInteger(Utils.DEBUG_VERBOSE); 161 162 { 163 props.setInteger(Pack200.Packer.PROGRESS, 0); 164 if (verbose > 0) Utils.log.info(props.toString()); 165 } 166 167 // Here's where the bits are collected before getting packed, we also 168 // initialize the version numbers now. 169 final Package pkg = new Package(Package.Version.makeVersion(props, "min.class"), 170 Package.Version.makeVersion(props, "max.class"), 171 Package.Version.makeVersion(props, "package")); 172 173 final String unknownAttrCommand; 174 { 175 String uaMode = props.getProperty(Pack200.Packer.UNKNOWN_ATTRIBUTE, Pack200.Packer.PASS); 176 if (!(Pack200.Packer.STRIP.equals(uaMode) || 177 Pack200.Packer.PASS.equals(uaMode) || 178 Pack200.Packer.ERROR.equals(uaMode))) { 179 throw new RuntimeException("Bad option: " + Pack200.Packer.UNKNOWN_ATTRIBUTE + " = " + uaMode); 180 } 181 unknownAttrCommand = uaMode.intern(); 182 } 183 final String classFormatCommand; 184 { 185 String fmtMode = props.getProperty(Utils.CLASS_FORMAT_ERROR, Pack200.Packer.PASS); 186 if (!(Pack200.Packer.PASS.equals(fmtMode) || 187 Pack200.Packer.ERROR.equals(fmtMode))) { 188 throw new RuntimeException("Bad option: " + Utils.CLASS_FORMAT_ERROR + " = " + fmtMode); 189 } 190 classFormatCommand = fmtMode.intern(); 191 } 192 193 final Map<Attribute.Layout, Attribute> attrDefs; 194 final Map<Attribute.Layout, String> attrCommands; 195 { 196 Map<Attribute.Layout, Attribute> lattrDefs = new HashMap<>(); 197 Map<Attribute.Layout, String> lattrCommands = new HashMap<>(); 198 String[] keys = { 199 Pack200.Packer.CLASS_ATTRIBUTE_PFX, 200 Pack200.Packer.FIELD_ATTRIBUTE_PFX, 201 Pack200.Packer.METHOD_ATTRIBUTE_PFX, 202 Pack200.Packer.CODE_ATTRIBUTE_PFX 203 }; 204 int[] ctypes = { 205 Constants.ATTR_CONTEXT_CLASS, 206 Constants.ATTR_CONTEXT_FIELD, 207 Constants.ATTR_CONTEXT_METHOD, 208 Constants.ATTR_CONTEXT_CODE 209 }; 210 for (int i = 0; i < ctypes.length; i++) { 211 String pfx = keys[i]; 212 Map<String, String> map = props.prefixMap(pfx); 213 for (String key : map.keySet()) { 214 assert(key.startsWith(pfx)); 215 String name = key.substring(pfx.length()); 216 String layout = props.getProperty(key); 217 Layout lkey = Attribute.keyForLookup(ctypes[i], name); 218 if (Pack200.Packer.STRIP.equals(layout) || 219 Pack200.Packer.PASS.equals(layout) || 220 Pack200.Packer.ERROR.equals(layout)) { 221 lattrCommands.put(lkey, layout.intern()); 222 } else { 223 Attribute.define(lattrDefs, ctypes[i], name, layout); 224 if (verbose > 1) { 225 Utils.log.fine("Added layout for "+Constants.ATTR_CONTEXT_NAME[i]+" attribute "+name+" = "+layout); 226 } 227 assert(lattrDefs.containsKey(lkey)); 228 } 229 } 230 } 231 this.attrDefs = (lattrDefs.isEmpty()) ? null : lattrDefs; 232 this.attrCommands = (lattrCommands.isEmpty()) ? null : lattrCommands; 233 } 234 235 final boolean keepFileOrder 236 = props.getBoolean(Pack200.Packer.KEEP_FILE_ORDER); 237 final boolean keepClassOrder 238 = props.getBoolean(Utils.PACK_KEEP_CLASS_ORDER); 239 240 final boolean keepModtime 241 = Pack200.Packer.KEEP.equals(props.getProperty(Pack200.Packer.MODIFICATION_TIME)); 242 final boolean latestModtime 243 = Pack200.Packer.LATEST.equals(props.getProperty(Pack200.Packer.MODIFICATION_TIME)); 244 final boolean keepDeflateHint 245 = Pack200.Packer.KEEP.equals(props.getProperty(Pack200.Packer.DEFLATE_HINT)); 246 { 247 if (!keepModtime && !latestModtime) { 248 int modtime = props.getTime(Pack200.Packer.MODIFICATION_TIME); 249 if (modtime != Constants.NO_MODTIME) { 250 pkg.default_modtime = modtime; 251 } 252 } 253 if (!keepDeflateHint) { 254 boolean deflate_hint = props.getBoolean(Pack200.Packer.DEFLATE_HINT); 255 if (deflate_hint) { 256 pkg.default_options |= Constants.AO_DEFLATE_HINT; 257 } 258 } 259 } 260 261 long totalOutputSize = 0; 262 int segmentCount = 0; 263 long segmentTotalSize = 0; 264 long segmentSize = 0; // running counter 265 final long segmentLimit; 266 { 267 long limit; 268 if (props.getProperty(Pack200.Packer.SEGMENT_LIMIT, "").equals("")) 269 limit = -1; 270 else 271 limit = props.getLong(Pack200.Packer.SEGMENT_LIMIT); 272 limit = Math.min(Integer.MAX_VALUE, limit); 273 limit = Math.max(-1, limit); 274 if (limit == -1) 275 limit = Long.MAX_VALUE; 276 segmentLimit = limit; 277 } 278 279 final List<String> passFiles; // parsed pack.pass.file options 280 { 281 // Which class files will be passed through? 282 passFiles = props.getProperties(Pack200.Packer.PASS_FILE_PFX); 283 for (ListIterator<String> i = passFiles.listIterator(); i.hasNext(); ) { 284 String file = i.next(); 285 if (file == null) { i.remove(); continue; } 286 file = Utils.getJarEntryName(file); // normalize '\\' to '/' 287 if (file.endsWith("/")) 288 file = file.substring(0, file.length()-1); 289 i.set(file); 290 } 291 if (verbose > 0) Utils.log.info("passFiles = " + passFiles); 292 } 293 294 { 295 // Hook for testing: Forces use of special archive modes. 296 int opt = props.getInteger(Utils.COM_PREFIX+"archive.options"); 297 if (opt != 0) 298 pkg.default_options |= opt; 299 } 300 301 // (Done collecting options from props.) 302 303 boolean isClassFile(String name) { 304 if (!name.endsWith(".class")) return false; 305 for (String prefix = name; ; ) { 306 if (passFiles.contains(prefix)) return false; 307 int chop = prefix.lastIndexOf('/'); 308 if (chop < 0) break; 309 prefix = prefix.substring(0, chop); 310 } 311 return true; 312 } 313 314 boolean isMetaInfFile(String name) { 315 return name.startsWith("/" + Utils.METAINF) || 316 name.startsWith(Utils.METAINF); 317 } 318 319 // Get a new package, based on the old one. 320 private void makeNextPackage() { 321 pkg.reset(); 322 } 323 324 final class InFile { 325 final String name; 326 final JarFile jf; 327 final JarEntry je; 328 final File f; 329 int modtime = Constants.NO_MODTIME; 330 int options; 331 InFile(String name) { 332 this.name = Utils.getJarEntryName(name); 333 this.f = new File(name); 334 this.jf = null; 335 this.je = null; 336 int timeSecs = getModtime(f.lastModified()); 337 if (keepModtime && timeSecs != Constants.NO_MODTIME) { 338 this.modtime = timeSecs; 339 } else if (latestModtime && timeSecs > pkg.default_modtime) { 340 pkg.default_modtime = timeSecs; 341 } 342 } 343 InFile(JarFile jf, JarEntry je) { 344 this.name = Utils.getJarEntryName(je.getName()); 345 this.f = null; 346 this.jf = jf; 347 this.je = je; 348 int timeSecs = getModtime(je.getTime()); 349 if (keepModtime && timeSecs != Constants.NO_MODTIME) { 350 this.modtime = timeSecs; 351 } else if (latestModtime && timeSecs > pkg.default_modtime) { 352 pkg.default_modtime = timeSecs; 353 } 354 if (keepDeflateHint && je.getMethod() == JarEntry.DEFLATED) { 355 options |= Constants.FO_DEFLATE_HINT; 356 } 357 } 358 InFile(JarEntry je) { 359 this(null, je); 360 } 361 long getInputLength() { 362 long len = (je != null)? je.getSize(): f.length(); 363 assert(len >= 0) : this+".len="+len; 364 // Bump size by pathname length and modtime/def-hint bytes. 365 return Math.max(0, len) + name.length() + 5; 366 } 367 int getModtime(long timeMillis) { 368 // Convert milliseconds to seconds. 369 long seconds = (timeMillis+500) / 1000; 370 if ((int)seconds == seconds) { 371 return (int)seconds; 372 } else { 373 Utils.log.warning("overflow in modtime for "+f); 374 return Constants.NO_MODTIME; 375 } 376 } 377 void copyTo(Package.File file) { 378 if (modtime != Constants.NO_MODTIME) 379 file.modtime = modtime; 380 file.options |= options; 381 } 382 InputStream getInputStream() throws IOException { 383 if (jf != null) 384 return jf.getInputStream(je); 385 else 386 return new FileInputStream(f); 387 } 388 389 public String toString() { 390 return name; 391 } 392 } 393 394 private int nread = 0; // used only if (verbose > 0) 395 private void noteRead(InFile f) { 396 nread++; 397 if (verbose > 2) 398 Utils.log.fine("...read "+f.name); 399 if (verbose > 0 && (nread % 1000) == 0) 400 Utils.log.info("Have read "+nread+" files..."); 401 } 402 403 void run(JarInputStream in, OutputStream out) throws IOException { 404 // First thing we do is get the manifest, as JIS does 405 // not provide the Manifest as an entry. 406 if (in.getManifest() != null) { 407 ByteArrayOutputStream tmp = new ByteArrayOutputStream(); 408 in.getManifest().write(tmp); 409 InputStream tmpIn = new ByteArrayInputStream(tmp.toByteArray()); 410 pkg.addFile(readFile(JarFile.MANIFEST_NAME, tmpIn)); 411 } 412 for (JarEntry je; (je = in.getNextJarEntry()) != null; ) { 413 InFile inFile = new InFile(je); 414 415 String name = inFile.name; 416 Package.File bits = readFile(name, in); 417 Package.File file = null; 418 // (5078608) : discount the resource files in META-INF 419 // from segment computation. 420 long inflen = (isMetaInfFile(name)) 421 ? 0L 422 : inFile.getInputLength(); 423 424 if ((segmentSize += inflen) > segmentLimit) { 425 segmentSize -= inflen; 426 int nextCount = -1; // don't know; it's a stream 427 flushPartial(out, nextCount); 428 } 429 if (verbose > 1) { 430 Utils.log.fine("Reading " + name); 431 } 432 433 assert(je.isDirectory() == name.endsWith("/")); 434 435 if (isClassFile(name)) { 436 file = readClass(name, bits.getInputStream()); 437 } 438 if (file == null) { 439 file = bits; 440 pkg.addFile(file); 441 } 442 inFile.copyTo(file); 443 noteRead(inFile); 444 } 445 flushAll(out); 446 } 447 448 void run(JarFile in, OutputStream out) throws IOException { 449 List<InFile> inFiles = scanJar(in); 450 451 if (verbose > 0) 452 Utils.log.info("Reading " + inFiles.size() + " files..."); 453 454 int numDone = 0; 455 for (InFile inFile : inFiles) { 456 String name = inFile.name; 457 // (5078608) : discount the resource files completely from segmenting 458 long inflen = (isMetaInfFile(name)) 459 ? 0L 460 : inFile.getInputLength() ; 461 if ((segmentSize += inflen) > segmentLimit) { 462 segmentSize -= inflen; 463 // Estimate number of remaining segments: 464 float filesDone = numDone+1; 465 float segsDone = segmentCount+1; 466 float filesToDo = inFiles.size() - filesDone; 467 float segsToDo = filesToDo * (segsDone/filesDone); 468 if (verbose > 1) 469 Utils.log.fine("Estimated segments to do: "+segsToDo); 470 flushPartial(out, (int) Math.ceil(segsToDo)); 471 } 472 InputStream strm = inFile.getInputStream(); 473 if (verbose > 1) 474 Utils.log.fine("Reading " + name); 475 Package.File file = null; 476 if (isClassFile(name)) { 477 file = readClass(name, strm); 478 if (file == null) { 479 strm.close(); 480 strm = inFile.getInputStream(); 481 } 482 } 483 if (file == null) { 484 file = readFile(name, strm); 485 pkg.addFile(file); 486 } 487 inFile.copyTo(file); 488 strm.close(); // tidy up 489 noteRead(inFile); 490 numDone += 1; 491 } 492 flushAll(out); 493 } 494 495 Package.File readClass(String fname, InputStream in) throws IOException { 496 Package.Class cls = pkg.new Class(fname); 497 in = new BufferedInputStream(in); 498 ClassReader reader = new ClassReader(cls, in); 499 reader.setAttrDefs(attrDefs); 500 reader.setAttrCommands(attrCommands); 501 reader.unknownAttrCommand = unknownAttrCommand; 502 try { 503 reader.read(); 504 } catch (IOException ioe) { 505 String message = "Passing class file uncompressed due to"; 506 if (ioe instanceof Attribute.FormatException) { 507 Attribute.FormatException ee = (Attribute.FormatException) ioe; 508 // He passed up the category to us in layout. 509 if (ee.layout.equals(Pack200.Packer.PASS)) { 510 Utils.log.info(ee.toString()); 511 Utils.log.warning(message + " unrecognized attribute: " + 512 fname); 513 return null; 514 } 515 } else if (ioe instanceof ClassReader.ClassFormatException) { 516 ClassReader.ClassFormatException ce = (ClassReader.ClassFormatException) ioe; 517 if (classFormatCommand.equals(Pack200.Packer.PASS)) { 518 Utils.log.info(ce.toString()); 519 Utils.log.warning(message + " unknown class format: " + 520 fname); 521 return null; 522 } 523 } 524 // Otherwise, it must be an error. 525 throw ioe; 526 } 527 pkg.addClass(cls); 528 return cls.file; 529 } 530 531 // Read raw data. 532 Package.File readFile(String fname, InputStream in) throws IOException { 533 534 Package.File file = pkg.new File(fname); 535 file.readFrom(in); 536 if (file.isDirectory() && file.getFileLength() != 0) 537 throw new IllegalArgumentException("Non-empty directory: "+file.getFileName()); 538 return file; 539 } 540 541 void flushPartial(OutputStream out, int nextCount) throws IOException { 542 if (pkg.files.isEmpty() && pkg.classes.isEmpty()) { 543 return; // do not flush an empty segment 544 } 545 flushPackage(out, Math.max(1, nextCount)); 546 props.setInteger(Pack200.Packer.PROGRESS, 25); 547 // In case there will be another segment: 548 makeNextPackage(); 549 segmentCount += 1; 550 segmentTotalSize += segmentSize; 551 segmentSize = 0; 552 } 553 554 void flushAll(OutputStream out) throws IOException { 555 props.setInteger(Pack200.Packer.PROGRESS, 50); 556 flushPackage(out, 0); 557 out.flush(); 558 props.setInteger(Pack200.Packer.PROGRESS, 100); 559 segmentCount += 1; 560 segmentTotalSize += segmentSize; 561 segmentSize = 0; 562 if (verbose > 0 && segmentCount > 1) { 563 Utils.log.info("Transmitted " 564 +segmentTotalSize+" input bytes in " 565 +segmentCount+" segments totaling " 566 +totalOutputSize+" bytes"); 567 } 568 } 569 570 571 /** Write all information in the current package segment 572 * to the output stream. 573 */ 574 void flushPackage(OutputStream out, int nextCount) throws IOException { 575 int nfiles = pkg.files.size(); 576 if (!keepFileOrder) { 577 // Keeping the order of classes costs about 1% 578 // Keeping the order of all files costs something more. 579 if (verbose > 1) Utils.log.fine("Reordering files."); 580 boolean stripDirectories = true; 581 pkg.reorderFiles(keepClassOrder, stripDirectories); 582 } else { 583 // Package builder must have created a stub for each class. 584 assert(pkg.files.containsAll(pkg.getClassStubs())); 585 // Order of stubs in file list must agree with classes. 586 List<Package.File> res = pkg.files; 587 assert((res = new ArrayList<>(pkg.files)) 588 .retainAll(pkg.getClassStubs()) || true); 589 assert(res.equals(pkg.getClassStubs())); 590 } 591 pkg.trimStubs(); 592 593 // Do some stripping, maybe. 594 if (props.getBoolean(Utils.COM_PREFIX+"strip.debug")) pkg.stripAttributeKind("Debug"); 595 if (props.getBoolean(Utils.COM_PREFIX+"strip.compile")) pkg.stripAttributeKind("Compile"); 596 if (props.getBoolean(Utils.COM_PREFIX+"strip.constants")) pkg.stripAttributeKind("Constant"); 597 if (props.getBoolean(Utils.COM_PREFIX+"strip.exceptions")) pkg.stripAttributeKind("Exceptions"); 598 if (props.getBoolean(Utils.COM_PREFIX+"strip.innerclasses")) pkg.stripAttributeKind("InnerClasses"); 599 600 PackageWriter pw = new PackageWriter(pkg, out); 601 pw.archiveNextCount = nextCount; 602 pw.write(); 603 out.flush(); 604 if (verbose > 0) { 605 long outSize = pw.archiveSize0+pw.archiveSize1; 606 totalOutputSize += outSize; 607 long inSize = segmentSize; 608 Utils.log.info("Transmitted " 609 +nfiles+" files of " 610 +inSize+" input bytes in a segment of " 611 +outSize+" bytes"); 612 } 613 } 614 615 List<InFile> scanJar(JarFile jf) throws IOException { 616 // Collect jar entries, preserving order. 617 List<InFile> inFiles = new ArrayList<>(); 618 try { 619 for (JarEntry je : Collections.list(jf.entries())) { 620 InFile inFile = new InFile(jf, je); 621 assert(je.isDirectory() == inFile.name.endsWith("/")); 622 inFiles.add(inFile); 623 } 624 } catch (IllegalStateException ise) { 625 throw new IOException(ise.getLocalizedMessage(), ise); 626 } 627 return inFiles; 628 } 629 } 630 }