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 com.android.apksig.zip.ZipFormatException; 20 import java.nio.BufferUnderflowException; 21 import java.nio.ByteBuffer; 22 import java.nio.ByteOrder; 23 import java.nio.charset.StandardCharsets; 24 import java.util.Comparator; 25 26 /** 27 * ZIP Central Directory (CD) Record. 28 */ 29 public class CentralDirectoryRecord { 30 31 /** 32 * Comparator which compares records by the offset of the corresponding Local File Header in the 33 * archive. 34 */ 35 public static final Comparator<CentralDirectoryRecord> BY_LOCAL_FILE_HEADER_OFFSET_COMPARATOR = 36 new ByLocalFileHeaderOffsetComparator(); 37 38 private static final int RECORD_SIGNATURE = 0x02014b50; 39 private static final int HEADER_SIZE_BYTES = 46; 40 41 private static final int GP_FLAGS_OFFSET = 8; 42 private static final int LOCAL_FILE_HEADER_OFFSET_OFFSET = 42; 43 private static final int NAME_OFFSET = HEADER_SIZE_BYTES; 44 45 private final ByteBuffer mData; 46 private final short mGpFlags; 47 private final short mCompressionMethod; 48 private final int mLastModificationTime; 49 private final int mLastModificationDate; 50 private final long mCrc32; 51 private final long mCompressedSize; 52 private final long mUncompressedSize; 53 private final long mLocalFileHeaderOffset; 54 private final String mName; 55 private final int mNameSizeBytes; 56 CentralDirectoryRecord( ByteBuffer data, short gpFlags, short compressionMethod, int lastModificationTime, int lastModificationDate, long crc32, long compressedSize, long uncompressedSize, long localFileHeaderOffset, String name, int nameSizeBytes)57 private CentralDirectoryRecord( 58 ByteBuffer data, 59 short gpFlags, 60 short compressionMethod, 61 int lastModificationTime, 62 int lastModificationDate, 63 long crc32, 64 long compressedSize, 65 long uncompressedSize, 66 long localFileHeaderOffset, 67 String name, 68 int nameSizeBytes) { 69 mData = data; 70 mGpFlags = gpFlags; 71 mCompressionMethod = compressionMethod; 72 mLastModificationDate = lastModificationDate; 73 mLastModificationTime = lastModificationTime; 74 mCrc32 = crc32; 75 mCompressedSize = compressedSize; 76 mUncompressedSize = uncompressedSize; 77 mLocalFileHeaderOffset = localFileHeaderOffset; 78 mName = name; 79 mNameSizeBytes = nameSizeBytes; 80 } 81 getSize()82 public int getSize() { 83 return mData.remaining(); 84 } 85 getName()86 public String getName() { 87 return mName; 88 } 89 getNameSizeBytes()90 public int getNameSizeBytes() { 91 return mNameSizeBytes; 92 } 93 getGpFlags()94 public short getGpFlags() { 95 return mGpFlags; 96 } 97 getCompressionMethod()98 public short getCompressionMethod() { 99 return mCompressionMethod; 100 } 101 getLastModificationTime()102 public int getLastModificationTime() { 103 return mLastModificationTime; 104 } 105 getLastModificationDate()106 public int getLastModificationDate() { 107 return mLastModificationDate; 108 } 109 getCrc32()110 public long getCrc32() { 111 return mCrc32; 112 } 113 getCompressedSize()114 public long getCompressedSize() { 115 return mCompressedSize; 116 } 117 getUncompressedSize()118 public long getUncompressedSize() { 119 return mUncompressedSize; 120 } 121 getLocalFileHeaderOffset()122 public long getLocalFileHeaderOffset() { 123 return mLocalFileHeaderOffset; 124 } 125 126 /** 127 * Returns the Central Directory Record starting at the current position of the provided buffer 128 * and advances the buffer's position immediately past the end of the record. 129 */ getRecord(ByteBuffer buf)130 public static CentralDirectoryRecord getRecord(ByteBuffer buf) throws ZipFormatException { 131 ZipUtils.assertByteOrderLittleEndian(buf); 132 if (buf.remaining() < HEADER_SIZE_BYTES) { 133 throw new ZipFormatException( 134 "Input too short. Need at least: " + HEADER_SIZE_BYTES 135 + " bytes, available: " + buf.remaining() + " bytes", 136 new BufferUnderflowException()); 137 } 138 int originalPosition = buf.position(); 139 int recordSignature = buf.getInt(); 140 if (recordSignature != RECORD_SIGNATURE) { 141 throw new ZipFormatException( 142 "Not a Central Directory record. Signature: 0x" 143 + Long.toHexString(recordSignature & 0xffffffffL)); 144 } 145 buf.position(originalPosition + GP_FLAGS_OFFSET); 146 short gpFlags = buf.getShort(); 147 short compressionMethod = buf.getShort(); 148 int lastModificationTime = ZipUtils.getUnsignedInt16(buf); 149 int lastModificationDate = ZipUtils.getUnsignedInt16(buf); 150 long crc32 = ZipUtils.getUnsignedInt32(buf); 151 long compressedSize = ZipUtils.getUnsignedInt32(buf); 152 long uncompressedSize = ZipUtils.getUnsignedInt32(buf); 153 int nameSize = ZipUtils.getUnsignedInt16(buf); 154 int extraSize = ZipUtils.getUnsignedInt16(buf); 155 int commentSize = ZipUtils.getUnsignedInt16(buf); 156 buf.position(originalPosition + LOCAL_FILE_HEADER_OFFSET_OFFSET); 157 long localFileHeaderOffset = ZipUtils.getUnsignedInt32(buf); 158 buf.position(originalPosition); 159 int recordSize = HEADER_SIZE_BYTES + nameSize + extraSize + commentSize; 160 if (recordSize > buf.remaining()) { 161 throw new ZipFormatException( 162 "Input too short. Need: " + recordSize + " bytes, available: " 163 + buf.remaining() + " bytes", 164 new BufferUnderflowException()); 165 } 166 String name = getName(buf, originalPosition + NAME_OFFSET, nameSize); 167 buf.position(originalPosition); 168 int originalLimit = buf.limit(); 169 int recordEndInBuf = originalPosition + recordSize; 170 ByteBuffer recordBuf; 171 try { 172 buf.limit(recordEndInBuf); 173 recordBuf = buf.slice(); 174 } finally { 175 buf.limit(originalLimit); 176 } 177 // Consume this record 178 buf.position(recordEndInBuf); 179 return new CentralDirectoryRecord( 180 recordBuf, 181 gpFlags, 182 compressionMethod, 183 lastModificationTime, 184 lastModificationDate, 185 crc32, 186 compressedSize, 187 uncompressedSize, 188 localFileHeaderOffset, 189 name, 190 nameSize); 191 } 192 copyTo(ByteBuffer output)193 public void copyTo(ByteBuffer output) { 194 output.put(mData.slice()); 195 } 196 createWithModifiedLocalFileHeaderOffset( long localFileHeaderOffset)197 public CentralDirectoryRecord createWithModifiedLocalFileHeaderOffset( 198 long localFileHeaderOffset) { 199 ByteBuffer result = ByteBuffer.allocate(mData.remaining()); 200 result.put(mData.slice()); 201 result.flip(); 202 result.order(ByteOrder.LITTLE_ENDIAN); 203 ZipUtils.setUnsignedInt32(result, LOCAL_FILE_HEADER_OFFSET_OFFSET, localFileHeaderOffset); 204 return new CentralDirectoryRecord( 205 result, 206 mGpFlags, 207 mCompressionMethod, 208 mLastModificationTime, 209 mLastModificationDate, 210 mCrc32, 211 mCompressedSize, 212 mUncompressedSize, 213 localFileHeaderOffset, 214 mName, 215 mNameSizeBytes); 216 } 217 createWithDeflateCompressedData( String name, int lastModifiedTime, int lastModifiedDate, long crc32, long compressedSize, long uncompressedSize, long localFileHeaderOffset)218 public static CentralDirectoryRecord createWithDeflateCompressedData( 219 String name, 220 int lastModifiedTime, 221 int lastModifiedDate, 222 long crc32, 223 long compressedSize, 224 long uncompressedSize, 225 long localFileHeaderOffset) { 226 byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8); 227 short gpFlags = ZipUtils.GP_FLAG_EFS; // UTF-8 character encoding used for entry name 228 short compressionMethod = ZipUtils.COMPRESSION_METHOD_DEFLATED; 229 int recordSize = HEADER_SIZE_BYTES + nameBytes.length; 230 ByteBuffer result = ByteBuffer.allocate(recordSize); 231 result.order(ByteOrder.LITTLE_ENDIAN); 232 result.putInt(RECORD_SIGNATURE); 233 ZipUtils.putUnsignedInt16(result, 0x14); // Version made by 234 ZipUtils.putUnsignedInt16(result, 0x14); // Minimum version needed to extract 235 result.putShort(gpFlags); 236 result.putShort(compressionMethod); 237 ZipUtils.putUnsignedInt16(result, lastModifiedTime); 238 ZipUtils.putUnsignedInt16(result, lastModifiedDate); 239 ZipUtils.putUnsignedInt32(result, crc32); 240 ZipUtils.putUnsignedInt32(result, compressedSize); 241 ZipUtils.putUnsignedInt32(result, uncompressedSize); 242 ZipUtils.putUnsignedInt16(result, nameBytes.length); 243 ZipUtils.putUnsignedInt16(result, 0); // Extra field length 244 ZipUtils.putUnsignedInt16(result, 0); // File comment length 245 ZipUtils.putUnsignedInt16(result, 0); // Disk number 246 ZipUtils.putUnsignedInt16(result, 0); // Internal file attributes 247 ZipUtils.putUnsignedInt32(result, 0); // External file attributes 248 ZipUtils.putUnsignedInt32(result, localFileHeaderOffset); 249 result.put(nameBytes); 250 251 if (result.hasRemaining()) { 252 throw new RuntimeException("pos: " + result.position() + ", limit: " + result.limit()); 253 } 254 result.flip(); 255 return new CentralDirectoryRecord( 256 result, 257 gpFlags, 258 compressionMethod, 259 lastModifiedTime, 260 lastModifiedDate, 261 crc32, 262 compressedSize, 263 uncompressedSize, 264 localFileHeaderOffset, 265 name, 266 nameBytes.length); 267 } 268 getName(ByteBuffer record, int position, int nameLengthBytes)269 static String getName(ByteBuffer record, int position, int nameLengthBytes) { 270 byte[] nameBytes; 271 int nameBytesOffset; 272 if (record.hasArray()) { 273 nameBytes = record.array(); 274 nameBytesOffset = record.arrayOffset() + position; 275 } else { 276 nameBytes = new byte[nameLengthBytes]; 277 nameBytesOffset = 0; 278 int originalPosition = record.position(); 279 try { 280 record.position(position); 281 record.get(nameBytes); 282 } finally { 283 record.position(originalPosition); 284 } 285 } 286 return new String(nameBytes, nameBytesOffset, nameLengthBytes, StandardCharsets.UTF_8); 287 } 288 289 private static class ByLocalFileHeaderOffsetComparator 290 implements Comparator<CentralDirectoryRecord> { 291 @Override compare(CentralDirectoryRecord r1, CentralDirectoryRecord r2)292 public int compare(CentralDirectoryRecord r1, CentralDirectoryRecord r2) { 293 long offset1 = r1.getLocalFileHeaderOffset(); 294 long offset2 = r2.getLocalFileHeaderOffset(); 295 if (offset1 > offset2) { 296 return 1; 297 } else if (offset1 < offset2) { 298 return -1; 299 } else { 300 return 0; 301 } 302 } 303 } 304 } 305