/*
* Copyright (C) 2016 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.apksig.internal.zip;
import com.android.apksig.zip.ZipFormatException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
/**
* ZIP Central Directory (CD) Record.
*/
public class CentralDirectoryRecord {
/**
* Comparator which compares records by the offset of the corresponding Local File Header in the
* archive.
*/
public static final Comparator<CentralDirectoryRecord> BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR =
new ByLocalFileHeaderOffsetComparator();
private static final int RECORD_SIGNATURE = 0x02014b50;
private static final int HEADER_SIZE_BYTES = 46;
private static final int GP_FLAGS_OFFSET = 8;
private static final int LOCAL_FILE_HEADER_OFFSET_OFFSET = 42;
private static final int NAME_OFFSET = HEADER_SIZE_BYTES;
private final ByteBuffer mData;
private final short mGpFlags;
private final short mCompressionMethod;
private final int mLastModificationTime;
private final int mLastModificationDate;
private final long mCrc32;
private final long mCompressedSize;
private final long mUncompressedSize;
private final long mLocalFileHeaderOffset;
private final String mName;
private final int mNameSizeBytes;
private CentralDirectoryRecord(
ByteBuffer data,
short gpFlags,
short compressionMethod,
int lastModificationTime,
int lastModificationDate,
long crc32,
long compressedSize,
long uncompressedSize,
long localFileHeaderOffset,
String name,
int nameSizeBytes) {
mData = data;
mGpFlags = gpFlags;
mCompressionMethod = compressionMethod;
mLastModificationDate = lastModificationDate;
mLastModificationTime = lastModificationTime;
mCrc32 = crc32;
mCompressedSize = compressedSize;
mUncompressedSize = uncompressedSize;
mLocalFileHeaderOffset = localFileHeaderOffset;
mName = name;
mNameSizeBytes = nameSizeBytes;
}
public int getSize() {
return mData.remaining();
}
public String getName() {
return mName;
}
public int getNameSizeBytes() {
return mNameSizeBytes;
}
public short getGpFlags() {
return mGpFlags;
}
public short getCompressionMethod() {
return mCompressionMethod;
}
public int getLastModificationTime() {
return mLastModificationTime;
}
public int getLastModificationDate() {
return mLastModificationDate;
}
public long getCrc32() {
return mCrc32;
}
public long getCompressedSize() {
return mCompressedSize;
}
public long getUncompressedSize() {
return mUncompressedSize;
}
public long getLocalFileHeaderOffset() {
return mLocalFileHeaderOffset;
}
/**
* Returns the Central Directory Record starting at the current position of the provided buffer
* and advances the buffer's position immediately past the end of the record.
*/
public static CentralDirectoryRecord getRecord(ByteBuffer buf) throws ZipFormatException {
ZipUtils.assertByteOrderLittleEndian(buf);
if (buf.remaining() < HEADER_SIZE_BYTES) {
throw new ZipFormatException(
"Input too short. Need at least: " + HEADER_SIZE_BYTES
+ " bytes, available: " + buf.remaining() + " bytes",
new BufferUnderflowException());
}
int originalPosition = buf.position();
int recordSignature = buf.getInt();
if (recordSignature != RECORD_SIGNATURE) {
throw new ZipFormatException(
"Not a Central Directory record. Signature: 0x"
+ Long.toHexString(recordSignature & 0xffffffffL));
}
buf.position(originalPosition + GP_FLAGS_OFFSET);
short gpFlags = buf.getShort();
short compressionMethod = buf.getShort();
int lastModificationTime = ZipUtils.getUnsignedInt16(buf);
int lastModificationDate = ZipUtils.getUnsignedInt16(buf);
long crc32 = ZipUtils.getUnsignedInt32(buf);
long compressedSize = ZipUtils.getUnsignedInt32(buf);
long uncompressedSize = ZipUtils.getUnsignedInt32(buf);
int nameSize = ZipUtils.getUnsignedInt16(buf);
int extraSize = ZipUtils.getUnsignedInt16(buf);
int commentSize = ZipUtils.getUnsignedInt16(buf);
buf.position(originalPosition + LOCAL_FILE_HEADER_OFFSET_OFFSET);
long localFileHeaderOffset = ZipUtils.getUnsignedInt32(buf);
buf.position(originalPosition);
int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize;
if (recordSize > buf.remaining()) {
throw new ZipFormatException(
"Input too short. Need: " + recordSize + " bytes, available: "
+ buf.remaining() + " bytes",
new BufferUnderflowException());
}
String name = getName(buf, originalPosition + NAME_OFFSET, nameSize);
buf.position(originalPosition);
int originalLimit = buf.limit();
int recordEndInBuf = originalPosition + recordSize;
ByteBuffer recordBuf;
try {
buf.limit(recordEndInBuf);
recordBuf = buf.slice();
} finally {
buf.limit(originalLimit);
}
// Consume this record
buf.position(recordEndInBuf);
return new CentralDirectoryRecord(
recordBuf,
gpFlags,
compressionMethod,
lastModificationTime,
lastModificationDate,
crc32,
compressedSize,
uncompressedSize,
localFileHeaderOffset,
name,
nameSize);
}
public void copyTo(ByteBuffer output) {
output.put(mData.slice());
}
public CentralDirectoryRecord createWithModifiedLocalFileHeaderOffset(
long localFileHeaderOffset) {
ByteBuffer result = ByteBuffer.allocate(mData.remaining());
result.put(mData.slice());
result.flip();
result.order(ByteOrder.LITTLE_ENDIAN);
ZipUtils.setUnsignedInt32(result, LOCAL_FILE_HEADER_OFFSET_OFFSET, localFileHeaderOffset);
return new CentralDirectoryRecord(
result,
mGpFlags,
mCompressionMethod,
mLastModificationTime,
mLastModificationDate,
mCrc32,
mCompressedSize,
mUncompressedSize,
localFileHeaderOffset,
mName,
mNameSizeBytes);
}
public static CentralDirectoryRecord createWithDeflateCompressedData(
String name,
int lastModifiedTime,
int lastModifiedDate,
long crc32,
long compressedSize,
long uncompressedSize,
long localFileHeaderOffset) {
byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
short gpFlags = ZipUtils.GP_FLAG_EFS; // UTF-8 character encoding used for entry name
short compressionMethod = ZipUtils.COMPRESSION_METHOD_DEFLATED;
int recordSize = HEADER_SIZE_BYTES + nameBytes.length;
ByteBuffer result = ByteBuffer.allocate(recordSize);
result.order(ByteOrder.LITTLE_ENDIAN);
result.putInt(RECORD_SIGNATURE);
ZipUtils.putUnsignedInt16(result, 0x14); // Version made by
ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract
result.putShort(gpFlags);
result.putShort(compressionMethod);
ZipUtils.putUnsignedInt16(result, lastModifiedTime);
ZipUtils.putUnsignedInt16(result, lastModifiedDate);
ZipUtils.putUnsignedInt32(result, crc32);
ZipUtils.putUnsignedInt32(result, compressedSize);
ZipUtils.putUnsignedInt32(result, uncompressedSize);
ZipUtils.putUnsignedInt16(result, nameBytes.length);
ZipUtils.putUnsignedInt16(result, 0); // Extra field length
ZipUtils.putUnsignedInt16(result, 0); // File comment length
ZipUtils.putUnsignedInt16(result, 0); // Disk number
ZipUtils.putUnsignedInt16(result, 0); // Internal file attributes
ZipUtils.putUnsignedInt32(result, 0); // External file attributes
ZipUtils.putUnsignedInt32(result, localFileHeaderOffset);
result.put(nameBytes);
if (result.hasRemaining()) {
throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit());
}
result.flip();
return new CentralDirectoryRecord(
result,
gpFlags,
compressionMethod,
lastModifiedTime,
lastModifiedDate,
crc32,
compressedSize,
uncompressedSize,
localFileHeaderOffset,
name,
nameBytes.length);
}
static String getName(ByteBuffer record, int position, int nameLengthBytes) {
byte[] nameBytes;
int nameBytesOffset;
if (record.hasArray()) {
nameBytes = record.array();
nameBytesOffset = record.arrayOffset() + position;
} else {
nameBytes = new byte[nameLengthBytes];
nameBytesOffset = 0;
int originalPosition = record.position();
try {
record.position(position);
record.get(nameBytes);
} finally {
record.position(originalPosition);
}
}
return new String(nameBytes, nameBytesOffset, nameLengthBytes, StandardCharsets.UTF_8);
}
private static class ByLocalFileHeaderOffsetComparator
implements Comparator<CentralDirectoryRecord> {
@Override
public int compare(CentralDirectoryRecord r1, CentralDirectoryRecord r2) {
long offset1 = r1.getLocalFileHeaderOffset();
long offset2 = r2.getLocalFileHeaderOffset();
if (offset1 > offset2) {
return 1;
} else if (offset1 < offset2) {
return -1;
} else {
return 0;
}
}
}
}