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 }