// Path: plugin/src/main/java/com/android/apksigner/core/internal/util/Pair.java
// public final class Pair<A, B> {
//     private final A mFirst;
//     private final B mSecond;
// 
//     private Pair(A first, B second) {
//         mFirst = first;
//         mSecond = second;
//     }
// 
//     public static <A, B> Pair<A, B> of(A first, B second) {
//         return new Pair<A, B>(first, second);
//     }
// 
//     public A getFirst() {
//         return mFirst;
//     }
// 
//     public B getSecond() {
//         return mSecond;
//     }
// 
//     @Override
//     public int hashCode() {
//         final int prime = 31;
//         int result = 1;
//         result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
//         result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
//         return result;
//     }
// 
//     @Override
//     public boolean equals(Object obj) {
//         if (this == obj) {
//             return true;
//         }
//         if (obj == null) {
//             return false;
//         }
//         if (getClass() != obj.getClass()) {
//             return false;
//         }
//         @SuppressWarnings("rawtypes")
//         Pair other = (Pair) obj;
//         if (mFirst == null) {
//             if (other.mFirst != null) {
//                 return false;
//             }
//         } else if (!mFirst.equals(other.mFirst)) {
//             return false;
//         }
//         if (mSecond == null) {
//             if (other.mSecond != null) {
//                 return false;
//             }
//         } else if (!mSecond.equals(other.mSecond)) {
//             return false;
//         }
//         return true;
//     }
// }
// 
// Path: plugin/src/main/java/com/android/apksigner/core/util/DataSource.java
// public interface DataSource {
// 
//     /**
//      * Returns the amount of data (in bytes) contained in this data source.
//      */
//     long size();
// 
//     /**
//      * Feeds the specified chunk from this data source into the provided sink.
//      *
//      * @param offset index (in bytes) at which the chunk starts inside data source
//      * @param size size (in bytes) of the chunk
//      */
//     void feed(long offset, long size, DataSink sink) throws IOException;
// 
//     /**
//      * Returns a buffer holding the contents of the specified chunk of data from this data source.
//      * Changes to the data source are not guaranteed to be reflected in the returned buffer.
//      * Similarly, changes in the buffer are not guaranteed to be reflected in the data source.
//      *
//      * <p>The returned buffer's position is {@code 0}, and the buffer's limit and capacity is
//      * {@code size}.
//      *
//      * @param offset index (in bytes) at which the chunk starts inside data source
//      * @param size size (in bytes) of the chunk
//      */
//     ByteBuffer getByteBuffer(long offset, int size) throws IOException;
// 
//     /**
//      * Copies the specified chunk from this data source into the provided destination buffer,
//      * advancing the destination buffer's position by {@code size}.
//      *
//      * @param offset index (in bytes) at which the chunk starts inside data source
//      * @param size size (in bytes) of the chunk
//      */
//     void copyTo(long offset, int size, ByteBuffer dest) throws IOException;
// 
//     /**
//      * Returns a data source representing the specified region of data of this data source. Changes
//      * to data represented by this data source will also be visible in the returned data source.
//      *
//      * @param offset index (in bytes) at which the region starts inside data source
//      * @param size size (in bytes) of the region
//      */
//     DataSource slice(long offset, long size);
// }


import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.android.apksigner.core.internal.util.Pair;
import com.android.apksigner.core.util.DataSource;
     * @throws IOException if an I/O error occurs while reading the file.
     */
    public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip)
            throws IOException {
        // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
        // The record can be identified by its 4-byte signature/magic which is located at the very
        // beginning of the record. A complication is that the record is variable-length because of
        // the comment field.
        // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
        // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
        // the candidate record's comment length is such that the remainder of the record takes up
        // exactly the remaining bytes in the buffer. The search is bounded because the maximum
        // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.

        long fileSize = zip.size();
        if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
            return null;
        }

        // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
        // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
        // reading more data.
        Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
        if (result != null) {
            return result;
        }

        // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
        // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
        // the comment length field is an unsigned 16-bit number.
// Path: plugin/src/main/java/com/android/apksigner/core/internal/util/Pair.java
// public final class Pair<A, B> {
//     private final A mFirst;
//     private final B mSecond;
// 
//     private Pair(A first, B second) {
//         mFirst = first;
//         mSecond = second;
//     }
// 
//     public static <A, B> Pair<A, B> of(A first, B second) {
//         return new Pair<A, B>(first, second);
//     }
// 
//     public A getFirst() {
//         return mFirst;
//     }
// 
//     public B getSecond() {
//         return mSecond;
//     }
// 
//     @Override
//     public int hashCode() {
//         final int prime = 31;
//         int result = 1;
//         result = prime * result + ((mFirst == null) ? 0 : mFirst.hashCode());
//         result = prime * result + ((mSecond == null) ? 0 : mSecond.hashCode());
//         return result;
//     }
// 
//     @Override
//     public boolean equals(Object obj) {
//         if (this == obj) {
//             return true;
//         }
//         if (obj == null) {
//             return false;
//         }
//         if (getClass() != obj.getClass()) {
//             return false;
//         }
//         @SuppressWarnings("rawtypes")
//         Pair other = (Pair) obj;
//         if (mFirst == null) {
//             if (other.mFirst != null) {
//                 return false;
//             }
//         } else if (!mFirst.equals(other.mFirst)) {
//             return false;
//         }
//         if (mSecond == null) {
//             if (other.mSecond != null) {
//                 return false;
//             }
//         } else if (!mSecond.equals(other.mSecond)) {
//             return false;
//         }
//         return true;
//     }
// }
// 
// Path: plugin/src/main/java/com/android/apksigner/core/util/DataSource.java
// public interface DataSource {
// 
//     /**
//      * Returns the amount of data (in bytes) contained in this data source.
//      */
//     long size();
// 
//     /**
//      * Feeds the specified chunk from this data source into the provided sink.
//      *
//      * @param offset index (in bytes) at which the chunk starts inside data source
//      * @param size size (in bytes) of the chunk
//      */
//     void feed(long offset, long size, DataSink sink) throws IOException;
// 
//     /**
//      * Returns a buffer holding the contents of the specified chunk of data from this data source.
//      * Changes to the data source are not guaranteed to be reflected in the returned buffer.
//      * Similarly, changes in the buffer are not guaranteed to be reflected in the data source.
//      *
//      * <p>The returned buffer's position is {@code 0}, and the buffer's limit and capacity is
//      * {@code size}.
//      *
//      * @param offset index (in bytes) at which the chunk starts inside data source
//      * @param size size (in bytes) of the chunk
//      */
//     ByteBuffer getByteBuffer(long offset, int size) throws IOException;
// 
//     /**
//      * Copies the specified chunk from this data source into the provided destination buffer,
//      * advancing the destination buffer's position by {@code size}.
//      *
//      * @param offset index (in bytes) at which the chunk starts inside data source
//      * @param size size (in bytes) of the chunk
//      */
//     void copyTo(long offset, int size, ByteBuffer dest) throws IOException;
// 
//     /**
//      * Returns a data source representing the specified region of data of this data source. Changes
//      * to data represented by this data source will also be visible in the returned data source.
//      *
//      * @param offset index (in bytes) at which the region starts inside data source
//      * @param size size (in bytes) of the region
//      */
//     DataSource slice(long offset, long size);
// }


// Path: plugin/src/main/java/com/android/apksigner/core/internal/zip/ZipUtils.java
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.android.apksigner.core.internal.util.Pair;
import com.android.apksigner.core.util.DataSource;

     * @throws IOException if an I/O error occurs while reading the file.
     */
    public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip)
            throws IOException {
        // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
        // The record can be identified by its 4-byte signature/magic which is located at the very
        // beginning of the record. A complication is that the record is variable-length because of
        // the comment field.
        // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
        // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
        // the candidate record's comment length is such that the remainder of the record takes up
        // exactly the remaining bytes in the buffer. The search is bounded because the maximum
        // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.

        long fileSize = zip.size();
        if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
            return null;
        }

        // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
        // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
        // reading more data.
        Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
        if (result != null) {
            return result;
        }

        // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
        // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
        // the comment length field is an unsigned 16-bit number.
        return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);