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