• 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 com.android.apksig.apk.ApkFormatException;
20 import com.android.apksig.internal.util.Pair;
21 import com.android.apksig.util.DataSource;
22 import com.android.apksig.zip.ZipFormatException;
23 import com.android.apksig.zip.ZipSections;
24 
25 import java.io.ByteArrayOutputStream;
26 import java.io.IOException;
27 import java.nio.ByteBuffer;
28 import java.nio.ByteOrder;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.zip.CRC32;
32 import java.util.zip.Deflater;
33 
34 /**
35  * Assorted ZIP format helpers.
36  *
37  * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
38  * order of these buffers is little-endian.
39  */
40 public abstract class ZipUtils {
ZipUtils()41     private ZipUtils() {}
42 
43     public static final short COMPRESSION_METHOD_STORED = 0;
44     public static final short COMPRESSION_METHOD_DEFLATED = 8;
45 
46     public static final short GP_FLAG_DATA_DESCRIPTOR_USED = 0x08;
47     public static final short GP_FLAG_EFS = 0x0800;
48 
49     private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
50     private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
51     private static final int ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET = 10;
52     private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
53     private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
54     private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
55 
56     public static final int ZIP64_RECORD_ID = 0x1;
57     public static final String ZIP64_UNCOMPRESSED_SIZE_FIELD_NAME = "uncompressedSize";
58     public static final String ZIP64_COMPRESSED_SIZE_FIELD_NAME = "compressedSize";
59     public static final String ZIP64_LFH_OFFSET_FIELD_NAME = "localFileHeaderOffset";
60 
61     public static final int UINT16_MAX_VALUE = 0xffff;
62     public static final long UINT32_MAX_VALUE = 0xffffffffL;
63 
64     /**
65      * Sets the offset of the start of the ZIP Central Directory in the archive.
66      *
67      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
68      */
setZipEocdCentralDirectoryOffset( ByteBuffer zipEndOfCentralDirectory, long offset)69     public static void setZipEocdCentralDirectoryOffset(
70             ByteBuffer zipEndOfCentralDirectory, long offset) {
71         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
72         setUnsignedInt32(
73                 zipEndOfCentralDirectory,
74                 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET,
75                 offset);
76     }
77 
78     /**
79      * Sets the length of EOCD comment.
80      *
81      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
82      */
updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory)83     public static void updateZipEocdCommentLen(ByteBuffer zipEndOfCentralDirectory) {
84         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
85         int commentLen = zipEndOfCentralDirectory.remaining() - ZIP_EOCD_REC_MIN_SIZE;
86         setUnsignedInt16(
87                 zipEndOfCentralDirectory,
88                 zipEndOfCentralDirectory.position() + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET,
89                 commentLen);
90     }
91 
92     /**
93      * Returns the offset of the start of the ZIP Central Directory in the archive.
94      *
95      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
96      */
getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory)97     public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
98         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
99         return getUnsignedInt32(
100                 zipEndOfCentralDirectory,
101                 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
102     }
103 
104     /**
105      * Returns the size (in bytes) of the ZIP Central Directory.
106      *
107      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
108      */
getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory)109     public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
110         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
111         return getUnsignedInt32(
112                 zipEndOfCentralDirectory,
113                 zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
114     }
115 
116     /**
117      * Returns the total number of records in ZIP Central Directory.
118      *
119      * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
120      */
getZipEocdCentralDirectoryTotalRecordCount( ByteBuffer zipEndOfCentralDirectory)121     public static int getZipEocdCentralDirectoryTotalRecordCount(
122             ByteBuffer zipEndOfCentralDirectory) {
123         assertByteOrderLittleEndian(zipEndOfCentralDirectory);
124         return getUnsignedInt16(
125                 zipEndOfCentralDirectory,
126                 zipEndOfCentralDirectory.position()
127                         + ZIP_EOCD_CENTRAL_DIR_TOTAL_RECORD_COUNT_OFFSET);
128     }
129 
130     /**
131      * Returns the ZIP End of Central Directory record of the provided ZIP file.
132      *
133      * @return contents of the ZIP End of Central Directory record and the record's offset in the
134      *         file or {@code null} if the file does not contain the record.
135      *
136      * @throws IOException if an I/O error occurs while reading the file.
137      */
findZipEndOfCentralDirectoryRecord(DataSource zip)138     public static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(DataSource zip)
139             throws IOException {
140         // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
141         // The record can be identified by its 4-byte signature/magic which is located at the very
142         // beginning of the record. A complication is that the record is variable-length because of
143         // the comment field.
144         // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
145         // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
146         // the candidate record's comment length is such that the remainder of the record takes up
147         // exactly the remaining bytes in the buffer. The search is bounded because the maximum
148         // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
149 
150         long fileSize = zip.size();
151         if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
152             return null;
153         }
154 
155         // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
156         // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
157         // reading more data.
158         Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
159         if (result != null) {
160             return result;
161         }
162 
163         // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
164         // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
165         // the comment length field is an unsigned 16-bit number.
166         return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
167     }
168 
169     /**
170      * Returns the ZIP End of Central Directory record of the provided ZIP file.
171      *
172      * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted
173      *        value is from 0 to 65535 inclusive. The smaller the value, the faster this method
174      *        locates the record, provided its comment field is no longer than this value.
175      *
176      * @return contents of the ZIP End of Central Directory record and the record's offset in the
177      *         file or {@code null} if the file does not contain the record.
178      *
179      * @throws IOException if an I/O error occurs while reading the file.
180      */
findZipEndOfCentralDirectoryRecord( DataSource zip, int maxCommentSize)181     private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(
182             DataSource zip, int maxCommentSize) throws IOException {
183         // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
184         // The record can be identified by its 4-byte signature/magic which is located at the very
185         // beginning of the record. A complication is that the record is variable-length because of
186         // the comment field.
187         // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
188         // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
189         // the candidate record's comment length is such that the remainder of the record takes up
190         // exactly the remaining bytes in the buffer. The search is bounded because the maximum
191         // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
192 
193         if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) {
194             throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
195         }
196 
197         long fileSize = zip.size();
198         if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
199             // No space for EoCD record in the file.
200             return null;
201         }
202         // Lower maxCommentSize if the file is too small.
203         maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
204 
205         int maxEocdSize = ZIP_EOCD_REC_MIN_SIZE + maxCommentSize;
206         long bufOffsetInFile = fileSize - maxEocdSize;
207         ByteBuffer buf = zip.getByteBuffer(bufOffsetInFile, maxEocdSize);
208         buf.order(ByteOrder.LITTLE_ENDIAN);
209         int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
210         if (eocdOffsetInBuf == -1) {
211             // No EoCD record found in the buffer
212             return null;
213         }
214         // EoCD found
215         buf.position(eocdOffsetInBuf);
216         ByteBuffer eocd = buf.slice();
217         eocd.order(ByteOrder.LITTLE_ENDIAN);
218         return Pair.of(eocd, bufOffsetInFile + eocdOffsetInBuf);
219     }
220 
221     /**
222      * Returns the position at which ZIP End of Central Directory record starts in the provided
223      * buffer or {@code -1} if the record is not present.
224      *
225      * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
226      */
findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents)227     private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
228         assertByteOrderLittleEndian(zipContents);
229 
230         // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
231         // The record can be identified by its 4-byte signature/magic which is located at the very
232         // beginning of the record. A complication is that the record is variable-length because of
233         // the comment field.
234         // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
235         // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
236         // the candidate record's comment length is such that the remainder of the record takes up
237         // exactly the remaining bytes in the buffer. The search is bounded because the maximum
238         // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
239 
240         int archiveSize = zipContents.capacity();
241         if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
242             return -1;
243         }
244         int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
245         int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
246         for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength;
247                 expectedCommentLength++) {
248             int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
249             if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
250                 int actualCommentLength =
251                         getUnsignedInt16(
252                                 zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
253                 if (actualCommentLength == expectedCommentLength) {
254                     return eocdStartPos;
255                 }
256             }
257         }
258 
259         return -1;
260     }
261 
262     /**
263      * Parses the provided extra field for the ZIP64 block and sets the fields in the provided
264      * {@code zip64Fields} that were affected by the 32-bit limit.
265      *
266      * <p>Since the ZIP64 block only includes those fields that exceed the limit, the specified
267      * {@code zip64Fields} is used to determine which fields should be read and updated from the
268      * ZIP64 block.
269      */
parseExtraField(ByteBuffer extra, Zip64Fields zip64Fields)270     static void parseExtraField(ByteBuffer extra, Zip64Fields zip64Fields)
271             throws ZipFormatException {
272         extra.order(ByteOrder.LITTLE_ENDIAN);
273         // Each record within the extra field must contain at least a UINT16 headerId and size
274         // FORMAT:
275         // * uint16: headerId
276         // * uint16: size
277         //   * Payload of the specified size
278         while (extra.remaining() > 4) {
279             int headerId = getUnsignedInt16(extra);
280             int extraRecordSize = getUnsignedInt16(extra);
281             if (extraRecordSize > extra.remaining()) {
282                 throw new ZipFormatException(
283                         "Extra field record with ID "
284                                 + Long.toHexString(headerId)
285                                 + " exceeds size of field; size of block: "
286                                 + extraRecordSize
287                                 + ", remaining extra buffer: "
288                                 + extra.remaining());
289             }
290             if (headerId == ZIP64_RECORD_ID) {
291                 // Each field in the ZIP64 record only exists if the corresponding field in the
292                 // local file header / central directory with the UINT32 max value; the fields must
293                 // always be in the order uncompressedSize, compressedSize, and
294                 // localFileHeaderOffset, where applicable.
295                 // ZIP64 FORMAT:
296                 // * uint64: uncompressed size (if the base uncompressed value is 0xffffffff)
297                 // * uint64: compressed size (if the base compressed value is 0xffffffff)
298                 // * uint64: local file header offset (if the base LFH offset value is 0xffffffff)
299                 if (zip64Fields.uncompressedSize == UINT32_MAX_VALUE) {
300                     if (extraRecordSize >= 8) {
301                         zip64Fields.uncompressedSize = extra.getLong();
302                         extraRecordSize -= 8;
303                     } else {
304                         throw new ZipFormatException(
305                                 "Expected an uncompressed size value in the ZIP64 record, "
306                                         + "remaining size of record: "
307                                         + extraRecordSize);
308                     }
309                 }
310                 if (zip64Fields.compressedSize == UINT32_MAX_VALUE) {
311                     if (extraRecordSize >= 8) {
312                         zip64Fields.compressedSize = extra.getLong();
313                         extraRecordSize -= 8;
314                     } else {
315                         throw new ZipFormatException(
316                                 "Expected a compressed size value in the ZIP64 record, "
317                                         + "remaining size of record: "
318                                         + extraRecordSize);
319                     }
320                 }
321                 if (zip64Fields.localFileHeaderOffset == UINT32_MAX_VALUE) {
322                     if (extraRecordSize >= 8) {
323                         zip64Fields.localFileHeaderOffset = extra.getLong();
324                     } else {
325                         throw new ZipFormatException(
326                                 "Expected a LFH offset in the ZIP64 record, "
327                                         + "remaining size of record: "
328                                         + extraRecordSize);
329                     }
330                 }
331                 // Once the ZIP64 record is found, no further parsing is required.
332                 break;
333             } else {
334                 // Skip over the unexpected record and check subsequent records.
335                 extra.position(extra.position() + extraRecordSize);
336             }
337         }
338     }
339 
340     /**
341      * Checks whether the provided {@code headerValue} from the LFH / CD Record exceeds the 32-bit
342      * limit and must be obtained from the Zip64 record; if so, then the specified {@code
343      * zip64Value} is verified and returned to the caller.
344      */
checkAndReturnZip64Value( long headerValue, long zip64Value, String name, String fieldName)345     static long checkAndReturnZip64Value(
346             long headerValue, long zip64Value, String name, String fieldName)
347             throws ZipFormatException {
348         // If the value in the header does not indicate that the value exceeds the 32-bit
349         // limitation and must be in the Zip64 record, then return the provided value.
350         if (headerValue != UINT32_MAX_VALUE) {
351             return headerValue;
352         }
353         if (zip64Value == UINT32_MAX_VALUE) {
354             throw new ZipFormatException(
355                     "Unable to obtain ZIP64 " + fieldName + " field for record: " + name);
356         }
357         return zip64Value;
358     }
359 
assertByteOrderLittleEndian(ByteBuffer buffer)360     static void assertByteOrderLittleEndian(ByteBuffer buffer) {
361         if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
362             throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
363         }
364     }
365 
getUnsignedInt16(ByteBuffer buffer, int offset)366     public static int getUnsignedInt16(ByteBuffer buffer, int offset) {
367         return buffer.getShort(offset) & 0xffff;
368     }
369 
getUnsignedInt16(ByteBuffer buffer)370     public static int getUnsignedInt16(ByteBuffer buffer) {
371         return buffer.getShort() & 0xffff;
372     }
373 
parseZipCentralDirectory( DataSource apk, ZipSections apkSections)374     public static List<CentralDirectoryRecord> parseZipCentralDirectory(
375             DataSource apk,
376             ZipSections apkSections)
377             throws IOException, ApkFormatException {
378         // Read the ZIP Central Directory
379         long cdSizeBytes = apkSections.getZipCentralDirectorySizeBytes();
380         if (cdSizeBytes > Integer.MAX_VALUE) {
381             throw new ApkFormatException("ZIP Central Directory too large: " + cdSizeBytes);
382         }
383         long cdOffset = apkSections.getZipCentralDirectoryOffset();
384         ByteBuffer cd = apk.getByteBuffer(cdOffset, (int) cdSizeBytes);
385         cd.order(ByteOrder.LITTLE_ENDIAN);
386 
387         // Parse the ZIP Central Directory
388         int expectedCdRecordCount = apkSections.getZipCentralDirectoryRecordCount();
389         List<CentralDirectoryRecord> cdRecords = new ArrayList<>(expectedCdRecordCount);
390         for (int i = 0; i < expectedCdRecordCount; i++) {
391             CentralDirectoryRecord cdRecord;
392             int offsetInsideCd = cd.position();
393             try {
394                 cdRecord = CentralDirectoryRecord.getRecord(cd);
395             } catch (ZipFormatException e) {
396                 throw new ApkFormatException(
397                         "Malformed ZIP Central Directory record #" + (i + 1)
398                                 + " at file offset " + (cdOffset + offsetInsideCd),
399                         e);
400             }
401             String entryName = cdRecord.getName();
402             if (entryName.endsWith("/")) {
403                 // Ignore directory entries
404                 continue;
405             }
406             cdRecords.add(cdRecord);
407         }
408         // There may be more data in Central Directory, but we don't warn or throw because Android
409         // ignores unused CD data.
410 
411         return cdRecords;
412     }
413 
setUnsignedInt16(ByteBuffer buffer, int offset, int value)414     static void setUnsignedInt16(ByteBuffer buffer, int offset, int value) {
415         if ((value < 0) || (value > 0xffff)) {
416             throw new IllegalArgumentException("uint16 value of out range: " + value);
417         }
418         buffer.putShort(offset, (short) value);
419     }
420 
setUnsignedInt32(ByteBuffer buffer, int offset, long value)421     static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
422         if ((value < 0) || (value > 0xffffffffL)) {
423             throw new IllegalArgumentException("uint32 value of out range: " + value);
424         }
425         buffer.putInt(offset, (int) value);
426     }
427 
putUnsignedInt16(ByteBuffer buffer, int value)428     public static void putUnsignedInt16(ByteBuffer buffer, int value) {
429         if ((value < 0) || (value > 0xffff)) {
430             throw new IllegalArgumentException("uint16 value of out range: " + value);
431         }
432         buffer.putShort((short) value);
433     }
434 
getUnsignedInt32(ByteBuffer buffer, int offset)435     static long getUnsignedInt32(ByteBuffer buffer, int offset) {
436         return buffer.getInt(offset) & 0xffffffffL;
437     }
438 
getUnsignedInt32(ByteBuffer buffer)439     static long getUnsignedInt32(ByteBuffer buffer) {
440         return buffer.getInt() & 0xffffffffL;
441     }
442 
putUnsignedInt32(ByteBuffer buffer, long value)443     static void putUnsignedInt32(ByteBuffer buffer, long value) {
444         if ((value < 0) || (value > 0xffffffffL)) {
445             throw new IllegalArgumentException("uint32 value of out range: " + value);
446         }
447         buffer.putInt((int) value);
448     }
449 
deflate(ByteBuffer input)450     public static DeflateResult deflate(ByteBuffer input) {
451         byte[] inputBuf;
452         int inputOffset;
453         int inputLength = input.remaining();
454         if (input.hasArray()) {
455             inputBuf = input.array();
456             inputOffset = input.arrayOffset() + input.position();
457             input.position(input.limit());
458         } else {
459             inputBuf = new byte[inputLength];
460             inputOffset = 0;
461             input.get(inputBuf);
462         }
463         CRC32 crc32 = new CRC32();
464         crc32.update(inputBuf, inputOffset, inputLength);
465         long crc32Value = crc32.getValue();
466         ByteArrayOutputStream out = new ByteArrayOutputStream();
467         Deflater deflater = new Deflater(9, true);
468         deflater.setInput(inputBuf, inputOffset, inputLength);
469         deflater.finish();
470         byte[] buf = new byte[65536];
471         while (!deflater.finished()) {
472             int chunkSize = deflater.deflate(buf);
473             out.write(buf, 0, chunkSize);
474         }
475         return new DeflateResult(inputLength, crc32Value, out.toByteArray());
476     }
477 
478     public static class DeflateResult {
479         public final int inputSizeBytes;
480         public final long inputCrc32;
481         public final byte[] output;
482 
DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output)483         public DeflateResult(int inputSizeBytes, long inputCrc32, byte[] output) {
484             this.inputSizeBytes = inputSizeBytes;
485             this.inputCrc32 = inputCrc32;
486             this.output = output;
487         }
488     }
489 
490     /**
491      * Class containing the file header / central directory fields that can be affected by the 32-
492      * bit limit. In the case that any of these fields exceed this limit, the value will be set to
493      * 0xffffffff, and the value can be found in the extra field. This class can be used with {@link
494      * #parseExtraField(ByteBuffer, Zip64Fields)} to obtain the corresponding values for each
495      * affected field.
496      */
497     static class Zip64Fields {
498         public long uncompressedSize;
499         public long compressedSize;
500         public long localFileHeaderOffset;
501 
Zip64Fields(long uncompressedSize, long compressedSize)502         Zip64Fields(long uncompressedSize, long compressedSize) {
503             this(uncompressedSize, compressedSize, -1);
504         }
505 
Zip64Fields(long uncompressedSize, long compressedSize, long localFileHeaderOffset)506         Zip64Fields(long uncompressedSize, long compressedSize, long localFileHeaderOffset) {
507             this.uncompressedSize = uncompressedSize;
508             this.compressedSize = compressedSize;
509             this.localFileHeaderOffset = localFileHeaderOffset;
510         }
511     }
512 }