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