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();
+  }
 }