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