/*
* 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 java.util.zip;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import static java.util.zip.ZipOutputStream.writeIntAsUint16;
import static java.util.zip.ZipOutputStream.writeLongAsUint32;
import static java.util.zip.ZipOutputStream.writeLongAsUint64;
/**
* @hide
*/
public class Zip64 {
/* Non instantiable */
private Zip64() {}
/**
* The maximum supported entry / archive size for standard (non zip64) entries and archives.
*
* @hide
*/
public static final long MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE = 0x00000000ffffffffL;
/**
* The header ID of the zip64 extended info header. This value is used to identify
* zip64 data in the "extra" field in the file headers.
*/
private static final short ZIP64_EXTENDED_INFO_HEADER_ID = 0x0001;
/*
* Size (in bytes) of the zip64 end of central directory locator. This will be located
* immediately before the end of central directory record if a given zipfile is in the
* zip64 format.
*/
private static final int ZIP64_LOCATOR_SIZE = 20;
/**
* The zip64 end of central directory locator signature (4 bytes wide).
*/
private static final int ZIP64_LOCATOR_SIGNATURE = 0x07064b50;
/**
* The zip64 end of central directory record singature (4 bytes wide).
*/
private static final int ZIP64_EOCD_RECORD_SIGNATURE = 0x06064b50;
/**
* The "effective" size of the zip64 eocd record. This excludes the fields that
* are proprietary, signature, or fields we aren't interested in. We include the
* following (contiguous) fields in this calculation :
* - disk number (4 bytes)
* - disk with start of central directory (4 bytes)
* - number of central directory entries on this disk (8 bytes)
* - total number of central directory entries (8 bytes)
* - size of the central directory (8 bytes)
* - offset of the start of the central directory (8 bytes)
*/
private static final int ZIP64_EOCD_RECORD_EFFECTIVE_SIZE = 40;
/**
* Parses the zip64 end of central directory record locator. The locator
* must be placed immediately before the end of central directory (eocd) record
* starting at {@code eocdOffset}.
*
* The position of the file cursor for {@code raf} after a call to this method
* is undefined an callers must reposition it after each call to this method.
*/
public static long parseZip64EocdRecordLocator(RandomAccessFile raf, long eocdOffset)
throws IOException {
// The spec stays curiously silent about whether a zip file with an EOCD record,
// a zip64 locator and a zip64 eocd record is considered "empty". In our implementation,
// we parse all records and read the counts from them instead of drawing any size or
// layout based information.
if (eocdOffset > ZIP64_LOCATOR_SIZE) {
raf.seek(eocdOffset - ZIP64_LOCATOR_SIZE);
if (Integer.reverseBytes(raf.readInt()) == ZIP64_LOCATOR_SIGNATURE) {
byte[] zip64EocdLocator = new byte[ZIP64_LOCATOR_SIZE - 4];
raf.readFully(zip64EocdLocator);
ByteBuffer buf = ByteBuffer.wrap(zip64EocdLocator).order(ByteOrder.LITTLE_ENDIAN);
final int diskWithCentralDir = buf.getInt();
final long zip64EocdRecordOffset = buf.getLong();
final int numDisks = buf.getInt();
if (numDisks != 1 || diskWithCentralDir != 0) {
throw new ZipException("Spanned archives not supported");
}
return zip64EocdRecordOffset;
}
}
return -1;
}
public static ZipFile.EocdRecord parseZip64EocdRecord(RandomAccessFile raf,
long eocdRecordOffset, int commentLength) throws IOException {
raf.seek(eocdRecordOffset);
final int signature = Integer.reverseBytes(raf.readInt());
if (signature != ZIP64_EOCD_RECORD_SIGNATURE) {
throw new ZipException("Invalid zip64 eocd record offset, sig="
+ Integer.toHexString(signature) + " offset=" + eocdRecordOffset);
}
// The zip64 eocd record specifies its own size as an 8 byte integral type. It is variable
// length because of the "zip64 extensible data sector" but that field is reserved for
// pkware's proprietary use. We therefore disregard it altogether and treat the end of
// central directory structure as fixed length.
//
// We also skip "version made by" (2 bytes) and "version needed to extract" (2 bytes)
// fields. We perform additional validation at the ZipEntry level, where applicable.
//
// That's a total of 12 bytes to skip
raf.skipBytes(12);
byte[] zip64Eocd = new byte[ZIP64_EOCD_RECORD_EFFECTIVE_SIZE];
raf.readFully(zip64Eocd);
ByteBuffer buf = ByteBuffer.wrap(zip64Eocd).order(ByteOrder.LITTLE_ENDIAN);
try {
int diskNumber = buf.getInt();
int diskWithCentralDirStart = buf.getInt();
long numEntries = buf.getLong();
long totalNumEntries = buf.getLong();
buf.getLong(); // Ignore the size of the central directory
long centralDirOffset = buf.getLong();
if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDirStart != 0) {
throw new ZipException("Spanned archives not supported :" +
" numEntries=" + numEntries + ", totalNumEntries=" + totalNumEntries +
", diskNumber=" + diskNumber + ", diskWithCentralDirStart=" +
diskWithCentralDirStart);
}
return new ZipFile.EocdRecord(numEntries, centralDirOffset, commentLength);
} catch (BufferUnderflowException bue) {
ZipException zipException = new ZipException("Error parsing zip64 eocd record.");
zipException.initCause(bue);
throw zipException;
}
}
/**
* Parse the zip64 extended info record from the extras present in {@code ze}.
*
* If {@code fromCentralDirectory} is true, we assume we're parsing a central directory
* record. We assume a local file header otherwise. The difference between the two is that
* a central directory entry is required to be complete, whereas a local file header isn't.
* This is due to the presence of an optional data descriptor after the file content.
*
* @return {@code} true iff. a zip64 extended info record was found.
*/
public static boolean parseZip64ExtendedInfo(ZipEntry ze, boolean fromCentralDirectory)
throws ZipException {
int extendedInfoSize = -1;
int extendedInfoStart = -1;
// If this file contains a zip64 central directory locator, entries might
// optionally contain a zip64 extended information extra entry.
if (ze.extra != null && ze.extra.length > 0) {
// Extensible data fields are of the form header1+data1 + header2+data2 and so
// on, where each header consists of a 2 byte header ID followed by a 2 byte size.
// We need to iterate through the entire list of headers to find the header ID
// for the zip64 extended information extra field (0x0001).
final ByteBuffer buf = ByteBuffer.wrap(ze.extra).order(ByteOrder.LITTLE_ENDIAN);
extendedInfoSize = getZip64ExtendedInfoSize(buf);
if (extendedInfoSize != -1) {
extendedInfoStart = buf.position();
try {
// The size & compressed size only make sense in the central directory *or* if
// we know them beforehand. If we don't know them beforehand, they're stored in
// the data descriptor and should be read from there.
//
// Note that the spec says that the local file header "MUST" contain the
// original and compressed size fields. We don't care too much about that.
// The spec claims that the order of fields is fixed anyway.
if (fromCentralDirectory || (ze.getMethod() == ZipEntry.STORED)) {
if (ze.size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
ze.size = buf.getLong();
}
if (ze.compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
ze.compressedSize = buf.getLong();
}
}
// The local header offset is significant only in the central directory. It makes no
// sense within the local header itself.
if (fromCentralDirectory) {
if (ze.localHeaderRelOffset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
ze.localHeaderRelOffset = buf.getLong();
}
}
} catch (BufferUnderflowException bue) {
ZipException zipException = new ZipException("Error parsing extended info");
zipException.initCause(bue);
throw zipException;
}
}
}
// This entry doesn't contain a zip64 extended information data entry header.
// We have to check that the compressedSize / size / localHeaderRelOffset values
// are valid and don't require the presence of the extended header.
if (extendedInfoSize == -1) {
if (ze.compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE ||
ze.size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE ||
ze.localHeaderRelOffset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
throw new ZipException("File contains no zip64 extended information: "
+ "name=" + ze.name + "compressedSize=" + ze.compressedSize + ", size="
+ ze.size + ", localHeader=" + ze.localHeaderRelOffset);
}
return false;
} else {
// If we're parsed the zip64 extended info header, we remove it from the extras
// so that applications that set their own extras will see the data they set.
// This is an unfortunate workaround needed due to a gap in the spec. The spec demands
// that extras are present in the "extensible" format, which means that each extra field
// must be prefixed with a header ID and a length. However, earlier versions of the spec
// made no mention of this, nor did any existing API enforce it. This means users could
// set "free form" extras without caring very much whether the implementation wanted to
// extend or add to them.
// The start of the extended info header.
final int extendedInfoHeaderStart = extendedInfoStart - 4;
// The total size of the extended info, including the header.
final int extendedInfoTotalSize = extendedInfoSize + 4;
final int extrasLen = ze.extra.length - extendedInfoTotalSize;
byte[] extrasWithoutZip64 = new byte[extrasLen];
System.arraycopy(ze.extra, 0, extrasWithoutZip64, 0, extendedInfoHeaderStart);
System.arraycopy(ze.extra, extendedInfoHeaderStart + extendedInfoTotalSize,
extrasWithoutZip64, extendedInfoHeaderStart, (extrasLen - extendedInfoHeaderStart));
ze.extra = extrasWithoutZip64;
return true;
}
}
/**
* Appends a zip64 extended info record to the extras contained in {@code ze}. If {@code ze}
* contains no extras, a new extras array is created.
*/
public static void insertZip64ExtendedInfoToExtras(ZipEntry ze) throws ZipException {
final byte[] output;
// We always write the size, uncompressed size and local rel header offset in all our
// Zip64 extended info headers (in both the local file header as well as the central
// directory). We always omit the disk number because we don't support spanned
// archives anyway.
//
// 2 bytes : Zip64 Extended Info Header ID
// 2 bytes : Zip64 Extended Info Field Size.
// 8 bytes : Uncompressed size
// 8 bytes : Compressed size
// 8 bytes : Local header rel offset.
// ----------
// 28 bytes : total
final int extendedInfoSize = 28;
if (ze.extra == null) {
output = new byte[extendedInfoSize];
} else {
// If the existing extras are already too big, we have no choice but to throw
// an error.
if (ze.extra.length + extendedInfoSize > 65535) {
throw new ZipException("No space in extras for zip64 extended entry info");
}
// We copy existing extras over and put the zip64 extended info at the beginning. This
// is to avoid breakages in the presence of "old style" extras which don't contain
// headers and lengths. The spec is again silent about these inconsistencies.
//
// This means that people that for ZipOutputStream users, the value ZipEntry.getExtra
// after an entry is written will be different from before. This shouldn't be an issue
// in practice.
output = new byte[ze.extra.length + extendedInfoSize];
System.arraycopy(ze.extra, 0, output, extendedInfoSize, ze.extra.length);
}
ByteBuffer bb = ByteBuffer.wrap(output).order(ByteOrder.LITTLE_ENDIAN);
bb.putShort(ZIP64_EXTENDED_INFO_HEADER_ID);
// We subtract four because extendedInfoSize includes the ID and field
// size itself.
bb.putShort((short) (extendedInfoSize - 4));
if (ze.getMethod() == ZipEntry.STORED) {
bb.putLong(ze.size);
bb.putLong(ze.compressedSize);
} else {
// Store these fields in the data descriptor instead.
bb.putLong(0); // size.
bb.putLong(0); // compressed size.
}
// The offset is only relevant in the central directory entry, but we write it out here
// anyway, since we know what it is.
bb.putLong(ze.localHeaderRelOffset);
ze.extra = output;
}
/**
* Returns the size of the extended info record if {@code extras} contains a zip64 extended info
* record, {@code -1} otherwise. The buffer will be positioned at the start of the extended info
* record.
*/
private static int getZip64ExtendedInfoSize(ByteBuffer extras) {
try {
while (extras.hasRemaining()) {
final int headerId = extras.getShort() & 0xffff;
final int length = extras.getShort() & 0xffff;
if (headerId == ZIP64_EXTENDED_INFO_HEADER_ID) {
if (extras.remaining() >= length) {
return length;
} else {
return -1;
}
} else {
extras.position(extras.position() + length);
}
}
return -1;
} catch (BufferUnderflowException bue) {
// We'll underflow if we have an incomplete header in our extras.
return -1;
} catch (IllegalArgumentException iae) {
// ByteBuffer.position() will throw if we have a truncated extra or
// an invalid length in the header.
return -1;
}
}
/**
* Copy the size, compressed size and local header offset fields from {@code ze} to
* inside {@code ze}'s extended info record. This is additional step is necessary when
* we could calculate the correct sizes only after writing out the entry. In this case,
* the local file header would not contain real sizes, and they would be present in the
* data descriptor and the central directory only.
*
* We choose the simplest strategy of always writing out the size, compressedSize and
* local header offset in all our Zip64 Extended info records.
*/
public static void refreshZip64ExtendedInfo(ZipEntry ze) {
if (ze.extra == null) {
throw new IllegalStateException("Zip64 entry has no available extras: " + ze);
}
ByteBuffer buf = ByteBuffer.wrap(ze.extra).order(ByteOrder.LITTLE_ENDIAN);
final int extendedInfoSize = getZip64ExtendedInfoSize(buf);
if (extendedInfoSize == -1) {
throw new IllegalStateException(
"Zip64 entry extras has no zip64 extended info record: " + ze);
}
try {
buf.putLong(ze.size);
buf.putLong(ze.compressedSize);
buf.putLong(ze.localHeaderRelOffset);
} catch (BufferOverflowException boe) {
throw new IllegalStateException("Invalid extended info extra", boe);
}
}
public static void writeZip64EocdRecordAndLocator(ByteArrayOutputStream baos,
long numEntries, long offset, long cDirSize) throws IOException {
// Step 1: Write out the zip64 EOCD record.
writeLongAsUint32(baos, ZIP64_EOCD_RECORD_SIGNATURE);
// The size of the zip64 eocd record. This is the effective size + the
// size of the "version made by" (2 bytes) and the "version needed to extract" (2 bytes)
// fields.
writeLongAsUint64(baos, ZIP64_EOCD_RECORD_EFFECTIVE_SIZE + 4);
// TODO: What values should we put here ? The pre-zip64 values we've chosen don't
// seem to make much sense either.
writeIntAsUint16(baos, 20);
writeIntAsUint16(baos, 20);
writeLongAsUint32(baos, 0L); // number of disk
writeLongAsUint32(baos, 0L); // number of disk with start of central dir.
writeLongAsUint64(baos, numEntries); // number of entries in this disk.
writeLongAsUint64(baos, numEntries); // number of entries in total.
writeLongAsUint64(baos, cDirSize); // size of the central directory.
writeLongAsUint64(baos, offset); // offset of the central directory wrt. this file.
// Step 2: Write out the zip64 EOCD record locator.
writeLongAsUint32(baos, ZIP64_LOCATOR_SIGNATURE);
writeLongAsUint32(baos, 0); // number of disk with start of central dir.
writeLongAsUint64(baos, offset + cDirSize); // offset of the eocd record wrt. this file.
writeLongAsUint32(baos, 1); // total number of disks.
}
}