rev 50498 : 8199871: Deprecate pack200 and unpack200 tools
Reviewed-by:

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