Support zip files where EOCD's offset to central dir is -1
When zip files contain more than 2**16 entries, the regular EOCD is
not sufficient to describe the number of files in the archive, and
a Zip64 EOCD needs to be written.
When a Zip64 EOCD is written, some zip libraries write -1 for many
of the fields in the regular EOCD, including the offset to central dir:
From APPNOTE.TXT ยง4.4.1.4
If one of the fields in the end of central directory
record is too small to hold required data, the field SHOULD be
set to -1 (0xFFFF or 0xFFFFFFFF) and the ZIP64 format record
SHOULD be created.
Previously FileMap assumed that the regular EOCD contained a valid
offset to central dir field. This broke recently when an experimental
Android SDK had more than 64k entries in the zip file and -1 for the
offset to the central dir.
Add support for reading the offset to the central from the Zip64 EOCD.
Also add a test case that generates a problematic zip file.
PiperOrigin-RevId: 492538213
diff --git a/resources/src/main/java/org/robolectric/res/android/FileMap.java b/resources/src/main/java/org/robolectric/res/android/FileMap.java
index f127268..0672bbd 100644
--- a/resources/src/main/java/org/robolectric/res/android/FileMap.java
+++ b/resources/src/main/java/org/robolectric/res/android/FileMap.java
@@ -7,6 +7,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Ints;
+import com.google.common.primitives.Longs;
import com.google.common.primitives.Shorts;
import java.io.File;
import java.io.FileInputStream;
@@ -23,11 +24,20 @@
/** ZIP archive central directory end header signature. */
private static final int ENDSIG = 0x6054b50;
- private static final int ENDHDR = 22;
+ private static final int EOCD_SIZE = 22;
+
+ private static final int ZIP64_EOCD_SIZE = 56;
+
+ private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
+
/** ZIP64 archive central directory end header signature. */
private static final int ENDSIG64 = 0x6064b50;
- /** the maximum size of the end of central directory section in bytes */
- private static final int MAXIMUM_ZIP_EOCD_SIZE = 64 * 1024 + ENDHDR;
+
+ private static final int MAX_COMMENT_SIZE = 64 * 1024; // 64k
+
+ /** the maximum size of the end of central directory sections in bytes */
+ private static final int MAXIMUM_ZIP_EOCD_SIZE =
+ MAX_COMMENT_SIZE + EOCD_SIZE + ZIP64_EOCD_SIZE + ZIP64_EOCD_LOCATOR_SIZE;
private ZipFile zipFile;
private ZipEntry zipEntry;
@@ -209,7 +219,6 @@
// First read the 'end of central directory record' in order to find the start of the central
// directory
- // The end of central directory record (EOCD) is max comment length (64K) + 22 bytes
int endOfCdSize = Math.min(MAXIMUM_ZIP_EOCD_SIZE, length);
int endofCdOffset = length - endOfCdSize;
randomAccessFile.seek(endofCdOffset);
@@ -217,7 +226,11 @@
randomAccessFile.readFully(buffer);
int centralDirOffset = findCentralDir(buffer);
-
+ if (centralDirOffset == -1) {
+ // If the zip file contains > 2^16 entries, a Zip64 EOCD is written, and the central
+ // dir offset in the regular EOCD may be -1.
+ centralDirOffset = findCentralDir64(buffer);
+ }
int offset = centralDirOffset - endofCdOffset;
if (offset < 0) {
// read the entire central directory record into memory
@@ -284,7 +297,7 @@
private static int findCentralDir(byte[] buffer) throws IOException {
// find start of central directory by scanning backwards
- int scanOffset = buffer.length - ENDHDR;
+ int scanOffset = buffer.length - EOCD_SIZE;
while (true) {
int val = readInt(buffer, scanOffset);
@@ -305,12 +318,48 @@
return offsetToCentralDir;
}
+ private static int findCentralDir64(byte[] buffer) throws IOException {
+ // find start of central directory by scanning backwards
+ int scanOffset = buffer.length - EOCD_SIZE - ZIP64_EOCD_LOCATOR_SIZE - ZIP64_EOCD_SIZE;
+
+ while (true) {
+ int val = readInt(buffer, scanOffset);
+ if (val == ENDSIG64) {
+ break;
+ }
+
+ // Ok, keep backing up looking for the ZIP end central directory
+ // signature.
+ --scanOffset;
+ if (scanOffset < 0) {
+ throw new ZipException("ZIP directory not found, not a ZIP archive.");
+ }
+ }
+ // scanOffset is now start of end of central directory record
+ // the 'offset to central dir' data is at position 16 in the record
+ long offsetToCentralDir = readLong(buffer, scanOffset + 48);
+ return (int) offsetToCentralDir;
+ }
+
/** Read a 32-bit integer from a bytebuffer in little-endian order. */
private static int readInt(byte[] buffer, int offset) {
return Ints.fromBytes(
buffer[offset + 3], buffer[offset + 2], buffer[offset + 1], buffer[offset]);
}
+ /** Read a 64-bit integer from a bytebuffer in little-endian order. */
+ private static long readLong(byte[] buffer, int offset) {
+ return Longs.fromBytes(
+ buffer[offset + 7],
+ buffer[offset + 6],
+ buffer[offset + 5],
+ buffer[offset + 4],
+ buffer[offset + 3],
+ buffer[offset + 2],
+ buffer[offset + 1],
+ buffer[offset]);
+ }
+
/** Read a 16-bit short from a bytebuffer in little-endian order. */
private static short readShort(byte[] buffer, int offset) {
return Shorts.fromBytes(buffer[offset + 1], buffer[offset]);
diff --git a/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java b/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java
index eebf365..cf48b21 100644
--- a/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java
+++ b/resources/src/test/java/org/robolectric/res/android/ZipFileROTest.java
@@ -3,9 +3,12 @@
import static com.google.common.truth.Truth.assertThat;
import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
+import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -63,4 +66,33 @@
ZipFileRO zipFile = ZipFileRO.open(blob.toString());
assertThat(zipFile).isNotNull();
}
+
+ @Test
+ public void testCreateJar() throws Exception {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ ZipOutputStream out = new ZipOutputStream(byteArrayOutputStream);
+ // Write 2^16 + 1 entries, forcing zip64 EOCD to be written.
+ for (int i = 0; i < 65537; i++) {
+ out.putNextEntry(new ZipEntry(Integer.toString(i)));
+ out.closeEntry();
+ }
+ out.close();
+ byte[] zipBytes = byteArrayOutputStream.toByteArray();
+ // Write 0xff for the following fields in the EOCD, which some zip libraries do.
+ // Entries in this disk (2 bytes)
+ // Total Entries (2 byte)
+ // Size of Central Dir (4 bytes)
+ // Offset to Central Dir (4 bytes)
+ // Total: 12 bytes
+ for (int i = 0; i < 12; i++) {
+ zipBytes[zipBytes.length - 3 - i] = (byte) 0xff;
+ }
+ File tmpFile = File.createTempFile("zip64eocd", "zip");
+ Files.write(zipBytes, tmpFile);
+ ZipFileRO zro = ZipFileRO.open(tmpFile.getAbsolutePath());
+ assertThat(zro).isNotNull();
+ assertThat(zro.findEntryByName("0")).isNotNull();
+ assertThat(zro.findEntryByName("65536")).isNotNull();
+ assertThat(zro.findEntryByName("65537")).isNull();
+ }
}