summaryrefslogtreecommitdiff
path: root/src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java')
-rw-r--r--src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java271
1 files changed, 271 insertions, 0 deletions
diff --git a/src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java b/src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java
new file mode 100644
index 0000000..a9da14b
--- /dev/null
+++ b/src/main/java/com/android/tools/build/apkzlib/zip/Eocd.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tools.build.apkzlib.zip;
+
+import com.android.tools.build.apkzlib.utils.CachedSupplier;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Verify;
+import com.google.common.primitives.Ints;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import javax.annotation.Nonnull;
+
+/**
+ * End Of Central Directory record in a zip file.
+ */
+class Eocd {
+ /**
+ * Field in the record: the record signature, fixed at this value by the specification.
+ */
+ private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x06054b50, "EOCD signature");
+
+ /**
+ * Field in the record: the number of the disk where the EOCD is located. It has to be zero
+ * because we do not support multi-file archives.
+ */
+ private static final ZipField.F2 F_NUMBER_OF_DISK = new ZipField.F2(F_SIGNATURE.endOffset(), 0,
+ "Number of this disk");
+
+ /**
+ * Field in the record: the number of the disk where the Central Directory starts. Has to be
+ * zero because we do not support multi-file archives.
+ */
+ private static final ZipField.F2 F_DISK_CD_START = new ZipField.F2(F_NUMBER_OF_DISK.endOffset(),
+ 0, "Disk where CD starts");
+
+ /**
+ * Field in the record: the number of entries in the Central Directory on this disk. Because
+ * we do not support multi-file archives, this is the same as {@link #F_RECORDS_TOTAL}.
+ */
+ private static final ZipField.F2 F_RECORDS_DISK = new ZipField.F2(F_DISK_CD_START.endOffset(),
+ "Record on disk count", new ZipFieldInvariantNonNegative());
+
+ /**
+ * Field in the record: the total number of entries in the Central Directory.
+ */
+ private static final ZipField.F2 F_RECORDS_TOTAL = new ZipField.F2(F_RECORDS_DISK.endOffset(),
+ "Total records", new ZipFieldInvariantNonNegative(),
+ new ZipFieldInvariantMaxValue(Integer.MAX_VALUE));
+
+ /**
+ * Field in the record: number of bytes of the Central Directory.
+ * This is not private because it is required in unit tests.
+ */
+ @VisibleForTesting
+ static final ZipField.F4 F_CD_SIZE = new ZipField.F4(F_RECORDS_TOTAL.endOffset(),
+ "Directory size", new ZipFieldInvariantNonNegative());
+
+ /**
+ * Field in the record: offset, from the archive start, where the Central Directory starts.
+ * This is not private because it is required in unit tests.
+ */
+ @VisibleForTesting
+ static final ZipField.F4 F_CD_OFFSET = new ZipField.F4(F_CD_SIZE.endOffset(),
+ "Directory offset", new ZipFieldInvariantNonNegative());
+
+ /**
+ * Field in the record: number of bytes of the file comment (located at the end of the EOCD
+ * record).
+ */
+ private static final ZipField.F2 F_COMMENT_SIZE = new ZipField.F2(F_CD_OFFSET.endOffset(),
+ "File comment size", new ZipFieldInvariantNonNegative());
+
+ /**
+ * Number of entries in the central directory.
+ */
+ private final int totalRecords;
+
+ /**
+ * Offset from the beginning of the archive where the Central Directory is located.
+ */
+ private final long directoryOffset;
+
+ /**
+ * Number of bytes of the Central Directory.
+ */
+ private final long directorySize;
+
+ /**
+ * Contents of the EOCD comment.
+ */
+ @Nonnull
+ private final byte[] comment;
+
+ /**
+ * Supplier of the byte representation of the EOCD.
+ */
+ @Nonnull
+ private final CachedSupplier<byte[]> byteSupplier;
+
+ /**
+ * Creates a new EOCD, reading it from a byte source. This method will parse the byte source
+ * and obtain the EOCD. It will check that the byte source starts with the EOCD signature.
+ *
+ * @param bytes the byte buffer with the EOCD data; when this method finishes, the byte
+ * buffer's position will have moved to the end of the EOCD
+ * @throws IOException failed to read information or the EOCD data is corrupt or invalid
+ */
+ Eocd(@Nonnull ByteBuffer bytes) throws IOException {
+
+ /*
+ * Read the EOCD record.
+ */
+ F_SIGNATURE.verify(bytes);
+ F_NUMBER_OF_DISK.verify(bytes);
+ F_DISK_CD_START.verify(bytes);
+ long totalRecords1 = F_RECORDS_DISK.read(bytes);
+ long totalRecords2 = F_RECORDS_TOTAL.read(bytes);
+ long directorySize = F_CD_SIZE.read(bytes);
+ long directoryOffset = F_CD_OFFSET.read(bytes);
+ int commentSize = Ints.checkedCast(F_COMMENT_SIZE.read(bytes));
+
+ /*
+ * Some sanity checks.
+ */
+ if (totalRecords1 != totalRecords2) {
+ throw new IOException("Zip states records split in multiple disks, which is not "
+ + "supported.");
+ }
+
+ Verify.verify(totalRecords1 <= Integer.MAX_VALUE);
+
+ totalRecords = Ints.checkedCast(totalRecords1);
+ this.directorySize = directorySize;
+ this.directoryOffset = directoryOffset;
+
+ if (bytes.remaining() < commentSize) {
+ throw new IOException("Corrupt EOCD record: not enough data for comment (comment "
+ + "size is " + commentSize + ").");
+ }
+
+ comment = new byte[commentSize];
+ bytes.get(comment);
+ byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
+ }
+
+ /**
+ * Creates a new EOCD. This is used when generating an EOCD for an Central Directory that has
+ * just been generated. The EOCD will be generated without any comment.
+ *
+ * @param totalRecords total number of records in the directory
+ * @param directoryOffset offset, since beginning of archive, where the Central Directory is
+ * located
+ * @param directorySize number of bytes of the Central Directory
+ * @param comment the EOCD comment
+ */
+ Eocd(int totalRecords, long directoryOffset, long directorySize, @Nonnull byte[] comment) {
+ Preconditions.checkArgument(totalRecords >= 0, "totalRecords < 0");
+ Preconditions.checkArgument(directoryOffset >= 0, "directoryOffset < 0");
+ Preconditions.checkArgument(directorySize >= 0, "directorySize < 0");
+
+ this.totalRecords = totalRecords;
+ this.directoryOffset = directoryOffset;
+ this.directorySize = directorySize;
+ this.comment = comment;
+ byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
+ }
+
+ /**
+ * Obtains the number of records in the Central Directory.
+ *
+ * @return the number of records
+ */
+ int getTotalRecords() {
+ return totalRecords;
+ }
+
+ /**
+ * Obtains the offset since the beginning of the zip archive where the Central Directory is
+ * located.
+ *
+ * @return the offset where the Central Directory is located
+ */
+ long getDirectoryOffset() {
+ return directoryOffset;
+ }
+
+ /**
+ * Obtains the size of the Central Directory.
+ *
+ * @return the number of bytes that make up the Central Directory
+ */
+ long getDirectorySize() {
+ return directorySize;
+ }
+
+ /**
+ * Obtains the size of the EOCD.
+ *
+ * @return the size, in bytes, of the EOCD
+ */
+ long getEocdSize() {
+ return (long) F_COMMENT_SIZE.endOffset() + comment.length;
+ }
+
+ /**
+ * Generates the EOCD data.
+ *
+ * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes
+ * @throws IOException failed to generate the EOCD data
+ */
+ @Nonnull
+ byte[] toBytes() throws IOException {
+ return byteSupplier.get();
+ }
+
+ /*
+ * Obtains the comment in the EOCD.
+ *
+ * @return the comment exactly as it is represented in the file (no encoding conversion is
+ * done)
+ */
+ @Nonnull
+ byte[] getComment() {
+ byte[] commentCopy = new byte[comment.length];
+ System.arraycopy(comment, 0, commentCopy, 0, comment.length);
+ return commentCopy;
+ }
+
+ /**
+ * Computes the byte representation of the EOCD.
+ *
+ * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes
+ * @throws UncheckedIOException failed to generate the EOCD data
+ */
+ @Nonnull
+ private byte[] computeByteRepresentation() {
+ ByteBuffer out = ByteBuffer.allocate(F_COMMENT_SIZE.endOffset() + comment.length);
+
+ try {
+ F_SIGNATURE.write(out);
+ F_NUMBER_OF_DISK.write(out);
+ F_DISK_CD_START.write(out);
+ F_RECORDS_DISK.write(out, totalRecords);
+ F_RECORDS_TOTAL.write(out, totalRecords);
+ F_CD_SIZE.write(out, directorySize);
+ F_CD_OFFSET.write(out, directoryOffset);
+ F_COMMENT_SIZE.write(out, comment.length);
+ out.put(comment);
+
+ return out.array();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+}