aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipUtil.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipUtil.java')
-rw-r--r--src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipUtil.java728
1 files changed, 728 insertions, 0 deletions
diff --git a/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipUtil.java b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipUtil.java
new file mode 100644
index 0000000000..2ba4caf4f2
--- /dev/null
+++ b/src/java_tools/singlejar/java/com/google/devtools/build/zip/ZipUtil.java
@@ -0,0 +1,728 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// 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.google.devtools.build.zip;
+
+import com.google.devtools.build.zip.ZipFileEntry.Compression;
+import com.google.devtools.build.zip.ZipFileEntry.Feature;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.GregorianCalendar;
+import java.util.zip.ZipException;
+
+/** A utility class for reading and writing {@link ZipFileEntry}s from byte arrays. */
+public class ZipUtil {
+
+ /**
+ * Midnight Jan 1st 1980. Uses the current time zone as the DOS format does not support time zones
+ * and will always assume the current zone.
+ */
+ public static final long DOS_EPOCH =
+ new GregorianCalendar(1980, Calendar.JANUARY, 1, 0, 0, 0).getTimeInMillis();
+
+ /** 23:59:59 Dec 31st 2107. The maximum date representable in DOS format. */
+ public static final long MAX_DOS_DATE =
+ new GregorianCalendar(2107, Calendar.DECEMBER, 31, 23, 59, 59).getTimeInMillis();
+
+ /** Converts a integral value to the corresponding little endian array. */
+ private static byte[] integerToLittleEndian(byte[] buf, int offset, long value, int numBytes) {
+ for (int i = 0; i < numBytes; i++) {
+ buf[i + offset] = (byte) ((value & (0xffL << (i * 8))) >> (i * 8));
+ }
+ return buf;
+ }
+
+ /** Converts a short to the corresponding 2-byte little endian array. */
+ static byte[] shortToLittleEndian(short value) {
+ return integerToLittleEndian(new byte[2], 0, value, 2);
+ }
+
+ /** Writes a short to the buffer as a 2-byte little endian array starting at offset. */
+ static byte[] shortToLittleEndian(byte[] buf, int offset, short value) {
+ return integerToLittleEndian(buf, offset, value, 2);
+ }
+
+ /** Converts an int to the corresponding 4-byte little endian array. */
+ static byte[] intToLittleEndian(int value) {
+ return integerToLittleEndian(new byte[4], 0, value, 4);
+ }
+
+ /** Writes an int to the buffer as a 4-byte little endian array starting at offset. */
+ static byte[] intToLittleEndian(byte[] buf, int offset, int value) {
+ return integerToLittleEndian(buf, offset, value, 4);
+ }
+
+ /** Converts a long to the corresponding 8-byte little endian array. */
+ static byte[] longToLittleEndian(long value) {
+ return integerToLittleEndian(new byte[8], 0, value, 8);
+ }
+
+ /** Writes a long to the buffer as a 8-byte little endian array starting at offset. */
+ static byte[] longToLittleEndian(byte[] buf, int offset, long value) {
+ return integerToLittleEndian(buf, offset, value, 8);
+ }
+
+ /** Reads 16 bits in little-endian byte order from the buffer at the given offset. */
+ static short get16(byte[] source, int offset) {
+ int a = source[offset + 0] & 0xff;
+ int b = source[offset + 1] & 0xff;
+ return (short) ((b << 8) | a);
+ }
+
+ /** Reads 32 bits in little-endian byte order from the buffer at the given offset. */
+ static int get32(byte[] source, int offset) {
+ int a = source[offset + 0] & 0xff;
+ int b = source[offset + 1] & 0xff;
+ int c = source[offset + 2] & 0xff;
+ int d = source[offset + 3] & 0xff;
+ return (d << 24) | (c << 16) | (b << 8) | a;
+ }
+
+ /** Reads 64 bits in little-endian byte order from the buffer at the given offset. */
+ static long get64(byte[] source, int offset) {
+ long a = source[offset + 0] & 0xffL;
+ long b = source[offset + 1] & 0xffL;
+ long c = source[offset + 2] & 0xffL;
+ long d = source[offset + 3] & 0xffL;
+ long e = source[offset + 4] & 0xffL;
+ long f = source[offset + 5] & 0xffL;
+ long g = source[offset + 6] & 0xffL;
+ long h = source[offset + 7] & 0xffL;
+ return (h << 56) | (g << 48) | (f << 40) | (e << 32) | (d << 24) | (c << 16) | (b << 8) | a;
+ }
+
+ /**
+ * Reads an unsigned short in little-endian byte order from the buffer at the given offset.
+ * Casts to an int to allow proper numerical comparison.
+ */
+ static int getUnsignedShort(byte[] source, int offset) {
+ return get16(source, offset) & 0xffff;
+ }
+
+ /**
+ * Reads an unsigned int in little-endian byte order from the buffer at the given offset.
+ * Casts to a long to allow proper numerical comparison.
+ */
+ static long getUnsignedInt(byte[] source, int offset) {
+ return get32(source, offset) & 0xffffffffL;
+ }
+
+ /**
+ * Reads an unsigned long in little-endian byte order from the buffer at the given offset.
+ * Performs bounds checking to see if the unsigned long will be properly represented in Java's
+ * signed value.
+ */
+ static long getUnsignedLong(byte[] source, int offset) throws ZipException {
+ long result = get64(source, offset);
+ if (result < 0) {
+ throw new ZipException("The requested unsigned long value is too large for Java's signed"
+ + "values. This Zip file is unsupported");
+ }
+ return result;
+ }
+
+ /** Checks if the timestamp is representable as a valid DOS timestamp. */
+ private static boolean isValidInDos(long timestamp) {
+ Calendar time = Calendar.getInstance();
+ time.setTimeInMillis(timestamp);
+ Calendar minTime = Calendar.getInstance();
+ minTime.setTimeInMillis(DOS_EPOCH);
+ Calendar maxTime = Calendar.getInstance();
+ maxTime.setTimeInMillis(MAX_DOS_DATE);
+ return (!time.before(minTime) && !time.after(maxTime));
+ }
+
+ /** Converts a unix timestamp into a 32-bit DOS timestamp. */
+ static int unixToDosTime(long timestamp) {
+ Calendar time = Calendar.getInstance();
+ time.setTimeInMillis(timestamp);
+
+ if (!isValidInDos(timestamp)) {
+ DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ throw new IllegalArgumentException(String.format("%s is not representable in the DOS time"
+ + " format. It must be in the range %s to %s", df.format(time.getTime()),
+ df.format(new Date(DOS_EPOCH)), df.format(new Date(MAX_DOS_DATE))));
+ }
+
+ int dos = time.get(Calendar.SECOND) >> 1;
+ dos |= time.get(Calendar.MINUTE) << 5;
+ dos |= time.get(Calendar.HOUR_OF_DAY) << 11;
+ dos |= time.get(Calendar.DAY_OF_MONTH) << 16;
+ dos |= (time.get(Calendar.MONTH) + 1) << 21;
+ dos |= (time.get(Calendar.YEAR) - 1980) << 25;
+ return dos;
+ }
+
+ /** Converts a 32-bit DOS timestamp into a unix timestamp. */
+ static long dosToUnixTime(int timestamp) {
+ Calendar time = Calendar.getInstance();
+ time.clear();
+ time.set(Calendar.SECOND, (timestamp << 1) & 0x3e);
+ time.set(Calendar.MINUTE, (timestamp >> 5) & 0x3f);
+ time.set(Calendar.HOUR_OF_DAY, (timestamp >> 11) & 0x1f);
+ time.set(Calendar.DAY_OF_MONTH, (timestamp >> 16) & 0x1f);
+ time.set(Calendar.MONTH, ((timestamp >> 21) & 0x0f) - 1);
+ time.set(Calendar.YEAR, ((timestamp >> 25) & 0x7f) + 1980);
+ return time.getTimeInMillis();
+ }
+
+ /** Checks if array starts with target. */
+ static boolean arrayStartsWith(byte[] array, byte[] target) {
+ if (array == null) {
+ return false;
+ }
+ if (target == null) {
+ return true;
+ }
+ if (target.length > array.length) {
+ return false;
+ }
+ for (int i = 0; i < target.length; i++) {
+ if (array[i] != target[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static class LocalFileHeader {
+ static final int SIGNATURE = 0x04034b50;
+ static final int FIXED_DATA_SIZE = 30;
+ static final int SIGNATURE_OFFSET = 0;
+ static final int VERSION_OFFSET = 4;
+ static final int FLAGS_OFFSET = 6;
+ static final int METHOD_OFFSET = 8;
+ static final int MOD_TIME_OFFSET = 10;
+ static final int CRC_OFFSET = 14;
+ static final int COMPRESSED_SIZE_OFFSET = 18;
+ static final int UNCOMPRESSED_SIZE_OFFSET = 22;
+ static final int FILENAME_LENGTH_OFFSET = 26;
+ static final int EXTRA_FIELD_LENGTH_OFFSET = 28;
+ static final int VARIABLE_DATA_OFFSET = 30;
+
+ /**
+ * Generates the raw byte data of the local file header for the {@link ZipFileEntry}. Uses the
+ * specified {@link ZipFileData} to encode the file name and comment.
+ * @throws IOException
+ */
+ static byte[] create(ZipFileEntry entry, ZipFileData file, boolean allowZip64)
+ throws IOException {
+ byte[] name = entry.getName().getBytes(file.getCharset());
+ ExtraDataList extra = entry.getExtra();
+
+ EnumSet<Feature> features = entry.getFeatureSet();
+ int size = (int) entry.getSize();
+ int csize = (int) entry.getCompressedSize();
+
+ if (features.contains(Feature.ZIP64_SIZE) || features.contains(Feature.ZIP64_CSIZE)) {
+ if (!allowZip64) {
+ throw new ZipException(String.format("Writing an entry of size %d(%d) without Zip64"
+ + " extensions is not supported.", entry.getSize(), entry.getCompressedSize()));
+ }
+ extra.remove((short) 0x0001);
+ int extraSize = 0;
+ if (features.contains(Feature.ZIP64_SIZE)) {
+ size = -1;
+ extraSize += 8;
+ }
+ if (features.contains(Feature.ZIP64_CSIZE)) {
+ csize = -1;
+ extraSize += 8;
+ }
+ byte[] zip64Extra = new byte[ExtraData.FIXED_DATA_SIZE + extraSize];
+ shortToLittleEndian(zip64Extra, ExtraData.ID_OFFSET, (short) 0x0001);
+ shortToLittleEndian(zip64Extra, ExtraData.LENGTH_OFFSET, (short) extraSize);
+ int offset = ExtraData.FIXED_DATA_SIZE;
+ if (features.contains(Feature.ZIP64_SIZE)) {
+ longToLittleEndian(zip64Extra, offset, entry.getSize());
+ offset += 8;
+ }
+ if (features.contains(Feature.ZIP64_CSIZE)) {
+ longToLittleEndian(zip64Extra, offset, entry.getCompressedSize());
+ offset += 8;
+ }
+ extra.add(new ExtraData(zip64Extra, 0));
+ } else {
+ extra.remove((short) 0x0001);
+ }
+
+ byte[] buf = new byte[FIXED_DATA_SIZE + name.length + extra.getLength()];
+ intToLittleEndian(buf, SIGNATURE_OFFSET, SIGNATURE);
+ shortToLittleEndian(buf, VERSION_OFFSET, entry.getVersionNeeded());
+ shortToLittleEndian(buf, FLAGS_OFFSET, entry.getFlags());
+ shortToLittleEndian(buf, METHOD_OFFSET, entry.getMethod().getValue());
+ intToLittleEndian(buf, MOD_TIME_OFFSET, unixToDosTime(entry.getTime()));
+ intToLittleEndian(buf, CRC_OFFSET, (int) (entry.getCrc() & 0xffffffff));
+ intToLittleEndian(buf, COMPRESSED_SIZE_OFFSET, csize);
+ intToLittleEndian(buf, UNCOMPRESSED_SIZE_OFFSET, size);
+ shortToLittleEndian(buf, FILENAME_LENGTH_OFFSET, (short) name.length);
+ shortToLittleEndian(buf, EXTRA_FIELD_LENGTH_OFFSET, (short) extra.getLength());
+ System.arraycopy(name, 0, buf, FIXED_DATA_SIZE, name.length);
+ extra.getByteStream().read(buf, FIXED_DATA_SIZE + name.length, extra.getLength());
+
+ return buf;
+ }
+ }
+
+ static class CentralDirectoryFileHeader {
+ static final int SIGNATURE = 0x02014b50;
+ static final int FIXED_DATA_SIZE = 46;
+ static final int SIGNATURE_OFFSET = 0;
+ static final int VERSION_OFFSET = 4;
+ static final int VERSION_NEEDED_OFFSET = 6;
+ static final int FLAGS_OFFSET = 8;
+ static final int METHOD_OFFSET = 10;
+ static final int MOD_TIME_OFFSET = 12;
+ static final int CRC_OFFSET = 16;
+ static final int COMPRESSED_SIZE_OFFSET = 20;
+ static final int UNCOMPRESSED_SIZE_OFFSET = 24;
+ static final int FILENAME_LENGTH_OFFSET = 28;
+ static final int EXTRA_FIELD_LENGTH_OFFSET = 30;
+ static final int COMMENT_LENGTH_OFFSET = 32;
+ static final int DISK_START_OFFSET = 34;
+ static final int INTERNAL_ATTRIBUTES_OFFSET = 36;
+ static final int EXTERNAL_ATTRIBUTES_OFFSET = 38;
+ static final int LOCAL_HEADER_OFFSET_OFFSET = 42;
+
+ /**
+ * Reads a {@link ZipFileEntry} from the input stream, using the specified {@link Charset} to
+ * decode the filename and comment.
+ */
+ static ZipFileEntry read(InputStream in, Charset charset)
+ throws IOException {
+ byte[] fixedSizeData = new byte[FIXED_DATA_SIZE];
+
+ if (in.read(fixedSizeData) != FIXED_DATA_SIZE) {
+ throw new ZipException(
+ "Unexpected end of file while reading Central Directory File Header.");
+ }
+ if (!arrayStartsWith(fixedSizeData, intToLittleEndian(SIGNATURE))) {
+ throw new ZipException(String.format(
+ "Malformed Central Directory File Header; does not start with %08x", SIGNATURE));
+ }
+
+ byte[] name = new byte[getUnsignedShort(fixedSizeData, FILENAME_LENGTH_OFFSET)];
+ byte[] extraField = new byte[getUnsignedShort(fixedSizeData, EXTRA_FIELD_LENGTH_OFFSET)];
+ byte[] comment = new byte[getUnsignedShort(fixedSizeData, COMMENT_LENGTH_OFFSET)];
+
+ if (name.length > 0 && in.read(name) != name.length) {
+ throw new ZipException(
+ "Unexpected end of file while reading Central Directory File Header.");
+ }
+ if (extraField.length > 0 && in.read(extraField) != extraField.length) {
+ throw new ZipException(
+ "Unexpected end of file while reading Central Directory File Header.");
+ }
+ if (comment.length > 0 && in.read(comment) != comment.length) {
+ throw new ZipException(
+ "Unexpected end of file while reading Central Directory File Header.");
+ }
+
+ ExtraDataList extra = new ExtraDataList(extraField);
+
+ long csize = getUnsignedInt(fixedSizeData, COMPRESSED_SIZE_OFFSET);
+ long size = getUnsignedInt(fixedSizeData, UNCOMPRESSED_SIZE_OFFSET);
+ long offset = getUnsignedInt(fixedSizeData, LOCAL_HEADER_OFFSET_OFFSET);
+ if (csize == 0xffffffffL || size == 0xffffffffL || offset == 0xffffffffL) {
+ ExtraData zip64Extra = extra.get((short) 0x0001);
+ if (zip64Extra != null) {
+ int index = 0;
+ if (size == 0xffffffffL) {
+ size = ZipUtil.getUnsignedLong(zip64Extra.getData(), index);
+ index += 8;
+ }
+ if (csize == 0xffffffffL) {
+ csize = ZipUtil.getUnsignedLong(zip64Extra.getData(), index);
+ index += 8;
+ }
+ if (offset == 0xffffffffL) {
+ offset = ZipUtil.getUnsignedLong(zip64Extra.getData(), index);
+ index += 8;
+ }
+ }
+ }
+
+ ZipFileEntry entry = new ZipFileEntry(new String(name, charset));
+ entry.setVersion(get16(fixedSizeData, VERSION_OFFSET));
+ entry.setVersionNeeded(get16(fixedSizeData, VERSION_NEEDED_OFFSET));
+ entry.setFlags(get16(fixedSizeData, FLAGS_OFFSET));
+ entry.setMethod(Compression.fromValue(get16(fixedSizeData, METHOD_OFFSET)));
+ long time = dosToUnixTime(get32(fixedSizeData, MOD_TIME_OFFSET));
+ entry.setTime(isValidInDos(time) ? time : DOS_EPOCH);
+ entry.setCrc(getUnsignedInt(fixedSizeData, CRC_OFFSET));
+ entry.setCompressedSize(csize);
+ entry.setSize(size);
+ entry.setInternalAttributes(get16(fixedSizeData, INTERNAL_ATTRIBUTES_OFFSET));
+ entry.setExternalAttributes(get32(fixedSizeData, EXTERNAL_ATTRIBUTES_OFFSET));
+ entry.setLocalHeaderOffset(offset);
+ entry.setExtra(extra);
+ entry.setComment(new String(comment, charset));
+
+ return entry;
+ }
+
+ /**
+ * Generates the raw byte data of the central directory file header for the ZipEntry. Uses the
+ * specified {@link ZipFileData} to encode the file name and comment.
+ * @throws ZipException
+ */
+ static byte[] create(ZipFileEntry entry, ZipFileData file, boolean allowZip64)
+ throws ZipException {
+ if (allowZip64) {
+ addZip64Extra(entry);
+ } else {
+ entry.getExtra().remove((short) 0x0001);
+ }
+ byte[] name = file.getBytes(entry.getName());
+ byte[] extra = entry.getExtra().getBytes();
+ byte[] comment = entry.getComment() != null
+ ? file.getBytes(entry.getComment()) : new byte[]{};
+
+ byte[] buf = new byte[FIXED_DATA_SIZE + name.length + extra.length + comment.length];
+
+ fillFixedSizeData(buf, entry, name.length, extra.length, comment.length, allowZip64);
+ System.arraycopy(name, 0, buf, FIXED_DATA_SIZE, name.length);
+ System.arraycopy(extra, 0, buf, FIXED_DATA_SIZE + name.length, extra.length);
+ System.arraycopy(comment, 0, buf, FIXED_DATA_SIZE + name.length + extra.length,
+ comment.length);
+
+ return buf;
+ }
+
+ /**
+ * Writes the central directory file header for the ZipEntry to an output stream. Uses the
+ * specified {@link ZipFileData} to encode the file name and comment.
+ */
+ static int write(ZipFileEntry entry, ZipFileData file, boolean allowZip64, byte[] buf,
+ OutputStream stream) throws IOException {
+ if (buf == null || buf.length < FIXED_DATA_SIZE) {
+ buf = new byte[FIXED_DATA_SIZE];
+ }
+
+ if (allowZip64) {
+ addZip64Extra(entry);
+ } else {
+ entry.getExtra().remove((short) 0x0001);
+ }
+ byte[] name = entry.getName().getBytes(file.getCharset());
+ byte[] extra = entry.getExtra().getBytes();
+ byte[] comment = entry.getComment() != null
+ ? entry.getComment().getBytes(file.getCharset()) : new byte[]{};
+
+ fillFixedSizeData(buf, entry, name.length, extra.length, comment.length, allowZip64);
+ stream.write(buf, 0, FIXED_DATA_SIZE);
+ stream.write(name);
+ stream.write(extra);
+ stream.write(comment);
+
+ return FIXED_DATA_SIZE + name.length + extra.length + comment.length;
+ }
+
+ /**
+ * Write the fixed size data portion for the specified ZIP entry to the buffer.
+ * @throws ZipException
+ */
+ private static void fillFixedSizeData(byte[] buf, ZipFileEntry entry, int nameLength,
+ int extraLength, int commentLength, boolean allowZip64) throws ZipException {
+ if (!allowZip64 && entry.getFeatureSet().contains(Feature.ZIP64_CSIZE)) {
+ throw new ZipException(String.format("Writing an entry with compressed size %d without"
+ + " Zip64 extensions is not supported.", entry.getCompressedSize()));
+ }
+ if (!allowZip64 && entry.getFeatureSet().contains(Feature.ZIP64_SIZE)) {
+ throw new ZipException(String.format("Writing an entry of size %d without"
+ + " Zip64 extensions is not supported.", entry.getSize()));
+ }
+ if (!allowZip64 && entry.getFeatureSet().contains(Feature.ZIP64_OFFSET)) {
+ throw new ZipException(String.format("Writing an entry with local header offset %d without"
+ + " Zip64 extensions is not supported.", entry.getLocalHeaderOffset()));
+ }
+ int csize = (int) (entry.getFeatureSet().contains(Feature.ZIP64_CSIZE)
+ ? -1 : entry.getCompressedSize());
+ int size = (int) (entry.getFeatureSet().contains(Feature.ZIP64_SIZE)
+ ? -1 : entry.getSize());
+ int offset = (int) (entry.getFeatureSet().contains(Feature.ZIP64_OFFSET)
+ ? -1 : entry.getLocalHeaderOffset());
+ intToLittleEndian(buf, SIGNATURE_OFFSET, SIGNATURE);
+ shortToLittleEndian(buf, VERSION_OFFSET, entry.getVersion());
+ shortToLittleEndian(buf, VERSION_NEEDED_OFFSET, entry.getVersionNeeded());
+ shortToLittleEndian(buf, FLAGS_OFFSET, entry.getFlags());
+ shortToLittleEndian(buf, METHOD_OFFSET, entry.getMethod().getValue());
+ intToLittleEndian(buf, MOD_TIME_OFFSET, unixToDosTime(entry.getTime()));
+ intToLittleEndian(buf, CRC_OFFSET, (int) (entry.getCrc() & 0xffffffff));
+ intToLittleEndian(buf, COMPRESSED_SIZE_OFFSET, csize);
+ intToLittleEndian(buf, UNCOMPRESSED_SIZE_OFFSET, size);
+ shortToLittleEndian(buf, FILENAME_LENGTH_OFFSET, (short) (nameLength & 0xffff));
+ shortToLittleEndian(buf, EXTRA_FIELD_LENGTH_OFFSET, (short) (extraLength & 0xffff));
+ shortToLittleEndian(buf, COMMENT_LENGTH_OFFSET, (short) (commentLength & 0xffff));
+ shortToLittleEndian(buf, DISK_START_OFFSET, (short) 0);
+ shortToLittleEndian(buf, INTERNAL_ATTRIBUTES_OFFSET, entry.getInternalAttributes());
+ intToLittleEndian(buf, EXTERNAL_ATTRIBUTES_OFFSET, entry.getExternalAttributes());
+ intToLittleEndian(buf, LOCAL_HEADER_OFFSET_OFFSET, offset);
+ }
+
+ /**
+ * Update the extra data fields to contain a Zip64 extended information field if required
+ */
+ private static void addZip64Extra(ZipFileEntry entry) {
+ EnumSet<Feature> features = entry.getFeatureSet();
+ ExtraDataList extra = entry.getExtra();
+ int extraSize = 0;
+ if (features.contains(Feature.ZIP64_SIZE)) {
+ extraSize += 8;
+ }
+ if (features.contains(Feature.ZIP64_CSIZE)) {
+ extraSize += 8;
+ }
+ if (features.contains(Feature.ZIP64_OFFSET)) {
+ extraSize += 8;
+ }
+ if (extraSize > 0) {
+ extra.remove((short) 0x0001);
+ byte[] zip64Extra = new byte[ExtraData.FIXED_DATA_SIZE + extraSize];
+ shortToLittleEndian(zip64Extra, ExtraData.ID_OFFSET, (short) 0x0001);
+ shortToLittleEndian(zip64Extra, ExtraData.LENGTH_OFFSET, (short) extraSize);
+ int offset = ExtraData.FIXED_DATA_SIZE;
+ if (features.contains(Feature.ZIP64_SIZE)) {
+ longToLittleEndian(zip64Extra, offset, entry.getSize());
+ offset += 8;
+ }
+ if (features.contains(Feature.ZIP64_CSIZE)) {
+ longToLittleEndian(zip64Extra, offset, entry.getCompressedSize());
+ offset += 8;
+ }
+ if (features.contains(Feature.ZIP64_OFFSET)) {
+ longToLittleEndian(zip64Extra, offset, entry.getLocalHeaderOffset());
+ }
+ extra.add(new ExtraData(zip64Extra, 0));
+ }
+ }
+ }
+
+ static class Zip64EndOfCentralDirectory {
+ static final int SIGNATURE = 0x06064b50;
+ static final int FIXED_DATA_SIZE = 56;
+ static final int SIGNATURE_OFFSET = 0;
+ static final int SIZE_OFFSET = 4;
+ static final int VERSION_OFFSET = 12;
+ static final int VERSION_NEEDED_OFFSET = 14;
+ static final int DISK_NUMBER_OFFSET = 16;
+ static final int CD_DISK_OFFSET = 20;
+ static final int DISK_ENTRIES_OFFSET = 24;
+ static final int TOTAL_ENTRIES_OFFSET = 32;
+ static final int CD_SIZE_OFFSET = 40;
+ static final int CD_OFFSET_OFFSET = 48;
+
+ /**
+ * Read the Zip64 end of central directory record from the input stream and parse additional
+ * {@link ZipFileData} from it.
+ */
+ static ZipFileData read(InputStream in, ZipFileData file) throws IOException {
+ if (file == null) {
+ throw new NullPointerException();
+ }
+
+ byte[] fixedSizeData = new byte[FIXED_DATA_SIZE];
+ if (in.read(fixedSizeData) != FIXED_DATA_SIZE) {
+ throw new ZipException(
+ "Unexpected end of file while reading Zip64 End of Central Directory Record.");
+ }
+ if (!arrayStartsWith(fixedSizeData, intToLittleEndian(SIGNATURE))) {
+ throw new ZipException(String.format(
+ "Malformed Zip64 End of Central Directory; does not start with %08x", SIGNATURE));
+ }
+ file.setZip64(true);
+ file.setCentralDirectoryOffset(getUnsignedLong(fixedSizeData, CD_OFFSET_OFFSET));
+ file.setExpectedEntries(getUnsignedLong(fixedSizeData, TOTAL_ENTRIES_OFFSET));
+ return file;
+ }
+
+ /**
+ * Generates the raw byte data of the Zip64 end of central directory record for the file.
+ */
+ static byte[] create(ZipFileData file) {
+ byte[] buf = new byte[FIXED_DATA_SIZE];
+ intToLittleEndian(buf, SIGNATURE_OFFSET, SIGNATURE);
+ longToLittleEndian(buf, SIZE_OFFSET, FIXED_DATA_SIZE - 12);
+ shortToLittleEndian(buf, VERSION_OFFSET, (short) 0x2d);
+ shortToLittleEndian(buf, VERSION_NEEDED_OFFSET, (short) 0x2d);
+ intToLittleEndian(buf, DISK_NUMBER_OFFSET, 0);
+ intToLittleEndian(buf, CD_DISK_OFFSET, 0);
+ longToLittleEndian(buf, DISK_ENTRIES_OFFSET, file.getNumEntries());
+ longToLittleEndian(buf, TOTAL_ENTRIES_OFFSET, file.getNumEntries());
+ longToLittleEndian(buf, CD_SIZE_OFFSET, file.getCentralDirectorySize());
+ longToLittleEndian(buf, CD_OFFSET_OFFSET, file.getCentralDirectoryOffset());
+ return buf;
+ }
+ }
+
+ static class Zip64EndOfCentralDirectoryLocator {
+ static final int SIGNATURE = 0x07064b50;
+ static final int FIXED_DATA_SIZE = 20;
+ static final int SIGNATURE_OFFSET = 0;
+ static final int ZIP64_EOCD_DISK_OFFSET = 4;
+ static final int ZIP64_EOCD_OFFSET_OFFSET = 8;
+ static final int DISK_NUMBER_OFFSET = 16;
+
+ /**
+ * Read the Zip64 end of central directory locator from the input stream and parse additional
+ * {@link ZipFileData} from it.
+ */
+ static ZipFileData read(InputStream in, ZipFileData file) throws IOException {
+ if (file == null) {
+ throw new NullPointerException();
+ }
+
+ byte[] fixedSizeData = new byte[FIXED_DATA_SIZE];
+ if (in.read(fixedSizeData) != FIXED_DATA_SIZE) {
+ throw new ZipException(
+ "Unexpected end of file while reading Zip64 End of Central Directory Locator.");
+ }
+ if (!arrayStartsWith(fixedSizeData, intToLittleEndian(SIGNATURE))) {
+ throw new ZipException(String.format(
+ "Malformed Zip64 Central Directory Locator; does not start with %08x", SIGNATURE));
+ }
+ file.setZip64(true);
+ file.setZip64EndOfCentralDirectoryOffset(
+ getUnsignedLong(fixedSizeData, ZIP64_EOCD_OFFSET_OFFSET));
+ return file;
+ }
+
+ /**
+ * Generates the raw byte data of the Zip64 end of central directory locator for the file.
+ */
+ static byte[] create(ZipFileData file) {
+ byte[] buf = new byte[FIXED_DATA_SIZE];
+ intToLittleEndian(buf, SIGNATURE_OFFSET, SIGNATURE);
+ intToLittleEndian(buf, ZIP64_EOCD_DISK_OFFSET, 0);
+ longToLittleEndian(buf, ZIP64_EOCD_OFFSET_OFFSET, file.getZip64EndOfCentralDirectoryOffset());
+ intToLittleEndian(buf, DISK_NUMBER_OFFSET, 1);
+ return buf;
+ }
+ }
+
+ static class EndOfCentralDirectoryRecord {
+ static final int SIGNATURE = 0x06054b50;
+ static final int FIXED_DATA_SIZE = 22;
+ static final int SIGNATURE_OFFSET = 0;
+ static final int DISK_NUMBER_OFFSET = 4;
+ static final int CD_DISK_OFFSET = 6;
+ static final int DISK_ENTRIES_OFFSET = 8;
+ static final int TOTAL_ENTRIES_OFFSET = 10;
+ static final int CD_SIZE_OFFSET = 12;
+ static final int CD_OFFSET_OFFSET = 16;
+ static final int COMMENT_LENGTH_OFFSET = 20;
+
+ /**
+ * Read the end of central directory record from the input stream and parse {@link ZipFileData}
+ * from it.
+ */
+ static ZipFileData read(InputStream in, ZipFileData file) throws IOException {
+ if (file == null) {
+ throw new NullPointerException();
+ }
+
+ byte[] fixedSizeData = new byte[FIXED_DATA_SIZE];
+ if (in.read(fixedSizeData) != FIXED_DATA_SIZE) {
+ throw new ZipException(
+ "Unexpected end of file while reading End of Central Directory Record.");
+ }
+ if (!arrayStartsWith(fixedSizeData, intToLittleEndian(SIGNATURE))) {
+ throw new ZipException(String.format(
+ "Malformed End of Central Directory Record; does not start with %08x", SIGNATURE));
+ }
+
+ byte[] comment = new byte[getUnsignedShort(fixedSizeData, COMMENT_LENGTH_OFFSET)];
+ if (comment.length > 0 && in.read(comment) != comment.length) {
+ throw new ZipException(
+ "Unexpected end of file while reading End of Central Directory Record.");
+ }
+ short diskNumber = get16(fixedSizeData, DISK_NUMBER_OFFSET);
+ short centralDirectoryDisk = get16(fixedSizeData, CD_DISK_OFFSET);
+ short entriesOnDisk = get16(fixedSizeData, DISK_ENTRIES_OFFSET);
+ short totalEntries = get16(fixedSizeData, TOTAL_ENTRIES_OFFSET);
+ int centralDirectorySize = get32(fixedSizeData, CD_SIZE_OFFSET);
+ int centralDirectoryOffset = get32(fixedSizeData, CD_OFFSET_OFFSET);
+ if (diskNumber == -1 || centralDirectoryDisk == -1 || entriesOnDisk == -1
+ || totalEntries == -1 || centralDirectorySize == -1 || centralDirectoryOffset == -1) {
+ file.setMaybeZip64(true);
+ }
+ file.setComment(comment);
+ file.setCentralDirectorySize(getUnsignedInt(fixedSizeData, CD_SIZE_OFFSET));
+ file.setCentralDirectoryOffset(getUnsignedInt(fixedSizeData, CD_OFFSET_OFFSET));
+ file.setExpectedEntries(getUnsignedShort(fixedSizeData, TOTAL_ENTRIES_OFFSET));
+ return file;
+ }
+
+ /**
+ * Generates the raw byte data of the end of central directory record for the specified
+ * {@link ZipFileData}.
+ * @throws ZipException if the file comment is too long
+ */
+ static byte[] create(ZipFileData file, boolean allowZip64) throws ZipException {
+ byte[] comment = file.getBytes(file.getComment());
+
+ byte[] buf = new byte[FIXED_DATA_SIZE + comment.length];
+
+ // Allow writing of Zip file without Zip64 extensions for large archives as a special case
+ // since many reading implementations can handle this.
+ short numEntries = (short) (file.getNumEntries() > 0xffff && allowZip64
+ ? -1 : file.getNumEntries());
+ int cdSize = (int) (file.getCentralDirectorySize() > 0xffffffffL && allowZip64
+ ? -1 : file.getCentralDirectorySize());
+ int cdOffset = (int) (file.getCentralDirectoryOffset() > 0xffffffffL && allowZip64
+ ? -1 : file.getCentralDirectoryOffset());
+ intToLittleEndian(buf, SIGNATURE_OFFSET, SIGNATURE);
+ shortToLittleEndian(buf, DISK_NUMBER_OFFSET, (short) 0);
+ shortToLittleEndian(buf, CD_DISK_OFFSET, (short) 0);
+ shortToLittleEndian(buf, DISK_ENTRIES_OFFSET, numEntries);
+ shortToLittleEndian(buf, TOTAL_ENTRIES_OFFSET, numEntries);
+ intToLittleEndian(buf, CD_SIZE_OFFSET, cdSize);
+ intToLittleEndian(buf, CD_OFFSET_OFFSET, cdOffset);
+ shortToLittleEndian(buf, COMMENT_LENGTH_OFFSET, (short) comment.length);
+ System.arraycopy(comment, 0, buf, FIXED_DATA_SIZE, comment.length);
+
+ return buf;
+ }
+ }
+
+ static class CentralDirectory {
+ /**
+ * Writes the central directory to an output stream for the specified {@link ZipFileData}.
+ */
+ static void write(ZipFileData file, boolean allowZip64, OutputStream stream)
+ throws IOException {
+ long directorySize = 0;
+ byte[] buf = new byte[CentralDirectoryFileHeader.FIXED_DATA_SIZE];
+ for (ZipFileEntry entry : file.getEntries()) {
+ directorySize += CentralDirectoryFileHeader.write(entry, file, allowZip64, buf, stream);
+ }
+ file.setCentralDirectorySize(directorySize);
+ if (file.isZip64() && allowZip64) {
+ file.setZip64EndOfCentralDirectoryOffset(file.getCentralDirectoryOffset()
+ + file.getCentralDirectorySize());
+ stream.write(Zip64EndOfCentralDirectory.create(file));
+ stream.write(Zip64EndOfCentralDirectoryLocator.create(file));
+ }
+ stream.write(EndOfCentralDirectoryRecord.create(file, allowZip64));
+ }
+ }
+}