001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 * 017 */ 018package org.apache.commons.compress.archivers.zip; 019 020import java.io.BufferedInputStream; 021import java.io.ByteArrayInputStream; 022import java.io.Closeable; 023import java.io.EOFException; 024import java.io.File; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.SequenceInputStream; 028import java.nio.Buffer; 029import java.nio.ByteBuffer; 030import java.nio.channels.FileChannel; 031import java.nio.channels.SeekableByteChannel; 032import java.nio.file.Files; 033import java.nio.file.StandardOpenOption; 034import java.util.Arrays; 035import java.util.Collections; 036import java.util.Comparator; 037import java.util.Enumeration; 038import java.util.EnumSet; 039import java.util.HashMap; 040import java.util.LinkedList; 041import java.util.List; 042import java.util.Map; 043import java.util.zip.Inflater; 044import java.util.zip.ZipException; 045 046import org.apache.commons.compress.archivers.EntryStreamOffsets; 047import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; 048import org.apache.commons.compress.compressors.deflate64.Deflate64CompressorInputStream; 049import org.apache.commons.compress.utils.CountingInputStream; 050import org.apache.commons.compress.utils.IOUtils; 051import org.apache.commons.compress.utils.InputStreamStatistics; 052 053import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD; 054import static org.apache.commons.compress.archivers.zip.ZipConstants.SHORT; 055import static org.apache.commons.compress.archivers.zip.ZipConstants.WORD; 056import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC; 057import static org.apache.commons.compress.archivers.zip.ZipConstants.ZIP64_MAGIC_SHORT; 058 059/** 060 * Replacement for <code>java.util.ZipFile</code>. 061 * 062 * <p>This class adds support for file name encodings other than UTF-8 063 * (which is required to work on ZIP files created by native zip tools 064 * and is able to skip a preamble like the one found in self 065 * extracting archives. Furthermore it returns instances of 066 * <code>org.apache.commons.compress.archivers.zip.ZipArchiveEntry</code> 067 * instead of <code>java.util.zip.ZipEntry</code>.</p> 068 * 069 * <p>It doesn't extend <code>java.util.zip.ZipFile</code> as it would 070 * have to reimplement all methods anyway. Like 071 * <code>java.util.ZipFile</code>, it uses SeekableByteChannel under the 072 * covers and supports compressed and uncompressed entries. As of 073 * Apache Commons Compress 1.3 it also transparently supports Zip64 074 * extensions and thus individual entries and archives larger than 4 075 * GB or with more than 65536 entries.</p> 076 * 077 * <p>The method signatures mimic the ones of 078 * <code>java.util.zip.ZipFile</code>, with a couple of exceptions: 079 * 080 * <ul> 081 * <li>There is no getName method.</li> 082 * <li>entries has been renamed to getEntries.</li> 083 * <li>getEntries and getEntry return 084 * <code>org.apache.commons.compress.archivers.zip.ZipArchiveEntry</code> 085 * instances.</li> 086 * <li>close is allowed to throw IOException.</li> 087 * </ul> 088 * 089 */ 090public class ZipFile implements Closeable { 091 private static final int HASH_SIZE = 509; 092 static final int NIBLET_MASK = 0x0f; 093 static final int BYTE_SHIFT = 8; 094 private static final int POS_0 = 0; 095 private static final int POS_1 = 1; 096 private static final int POS_2 = 2; 097 private static final int POS_3 = 3; 098 private static final byte[] ONE_ZERO_BYTE = new byte[1]; 099 100 /** 101 * List of entries in the order they appear inside the central 102 * directory. 103 */ 104 private final List<ZipArchiveEntry> entries = 105 new LinkedList<>(); 106 107 /** 108 * Maps String to list of ZipArchiveEntrys, name -> actual entries. 109 */ 110 private final Map<String, LinkedList<ZipArchiveEntry>> nameMap = 111 new HashMap<>(HASH_SIZE); 112 113 /** 114 * The encoding to use for file names and the file comment. 115 * 116 * <p>For a list of possible values see <a 117 * href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html</a>. 118 * Defaults to UTF-8.</p> 119 */ 120 private final String encoding; 121 122 /** 123 * The zip encoding to use for file names and the file comment. 124 */ 125 private final ZipEncoding zipEncoding; 126 127 /** 128 * File name of actual source. 129 */ 130 private final String archiveName; 131 132 /** 133 * The actual data source. 134 */ 135 private final SeekableByteChannel archive; 136 137 /** 138 * Whether to look for and use Unicode extra fields. 139 */ 140 private final boolean useUnicodeExtraFields; 141 142 /** 143 * Whether the file is closed. 144 */ 145 private volatile boolean closed = true; 146 147 // cached buffers - must only be used locally in the class (COMPRESS-172 - reduce garbage collection) 148 private final byte[] dwordBuf = new byte[DWORD]; 149 private final byte[] wordBuf = new byte[WORD]; 150 private final byte[] cfhBuf = new byte[CFH_LEN]; 151 private final byte[] shortBuf = new byte[SHORT]; 152 private final ByteBuffer dwordBbuf = ByteBuffer.wrap(dwordBuf); 153 private final ByteBuffer wordBbuf = ByteBuffer.wrap(wordBuf); 154 private final ByteBuffer cfhBbuf = ByteBuffer.wrap(cfhBuf); 155 156 /** 157 * Opens the given file for reading, assuming "UTF8" for file names. 158 * 159 * @param f the archive. 160 * 161 * @throws IOException if an error occurs while reading the file. 162 */ 163 public ZipFile(final File f) throws IOException { 164 this(f, ZipEncodingHelper.UTF8); 165 } 166 167 /** 168 * Opens the given file for reading, assuming "UTF8". 169 * 170 * @param name name of the archive. 171 * 172 * @throws IOException if an error occurs while reading the file. 173 */ 174 public ZipFile(final String name) throws IOException { 175 this(new File(name), ZipEncodingHelper.UTF8); 176 } 177 178 /** 179 * Opens the given file for reading, assuming the specified 180 * encoding for file names, scanning unicode extra fields. 181 * 182 * @param name name of the archive. 183 * @param encoding the encoding to use for file names, use null 184 * for the platform's default encoding 185 * 186 * @throws IOException if an error occurs while reading the file. 187 */ 188 public ZipFile(final String name, final String encoding) throws IOException { 189 this(new File(name), encoding, true); 190 } 191 192 /** 193 * Opens the given file for reading, assuming the specified 194 * encoding for file names and scanning for unicode extra fields. 195 * 196 * @param f the archive. 197 * @param encoding the encoding to use for file names, use null 198 * for the platform's default encoding 199 * 200 * @throws IOException if an error occurs while reading the file. 201 */ 202 public ZipFile(final File f, final String encoding) throws IOException { 203 this(f, encoding, true); 204 } 205 206 /** 207 * Opens the given file for reading, assuming the specified 208 * encoding for file names. 209 * 210 * @param f the archive. 211 * @param encoding the encoding to use for file names, use null 212 * for the platform's default encoding 213 * @param useUnicodeExtraFields whether to use InfoZIP Unicode 214 * Extra Fields (if present) to set the file names. 215 * 216 * @throws IOException if an error occurs while reading the file. 217 */ 218 public ZipFile(final File f, final String encoding, final boolean useUnicodeExtraFields) 219 throws IOException { 220 this(f, encoding, useUnicodeExtraFields, false); 221 } 222 223 /** 224 * Opens the given file for reading, assuming the specified 225 * encoding for file names. 226 * 227 * 228 * <p>By default the central directory record and all local file headers of the archive will be read immediately 229 * which may take a considerable amount of time when the archive is big. The {@code ignoreLocalFileHeader} parameter 230 * can be set to {@code true} which restricts parsing to the central directory. Unfortunately the local file header 231 * may contain information not present inside of the central directory which will not be available when the argument 232 * is set to {@code true}. This includes the content of the Unicode extra field, so setting {@code 233 * ignoreLocalFileHeader} to {@code true} means {@code useUnicodeExtraFields} will be ignored effectively. Also 234 * {@link #getRawInputStream} is always going to return {@code null} if {@code ignoreLocalFileHeader} is {@code 235 * true}.</p> 236 * 237 * @param f the archive. 238 * @param encoding the encoding to use for file names, use null 239 * for the platform's default encoding 240 * @param useUnicodeExtraFields whether to use InfoZIP Unicode 241 * Extra Fields (if present) to set the file names. 242 * @param ignoreLocalFileHeader whether to ignore information 243 * stored inside the local file header (see the notes in this method's javadoc) 244 * 245 * @throws IOException if an error occurs while reading the file. 246 * @since 1.19 247 */ 248 public ZipFile(final File f, final String encoding, final boolean useUnicodeExtraFields, 249 final boolean ignoreLocalFileHeader) 250 throws IOException { 251 this(Files.newByteChannel(f.toPath(), EnumSet.of(StandardOpenOption.READ)), 252 f.getAbsolutePath(), encoding, useUnicodeExtraFields, true, ignoreLocalFileHeader); 253 } 254 255 /** 256 * Opens the given channel for reading, assuming "UTF8" for file names. 257 * 258 * <p>{@link 259 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 260 * allows you to read from an in-memory archive.</p> 261 * 262 * @param channel the archive. 263 * 264 * @throws IOException if an error occurs while reading the file. 265 * @since 1.13 266 */ 267 public ZipFile(final SeekableByteChannel channel) 268 throws IOException { 269 this(channel, "unknown archive", ZipEncodingHelper.UTF8, true); 270 } 271 272 /** 273 * Opens the given channel for reading, assuming the specified 274 * encoding for file names. 275 * 276 * <p>{@link 277 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 278 * allows you to read from an in-memory archive.</p> 279 * 280 * @param channel the archive. 281 * @param encoding the encoding to use for file names, use null 282 * for the platform's default encoding 283 * 284 * @throws IOException if an error occurs while reading the file. 285 * @since 1.13 286 */ 287 public ZipFile(final SeekableByteChannel channel, final String encoding) 288 throws IOException { 289 this(channel, "unknown archive", encoding, true); 290 } 291 292 /** 293 * Opens the given channel for reading, assuming the specified 294 * encoding for file names. 295 * 296 * <p>{@link 297 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 298 * allows you to read from an in-memory archive.</p> 299 * 300 * @param channel the archive. 301 * @param archiveName name of the archive, used for error messages only. 302 * @param encoding the encoding to use for file names, use null 303 * for the platform's default encoding 304 * @param useUnicodeExtraFields whether to use InfoZIP Unicode 305 * Extra Fields (if present) to set the file names. 306 * 307 * @throws IOException if an error occurs while reading the file. 308 * @since 1.13 309 */ 310 public ZipFile(final SeekableByteChannel channel, final String archiveName, 311 final String encoding, final boolean useUnicodeExtraFields) 312 throws IOException { 313 this(channel, archiveName, encoding, useUnicodeExtraFields, false, false); 314 } 315 316 /** 317 * Opens the given channel for reading, assuming the specified 318 * encoding for file names. 319 * 320 * <p>{@link 321 * org.apache.commons.compress.utils.SeekableInMemoryByteChannel} 322 * allows you to read from an in-memory archive.</p> 323 * 324 * <p>By default the central directory record and all local file headers of the archive will be read immediately 325 * which may take a considerable amount of time when the archive is big. The {@code ignoreLocalFileHeader} parameter 326 * can be set to {@code true} which restricts parsing to the central directory. Unfortunately the local file header 327 * may contain information not present inside of the central directory which will not be available when the argument 328 * is set to {@code true}. This includes the content of the Unicode extra field, so setting {@code 329 * ignoreLocalFileHeader} to {@code true} means {@code useUnicodeExtraFields} will be ignored effectively. Also 330 * {@link #getRawInputStream} is always going to return {@code null} if {@code ignoreLocalFileHeader} is {@code 331 * true}.</p> 332 * 333 * @param channel the archive. 334 * @param archiveName name of the archive, used for error messages only. 335 * @param encoding the encoding to use for file names, use null 336 * for the platform's default encoding 337 * @param useUnicodeExtraFields whether to use InfoZIP Unicode 338 * Extra Fields (if present) to set the file names. 339 * @param ignoreLocalFileHeader whether to ignore information 340 * stored inside the local file header (see the notes in this method's javadoc) 341 * 342 * @throws IOException if an error occurs while reading the file. 343 * @since 1.19 344 */ 345 public ZipFile(final SeekableByteChannel channel, final String archiveName, 346 final String encoding, final boolean useUnicodeExtraFields, 347 final boolean ignoreLocalFileHeader) 348 throws IOException { 349 this(channel, archiveName, encoding, useUnicodeExtraFields, false, ignoreLocalFileHeader); 350 } 351 352 private ZipFile(final SeekableByteChannel channel, final String archiveName, 353 final String encoding, final boolean useUnicodeExtraFields, 354 final boolean closeOnError, final boolean ignoreLocalFileHeader) 355 throws IOException { 356 this.archiveName = archiveName; 357 this.encoding = encoding; 358 this.zipEncoding = ZipEncodingHelper.getZipEncoding(encoding); 359 this.useUnicodeExtraFields = useUnicodeExtraFields; 360 archive = channel; 361 boolean success = false; 362 try { 363 final Map<ZipArchiveEntry, NameAndComment> entriesWithoutUTF8Flag = 364 populateFromCentralDirectory(); 365 if (!ignoreLocalFileHeader) { 366 resolveLocalFileHeaderData(entriesWithoutUTF8Flag); 367 } 368 fillNameMap(); 369 success = true; 370 } finally { 371 closed = !success; 372 if (!success && closeOnError) { 373 IOUtils.closeQuietly(archive); 374 } 375 } 376 } 377 378 /** 379 * The encoding to use for file names and the file comment. 380 * 381 * @return null if using the platform's default character encoding. 382 */ 383 public String getEncoding() { 384 return encoding; 385 } 386 387 /** 388 * Closes the archive. 389 * @throws IOException if an error occurs closing the archive. 390 */ 391 @Override 392 public void close() throws IOException { 393 // this flag is only written here and read in finalize() which 394 // can never be run in parallel. 395 // no synchronization needed. 396 closed = true; 397 398 archive.close(); 399 } 400 401 /** 402 * close a zipfile quietly; throw no io fault, do nothing 403 * on a null parameter 404 * @param zipfile file to close, can be null 405 */ 406 public static void closeQuietly(final ZipFile zipfile) { 407 IOUtils.closeQuietly(zipfile); 408 } 409 410 /** 411 * Returns all entries. 412 * 413 * <p>Entries will be returned in the same order they appear 414 * within the archive's central directory.</p> 415 * 416 * @return all entries as {@link ZipArchiveEntry} instances 417 */ 418 public Enumeration<ZipArchiveEntry> getEntries() { 419 return Collections.enumeration(entries); 420 } 421 422 /** 423 * Returns all entries in physical order. 424 * 425 * <p>Entries will be returned in the same order their contents 426 * appear within the archive.</p> 427 * 428 * @return all entries as {@link ZipArchiveEntry} instances 429 * 430 * @since 1.1 431 */ 432 public Enumeration<ZipArchiveEntry> getEntriesInPhysicalOrder() { 433 final ZipArchiveEntry[] allEntries = entries.toArray(new ZipArchiveEntry[entries.size()]); 434 Arrays.sort(allEntries, offsetComparator); 435 return Collections.enumeration(Arrays.asList(allEntries)); 436 } 437 438 /** 439 * Returns a named entry - or {@code null} if no entry by 440 * that name exists. 441 * 442 * <p>If multiple entries with the same name exist the first entry 443 * in the archive's central directory by that name is 444 * returned.</p> 445 * 446 * @param name name of the entry. 447 * @return the ZipArchiveEntry corresponding to the given name - or 448 * {@code null} if not present. 449 */ 450 public ZipArchiveEntry getEntry(final String name) { 451 final LinkedList<ZipArchiveEntry> entriesOfThatName = nameMap.get(name); 452 return entriesOfThatName != null ? entriesOfThatName.getFirst() : null; 453 } 454 455 /** 456 * Returns all named entries in the same order they appear within 457 * the archive's central directory. 458 * 459 * @param name name of the entry. 460 * @return the Iterable<ZipArchiveEntry> corresponding to the 461 * given name 462 * @since 1.6 463 */ 464 public Iterable<ZipArchiveEntry> getEntries(final String name) { 465 final List<ZipArchiveEntry> entriesOfThatName = nameMap.get(name); 466 return entriesOfThatName != null ? entriesOfThatName 467 : Collections.<ZipArchiveEntry>emptyList(); 468 } 469 470 /** 471 * Returns all named entries in the same order their contents 472 * appear within the archive. 473 * 474 * @param name name of the entry. 475 * @return the Iterable<ZipArchiveEntry> corresponding to the 476 * given name 477 * @since 1.6 478 */ 479 public Iterable<ZipArchiveEntry> getEntriesInPhysicalOrder(final String name) { 480 ZipArchiveEntry[] entriesOfThatName = new ZipArchiveEntry[0]; 481 if (nameMap.containsKey(name)) { 482 entriesOfThatName = nameMap.get(name).toArray(entriesOfThatName); 483 Arrays.sort(entriesOfThatName, offsetComparator); 484 } 485 return Arrays.asList(entriesOfThatName); 486 } 487 488 /** 489 * Whether this class is able to read the given entry. 490 * 491 * <p>May return false if it is set up to use encryption or a 492 * compression method that hasn't been implemented yet.</p> 493 * @since 1.1 494 * @param ze the entry 495 * @return whether this class is able to read the given entry. 496 */ 497 public boolean canReadEntryData(final ZipArchiveEntry ze) { 498 return ZipUtil.canHandleEntryData(ze); 499 } 500 501 /** 502 * Expose the raw stream of the archive entry (compressed form). 503 * 504 * <p>This method does not relate to how/if we understand the payload in the 505 * stream, since we really only intend to move it on to somewhere else.</p> 506 * 507 * @param ze The entry to get the stream for 508 * @return The raw input stream containing (possibly) compressed data. 509 * @since 1.11 510 */ 511 public InputStream getRawInputStream(final ZipArchiveEntry ze) { 512 if (!(ze instanceof Entry)) { 513 return null; 514 } 515 final long start = ze.getDataOffset(); 516 if (start == EntryStreamOffsets.OFFSET_UNKNOWN) { 517 return null; 518 } 519 return createBoundedInputStream(start, ze.getCompressedSize()); 520 } 521 522 523 /** 524 * Transfer selected entries from this zipfile to a given #ZipArchiveOutputStream. 525 * Compression and all other attributes will be as in this file. 526 * <p>This method transfers entries based on the central directory of the zip file.</p> 527 * 528 * @param target The zipArchiveOutputStream to write the entries to 529 * @param predicate A predicate that selects which entries to write 530 * @throws IOException on error 531 */ 532 public void copyRawEntries(final ZipArchiveOutputStream target, final ZipArchiveEntryPredicate predicate) 533 throws IOException { 534 final Enumeration<ZipArchiveEntry> src = getEntriesInPhysicalOrder(); 535 while (src.hasMoreElements()) { 536 final ZipArchiveEntry entry = src.nextElement(); 537 if (predicate.test( entry)) { 538 target.addRawArchiveEntry(entry, getRawInputStream(entry)); 539 } 540 } 541 } 542 543 /** 544 * Returns an InputStream for reading the contents of the given entry. 545 * 546 * @param ze the entry to get the stream for. 547 * @return a stream to read the entry from. The returned stream 548 * implements {@link InputStreamStatistics}. 549 * @throws IOException if unable to create an input stream from the zipentry 550 */ 551 public InputStream getInputStream(final ZipArchiveEntry ze) 552 throws IOException { 553 if (!(ze instanceof Entry)) { 554 return null; 555 } 556 // cast validity is checked just above 557 ZipUtil.checkRequestedFeatures(ze); 558 final long start = getDataOffset(ze); 559 560 // doesn't get closed if the method is not supported - which 561 // should never happen because of the checkRequestedFeatures 562 // call above 563 final InputStream is = 564 new BufferedInputStream(createBoundedInputStream(start, ze.getCompressedSize())); //NOSONAR 565 switch (ZipMethod.getMethodByCode(ze.getMethod())) { 566 case STORED: 567 return new StoredStatisticsStream(is); 568 case UNSHRINKING: 569 return new UnshrinkingInputStream(is); 570 case IMPLODING: 571 return new ExplodingInputStream(ze.getGeneralPurposeBit().getSlidingDictionarySize(), 572 ze.getGeneralPurposeBit().getNumberOfShannonFanoTrees(), is); 573 case DEFLATED: 574 final Inflater inflater = new Inflater(true); 575 // Inflater with nowrap=true has this odd contract for a zero padding 576 // byte following the data stream; this used to be zlib's requirement 577 // and has been fixed a long time ago, but the contract persists so 578 // we comply. 579 // https://docs.oracle.com/javase/7/docs/api/java/util/zip/Inflater.html#Inflater(boolean) 580 return new InflaterInputStreamWithStatistics(new SequenceInputStream(is, new ByteArrayInputStream(ONE_ZERO_BYTE)), 581 inflater) { 582 @Override 583 public void close() throws IOException { 584 try { 585 super.close(); 586 } finally { 587 inflater.end(); 588 } 589 } 590 }; 591 case BZIP2: 592 return new BZip2CompressorInputStream(is); 593 case ENHANCED_DEFLATED: 594 return new Deflate64CompressorInputStream(is); 595 case AES_ENCRYPTED: 596 case EXPANDING_LEVEL_1: 597 case EXPANDING_LEVEL_2: 598 case EXPANDING_LEVEL_3: 599 case EXPANDING_LEVEL_4: 600 case JPEG: 601 case LZMA: 602 case PKWARE_IMPLODING: 603 case PPMD: 604 case TOKENIZATION: 605 case UNKNOWN: 606 case WAVPACK: 607 case XZ: 608 default: 609 throw new UnsupportedZipFeatureException(ZipMethod.getMethodByCode(ze.getMethod()), ze); 610 } 611 } 612 613 /** 614 * <p> 615 * Convenience method to return the entry's content as a String if isUnixSymlink() 616 * returns true for it, otherwise returns null. 617 * </p> 618 * 619 * <p>This method assumes the symbolic link's file name uses the 620 * same encoding that as been specified for this ZipFile.</p> 621 * 622 * @param entry ZipArchiveEntry object that represents the symbolic link 623 * @return entry's content as a String 624 * @throws IOException problem with content's input stream 625 * @since 1.5 626 */ 627 public String getUnixSymlink(final ZipArchiveEntry entry) throws IOException { 628 if (entry != null && entry.isUnixSymlink()) { 629 try (InputStream in = getInputStream(entry)) { 630 return zipEncoding.decode(IOUtils.toByteArray(in)); 631 } 632 } 633 return null; 634 } 635 636 /** 637 * Ensures that the close method of this zipfile is called when 638 * there are no more references to it. 639 * @see #close() 640 */ 641 @Override 642 protected void finalize() throws Throwable { 643 try { 644 if (!closed) { 645 System.err.println("Cleaning up unclosed ZipFile for archive " 646 + archiveName); 647 close(); 648 } 649 } finally { 650 super.finalize(); 651 } 652 } 653 654 /** 655 * Length of a "central directory" entry structure without file 656 * name, extra fields or comment. 657 */ 658 private static final int CFH_LEN = 659 /* version made by */ SHORT 660 /* version needed to extract */ + SHORT 661 /* general purpose bit flag */ + SHORT 662 /* compression method */ + SHORT 663 /* last mod file time */ + SHORT 664 /* last mod file date */ + SHORT 665 /* crc-32 */ + WORD 666 /* compressed size */ + WORD 667 /* uncompressed size */ + WORD 668 /* file name length */ + SHORT 669 /* extra field length */ + SHORT 670 /* file comment length */ + SHORT 671 /* disk number start */ + SHORT 672 /* internal file attributes */ + SHORT 673 /* external file attributes */ + WORD 674 /* relative offset of local header */ + WORD; 675 676 private static final long CFH_SIG = 677 ZipLong.getValue(ZipArchiveOutputStream.CFH_SIG); 678 679 /** 680 * Reads the central directory of the given archive and populates 681 * the internal tables with ZipArchiveEntry instances. 682 * 683 * <p>The ZipArchiveEntrys will know all data that can be obtained from 684 * the central directory alone, but not the data that requires the 685 * local file header or additional data to be read.</p> 686 * 687 * @return a map of zipentries that didn't have the language 688 * encoding flag set when read. 689 */ 690 private Map<ZipArchiveEntry, NameAndComment> populateFromCentralDirectory() 691 throws IOException { 692 final HashMap<ZipArchiveEntry, NameAndComment> noUTF8Flag = 693 new HashMap<>(); 694 695 positionAtCentralDirectory(); 696 697 ((Buffer)wordBbuf).rewind(); 698 IOUtils.readFully(archive, wordBbuf); 699 long sig = ZipLong.getValue(wordBuf); 700 701 if (sig != CFH_SIG && startsWithLocalFileHeader()) { 702 throw new IOException("Central directory is empty, can't expand" 703 + " corrupt archive."); 704 } 705 706 while (sig == CFH_SIG) { 707 readCentralDirectoryEntry(noUTF8Flag); 708 ((Buffer)wordBbuf).rewind(); 709 IOUtils.readFully(archive, wordBbuf); 710 sig = ZipLong.getValue(wordBuf); 711 } 712 return noUTF8Flag; 713 } 714 715 /** 716 * Reads an individual entry of the central directory, creats an 717 * ZipArchiveEntry from it and adds it to the global maps. 718 * 719 * @param noUTF8Flag map used to collect entries that don't have 720 * their UTF-8 flag set and whose name will be set by data read 721 * from the local file header later. The current entry may be 722 * added to this map. 723 */ 724 private void 725 readCentralDirectoryEntry(final Map<ZipArchiveEntry, NameAndComment> noUTF8Flag) 726 throws IOException { 727 ((Buffer)cfhBbuf).rewind(); 728 IOUtils.readFully(archive, cfhBbuf); 729 int off = 0; 730 final Entry ze = new Entry(); 731 732 final int versionMadeBy = ZipShort.getValue(cfhBuf, off); 733 off += SHORT; 734 ze.setVersionMadeBy(versionMadeBy); 735 ze.setPlatform((versionMadeBy >> BYTE_SHIFT) & NIBLET_MASK); 736 737 ze.setVersionRequired(ZipShort.getValue(cfhBuf, off)); 738 off += SHORT; // version required 739 740 final GeneralPurposeBit gpFlag = GeneralPurposeBit.parse(cfhBuf, off); 741 final boolean hasUTF8Flag = gpFlag.usesUTF8ForNames(); 742 final ZipEncoding entryEncoding = 743 hasUTF8Flag ? ZipEncodingHelper.UTF8_ZIP_ENCODING : zipEncoding; 744 if (hasUTF8Flag) { 745 ze.setNameSource(ZipArchiveEntry.NameSource.NAME_WITH_EFS_FLAG); 746 } 747 ze.setGeneralPurposeBit(gpFlag); 748 ze.setRawFlag(ZipShort.getValue(cfhBuf, off)); 749 750 off += SHORT; 751 752 //noinspection MagicConstant 753 ze.setMethod(ZipShort.getValue(cfhBuf, off)); 754 off += SHORT; 755 756 final long time = ZipUtil.dosToJavaTime(ZipLong.getValue(cfhBuf, off)); 757 ze.setTime(time); 758 off += WORD; 759 760 ze.setCrc(ZipLong.getValue(cfhBuf, off)); 761 off += WORD; 762 763 ze.setCompressedSize(ZipLong.getValue(cfhBuf, off)); 764 off += WORD; 765 766 ze.setSize(ZipLong.getValue(cfhBuf, off)); 767 off += WORD; 768 769 final int fileNameLen = ZipShort.getValue(cfhBuf, off); 770 off += SHORT; 771 772 final int extraLen = ZipShort.getValue(cfhBuf, off); 773 off += SHORT; 774 775 final int commentLen = ZipShort.getValue(cfhBuf, off); 776 off += SHORT; 777 778 final int diskStart = ZipShort.getValue(cfhBuf, off); 779 off += SHORT; 780 781 ze.setInternalAttributes(ZipShort.getValue(cfhBuf, off)); 782 off += SHORT; 783 784 ze.setExternalAttributes(ZipLong.getValue(cfhBuf, off)); 785 off += WORD; 786 787 final byte[] fileName = new byte[fileNameLen]; 788 IOUtils.readFully(archive, ByteBuffer.wrap(fileName)); 789 ze.setName(entryEncoding.decode(fileName), fileName); 790 791 // LFH offset, 792 ze.setLocalHeaderOffset(ZipLong.getValue(cfhBuf, off)); 793 // data offset will be filled later 794 entries.add(ze); 795 796 final byte[] cdExtraData = new byte[extraLen]; 797 IOUtils.readFully(archive, ByteBuffer.wrap(cdExtraData)); 798 ze.setCentralDirectoryExtra(cdExtraData); 799 800 setSizesAndOffsetFromZip64Extra(ze, diskStart); 801 802 final byte[] comment = new byte[commentLen]; 803 IOUtils.readFully(archive, ByteBuffer.wrap(comment)); 804 ze.setComment(entryEncoding.decode(comment)); 805 806 if (!hasUTF8Flag && useUnicodeExtraFields) { 807 noUTF8Flag.put(ze, new NameAndComment(fileName, comment)); 808 } 809 810 ze.setStreamContiguous(true); 811 } 812 813 /** 814 * If the entry holds a Zip64 extended information extra field, 815 * read sizes from there if the entry's sizes are set to 816 * 0xFFFFFFFFF, do the same for the offset of the local file 817 * header. 818 * 819 * <p>Ensures the Zip64 extra either knows both compressed and 820 * uncompressed size or neither of both as the internal logic in 821 * ExtraFieldUtils forces the field to create local header data 822 * even if they are never used - and here a field with only one 823 * size would be invalid.</p> 824 */ 825 private void setSizesAndOffsetFromZip64Extra(final ZipArchiveEntry ze, 826 final int diskStart) 827 throws IOException { 828 final Zip64ExtendedInformationExtraField z64 = 829 (Zip64ExtendedInformationExtraField) 830 ze.getExtraField(Zip64ExtendedInformationExtraField.HEADER_ID); 831 if (z64 != null) { 832 final boolean hasUncompressedSize = ze.getSize() == ZIP64_MAGIC; 833 final boolean hasCompressedSize = ze.getCompressedSize() == ZIP64_MAGIC; 834 final boolean hasRelativeHeaderOffset = 835 ze.getLocalHeaderOffset() == ZIP64_MAGIC; 836 z64.reparseCentralDirectoryData(hasUncompressedSize, 837 hasCompressedSize, 838 hasRelativeHeaderOffset, 839 diskStart == ZIP64_MAGIC_SHORT); 840 841 if (hasUncompressedSize) { 842 ze.setSize(z64.getSize().getLongValue()); 843 } else if (hasCompressedSize) { 844 z64.setSize(new ZipEightByteInteger(ze.getSize())); 845 } 846 847 if (hasCompressedSize) { 848 ze.setCompressedSize(z64.getCompressedSize().getLongValue()); 849 } else if (hasUncompressedSize) { 850 z64.setCompressedSize(new ZipEightByteInteger(ze.getCompressedSize())); 851 } 852 853 if (hasRelativeHeaderOffset) { 854 ze.setLocalHeaderOffset(z64.getRelativeHeaderOffset().getLongValue()); 855 } 856 } 857 } 858 859 /** 860 * Length of the "End of central directory record" - which is 861 * supposed to be the last structure of the archive - without file 862 * comment. 863 */ 864 static final int MIN_EOCD_SIZE = 865 /* end of central dir signature */ WORD 866 /* number of this disk */ + SHORT 867 /* number of the disk with the */ 868 /* start of the central directory */ + SHORT 869 /* total number of entries in */ 870 /* the central dir on this disk */ + SHORT 871 /* total number of entries in */ 872 /* the central dir */ + SHORT 873 /* size of the central directory */ + WORD 874 /* offset of start of central */ 875 /* directory with respect to */ 876 /* the starting disk number */ + WORD 877 /* zipfile comment length */ + SHORT; 878 879 /** 880 * Maximum length of the "End of central directory record" with a 881 * file comment. 882 */ 883 private static final int MAX_EOCD_SIZE = MIN_EOCD_SIZE 884 /* maximum length of zipfile comment */ + ZIP64_MAGIC_SHORT; 885 886 /** 887 * Offset of the field that holds the location of the first 888 * central directory entry inside the "End of central directory 889 * record" relative to the start of the "End of central directory 890 * record". 891 */ 892 private static final int CFD_LOCATOR_OFFSET = 893 /* end of central dir signature */ WORD 894 /* number of this disk */ + SHORT 895 /* number of the disk with the */ 896 /* start of the central directory */ + SHORT 897 /* total number of entries in */ 898 /* the central dir on this disk */ + SHORT 899 /* total number of entries in */ 900 /* the central dir */ + SHORT 901 /* size of the central directory */ + WORD; 902 903 /** 904 * Length of the "Zip64 end of central directory locator" - which 905 * should be right in front of the "end of central directory 906 * record" if one is present at all. 907 */ 908 private static final int ZIP64_EOCDL_LENGTH = 909 /* zip64 end of central dir locator sig */ WORD 910 /* number of the disk with the start */ 911 /* start of the zip64 end of */ 912 /* central directory */ + WORD 913 /* relative offset of the zip64 */ 914 /* end of central directory record */ + DWORD 915 /* total number of disks */ + WORD; 916 917 /** 918 * Offset of the field that holds the location of the "Zip64 end 919 * of central directory record" inside the "Zip64 end of central 920 * directory locator" relative to the start of the "Zip64 end of 921 * central directory locator". 922 */ 923 private static final int ZIP64_EOCDL_LOCATOR_OFFSET = 924 /* zip64 end of central dir locator sig */ WORD 925 /* number of the disk with the start */ 926 /* start of the zip64 end of */ 927 /* central directory */ + WORD; 928 929 /** 930 * Offset of the field that holds the location of the first 931 * central directory entry inside the "Zip64 end of central 932 * directory record" relative to the start of the "Zip64 end of 933 * central directory record". 934 */ 935 private static final int ZIP64_EOCD_CFD_LOCATOR_OFFSET = 936 /* zip64 end of central dir */ 937 /* signature */ WORD 938 /* size of zip64 end of central */ 939 /* directory record */ + DWORD 940 /* version made by */ + SHORT 941 /* version needed to extract */ + SHORT 942 /* number of this disk */ + WORD 943 /* number of the disk with the */ 944 /* start of the central directory */ + WORD 945 /* total number of entries in the */ 946 /* central directory on this disk */ + DWORD 947 /* total number of entries in the */ 948 /* central directory */ + DWORD 949 /* size of the central directory */ + DWORD; 950 951 /** 952 * Searches for either the "Zip64 end of central directory 953 * locator" or the "End of central dir record", parses 954 * it and positions the stream at the first central directory 955 * record. 956 */ 957 private void positionAtCentralDirectory() 958 throws IOException { 959 positionAtEndOfCentralDirectoryRecord(); 960 boolean found = false; 961 final boolean searchedForZip64EOCD = 962 archive.position() > ZIP64_EOCDL_LENGTH; 963 if (searchedForZip64EOCD) { 964 archive.position(archive.position() - ZIP64_EOCDL_LENGTH); 965 ((Buffer)wordBbuf).rewind(); 966 IOUtils.readFully(archive, wordBbuf); 967 found = Arrays.equals(ZipArchiveOutputStream.ZIP64_EOCD_LOC_SIG, 968 wordBuf); 969 } 970 if (!found) { 971 // not a ZIP64 archive 972 if (searchedForZip64EOCD) { 973 skipBytes(ZIP64_EOCDL_LENGTH - WORD); 974 } 975 positionAtCentralDirectory32(); 976 } else { 977 positionAtCentralDirectory64(); 978 } 979 } 980 981 /** 982 * Parses the "Zip64 end of central directory locator", 983 * finds the "Zip64 end of central directory record" using the 984 * parsed information, parses that and positions the stream at the 985 * first central directory record. 986 * 987 * Expects stream to be positioned right behind the "Zip64 988 * end of central directory locator"'s signature. 989 */ 990 private void positionAtCentralDirectory64() 991 throws IOException { 992 skipBytes(ZIP64_EOCDL_LOCATOR_OFFSET 993 - WORD /* signature has already been read */); 994 ((Buffer)dwordBbuf).rewind(); 995 IOUtils.readFully(archive, dwordBbuf); 996 archive.position(ZipEightByteInteger.getLongValue(dwordBuf)); 997 ((Buffer)wordBbuf).rewind(); 998 IOUtils.readFully(archive, wordBbuf); 999 if (!Arrays.equals(wordBuf, ZipArchiveOutputStream.ZIP64_EOCD_SIG)) { 1000 throw new ZipException("Archive's ZIP64 end of central " 1001 + "directory locator is corrupt."); 1002 } 1003 skipBytes(ZIP64_EOCD_CFD_LOCATOR_OFFSET 1004 - WORD /* signature has already been read */); 1005 ((Buffer)dwordBbuf).rewind(); 1006 IOUtils.readFully(archive, dwordBbuf); 1007 archive.position(ZipEightByteInteger.getLongValue(dwordBuf)); 1008 } 1009 1010 /** 1011 * Parses the "End of central dir record" and positions 1012 * the stream at the first central directory record. 1013 * 1014 * Expects stream to be positioned at the beginning of the 1015 * "End of central dir record". 1016 */ 1017 private void positionAtCentralDirectory32() 1018 throws IOException { 1019 skipBytes(CFD_LOCATOR_OFFSET); 1020 ((Buffer)wordBbuf).rewind(); 1021 IOUtils.readFully(archive, wordBbuf); 1022 archive.position(ZipLong.getValue(wordBuf)); 1023 } 1024 1025 /** 1026 * Searches for the and positions the stream at the start of the 1027 * "End of central dir record". 1028 */ 1029 private void positionAtEndOfCentralDirectoryRecord() 1030 throws IOException { 1031 final boolean found = tryToLocateSignature(MIN_EOCD_SIZE, MAX_EOCD_SIZE, 1032 ZipArchiveOutputStream.EOCD_SIG); 1033 if (!found) { 1034 throw new ZipException("Archive is not a ZIP archive"); 1035 } 1036 } 1037 1038 /** 1039 * Searches the archive backwards from minDistance to maxDistance 1040 * for the given signature, positions the RandomaccessFile right 1041 * at the signature if it has been found. 1042 */ 1043 private boolean tryToLocateSignature(final long minDistanceFromEnd, 1044 final long maxDistanceFromEnd, 1045 final byte[] sig) throws IOException { 1046 boolean found = false; 1047 long off = archive.size() - minDistanceFromEnd; 1048 final long stopSearching = 1049 Math.max(0L, archive.size() - maxDistanceFromEnd); 1050 if (off >= 0) { 1051 for (; off >= stopSearching; off--) { 1052 archive.position(off); 1053 try { 1054 ((Buffer)wordBbuf).rewind(); 1055 IOUtils.readFully(archive, wordBbuf); 1056 ((Buffer)wordBbuf).flip(); 1057 } catch (EOFException ex) { // NOSONAR 1058 break; 1059 } 1060 int curr = wordBbuf.get(); 1061 if (curr == sig[POS_0]) { 1062 curr = wordBbuf.get(); 1063 if (curr == sig[POS_1]) { 1064 curr = wordBbuf.get(); 1065 if (curr == sig[POS_2]) { 1066 curr = wordBbuf.get(); 1067 if (curr == sig[POS_3]) { 1068 found = true; 1069 break; 1070 } 1071 } 1072 } 1073 } 1074 } 1075 } 1076 if (found) { 1077 archive.position(off); 1078 } 1079 return found; 1080 } 1081 1082 /** 1083 * Skips the given number of bytes or throws an EOFException if 1084 * skipping failed. 1085 */ 1086 private void skipBytes(final int count) throws IOException { 1087 long currentPosition = archive.position(); 1088 long newPosition = currentPosition + count; 1089 if (newPosition > archive.size()) { 1090 throw new EOFException(); 1091 } 1092 archive.position(newPosition); 1093 } 1094 1095 /** 1096 * Number of bytes in local file header up to the "length of 1097 * file name" entry. 1098 */ 1099 private static final long LFH_OFFSET_FOR_FILENAME_LENGTH = 1100 /* local file header signature */ WORD 1101 /* version needed to extract */ + SHORT 1102 /* general purpose bit flag */ + SHORT 1103 /* compression method */ + SHORT 1104 /* last mod file time */ + SHORT 1105 /* last mod file date */ + SHORT 1106 /* crc-32 */ + WORD 1107 /* compressed size */ + WORD 1108 /* uncompressed size */ + (long) WORD; 1109 1110 /** 1111 * Walks through all recorded entries and adds the data available 1112 * from the local file header. 1113 * 1114 * <p>Also records the offsets for the data to read from the 1115 * entries.</p> 1116 */ 1117 private void resolveLocalFileHeaderData(final Map<ZipArchiveEntry, NameAndComment> 1118 entriesWithoutUTF8Flag) 1119 throws IOException { 1120 for (final ZipArchiveEntry zipArchiveEntry : entries) { 1121 // entries is filled in populateFromCentralDirectory and 1122 // never modified 1123 final Entry ze = (Entry) zipArchiveEntry; 1124 int[] lens = setDataOffset(ze); 1125 final int fileNameLen = lens[0]; 1126 final int extraFieldLen = lens[1]; 1127 skipBytes(fileNameLen); 1128 final byte[] localExtraData = new byte[extraFieldLen]; 1129 IOUtils.readFully(archive, ByteBuffer.wrap(localExtraData)); 1130 ze.setExtra(localExtraData); 1131 1132 if (entriesWithoutUTF8Flag.containsKey(ze)) { 1133 final NameAndComment nc = entriesWithoutUTF8Flag.get(ze); 1134 ZipUtil.setNameAndCommentFromExtraFields(ze, nc.name, 1135 nc.comment); 1136 } 1137 } 1138 } 1139 1140 private void fillNameMap() { 1141 for (final ZipArchiveEntry ze : entries) { 1142 // entries is filled in populateFromCentralDirectory and 1143 // never modified 1144 final String name = ze.getName(); 1145 LinkedList<ZipArchiveEntry> entriesOfThatName = nameMap.get(name); 1146 if (entriesOfThatName == null) { 1147 entriesOfThatName = new LinkedList<>(); 1148 nameMap.put(name, entriesOfThatName); 1149 } 1150 entriesOfThatName.addLast(ze); 1151 } 1152 } 1153 1154 private int[] setDataOffset(ZipArchiveEntry ze) throws IOException { 1155 final long offset = ze.getLocalHeaderOffset(); 1156 archive.position(offset + LFH_OFFSET_FOR_FILENAME_LENGTH); 1157 ((Buffer)wordBbuf).rewind(); 1158 IOUtils.readFully(archive, wordBbuf); 1159 ((Buffer)wordBbuf).flip(); 1160 wordBbuf.get(shortBuf); 1161 final int fileNameLen = ZipShort.getValue(shortBuf); 1162 wordBbuf.get(shortBuf); 1163 final int extraFieldLen = ZipShort.getValue(shortBuf); 1164 ze.setDataOffset(offset + LFH_OFFSET_FOR_FILENAME_LENGTH 1165 + SHORT + SHORT + fileNameLen + extraFieldLen); 1166 return new int[] { fileNameLen, extraFieldLen }; 1167 } 1168 1169 private long getDataOffset(ZipArchiveEntry ze) throws IOException { 1170 long s = ze.getDataOffset(); 1171 if (s == EntryStreamOffsets.OFFSET_UNKNOWN) { 1172 setDataOffset(ze); 1173 return ze.getDataOffset(); 1174 } 1175 return s; 1176 } 1177 1178 /** 1179 * Checks whether the archive starts with a LFH. If it doesn't, 1180 * it may be an empty archive. 1181 */ 1182 private boolean startsWithLocalFileHeader() throws IOException { 1183 archive.position(0); 1184 ((Buffer)wordBbuf).rewind(); 1185 IOUtils.readFully(archive, wordBbuf); 1186 return Arrays.equals(wordBuf, ZipArchiveOutputStream.LFH_SIG); 1187 } 1188 1189 /** 1190 * Creates new BoundedInputStream, according to implementation of 1191 * underlying archive channel. 1192 */ 1193 private BoundedInputStream createBoundedInputStream(long start, long remaining) { 1194 return archive instanceof FileChannel ? 1195 new BoundedFileChannelInputStream(start, remaining) : 1196 new BoundedInputStream(start, remaining); 1197 } 1198 1199 /** 1200 * InputStream that delegates requests to the underlying 1201 * SeekableByteChannel, making sure that only bytes from a certain 1202 * range can be read. 1203 */ 1204 private class BoundedInputStream extends InputStream { 1205 private ByteBuffer singleByteBuffer; 1206 private final long end; 1207 private long loc; 1208 1209 BoundedInputStream(final long start, final long remaining) { 1210 this.end = start+remaining; 1211 if (this.end < start) { 1212 // check for potential vulnerability due to overflow 1213 throw new IllegalArgumentException("Invalid length of stream at offset="+start+", length="+remaining); 1214 } 1215 loc = start; 1216 } 1217 1218 @Override 1219 public synchronized int read() throws IOException { 1220 if (loc >= end) { 1221 return -1; 1222 } 1223 if (singleByteBuffer == null) { 1224 singleByteBuffer = ByteBuffer.allocate(1); 1225 } 1226 else { 1227 ((Buffer)singleByteBuffer).rewind(); 1228 } 1229 int read = read(loc, singleByteBuffer); 1230 if (read < 0) { 1231 return read; 1232 } 1233 loc++; 1234 return singleByteBuffer.get() & 0xff; 1235 } 1236 1237 @Override 1238 public synchronized int read(final byte[] b, final int off, int len) throws IOException { 1239 if (len <= 0) { 1240 return 0; 1241 } 1242 1243 if (len > end-loc) { 1244 if (loc >= end) { 1245 return -1; 1246 } 1247 len = (int)(end-loc); 1248 } 1249 1250 ByteBuffer buf; 1251 buf = ByteBuffer.wrap(b, off, len); 1252 int ret = read(loc, buf); 1253 if (ret > 0) { 1254 loc += ret; 1255 return ret; 1256 } 1257 return ret; 1258 } 1259 1260 protected int read(long pos, ByteBuffer buf) throws IOException { 1261 int read; 1262 synchronized (archive) { 1263 archive.position(pos); 1264 read = archive.read(buf); 1265 } 1266 ((Buffer)buf).flip(); 1267 return read; 1268 } 1269 } 1270 1271 /** 1272 * Lock-free implementation of BoundedInputStream. The 1273 * implementation uses positioned reads on the underlying archive 1274 * file channel and therefore performs significantly faster in 1275 * concurrent environment. 1276 */ 1277 private class BoundedFileChannelInputStream extends BoundedInputStream { 1278 private final FileChannel archive; 1279 1280 BoundedFileChannelInputStream(final long start, final long remaining) { 1281 super(start, remaining); 1282 archive = (FileChannel)ZipFile.this.archive; 1283 } 1284 1285 @Override 1286 protected int read(long pos, ByteBuffer buf) throws IOException { 1287 int read = archive.read(buf, pos); 1288 ((Buffer)buf).flip(); 1289 return read; 1290 } 1291 } 1292 1293 private static final class NameAndComment { 1294 private final byte[] name; 1295 private final byte[] comment; 1296 private NameAndComment(final byte[] name, final byte[] comment) { 1297 this.name = name; 1298 this.comment = comment; 1299 } 1300 } 1301 1302 /** 1303 * Compares two ZipArchiveEntries based on their offset within the archive. 1304 * 1305 * <p>Won't return any meaningful results if one of the entries 1306 * isn't part of the archive at all.</p> 1307 * 1308 * @since 1.1 1309 */ 1310 private final Comparator<ZipArchiveEntry> offsetComparator = 1311 new Comparator<ZipArchiveEntry>() { 1312 @Override 1313 public int compare(final ZipArchiveEntry e1, final ZipArchiveEntry e2) { 1314 if (e1 == e2) { 1315 return 0; 1316 } 1317 1318 final Entry ent1 = e1 instanceof Entry ? (Entry) e1 : null; 1319 final Entry ent2 = e2 instanceof Entry ? (Entry) e2 : null; 1320 if (ent1 == null) { 1321 return 1; 1322 } 1323 if (ent2 == null) { 1324 return -1; 1325 } 1326 final long val = (ent1.getLocalHeaderOffset() 1327 - ent2.getLocalHeaderOffset()); 1328 return val == 0 ? 0 : val < 0 ? -1 : +1; 1329 } 1330 }; 1331 1332 /** 1333 * Extends ZipArchiveEntry to store the offset within the archive. 1334 */ 1335 private static class Entry extends ZipArchiveEntry { 1336 1337 Entry() { 1338 } 1339 1340 @Override 1341 public int hashCode() { 1342 return 3 * super.hashCode() 1343 + (int) getLocalHeaderOffset()+(int)(getLocalHeaderOffset()>>32); 1344 } 1345 1346 @Override 1347 public boolean equals(final Object other) { 1348 if (super.equals(other)) { 1349 // super.equals would return false if other were not an Entry 1350 final Entry otherEntry = (Entry) other; 1351 return getLocalHeaderOffset() 1352 == otherEntry.getLocalHeaderOffset() 1353 && super.getDataOffset() 1354 == otherEntry.getDataOffset(); 1355 } 1356 return false; 1357 } 1358 } 1359 1360 private static class StoredStatisticsStream extends CountingInputStream implements InputStreamStatistics { 1361 StoredStatisticsStream(InputStream in) { 1362 super(in); 1363 } 1364 1365 @Override 1366 public long getCompressedCount() { 1367 return super.getBytesRead(); 1368 } 1369 1370 @Override 1371 public long getUncompressedCount() { 1372 return getCompressedCount(); 1373 } 1374 } 1375}