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