Skip to content

Commit

Permalink
Support zip files where EOCD's offset to central dir is -1
Browse files Browse the repository at this point in the history
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
  • Loading branch information
hoisie committed Dec 19, 2022
1 parent 73b8c53 commit 9b36bc6
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 6 deletions.
61 changes: 55 additions & 6 deletions resources/src/main/java/org/robolectric/res/android/FileMap.java
Expand Up @@ -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;
Expand All @@ -23,11 +24,20 @@ public class FileMap {
/** 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;
Expand Down Expand Up @@ -209,15 +219,18 @@ static ImmutableMap<String, Long> guessDataOffsets(File zipFile, int length) {

// 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);
byte[] buffer = new byte[endOfCdSize];
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
Expand Down Expand Up @@ -284,7 +297,7 @@ private static Charset getEncoding(int bitFlags) {

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);
Expand All @@ -305,12 +318,48 @@ private static int findCentralDir(byte[] buffer) throws IOException {
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]);
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -63,4 +66,33 @@ public void open_emptyZip() throws Exception {
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();
}
}

0 comments on commit 9b36bc6

Please sign in to comment.