Support zip64 extensible data sectors

PiperOrigin-RevId: 440187167
diff --git a/java/com/google/turbine/zip/Zip.java b/java/com/google/turbine/zip/Zip.java
index fa0f0e0..366cd6b 100644
--- a/java/com/google/turbine/zip/Zip.java
+++ b/java/com/google/turbine/zip/Zip.java
@@ -74,6 +74,7 @@
 public final class Zip {
 
   static final int ZIP64_ENDSIG = 0x06064b50;
+  static final int ZIP64_LOCSIG = 0x07064b50;
 
   static final int LOCHDR = 30; // LOC header size
   static final int CENHDR = 46; // CEN header size
@@ -196,20 +197,44 @@
       if (totalEntries == ZIP64_MAGICCOUNT) {
         // Assume the zip64 EOCD has the usual size; we don't support zip64 extensible data sectors.
         long zip64eocdOffset = size - ENDHDR - ZIP64_LOCHDR - ZIP64_ENDHDR;
-        MappedByteBuffer zip64eocd = chan.map(MapMode.READ_ONLY, zip64eocdOffset, ZIP64_ENDHDR);
-        zip64eocd.order(ByteOrder.LITTLE_ENDIAN);
         // Note that zip reading is necessarily best-effort, since an archive could contain 0xFFFF
         // entries and the last entry's data could contain a ZIP64_ENDSIG. Some implementations
         // read the full EOCD records and compare them.
-        if (zip64eocd.getInt(0) == ZIP64_ENDSIG) {
-          cdsize = zip64eocd.getLong(ZIP64_ENDSIZ);
+        long zip64cdsize = zip64cdsize(chan, zip64eocdOffset);
+        if (zip64cdsize != -1) {
           eocdOffset = zip64eocdOffset;
+          cdsize = zip64cdsize;
+        } else {
+          // If we couldn't find a zip64 EOCD at a fixed offset, either it doesn't exist
+          // or there was a zip64 extensible data sector, so try going through the
+          // locator. This approach doesn't work if data was prepended to the archive
+          // without updating the offset in the locator.
+          MappedByteBuffer zip64loc =
+              chan.map(MapMode.READ_ONLY, size - ENDHDR - ZIP64_LOCHDR, ZIP64_LOCHDR);
+          zip64loc.order(ByteOrder.LITTLE_ENDIAN);
+          if (zip64loc.getInt(0) == ZIP64_LOCSIG) {
+            zip64eocdOffset = zip64loc.getLong(8);
+            zip64cdsize = zip64cdsize(chan, zip64eocdOffset);
+            if (zip64cdsize != -1) {
+              eocdOffset = zip64eocdOffset;
+              cdsize = zip64cdsize;
+            }
+          }
         }
       }
       this.cd = chan.map(MapMode.READ_ONLY, eocdOffset - cdsize, cdsize);
       cd.order(ByteOrder.LITTLE_ENDIAN);
     }
 
+    static long zip64cdsize(FileChannel chan, long eocdOffset) throws IOException {
+      MappedByteBuffer zip64eocd = chan.map(MapMode.READ_ONLY, eocdOffset, ZIP64_ENDHDR);
+      zip64eocd.order(ByteOrder.LITTLE_ENDIAN);
+      if (zip64eocd.getInt(0) == ZIP64_ENDSIG) {
+        return zip64eocd.getLong(ZIP64_ENDSIZ);
+      }
+      return -1;
+    }
+
     @Override
     public Iterator<Entry> iterator() {
       return new ZipIterator(path, chan, cd);
diff --git a/javatests/com/google/turbine/zip/ZipTest.java b/javatests/com/google/turbine/zip/ZipTest.java
index e9dfc44..2b6636d 100644
--- a/javatests/com/google/turbine/zip/ZipTest.java
+++ b/javatests/com/google/turbine/zip/ZipTest.java
@@ -25,6 +25,8 @@
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
 import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
@@ -38,6 +40,7 @@
 import java.util.jar.JarFile;
 import java.util.jar.JarOutputStream;
 import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
 import java.util.zip.ZipOutputStream;
 import org.junit.Rule;
 import org.junit.Test;
@@ -164,4 +167,164 @@
     ZipException e = assertThrows(ZipException.class, () -> actual(path));
     assertThat(e).hasMessageThat().isEqualTo("zip file comment length was 33, expected 17");
   }
+
+  // Create a zip64 archive with an extensible data sector
+  @Test
+  public void zip64extension() throws IOException {
+
+    ByteBuffer buf = ByteBuffer.allocate(1000);
+    buf.order(ByteOrder.LITTLE_ENDIAN);
+
+    // The jar has a single entry named 'hello', with the value 'world'
+    byte[] name = "hello".getBytes(UTF_8);
+    byte[] value = "world".getBytes(UTF_8);
+    int crc = Hashing.crc32().hashBytes(value).asInt();
+
+    int localHeaderPosition = buf.position();
+
+    // local file header signature     4 bytes  (0x04034b50)
+    buf.putInt((int) ZipFile.LOCSIG);
+    // version needed to extract       2 bytes
+    buf.putShort((short) 0);
+    // general purpose bit flag        2 bytes
+    buf.putShort((short) 0);
+    // compression method              2 bytes
+    buf.putShort((short) 0);
+    // last mod file time              2 bytes
+    buf.putShort((short) 0);
+    // last mod file date              2 bytes
+    buf.putShort((short) 0);
+    // crc-32                          4 bytes
+    buf.putInt(crc);
+    // compressed size                 4 bytes
+    buf.putInt(value.length);
+    // uncompressed size               4 bytes
+    buf.putInt(value.length);
+    // file name length                2 bytes
+    buf.putShort((short) name.length);
+    // extra field length              2 bytes
+    buf.putShort((short) 0);
+    // file name (variable size)
+    buf.put(name);
+    // extra field (variable size)
+    // file data
+    buf.put(value);
+
+    int centralDirectoryPosition = buf.position();
+
+    // central file header signature   4 bytes  (0x02014b50)
+    buf.putInt((int) ZipFile.CENSIG);
+    // version made by                 2 bytes
+    buf.putShort((short) 0);
+    // version needed to extract       2 bytes
+    buf.putShort((short) 0);
+    // general purpose bit flag        2 bytes
+    buf.putShort((short) 0);
+    // compression method              2 bytes
+    buf.putShort((short) 0);
+    // last mod file time              2 bytes
+    buf.putShort((short) 0);
+    // last mod file date              2 bytes
+    buf.putShort((short) 0);
+    // crc-32                          4 bytes
+    buf.putInt(crc);
+    // compressed size                 4 bytes
+    buf.putInt(value.length);
+    // uncompressed size               4 bytes
+    buf.putInt(value.length);
+    // file name length                2 bytes
+    buf.putShort((short) name.length);
+    // extra field length              2 bytes
+    buf.putShort((short) 0);
+    // file comment length             2 bytes
+    buf.putShort((short) 0);
+    // disk number start               2 bytes
+    buf.putShort((short) 0);
+    // internal file attributes        2 bytes
+    buf.putShort((short) 0);
+    // external file attributes        4 bytes
+    buf.putInt(0);
+    // relative offset of local header 4 bytes
+    buf.putInt(localHeaderPosition);
+    // file name (variable size)
+    buf.put(name);
+
+    int centralDirectorySize = buf.position() - centralDirectoryPosition;
+    int zip64eocdPosition = buf.position();
+
+    // zip64 end of central dir
+    // signature                       4 bytes  (0x06064b50)
+    buf.putInt(Zip.ZIP64_ENDSIG);
+    // size of zip64 end of central
+    // directory record                8 bytes
+    buf.putLong(Zip.ZIP64_ENDSIZ + 5);
+    // version made by                 2 bytes
+    buf.putShort((short) 0);
+    // version needed to extract       2 bytes
+    buf.putShort((short) 0);
+    // number of this disk             4 bytes
+    buf.putInt(0);
+    // number of the disk with the
+    // start of the central directory  4 bytes
+    buf.putInt(0);
+    // total number of entries in the
+    // central directory on this disk  8 bytes
+    buf.putLong(1);
+    // total number of entries in the
+    // central directory               8 bytes
+    buf.putLong(1);
+    // size of the central directory   8 bytes
+    buf.putLong(centralDirectorySize);
+    // offset of start of central
+    // directory with respect to
+    // offset of start of central
+    // the starting disk number        8 bytes
+    buf.putLong(centralDirectoryPosition);
+    // zip64 extensible data sector    (variable size)
+    buf.put((byte) 3);
+    buf.putInt(42);
+
+    // zip64 end of central dir locator
+    // signature                       4 bytes  (0x07064b50)
+    buf.putInt(Zip.ZIP64_LOCSIG);
+    // number of the disk with the
+    // start of the zip64 end of
+    // central directory               4 bytes
+    buf.putInt(0);
+    // relative offset of the zip64
+    // end of central directory record 8 bytes
+    buf.putLong(zip64eocdPosition);
+    // total number of disks           4 bytes
+    buf.putInt(0);
+
+    // end of central dir signature    4 bytes  (0x06054b50)
+    buf.putInt((int) ZipFile.ENDSIG);
+    // number of this disk             2 bytes
+    buf.putShort((short) 0);
+    // number of the disk with the
+    // start of the central directory  2 bytes
+    buf.putShort((short) 0);
+    // total number of entries in the
+    // central directory on this disk  2 bytes
+    buf.putShort((short) 1);
+    // total number of entries in
+    // the central directory           2 bytes
+    buf.putShort((short) Zip.ZIP64_MAGICCOUNT);
+    // size of the central directory   4 bytes
+    buf.putInt(centralDirectorySize);
+    // offset of start of central
+    // directory with respect to
+    // the starting disk number        4 bytes
+    buf.putInt(centralDirectoryPosition);
+    //         .ZIP file comment length        2 bytes
+    buf.putShort((short) 0);
+    //         .ZIP file comment       (variable size)
+
+    byte[] bytes = new byte[buf.position()];
+    buf.rewind();
+    buf.get(bytes);
+    Path path = temporaryFolder.newFile("test.jar").toPath();
+    Files.write(path, bytes);
+    assertThat(actual(path)).isEqualTo(expected(path));
+  }
 }