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 }