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