diff options
-rw-r--r-- | expectations/knownfailures.txt | 9 | ||||
-rw-r--r-- | luni/src/main/java/java/util/zip/Zip64.java | 401 | ||||
-rw-r--r-- | luni/src/main/java/java/util/zip/ZipEntry.java | 17 | ||||
-rw-r--r-- | luni/src/main/java/java/util/zip/ZipFile.java | 95 | ||||
-rw-r--r-- | luni/src/main/java/java/util/zip/ZipInputStream.java | 33 | ||||
-rw-r--r-- | luni/src/main/java/java/util/zip/ZipOutputStream.java | 279 | ||||
-rw-r--r-- | luni/src/main/java/libcore/util/CountingOutputStream.java | 59 | ||||
-rw-r--r-- | luni/src/test/java/libcore/java/util/zip/ZipEntryTest.java | 35 | ||||
-rw-r--r-- | luni/src/test/java/libcore/java/util/zip/ZipFileTest.java | 60 |
9 files changed, 879 insertions, 109 deletions
diff --git a/expectations/knownfailures.txt b/expectations/knownfailures.txt index c6d3dd7..77d301e 100644 --- a/expectations/knownfailures.txt +++ b/expectations/knownfailures.txt @@ -1492,5 +1492,14 @@ names: [ "org.apache.harmony.tests.java.nio.charset.CharsetTest#testForName_withProviderWithRecursiveCall" ] +}, +{ + description: "Zip64 tests take too long to execute and need more than 5GB of space to run.", + result: EXEC_FAILED, + names: [ + "libcore.java.util.zip.ZipFileTest#testZip64Support_largeNumberOfEntries", + "libcore.java.util.zip.ZipFileTest#testZip64Support_totalLargerThan4G", + "libcore.java.util.zip.ZipFileTest#testZip64Support_hugeEntry" + ] } ] diff --git a/luni/src/main/java/java/util/zip/Zip64.java b/luni/src/main/java/java/util/zip/Zip64.java new file mode 100644 index 0000000..9be3d1c --- /dev/null +++ b/luni/src/main/java/java/util/zip/Zip64.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package java.util.zip; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static java.util.zip.ZipOutputStream.writeIntAsUint16; +import static java.util.zip.ZipOutputStream.writeLongAsUint32; +import static java.util.zip.ZipOutputStream.writeLongAsUint64; + +/** + * @hide + */ +public class Zip64 { + + /* Non instantiable */ + private Zip64() {} + + /** + * The maximum supported entry / archive size for standard (non zip64) entries and archives. + */ + static final long MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE = 0x00000000ffffffffL; + + /** + * The header ID of the zip64 extended info header. This value is used to identify + * zip64 data in the "extra" field in the file headers. + */ + private static final short ZIP64_EXTENDED_INFO_HEADER_ID = 0x0001; + + /** + * The minimum size of the zip64 extended info header. This excludes the 2 byte header ID + * and the 2 byte size. + */ + private static final int ZIP64_EXTENDED_INFO_MIN_SIZE = 28; + + /* + * Size (in bytes) of the zip64 end of central directory locator. This will be located + * immediately before the end of central directory record if a given zipfile is in the + * zip64 format. + */ + private static final int ZIP64_LOCATOR_SIZE = 20; + + /** + * The zip64 end of central directory locator signature (4 bytes wide). + */ + private static final int ZIP64_LOCATOR_SIGNATURE = 0x07064b50; + + /** + * The zip64 end of central directory record singature (4 bytes wide). + */ + private static final int ZIP64_EOCD_RECORD_SIGNATURE = 0x06064b50; + + /** + * The "effective" size of the zip64 eocd record. This excludes the fields that + * are proprietary, signature, or fields we aren't interested in. We include the + * following (contiguous) fields in this calculation : + * - disk number (4 bytes) + * - disk with start of central directory (4 bytes) + * - number of central directory entries on this disk (8 bytes) + * - total number of central directory entries (8 bytes) + * - size of the central directory (8 bytes) + * - offset of the start of the central directory (8 bytes) + */ + private static final int ZIP64_EOCD_RECORD_EFFECTIVE_SIZE = 40; + + /** + * Parses the zip64 end of central directory record locator. The locator + * must be placed immediately before the end of central directory (eocd) record + * starting at {@code eocdOffset}. + * + * The position of the file cursor for {@code raf} after a call to this method + * is undefined an callers must reposition it after each call to this method. + */ + public static long parseZip64EocdRecordLocator(RandomAccessFile raf, long eocdOffset) + throws IOException { + // The spec stays curiously silent about whether a zip file with an EOCD record, + // a zip64 locator and a zip64 eocd record is considered "empty". In our implementation, + // we parse all records and read the counts from them instead of drawing any size or + // layout based information. + if (eocdOffset > ZIP64_LOCATOR_SIZE) { + raf.seek(eocdOffset - ZIP64_LOCATOR_SIZE); + if (Integer.reverseBytes(raf.readInt()) == ZIP64_LOCATOR_SIGNATURE) { + byte[] zip64EocdLocator = new byte[ZIP64_LOCATOR_SIZE - 4]; + raf.readFully(zip64EocdLocator); + ByteBuffer buf = ByteBuffer.wrap(zip64EocdLocator).order(ByteOrder.LITTLE_ENDIAN); + + final int diskWithCentralDir = buf.getInt(); + final long zip64EocdRecordOffset = buf.getLong(); + final int numDisks = buf.getInt(); + + if (numDisks != 1 || diskWithCentralDir != 0) { + throw new ZipException("Spanned archives not supported"); + } + + return zip64EocdRecordOffset; + } + } + + return -1; + } + + public static ZipFile.EocdRecord parseZip64EocdRecord(RandomAccessFile raf, + long eocdRecordOffset, int commentLength) throws IOException { + raf.seek(eocdRecordOffset); + final int signature = Integer.reverseBytes(raf.readInt()); + if (signature != ZIP64_EOCD_RECORD_SIGNATURE) { + throw new ZipException("Invalid zip64 eocd record offset, sig=" + + Integer.toHexString(signature) + " offset=" + eocdRecordOffset); + } + + // The zip64 eocd record specifies its own size as an 8 byte integral type. It is variable + // length because of the "zip64 extensible data sector" but that field is reserved for + // pkware's proprietary use. We therefore disregard it altogether and treat the end of + // central directory structure as fixed length. + // + // We also skip "version made by" (2 bytes) and "version needed to extract" (2 bytes) + // fields. We perform additional validation at the ZipEntry level, where applicable. + // + // That's a total of 12 bytes to skip + raf.skipBytes(12); + + byte[] zip64Eocd = new byte[ZIP64_EOCD_RECORD_EFFECTIVE_SIZE]; + raf.readFully(zip64Eocd); + + ByteBuffer buf = ByteBuffer.wrap(zip64Eocd).order(ByteOrder.LITTLE_ENDIAN); + try { + int diskNumber = buf.getInt(); + int diskWithCentralDirStart = buf.getInt(); + long numEntries = buf.getLong(); + long totalNumEntries = buf.getLong(); + buf.getLong(); // Ignore the size of the central directory + long centralDirOffset = buf.getLong(); + + if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDirStart != 0) { + throw new ZipException("Spanned archives not supported :" + + " numEntries=" + numEntries + ", totalNumEntries=" + totalNumEntries + + ", diskNumber=" + diskNumber + ", diskWithCentralDirStart=" + + diskWithCentralDirStart); + } + + return new ZipFile.EocdRecord(numEntries, centralDirOffset, commentLength); + } catch (BufferUnderflowException bue) { + ZipException zipException = new ZipException("Error parsing zip64 eocd record."); + zipException.initCause(bue); + throw zipException; + } + } + + /** + * Parse the zip64 extended info record from the extras present in {@code ze}. + * + * If {@code fromCentralDirectory} is true, we assume we're parsing a central directory + * record. We assume a local file header otherwise. The difference between the two is that + * a central directory entry is required to be complete, whereas a local file header isn't. + * This is due to the presence of an optional data descriptor after the file content. + * + * @return {@code} true iff. a zip64 extended info record was found. + */ + public static boolean parseZip64ExtendedInfo(ZipEntry ze, boolean fromCentralDirectory) + throws ZipException { + int extendedInfoSize = -1; + int extendedInfoStart = -1; + // If this file contains a zip64 central directory locator, entries might + // optionally contain a zip64 extended information extra entry. + if (ze.extra != null && ze.extra.length > 0) { + // Extensible data fields are of the form header1+data1 + header2+data2 and so + // on, where each header consists of a 2 byte header ID followed by a 2 byte size. + // We need to iterate through the entire list of headers to find the header ID + // for the zip64 extended information extra field (0x0001). + final ByteBuffer buf = ByteBuffer.wrap(ze.extra).order(ByteOrder.LITTLE_ENDIAN); + extendedInfoSize = getZip64ExtendedInfoSize(buf); + if (extendedInfoSize != -1) { + extendedInfoStart = buf.position(); + try { + if (extendedInfoSize < ZIP64_EXTENDED_INFO_MIN_SIZE) { + throw new ZipException("Invalid zip64 extended info size: " + extendedInfoSize); + } + + // The size & compressed size only make sense in the central directory *or* if + // we know them beforehand. If we don't know them beforehand, they're stored in + // the data descriptor and should be read from there. + if (fromCentralDirectory || (ze.getMethod() == ZipEntry.STORED)) { + final long zip64Size = buf.getLong(); + if (ze.size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) { + ze.size = zip64Size; + } + + final long zip64CompressedSize = buf.getLong(); + if (ze.compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) { + ze.compressedSize = zip64CompressedSize; + } + } + + // The local header offset is significant only in the central directory. It makes no + // sense within the local header itself. + if (fromCentralDirectory) { + final long zip64LocalHeaderRelOffset = buf.getLong(); + if (ze.localHeaderRelOffset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) { + ze.localHeaderRelOffset = zip64LocalHeaderRelOffset; + } + } + } catch (BufferUnderflowException bue) { + ZipException zipException = new ZipException("Error parsing extendend info "); + zipException.initCause(bue); + throw zipException; + } + } + } + + // This entry doesn't contain a zip64 extended information data entry header. + // We have to check that the compressedSize / size / localHeaderRelOffset values + // are valid and don't require the presence of the extended header. + if (extendedInfoSize == -1) { + if (ze.compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE || + ze.size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE || + ze.localHeaderRelOffset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) { + throw new ZipException("File contains no zip64 extended information: " + + "name=" + ze.name + "compressedSize=" + ze.compressedSize + ", size=" + + ze.size + ", localHeader=" + ze.localHeaderRelOffset); + } + + return false; + } else { + // If we're parsed the zip64 extended info header, we remove it from the extras + // so that applications that set their own extras will see the data they set. + + // This is an unfortunate workaround needed due to a gap in the spec. The spec demands + // that extras are present in the "extensible" format, which means that each extra field + // must be prefixed with a header ID and a length. However, earlier versions of the spec + // made no mention of this, nor did any existing API enforce it. This means users could + // set "free form" extras without caring very much whether the implementation wanted to + // extend or add to them. + + // The start of the extended info header. + final int extendedInfoHeaderStart = extendedInfoStart - 4; + // The total size of the extended info, including the header. + final int extendedInfoTotalSize = extendedInfoSize + 4; + + final int extrasLen = ze.extra.length - extendedInfoTotalSize; + byte[] extrasWithoutZip64 = new byte[extrasLen]; + + System.arraycopy(ze.extra, 0, extrasWithoutZip64, 0, extendedInfoHeaderStart); + System.arraycopy(ze.extra, extendedInfoHeaderStart + extendedInfoTotalSize, + extrasWithoutZip64, extendedInfoHeaderStart, (extrasLen - extendedInfoHeaderStart)); + + ze.extra = extrasWithoutZip64; + return true; + } + } + + /** + * Appends a zip64 extended info record to the extras contained in {@code ze}. If {@code ze} + * contains no extras, a new extras array is created. + */ + public static void insertZip64ExtendedInfoToExtras(ZipEntry ze) throws ZipException { + final byte[] output; + // We add 4 to ZIP64_EXTENDED_INFO_MIN_SIZE to account for the 2 byte header and length. + final int extendedInfoSize = ZIP64_EXTENDED_INFO_MIN_SIZE + 4; + if (ze.extra == null) { + output = new byte[extendedInfoSize]; + } else { + // If the existing extras are already too big, we have no choice but to throw + // an error. + if (ze.extra.length + extendedInfoSize > 65535) { + throw new ZipException("No space in extras for zip64 extended entry info"); + } + + // We copy existing extras over and put the zip64 extended info at the beginning. This + // is to avoid breakages in the presence of "old style" extras which don't contain + // headers and lengths. The spec is again silent about these inconsistencies. + // + // This means that people that for ZipOutputStream users, the value ZipEntry.getExtra + // after an entry is written will be different from before. This shouldn't be an issue + // in practice. + output = new byte[ze.extra.length + ZIP64_EXTENDED_INFO_MIN_SIZE + 4]; + System.arraycopy(ze.extra, 0, output, ZIP64_EXTENDED_INFO_MIN_SIZE + 4, ze.extra.length); + } + + ByteBuffer bb = ByteBuffer.wrap(output).order(ByteOrder.LITTLE_ENDIAN); + bb.putShort(ZIP64_EXTENDED_INFO_HEADER_ID); + bb.putShort((short) ZIP64_EXTENDED_INFO_MIN_SIZE); + + if (ze.getMethod() == ZipEntry.STORED) { + bb.putLong(ze.size); + bb.putLong(ze.compressedSize); + } else { + // Store these fields in the data descriptor instead. + bb.putLong(0); // size. + bb.putLong(0); // compressed size. + } + + // The offset is only relevant in the central directory entry, but we write it out here + // anyway, since we know what it is. + bb.putLong(ze.localHeaderRelOffset); + bb.putInt(0); // disk number + + ze.extra = output; + } + + /** + * Returns the size of the extended info record if {@code extras} contains a zip64 extended info + * record, {@code -1} otherwise. The buffer will be positioned at the start of the extended info + * record. + */ + private static int getZip64ExtendedInfoSize(ByteBuffer extras) { + try { + while (extras.hasRemaining()) { + final int headerId = extras.getShort() & 0xffff; + final int length = extras.getShort() & 0xffff; + if (headerId == ZIP64_EXTENDED_INFO_HEADER_ID) { + if (extras.remaining() >= length) { + return length; + } else { + return -1; + } + } else { + extras.position(extras.position() + length); + } + } + + return -1; + } catch (BufferUnderflowException bue) { + // We'll underflow if we have an incomplete header in our extras. + return -1; + } catch (IllegalArgumentException iae) { + // ByteBuffer.position() will throw if we have a truncated extra or + // an invalid length in the header. + return -1; + } + } + + /** + * Copy the size, compressed size and local header offset fields from {@code ze} to + * inside {@code ze}'s extended info record. This is additional step is necessary when + * we could calculate the correct sizes only after writing out the entry. In this case, + * the local file header would not contain real sizes, and they would be present in the + * data descriptor and the central directory only. + */ + public static void refreshZip64ExtendedInfo(ZipEntry ze) { + if (ze.extra == null || ze.extra.length < ZIP64_EXTENDED_INFO_MIN_SIZE) { + throw new IllegalStateException("Zip64 entry has no available extras: " + ze); + } + + + ByteBuffer buf = ByteBuffer.wrap(ze.extra).order(ByteOrder.LITTLE_ENDIAN); + if (getZip64ExtendedInfoSize(buf) == -1) { + throw new IllegalStateException( + "Zip64 entry extras has no zip64 extended info record: " + ze); + } + + buf.putLong(ze.size); + buf.putLong(ze.compressedSize); + buf.putLong(ze.localHeaderRelOffset); + buf.putInt(0); // disk number. + } + + public static void writeZip64EocdRecordAndLocator(ByteArrayOutputStream baos, + long numEntries, long offset, long cDirSize) throws IOException { + // Step 1: Write out the zip64 EOCD record. + writeLongAsUint32(baos, ZIP64_EOCD_RECORD_SIGNATURE); + // The size of the zip64 eocd record. This is the effective size + the + // size of the "version made by" (2 bytes) and the "version needed to extract" (2 bytes) + // fields. + writeLongAsUint64(baos, ZIP64_EOCD_RECORD_EFFECTIVE_SIZE + 4); + // TODO: What values should we put here ? The pre-zip64 values we've chosen don't + // seem to make much sense either. + writeIntAsUint16(baos, 20); + writeIntAsUint16(baos, 20); + writeLongAsUint32(baos, 0L); // number of disk + writeLongAsUint32(baos, 0L); // number of disk with start of central dir. + writeLongAsUint64(baos, numEntries); // number of entries in this disk. + writeLongAsUint64(baos, numEntries); // number of entries in total. + writeLongAsUint64(baos, cDirSize); // size of the central directory. + writeLongAsUint64(baos, offset); // offset of the central directory wrt. this file. + + // Step 2: Write out the zip64 EOCD record locator. + writeLongAsUint32(baos, ZIP64_LOCATOR_SIGNATURE); + writeLongAsUint32(baos, 0); // number of disk with start of central dir. + writeLongAsUint64(baos, offset + cDirSize); // offset of the eocd record wrt. this file. + writeLongAsUint32(baos, 1); // total number of disks. + } +} diff --git a/luni/src/main/java/java/util/zip/ZipEntry.java b/luni/src/main/java/java/util/zip/ZipEntry.java index 771af83..d6781b8 100644 --- a/luni/src/main/java/java/util/zip/ZipEntry.java +++ b/luni/src/main/java/java/util/zip/ZipEntry.java @@ -268,17 +268,10 @@ public class ZipEntry implements ZipConstants, Cloneable { /** * Sets the uncompressed size of this {@code ZipEntry}. * - * @param value - * the uncompressed size for this entry. - * @throws IllegalArgumentException - * if {@code value} < 0 or {@code value} > 0xFFFFFFFFL. + * @param value the uncompressed size for this entry. */ public void setSize(long value) { - if (value >= 0 && value <= 0xFFFFFFFFL) { - size = value; - } else { - throw new IllegalArgumentException("Bad size: " + value); - } + size = value; } /** @@ -380,7 +373,7 @@ public class ZipEntry implements ZipConstants, Cloneable { * On exit, "in" will be positioned at the start of the next entry * in the Central Directory. */ - ZipEntry(byte[] cdeHdrBuf, InputStream cdStream, Charset defaultCharset) throws IOException { + ZipEntry(byte[] cdeHdrBuf, InputStream cdStream, Charset defaultCharset, boolean isZip64) throws IOException { Streams.readFully(cdStream, cdeHdrBuf, 0, cdeHdrBuf.length); BufferIterator it = HeapBufferIterator.iterator(cdeHdrBuf, 0, cdeHdrBuf.length, @@ -439,6 +432,10 @@ public class ZipEntry implements ZipConstants, Cloneable { Streams.readFully(cdStream, commentBytes, 0, commentByteCount); comment = new String(commentBytes, 0, commentBytes.length, charset); } + + if (isZip64) { + Zip64.parseZip64ExtendedInfo(this, true /* from central directory */); + } } private static boolean containsNulByte(byte[] bytes) { diff --git a/luni/src/main/java/java/util/zip/ZipFile.java b/luni/src/main/java/java/util/zip/ZipFile.java index b44156e..307e7fe 100644 --- a/luni/src/main/java/java/util/zip/ZipFile.java +++ b/luni/src/main/java/java/util/zip/ZipFile.java @@ -107,6 +107,18 @@ public class ZipFile implements Closeable, ZipConstants { private final CloseGuard guard = CloseGuard.get(); + static class EocdRecord { + final long numEntries; + final long centralDirOffset; + final int commentLength; + + EocdRecord(long numEntries, long centralDirOffset, int commentLength) { + this.numEntries = numEntries; + this.centralDirOffset = centralDirOffset; + this.commentLength = commentLength; + } + } + /** * Constructs a new {@code ZipFile} allowing read access to the contents of the given file. * @@ -390,9 +402,11 @@ public class ZipFile implements Closeable, ZipConstants { stopOffset = 0; } + long eocdOffset; while (true) { raf.seek(scanOffset); if (Integer.reverseBytes(raf.readInt()) == ENDSIG) { + eocdOffset = scanOffset; break; } @@ -402,41 +416,35 @@ public class ZipFile implements Closeable, ZipConstants { } } - // Read the End Of Central Directory. ENDHDR includes the signature bytes, - // which we've already read. - byte[] eocd = new byte[ENDHDR - 4]; - raf.readFully(eocd); + final long zip64EocdRecordOffset = Zip64.parseZip64EocdRecordLocator(raf, eocdOffset); - // Pull out the information we need. - BufferIterator it = HeapBufferIterator.iterator(eocd, 0, eocd.length, ByteOrder.LITTLE_ENDIAN); - int diskNumber = it.readShort() & 0xffff; - int diskWithCentralDir = it.readShort() & 0xffff; - int numEntries = it.readShort() & 0xffff; - int totalNumEntries = it.readShort() & 0xffff; - it.skip(4); // Ignore centralDirSize. - long centralDirOffset = ((long) it.readInt()) & 0xffffffffL; - int commentLength = it.readShort() & 0xffff; - - if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDir != 0) { - throw new ZipException("Spanned archives not supported"); - } - - if (commentLength > 0) { - byte[] commentBytes = new byte[commentLength]; + // Seek back past the eocd signature so that we can continue with our search. + // Note that we add 4 bytes to the offset to skip past the signature. + EocdRecord record = parseEocdRecord(raf, eocdOffset + 4, (zip64EocdRecordOffset != -1) /* isZip64 */); + // Read the comment now to avoid an additional seek. We also know the commentLength + // won't change because that information isn't present in the zip64 eocd record. + if (record.commentLength > 0) { + byte[] commentBytes = new byte[record.commentLength]; raf.readFully(commentBytes); comment = new String(commentBytes, 0, commentBytes.length, StandardCharsets.UTF_8); } + // We have a zip64 eocd record : use that for getting the information we need. + if (zip64EocdRecordOffset != -1) { + record = Zip64.parseZip64EocdRecord(raf, zip64EocdRecordOffset, record.commentLength); + } + // Seek to the first CDE and read all entries. // We have to do this now (from the constructor) rather than lazily because the // public API doesn't allow us to throw IOException except from the constructor // or from getInputStream. - RAFStream rafStream = new RAFStream(raf, centralDirOffset); + RAFStream rafStream = new RAFStream(raf, record.centralDirOffset); BufferedInputStream bufferedStream = new BufferedInputStream(rafStream, 4096); byte[] hdrBuf = new byte[CENHDR]; // Reuse the same buffer for each entry. - for (int i = 0; i < numEntries; ++i) { - ZipEntry newEntry = new ZipEntry(hdrBuf, bufferedStream, StandardCharsets.UTF_8); - if (newEntry.localHeaderRelOffset >= centralDirOffset) { + for (long i = 0; i < record.numEntries; ++i) { + ZipEntry newEntry = new ZipEntry(hdrBuf, bufferedStream, StandardCharsets.UTF_8, + (zip64EocdRecordOffset != -1) /* isZip64 */); + if (newEntry.localHeaderRelOffset >= record.centralDirOffset) { throw new ZipException("Local file header offset is after central directory"); } String entryName = newEntry.getName(); @@ -446,6 +454,45 @@ public class ZipFile implements Closeable, ZipConstants { } } + private static EocdRecord parseEocdRecord(RandomAccessFile raf, long offset, boolean isZip64) throws IOException { + raf.seek(offset); + + // Read the End Of Central Directory. ENDHDR includes the signature bytes, + // which we've already read. + byte[] eocd = new byte[ENDHDR - 4]; + raf.readFully(eocd); + + BufferIterator it = HeapBufferIterator.iterator(eocd, 0, eocd.length, ByteOrder.LITTLE_ENDIAN); + final long numEntries; + final long centralDirOffset; + if (isZip64) { + numEntries = -1; + centralDirOffset = -1; + + // If we have a zip64 end of central directory record, we skip through the regular + // end of central directory record and use the information from the zip64 eocd record. + // We're still forced to read the comment length (below) since it isn't present in the + // zip64 eocd record. + it.skip(16); + } else { + // If we don't have a zip64 eocd record, we read values from the "regular" + // eocd record. + int diskNumber = it.readShort() & 0xffff; + int diskWithCentralDir = it.readShort() & 0xffff; + numEntries = it.readShort() & 0xffff; + int totalNumEntries = it.readShort() & 0xffff; + it.skip(4); // Ignore centralDirSize. + + centralDirOffset = ((long) it.readInt()) & 0xffffffffL; + if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDir != 0) { + throw new ZipException("Spanned archives not supported"); + } + } + + final int commentLength = it.readShort() & 0xffff; + return new EocdRecord(numEntries, centralDirOffset, commentLength); + } + static void throwZipException(String msg, int magic) throws ZipException { final String hexString = IntegralToString.intToHexString(magic, true, 8); throw new ZipException(msg + " signature not found; was " + hexString); diff --git a/luni/src/main/java/java/util/zip/ZipInputStream.java b/luni/src/main/java/java/util/zip/ZipInputStream.java index 4c0034e..f3ca74e 100644 --- a/luni/src/main/java/java/util/zip/ZipInputStream.java +++ b/luni/src/main/java/java/util/zip/ZipInputStream.java @@ -81,7 +81,9 @@ public class ZipInputStream extends InflaterInputStream implements ZipConstants private ZipEntry currentEntry; - private final byte[] hdrBuf = new byte[LOCHDR - LOCVER]; + private boolean currentEntryIsZip64; + + private final byte[] hdrBuf = new byte[LOCHDR - LOCVER + 8]; private final CRC32 crc = new CRC32(); @@ -159,7 +161,7 @@ public class ZipInputStream extends InflaterInputStream implements ZipConstants } try { - readAndVerifyDataDescriptor(inB, out); + readAndVerifyDataDescriptor(inB, out, currentEntryIsZip64); } catch (Exception e) { if (failure == null) { // otherwise we're already going to throw failure = e; @@ -183,16 +185,31 @@ public class ZipInputStream extends InflaterInputStream implements ZipConstants } } - private void readAndVerifyDataDescriptor(int inB, int out) throws IOException { + private void readAndVerifyDataDescriptor(long inB, long out, boolean isZip64) throws IOException { if (hasDD) { - Streams.readFully(in, hdrBuf, 0, EXTHDR); + if (isZip64) { + // 8 additional bytes since the compressed / uncompressed size fields + // in the extended header are 8 bytes each, instead of 4 bytes each. + Streams.readFully(in, hdrBuf, 0, EXTHDR + 8); + } else { + Streams.readFully(in, hdrBuf, 0, EXTHDR); + } + int sig = Memory.peekInt(hdrBuf, 0, ByteOrder.LITTLE_ENDIAN); if (sig != (int) EXTSIG) { throw new ZipException(String.format("unknown format (EXTSIG=%x)", sig)); } currentEntry.crc = ((long) Memory.peekInt(hdrBuf, EXTCRC, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL; - currentEntry.compressedSize = ((long) Memory.peekInt(hdrBuf, EXTSIZ, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL; - currentEntry.size = ((long) Memory.peekInt(hdrBuf, EXTLEN, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL; + + if (isZip64) { + currentEntry.compressedSize = Memory.peekLong(hdrBuf, EXTSIZ, ByteOrder.LITTLE_ENDIAN); + // Note that we apply an adjustment of 4 bytes to the offset of EXTLEN to account + // for the 8 byte size for zip64. + currentEntry.size = Memory.peekLong(hdrBuf, EXTLEN + 4, ByteOrder.LITTLE_ENDIAN); + } else { + currentEntry.compressedSize = ((long) Memory.peekInt(hdrBuf, EXTSIZ, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL; + currentEntry.size = ((long) Memory.peekInt(hdrBuf, EXTLEN, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL; + } } if (currentEntry.crc != crc.getValue()) { throw new ZipException("CRC mismatch"); @@ -266,7 +283,11 @@ public class ZipInputStream extends InflaterInputStream implements ZipConstants byte[] extraData = new byte[extraLength]; Streams.readFully(in, extraData, 0, extraLength); currentEntry.setExtra(extraData); + currentEntryIsZip64 = Zip64.parseZip64ExtendedInfo(currentEntry, false /* from central directory */); + } else { + currentEntryIsZip64 = false; } + return currentEntry; } diff --git a/luni/src/main/java/java/util/zip/ZipOutputStream.java b/luni/src/main/java/java/util/zip/ZipOutputStream.java index 8278355..4995f5b 100644 --- a/luni/src/main/java/java/util/zip/ZipOutputStream.java +++ b/luni/src/main/java/java/util/zip/ZipOutputStream.java @@ -23,6 +23,8 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashSet; + +import libcore.util.CountingOutputStream; import libcore.util.EmptyArray; /** @@ -85,7 +87,7 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant private final CRC32 crc = new CRC32(); - private int offset = 0, curOffset = 0; + private long offset = 0; /** The charset-encoded name for the current entry. */ private byte[] nameBytes; @@ -93,6 +95,31 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant /** The charset-encoded comment for the current entry. */ private byte[] entryCommentBytes; + private static final byte[] ZIP64_PLACEHOLDER_BYTES = + new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff }; + + /** + * Whether this zip file needs a Zip64 EOCD record / zip64 EOCD record locator. This + * will be true if we wrote an entry whose size or compressed size was too large for + * the standard zip format or if we exceeded the maximum number of entries allowed + * in the standard format. + */ + private boolean archiveNeedsZip64EocdRecord; + + /** + * Whether the current entry being processed needs a zip64 extended info record. This + * will be true if the entry is too large for the standard zip format or if the offset + * to the start of the current entry header is greater than 0xFFFFFFFF. + */ + private boolean currentEntryNeedsZip64; + + /** + * Whether we force all entries in this archive to have a zip64 extended info record. + * This of course implies that the {@code currentEntryNeedsZip64} and + * {@code archiveNeedsZip64EocdRecord} are always {@code true}. + */ + private final boolean forceZip64; + /** * Constructs a new {@code ZipOutputStream} that writes a zip file to the given * {@code OutputStream}. @@ -100,7 +127,15 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant * <p>UTF-8 will be used to encode the file comment, entry names and comments. */ public ZipOutputStream(OutputStream os) { - super(os, new Deflater(Deflater.DEFAULT_COMPRESSION, true)); + this(os, false /* forceZip64 */); + } + + /** + * @hide for testing only. + */ + public ZipOutputStream(OutputStream os, boolean forceZip64) { + super(new CountingOutputStream(os), new Deflater(Deflater.DEFAULT_COMPRESSION, true)); + this.forceZip64 = forceZip64; } /** @@ -146,15 +181,30 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant throw new ZipException("Size mismatch"); } } - curOffset = LOCHDR; + + long curOffset = LOCHDR; // Write the DataDescriptor if (currentEntry.getMethod() != STORED) { curOffset += EXTHDR; - writeLong(out, EXTSIG); - writeLong(out, currentEntry.crc = crc.getValue()); - writeLong(out, currentEntry.compressedSize = def.getTotalOut()); - writeLong(out, currentEntry.size = def.getTotalIn()); + + // Data descriptor signature and CRC are 4 bytes each for both zip and zip64. + writeLongAsUint32(out, EXTSIG); + writeLongAsUint32(out, currentEntry.crc = crc.getValue()); + + currentEntry.compressedSize = def.getBytesWritten(); + currentEntry.size = def.getBytesRead(); + + if (currentEntryNeedsZip64) { + // We need an additional 8 bytes to store 8 byte compressed / uncompressed + // sizes. + curOffset += 8; + writeLongAsUint64(out, currentEntry.compressedSize); + writeLongAsUint64(out, currentEntry.size); + } else { + writeLongAsUint32(out, currentEntry.compressedSize); + writeLongAsUint32(out, currentEntry.size); + } } // Update the CentralDirectory // http://www.pkware.com/documents/casestudies/APPNOTE.TXT @@ -163,33 +213,54 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant // Some tools insist that the central directory have the UTF-8 flag. // http://code.google.com/p/android/issues/detail?id=20214 flags |= ZipFile.GPBF_UTF8_FLAG; - writeLong(cDir, CENSIG); - writeShort(cDir, ZIP_VERSION_2_0); // Version this file was made by. - writeShort(cDir, ZIP_VERSION_2_0); // Minimum version needed to extract. - writeShort(cDir, flags); - writeShort(cDir, currentEntry.getMethod()); - writeShort(cDir, currentEntry.time); - writeShort(cDir, currentEntry.modDate); - writeLong(cDir, crc.getValue()); + writeLongAsUint32(cDir, CENSIG); + writeIntAsUint16(cDir, ZIP_VERSION_2_0); // Version this file was made by. + writeIntAsUint16(cDir, ZIP_VERSION_2_0); // Minimum version needed to extract. + writeIntAsUint16(cDir, flags); + writeIntAsUint16(cDir, currentEntry.getMethod()); + writeIntAsUint16(cDir, currentEntry.time); + writeIntAsUint16(cDir, currentEntry.modDate); + writeLongAsUint32(cDir, crc.getValue()); + if (currentEntry.getMethod() == DEFLATED) { - curOffset += writeLong(cDir, def.getTotalOut()); - writeLong(cDir, def.getTotalIn()); + currentEntry.setCompressedSize(def.getBytesWritten()); + currentEntry.setSize(def.getBytesRead()); + curOffset += currentEntry.getCompressedSize(); } else { - curOffset += writeLong(cDir, crc.tbytes); - writeLong(cDir, crc.tbytes); + currentEntry.setCompressedSize(crc.tbytes); + currentEntry.setSize(crc.tbytes); + curOffset += currentEntry.getSize(); } - curOffset += writeShort(cDir, nameBytes.length); + + if (currentEntryNeedsZip64) { + // Refresh the extended info with the compressed size / size before + // writing it to the central directory. + Zip64.refreshZip64ExtendedInfo(currentEntry); + + // NOTE: We would've written out the zip64 extended info locator to the entry + // extras while constructing the local file header. There's no need to do it again + // here. If we do, there will be a size mismatch since we're calculating offsets + // based on the *current* size of the extra data and not based on the size + // at the point of writing the LFH. + writeLongAsUint32(cDir, Zip64.MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE); + writeLongAsUint32(cDir, Zip64.MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE); + } else { + writeLongAsUint32(cDir, currentEntry.getCompressedSize()); + writeLongAsUint32(cDir, currentEntry.getSize()); + } + + curOffset += writeIntAsUint16(cDir, nameBytes.length); if (currentEntry.extra != null) { - curOffset += writeShort(cDir, currentEntry.extra.length); + curOffset += writeIntAsUint16(cDir, currentEntry.extra.length); } else { - writeShort(cDir, 0); + writeIntAsUint16(cDir, 0); } - writeShort(cDir, entryCommentBytes.length); // Comment length. - writeShort(cDir, 0); // Disk Start - writeShort(cDir, 0); // Internal File Attributes - writeLong(cDir, 0); // External File Attributes - writeLong(cDir, offset); + writeIntAsUint16(cDir, entryCommentBytes.length); // Comment length. + writeIntAsUint16(cDir, 0); // Disk Start + writeIntAsUint16(cDir, 0); // Internal File Attributes + writeLongAsUint32(cDir, 0); // External File Attributes + writeLongAsUint32(cDir, offset); cDir.write(nameBytes); nameBytes = null; if (currentEntry.extra != null) { @@ -228,16 +299,32 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant if (currentEntry != null) { closeEntry(); } - int cdirSize = cDir.size(); + + int cdirEntriesSize = cDir.size(); + if (archiveNeedsZip64EocdRecord) { + Zip64.writeZip64EocdRecordAndLocator(cDir, entries.size(), offset, cdirEntriesSize); + } + // Write Central Dir End - writeLong(cDir, ENDSIG); - writeShort(cDir, 0); // Disk Number - writeShort(cDir, 0); // Start Disk - writeShort(cDir, entries.size()); // Number of entries - writeShort(cDir, entries.size()); // Number of entries - writeLong(cDir, cdirSize); // Size of central dir - writeLong(cDir, offset); // Offset of central dir - writeShort(cDir, commentBytes.length); + writeLongAsUint32(cDir, ENDSIG); + writeIntAsUint16(cDir, 0); // Disk Number + writeIntAsUint16(cDir, 0); // Start Disk + + // Instead of trying to figure out *why* this archive needed a zip64 eocd record, + // just delegate all these values to the zip64 eocd record. + if (archiveNeedsZip64EocdRecord) { + writeIntAsUint16(cDir, 0xFFFF); // Number of entries + writeIntAsUint16(cDir, 0xFFFF); // Number of entries + writeLongAsUint32(cDir, 0xFFFFFFFF); // Size of central dir + writeLongAsUint32(cDir, 0xFFFFFFFF); // Offset of central dir; + } else { + writeIntAsUint16(cDir, entries.size()); // Number of entries + writeIntAsUint16(cDir, entries.size()); // Number of entries + writeLongAsUint32(cDir, cdirEntriesSize); // Size of central dir + writeLongAsUint32(cDir, offset); // Offset of central dir + } + + writeIntAsUint16(cDir, commentBytes.length); if (commentBytes.length > 0) { cDir.write(commentBytes); } @@ -288,14 +375,8 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant } checkOpen(); + checkAndSetZip64Requirements(ze); - if (entries.contains(ze.name)) { - throw new ZipException("Entry already exists: " + ze.name); - } - if (entries.size() == 64*1024-1) { - // TODO: support Zip64. - throw new ZipException("Too many entries for the zip file format's 16-bit entry count"); - } nameBytes = ze.name.getBytes(StandardCharsets.UTF_8); checkSizeIsWithinShort("Name", nameBytes); entryCommentBytes = EmptyArray.BYTE; @@ -310,6 +391,7 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant ze.setMethod(method); currentEntry = ze; + currentEntry.localHeaderRelOffset = offset; entries.add(currentEntry.name); // Local file header. @@ -318,30 +400,48 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant // Java always outputs UTF-8 filenames. (Before Java 7, the RI didn't set this flag and used // modified UTF-8. From Java 7, when using UTF_8 it sets this flag and uses normal UTF-8.) flags |= ZipFile.GPBF_UTF8_FLAG; - writeLong(out, LOCSIG); // Entry header - writeShort(out, ZIP_VERSION_2_0); // Minimum version needed to extract. - writeShort(out, flags); - writeShort(out, method); + writeLongAsUint32(out, LOCSIG); // Entry header + writeIntAsUint16(out, ZIP_VERSION_2_0); // Minimum version needed to extract. + writeIntAsUint16(out, flags); + writeIntAsUint16(out, method); if (currentEntry.getTime() == -1) { currentEntry.setTime(System.currentTimeMillis()); } - writeShort(out, currentEntry.time); - writeShort(out, currentEntry.modDate); + writeIntAsUint16(out, currentEntry.time); + writeIntAsUint16(out, currentEntry.modDate); if (method == STORED) { - writeLong(out, currentEntry.crc); - writeLong(out, currentEntry.size); - writeLong(out, currentEntry.size); + writeLongAsUint32(out, currentEntry.crc); + + if (currentEntryNeedsZip64) { + // NOTE: According to the spec, we're allowed to use these fields under zip64 + // as long as the sizes are <= 4G (and omit writing the zip64 extended information header). + // + // For simplicity, we write the zip64 extended info here even if we only need it + // in the central directory (i.e, the case where we're turning on zip64 because the + // offset to this entries LFH is > 0xFFFFFFFF). + out.write(ZIP64_PLACEHOLDER_BYTES); // compressed size + out.write(ZIP64_PLACEHOLDER_BYTES); // uncompressed size + } else { + writeLongAsUint32(out, currentEntry.size); + writeLongAsUint32(out, currentEntry.size); + } } else { - writeLong(out, 0); - writeLong(out, 0); - writeLong(out, 0); + writeLongAsUint32(out, 0); + writeLongAsUint32(out, 0); + writeLongAsUint32(out, 0); } - writeShort(out, nameBytes.length); + + writeIntAsUint16(out, nameBytes.length); + + if (currentEntryNeedsZip64) { + Zip64.insertZip64ExtendedInfoToExtras(currentEntry); + } + if (currentEntry.extra != null) { - writeShort(out, currentEntry.extra.length); + writeIntAsUint16(out, currentEntry.extra.length); } else { - writeShort(out, 0); + writeIntAsUint16(out, 0); } out.write(nameBytes); if (currentEntry.extra != null) { @@ -349,6 +449,42 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant } } + private void checkAndSetZip64Requirements(ZipEntry entry) { + final long totalBytesWritten = getBytesWritten(); + final long entriesWritten = entries.size(); + + currentEntryNeedsZip64 = false; + if (forceZip64) { + currentEntryNeedsZip64 = true; + archiveNeedsZip64EocdRecord = true; + return; + } + + // In this particular case, we'll write a zip64 eocd record locator and a zip64 eocd + // record but we won't actually need zip64 extended info records for any of the individual + // entries (unless they trigger the checks below). + if (entriesWritten == 64*1024-1) { + archiveNeedsZip64EocdRecord = true; + } + + // Check whether we'll need to write out a zip64 extended info record in both the local file header + // and the central directory. In addition, we will need a zip64 eocd record locator + // and record to mark this archive as zip64. + // + // TODO: This is an imprecise check. When method != STORED it's possible that the compressed + // size will be (slightly) larger than the actual size. How can we improve this ? + // + // TODO: Will we regret forcing zip64 for archive entries with unknown entry sizes ? This is + // standard "zip" behaviour on linux but i'm not sure if we'll end up breaking somebody as a + // result. + if (totalBytesWritten > Zip64.MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE || + (entry.getSize() > Zip64.MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) || + (entry.getSize() == -1)) { + currentEntryNeedsZip64 = true; + archiveNeedsZip64EocdRecord = true; + } + } + /** * Sets the comment associated with the file being written. See {@link ZipFile#getComment}. * @throws IllegalArgumentException if the comment is >= 64 Ki encoded bytes. @@ -386,7 +522,7 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant defaultCompressionMethod = method; } - private long writeLong(OutputStream os, long i) throws IOException { + static long writeLongAsUint32(OutputStream os, long i) throws IOException { // Write out the long value as an unsigned int os.write((int) (i & 0xFF)); os.write((int) (i >> 8) & 0xFF); @@ -395,7 +531,23 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant return i; } - private int writeShort(OutputStream os, int i) throws IOException { + static long writeLongAsUint64(OutputStream os, long i) throws IOException { + int i1 = (int) i; + os.write(i1 & 0xFF); + os.write((i1 >> 8) & 0xFF); + os.write((i1 >> 16) & 0xFF); + os.write((i1 >> 24) & 0xFF); + + int i2 = (int) (i >> 32); + os.write(i2 & 0xFF); + os.write((i2 >> 8) & 0xFF); + os.write((i2 >> 16) & 0xFF); + os.write((i2 >> 24) & 0xFF); + + return i; + } + + static int writeIntAsUint16(OutputStream os, int i) throws IOException { os.write(i & 0xFF); os.write((i >> 8) & 0xFF); return i; @@ -434,4 +586,11 @@ public class ZipOutputStream extends DeflaterOutputStream implements ZipConstant " bytes"); } } + + private long getBytesWritten() { + // This cast is somewhat messy but less error prone than keeping an + // CountingOutputStream reference around in addition to the FilterOutputStream's + // out. + return ((CountingOutputStream) out).getCount(); + } } diff --git a/luni/src/main/java/libcore/util/CountingOutputStream.java b/luni/src/main/java/libcore/util/CountingOutputStream.java new file mode 100644 index 0000000..cc0e1f2 --- /dev/null +++ b/luni/src/main/java/libcore/util/CountingOutputStream.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package libcore.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * An output stream that keeps count of the number of bytes written to it. + * + * Useful when we need to make decisions based on the size of the output, such + * as deciding what sort of metadata to writes to zip files. + */ +public class CountingOutputStream extends FilterOutputStream { + + private long count; + + /** + * Constructs a new {@code FilterOutputStream} with {@code out} as its + * target stream. + * + * @param out the target stream that this stream writes to. + */ + public CountingOutputStream(OutputStream out) { + super(out); + count = 0; + } + + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + out.write(buffer, offset, length); + count += length; + } + + @Override + public void write(int oneByte) throws IOException { + out.write(oneByte); + count++; + } + + public long getCount() { + return count; + } +} diff --git a/luni/src/test/java/libcore/java/util/zip/ZipEntryTest.java b/luni/src/test/java/libcore/java/util/zip/ZipEntryTest.java index 550ddfb..9c3e870 100644 --- a/luni/src/test/java/libcore/java/util/zip/ZipEntryTest.java +++ b/luni/src/test/java/libcore/java/util/zip/ZipEntryTest.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.List; import java.util.jar.JarEntry; import java.util.zip.ZipEntry; +import java.util.zip.ZipException; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; @@ -132,6 +133,7 @@ public class ZipEntryTest extends junit.framework.TestCase { File f = createTemporaryZipFile(); ZipOutputStream out = createZipOutputStream(f); ZipEntry ze = new ZipEntry("x"); + ze.setSize(0); ze.setExtra(maxLengthExtra); out.putNextEntry(ze); out.closeEntry(); @@ -143,7 +145,25 @@ public class ZipEntryTest extends junit.framework.TestCase { zipFile.close(); } - public void testTooLongComment() throws Exception { + public void testMaxLengthExtra_zip64() throws Exception { + // Not quite the max length (65535), but large enough that there's no space + // for the zip64 extended info header. + byte[] maxLengthExtra = new byte[65530]; + + File f = createTemporaryZipFile(); + ZipOutputStream out = createZipOutputStream(f); + ZipEntry ze = new ZipEntry("x"); + + ze.setExtra(maxLengthExtra); + try { + out.putNextEntry(ze); + fail(); + } catch (ZipException expected) { + } + } + + + public void testTooLongComment() throws Exception { String tooLongComment = makeString(65536, "z"); ZipEntry ze = new ZipEntry("x"); try { @@ -176,7 +196,17 @@ public class ZipEntryTest extends junit.framework.TestCase { File f = createTemporaryZipFile(); ZipOutputStream out = createZipOutputStream(f); + + // Regular (non zip64) format. ZipEntry ze = new ZipEntry("x"); + ze.setSize(0); + ze.setExtra(extra); + ze.setComment(comment); + out.putNextEntry(ze); + out.closeEntry(); + + // An entry without a length is assumed to be zip64. + ze = new ZipEntry("y"); ze.setExtra(extra); ze.setComment(comment); out.putNextEntry(ze); @@ -188,6 +218,9 @@ public class ZipEntryTest extends junit.framework.TestCase { try { assertEquals(comment, zipFile.getEntry("x").getComment()); assertTrue(Arrays.equals(extra, zipFile.getEntry("x").getExtra())); + + assertEquals(comment, zipFile.getEntry("y").getComment()); + assertTrue(Arrays.equals(extra, zipFile.getEntry("y").getExtra())); } finally { zipFile.close(); } diff --git a/luni/src/test/java/libcore/java/util/zip/ZipFileTest.java b/luni/src/test/java/libcore/java/util/zip/ZipFileTest.java index 1826140..bf18978 100644 --- a/luni/src/test/java/libcore/java/util/zip/ZipFileTest.java +++ b/luni/src/test/java/libcore/java/util/zip/ZipFileTest.java @@ -196,7 +196,7 @@ public final class ZipFileTest extends TestCase { ZipFile zipFile = new ZipFile(f); int entryCount = 0; for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements(); ) { - ZipEntry zipEntry = e.nextElement(); + e.nextElement(); ++entryCount; } assertEquals(1024, entryCount); @@ -204,18 +204,62 @@ public final class ZipFileTest extends TestCase { } } - public void testZip64Support() throws IOException { + public void testZip64Support_largeNumberOfEntries() throws IOException { + File file = createZipFile(65550, 2); + ZipFile zf = null; try { - createZipFile(64*1024, 0); - fail(); // Make this test more like testHugeZipFile when we have Zip64 support. - } catch (ZipException expected) { + zf = new ZipFile(file); + assertEquals(65550, zf.size()); + + Enumeration<? extends ZipEntry> entries = zf.entries(); + assertTrue(entries.hasMoreElements()); + ZipEntry ze = entries.nextElement(); + assertEquals(2, ze.getSize()); + } finally { + if (zf != null) { + zf.close(); + } + } + } + + public void testZip64Support_totalLargerThan4G() throws IOException { + final File file = createZipFile(5, 1073741824L); + ZipFile zf = null; + try { + zf = new ZipFile(file); + assertEquals(5, zf.size()); + Enumeration<? extends ZipEntry> entries = zf.entries(); + assertTrue(entries.hasMoreElements()); + ZipEntry ze = entries.nextElement(); + assertEquals(1073741824L, ze.getSize()); + } finally { + if (zf != null) { + zf.close(); + } + } + } + + public void testZip64Support_hugeEntry() throws IOException { + final File file = createZipFile(1, 4294967410L); + ZipFile zf = null; + try { + zf = new ZipFile(file); + assertEquals(1, zf.size()); + Enumeration<? extends ZipEntry> entries = zf.entries(); + assertTrue(entries.hasMoreElements()); + ZipEntry ze = entries.nextElement(); + assertEquals(4294967410L, ze.getSize()); + } finally { + if (zf != null) { + zf.close(); + } } } /** * Compresses the given number of files, each of the given size, into a .zip archive. */ - private static File createZipFile(int entryCount, int entrySize) throws IOException { + private static File createZipFile(int entryCount, long entrySize) throws IOException { File result = createTemporaryZipFile(); byte[] writeBuffer = new byte[8192]; @@ -227,9 +271,9 @@ public final class ZipFileTest extends TestCase { ZipEntry ze = new ZipEntry(Integer.toHexString(entry)); out.putNextEntry(ze); - for (int i = 0; i < entrySize; i += writeBuffer.length) { + for (long i = 0; i < entrySize; i += writeBuffer.length) { random.nextBytes(writeBuffer); - int byteCount = Math.min(writeBuffer.length, entrySize - i); + int byteCount = (int) Math.min(writeBuffer.length, entrySize - i); out.write(writeBuffer, 0, byteCount); } |