• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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