1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.apksig.internal.zip; 18 19 import static com.android.apksig.internal.zip.ZipUtils.UINT32_MAX_VALUE; 20 import static com.android.apksig.internal.zip.ZipUtils.ZIP64_COMPRESSED_SIZE_FIELD_NAME; 21 import static com.android.apksig.internal.zip.ZipUtils.ZIP64_LFH_OFFSET_FIELD_NAME; 22 import static com.android.apksig.internal.zip.ZipUtils.ZIP64_UNCOMPRESSED_SIZE_FIELD_NAME; 23 24 import com.android.apksig.internal.zip.ZipUtils.Zip64Fields; 25 import com.android.apksig.zip.ZipFormatException; 26 27 import java.nio.BufferUnderflowException; 28 import java.nio.ByteBuffer; 29 import java.nio.ByteOrder; 30 import java.nio.charset.StandardCharsets; 31 import java.util.Comparator; 32 33 /** 34 * ZIP Central Directory (CD) Record. 35 */ 36 public class CentralDirectoryRecord { 37 38 /** 39 * Comparator which compares records by the offset of the corresponding Local File Header in the 40 * archive. 41 */ 42 public static final Comparator<CentralDirectoryRecord> BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR = 43 new ByLocalFileHeaderOffsetComparator(); 44 45 private static final int RECORD_SIGNATURE = 0x02014b50; 46 private static final int HEADER_SIZE_BYTES = 46; 47 48 private static final int GP_FLAGS_OFFSET = 8; 49 private static final int LOCAL_FILE_HEADER_OFFSET_OFFSET = 42; 50 private static final int EXTRA_FIELD_OFFSET = 46; 51 private static final int NAME_OFFSET = HEADER_SIZE_BYTES; 52 53 private final ByteBuffer mData; 54 private final short mGpFlags; 55 private final short mCompressionMethod; 56 private final int mLastModificationTime; 57 private final int mLastModificationDate; 58 private final long mCrc32; 59 private final long mCompressedSize; 60 private final long mUncompressedSize; 61 private final long mLocalFileHeaderOffset; 62 private final String mName; 63 private final int mNameSizeBytes; 64 CentralDirectoryRecord( ByteBuffer data, short gpFlags, short compressionMethod, int lastModificationTime, int lastModificationDate, long crc32, long compressedSize, long uncompressedSize, long localFileHeaderOffset, String name, int nameSizeBytes)65 private CentralDirectoryRecord( 66 ByteBuffer data, 67 short gpFlags, 68 short compressionMethod, 69 int lastModificationTime, 70 int lastModificationDate, 71 long crc32, 72 long compressedSize, 73 long uncompressedSize, 74 long localFileHeaderOffset, 75 String name, 76 int nameSizeBytes) { 77 mData = data; 78 mGpFlags = gpFlags; 79 mCompressionMethod = compressionMethod; 80 mLastModificationDate = lastModificationDate; 81 mLastModificationTime = lastModificationTime; 82 mCrc32 = crc32; 83 mCompressedSize = compressedSize; 84 mUncompressedSize = uncompressedSize; 85 mLocalFileHeaderOffset = localFileHeaderOffset; 86 mName = name; 87 mNameSizeBytes = nameSizeBytes; 88 } 89 getSize()90 public int getSize() { 91 return mData.remaining(); 92 } 93 getName()94 public String getName() { 95 return mName; 96 } 97 getNameSizeBytes()98 public int getNameSizeBytes() { 99 return mNameSizeBytes; 100 } 101 getGpFlags()102 public short getGpFlags() { 103 return mGpFlags; 104 } 105 getCompressionMethod()106 public short getCompressionMethod() { 107 return mCompressionMethod; 108 } 109 getLastModificationTime()110 public int getLastModificationTime() { 111 return mLastModificationTime; 112 } 113 getLastModificationDate()114 public int getLastModificationDate() { 115 return mLastModificationDate; 116 } 117 getCrc32()118 public long getCrc32() { 119 return mCrc32; 120 } 121 getCompressedSize()122 public long getCompressedSize() { 123 return mCompressedSize; 124 } 125 getUncompressedSize()126 public long getUncompressedSize() { 127 return mUncompressedSize; 128 } 129 getLocalFileHeaderOffset()130 public long getLocalFileHeaderOffset() { 131 return mLocalFileHeaderOffset; 132 } 133 134 /** 135 * Returns the Central Directory Record starting at the current position of the provided buffer 136 * and advances the buffer's position immediately past the end of the record. 137 */ getRecord(ByteBuffer buf)138 public static CentralDirectoryRecord getRecord(ByteBuffer buf) throws ZipFormatException { 139 ZipUtils.assertByteOrderLittleEndian(buf); 140 if (buf.remaining() < HEADER_SIZE_BYTES) { 141 throw new ZipFormatException( 142 "Input too short. Need at least: " + HEADER_SIZE_BYTES 143 + " bytes, available: " + buf.remaining() + " bytes", 144 new BufferUnderflowException()); 145 } 146 int originalPosition = buf.position(); 147 int recordSignature = buf.getInt(); 148 if (recordSignature != RECORD_SIGNATURE) { 149 throw new ZipFormatException( 150 "Not a Central Directory record. Signature: 0x" 151 + Long.toHexString(recordSignature & 0xffffffffL)); 152 } 153 buf.position(originalPosition + GP_FLAGS_OFFSET); 154 short gpFlags = buf.getShort(); 155 short compressionMethod = buf.getShort(); 156 int lastModificationTime = ZipUtils.getUnsignedInt16(buf); 157 int lastModificationDate = ZipUtils.getUnsignedInt16(buf); 158 long crc32 = ZipUtils.getUnsignedInt32(buf); 159 long compressedSize = ZipUtils.getUnsignedInt32(buf); 160 long uncompressedSize = ZipUtils.getUnsignedInt32(buf); 161 int nameSize = ZipUtils.getUnsignedInt16(buf); 162 int extraSize = ZipUtils.getUnsignedInt16(buf); 163 int commentSize = ZipUtils.getUnsignedInt16(buf); 164 buf.position(originalPosition + LOCAL_FILE_HEADER_OFFSET_OFFSET); 165 long localFileHeaderOffset = ZipUtils.getUnsignedInt32(buf); 166 buf.position(originalPosition); 167 int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize; 168 if (recordSize > buf.remaining()) { 169 throw new ZipFormatException( 170 "Input too short. Need: " + recordSize + " bytes, available: " 171 + buf.remaining() + " bytes", 172 new BufferUnderflowException()); 173 } 174 String name = getName(buf, originalPosition + NAME_OFFSET, nameSize); 175 // If the record contains an extra field and any of the other fields subject to the 32-bit 176 // limitation indicate the presence of a ZIP64 block, then check the extra field for this 177 // block to obtain the actual values of the affected fields. 178 if (extraSize > 0 179 && (uncompressedSize == UINT32_MAX_VALUE 180 || compressedSize == UINT32_MAX_VALUE 181 || localFileHeaderOffset == UINT32_MAX_VALUE)) { 182 buf.position(originalPosition + EXTRA_FIELD_OFFSET + nameSize); 183 int originalLimit = buf.limit(); 184 ByteBuffer extra = buf.slice(); 185 buf.limit(originalLimit); 186 Zip64Fields zip64Fields = 187 new Zip64Fields(uncompressedSize, compressedSize, localFileHeaderOffset); 188 ZipUtils.parseExtraField(extra, zip64Fields); 189 uncompressedSize = 190 ZipUtils.checkAndReturnZip64Value( 191 uncompressedSize, 192 zip64Fields.uncompressedSize, 193 name, 194 ZIP64_UNCOMPRESSED_SIZE_FIELD_NAME); 195 compressedSize = 196 ZipUtils.checkAndReturnZip64Value( 197 compressedSize, 198 zip64Fields.compressedSize, 199 name, 200 ZIP64_COMPRESSED_SIZE_FIELD_NAME); 201 localFileHeaderOffset = 202 ZipUtils.checkAndReturnZip64Value( 203 localFileHeaderOffset, 204 zip64Fields.localFileHeaderOffset, 205 name, 206 ZIP64_LFH_OFFSET_FIELD_NAME); 207 } 208 buf.position(originalPosition); 209 int originalLimit = buf.limit(); 210 int recordEndInBuf = originalPosition + recordSize; 211 ByteBuffer recordBuf; 212 try { 213 buf.limit(recordEndInBuf); 214 recordBuf = buf.slice(); 215 } finally { 216 buf.limit(originalLimit); 217 } 218 // Consume this record 219 buf.position(recordEndInBuf); 220 return new CentralDirectoryRecord( 221 recordBuf, 222 gpFlags, 223 compressionMethod, 224 lastModificationTime, 225 lastModificationDate, 226 crc32, 227 compressedSize, 228 uncompressedSize, 229 localFileHeaderOffset, 230 name, 231 nameSize); 232 } 233 copyTo(ByteBuffer output)234 public void copyTo(ByteBuffer output) { 235 output.put(mData.slice()); 236 } 237 createWithModifiedLocalFileHeaderOffset( long localFileHeaderOffset)238 public CentralDirectoryRecord createWithModifiedLocalFileHeaderOffset( 239 long localFileHeaderOffset) { 240 ByteBuffer result = ByteBuffer.allocate(mData.remaining()); 241 result.put(mData.slice()); 242 result.flip(); 243 result.order(ByteOrder.LITTLE_ENDIAN); 244 ZipUtils.setUnsignedInt32(result, LOCAL_FILE_HEADER_OFFSET_OFFSET, localFileHeaderOffset); 245 return new CentralDirectoryRecord( 246 result, 247 mGpFlags, 248 mCompressionMethod, 249 mLastModificationTime, 250 mLastModificationDate, 251 mCrc32, 252 mCompressedSize, 253 mUncompressedSize, 254 localFileHeaderOffset, 255 mName, 256 mNameSizeBytes); 257 } 258 createWithDeflateCompressedData( String name, int lastModifiedTime, int lastModifiedDate, long crc32, long compressedSize, long uncompressedSize, long localFileHeaderOffset)259 public static CentralDirectoryRecord createWithDeflateCompressedData( 260 String name, 261 int lastModifiedTime, 262 int lastModifiedDate, 263 long crc32, 264 long compressedSize, 265 long uncompressedSize, 266 long localFileHeaderOffset) { 267 byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); 268 short gpFlags = ZipUtils.GP_FLAG_EFS; // UTF-8 character encoding used for entry name 269 short compressionMethod = ZipUtils.COMPRESSION_METHOD_DEFLATED; 270 int recordSize = HEADER_SIZE_BYTES + nameBytes.length; 271 ByteBuffer result = ByteBuffer.allocate(recordSize); 272 result.order(ByteOrder.LITTLE_ENDIAN); 273 result.putInt(RECORD_SIGNATURE); 274 ZipUtils.putUnsignedInt16(result, 0x14); // Version made by 275 ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract 276 result.putShort(gpFlags); 277 result.putShort(compressionMethod); 278 ZipUtils.putUnsignedInt16(result, lastModifiedTime); 279 ZipUtils.putUnsignedInt16(result, lastModifiedDate); 280 ZipUtils.putUnsignedInt32(result, crc32); 281 ZipUtils.putUnsignedInt32(result, compressedSize); 282 ZipUtils.putUnsignedInt32(result, uncompressedSize); 283 ZipUtils.putUnsignedInt16(result, nameBytes.length); 284 ZipUtils.putUnsignedInt16(result, 0); // Extra field length 285 ZipUtils.putUnsignedInt16(result, 0); // File comment length 286 ZipUtils.putUnsignedInt16(result, 0); // Disk number 287 ZipUtils.putUnsignedInt16(result, 0); // Internal file attributes 288 ZipUtils.putUnsignedInt32(result, 0); // External file attributes 289 ZipUtils.putUnsignedInt32(result, localFileHeaderOffset); 290 result.put(nameBytes); 291 292 if (result.hasRemaining()) { 293 throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit()); 294 } 295 result.flip(); 296 return new CentralDirectoryRecord( 297 result, 298 gpFlags, 299 compressionMethod, 300 lastModifiedTime, 301 lastModifiedDate, 302 crc32, 303 compressedSize, 304 uncompressedSize, 305 localFileHeaderOffset, 306 name, 307 nameBytes.length); 308 } 309 getName(ByteBuffer record, int position, int nameLengthBytes)310 static String getName(ByteBuffer record, int position, int nameLengthBytes) { 311 byte[] nameBytes; 312 int nameBytesOffset; 313 if (record.hasArray()) { 314 nameBytes = record.array(); 315 nameBytesOffset = record.arrayOffset() + position; 316 } else { 317 nameBytes = new byte[nameLengthBytes]; 318 nameBytesOffset = 0; 319 int originalPosition = record.position(); 320 try { 321 record.position(position); 322 record.get(nameBytes); 323 } finally { 324 record.position(originalPosition); 325 } 326 } 327 return new String(nameBytes, nameBytesOffset, nameLengthBytes, StandardCharsets.UTF_8); 328 } 329 330 private static class ByLocalFileHeaderOffsetComparator 331 implements Comparator<CentralDirectoryRecord> { 332 @Override compare(CentralDirectoryRecord r1, CentralDirectoryRecord r2)333 public int compare(CentralDirectoryRecord r1, CentralDirectoryRecord r2) { 334 long offset1 = r1.getLocalFileHeaderOffset(); 335 long offset2 = r2.getLocalFileHeaderOffset(); 336 if (offset1 > offset2) { 337 return 1; 338 } else if (offset1 < offset2) { 339 return -1; 340 } else { 341 return 0; 342 } 343 } 344 } 345 } 346