1 /*
2  * Copyright (C) 2015 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.tools.build.apkzlib.zip;
18 
19 import com.android.tools.build.apkzlib.utils.CachedSupplier;
20 import com.android.tools.build.apkzlib.zip.utils.MsDosDateTimeUtils;
21 import com.google.common.annotations.VisibleForTesting;
22 import com.google.common.base.Preconditions;
23 import com.google.common.collect.ImmutableMap;
24 import com.google.common.collect.Lists;
25 import com.google.common.collect.Maps;
26 import com.google.common.primitives.Ints;
27 import com.google.common.util.concurrent.Futures;
28 import com.google.common.util.concurrent.ListenableFuture;
29 import java.io.IOException;
30 import java.io.UncheckedIOException;
31 import java.nio.ByteBuffer;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 import javax.annotation.Nonnull;
36 
37 /**
38  * Representation of the central directory of a zip archive.
39  */
40 class CentralDirectory {
41 
42     /**
43      * Field in the central directory with the central directory signature.
44      */
45     private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x02014b50, "Signature");
46 
47     /**
48      * Field in the central directory with the "made by" code.
49      */
50     private static final ZipField.F2 F_MADE_BY = new ZipField.F2(F_SIGNATURE.endOffset(),
51             "Made by", new ZipFieldInvariantNonNegative());
52 
53     /**
54      * Field in the central directory with the minimum version required to extract the entry.
55      */
56     @VisibleForTesting
57     static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(F_MADE_BY.endOffset(),
58             "Version to extract", new ZipFieldInvariantNonNegative());
59 
60     /**
61      * Field in the central directory with the GP bit flag.
62      */
63     private static final ZipField.F2 F_GP_BIT = new ZipField.F2(F_VERSION_EXTRACT.endOffset(),
64             "GP bit");
65 
66     /**
67      * Field in the central directory with the code of the compression method. See
68      * {@link CompressionMethod#fromCode(long)}.
69      */
70     private static final ZipField.F2 F_METHOD = new ZipField.F2(F_GP_BIT.endOffset(), "Method");
71 
72     /**
73      * Field in the central directory with the last modification time in MS-DOS format (see
74      * {@link MsDosDateTimeUtils#packTime(long)}).
75      */
76     private static final ZipField.F2 F_LAST_MOD_TIME = new ZipField.F2(F_METHOD.endOffset(),
77             "Last modification time");
78 
79     /**
80      * Field in the central directory with the last modification date in MS-DOS format. See
81      * {@link MsDosDateTimeUtils#packDate(long)}.
82      */
83     private static final ZipField.F2 F_LAST_MOD_DATE = new ZipField.F2(F_LAST_MOD_TIME.endOffset(),
84             "Last modification date");
85 
86     /**
87      * Field in the central directory with the CRC32 checksum of the entry. This will be zero for
88      * directories and files with no content.
89      */
90     private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(),
91             "CRC32");
92 
93     /**
94      * Field in the central directory with the entry's compressed size, <em>i.e.</em>, the file on
95      * the archive. This will be the same as the uncompressed size if the method is
96      * {@link CompressionMethod#STORE}.
97      */
98     private static final ZipField.F4 F_COMPRESSED_SIZE = new ZipField.F4(F_CRC32.endOffset(),
99             "Compressed size", new ZipFieldInvariantNonNegative());
100 
101     /**
102      * Field in the central directory with the entry's uncompressed size, <em>i.e.</em>, the size
103      * the file will have when extracted from the zip. This will be zero for directories and empty
104      * files and will be the same as the compressed size if the method is
105      * {@link CompressionMethod#STORE}.
106      */
107     private static final ZipField.F4 F_UNCOMPRESSED_SIZE = new ZipField.F4(
108             F_COMPRESSED_SIZE.endOffset(), "Uncompressed size", new ZipFieldInvariantNonNegative());
109 
110     /**
111      * Field in the central directory with the length of the file name. The file name is stored
112      * after the offset field ({@link #F_OFFSET}). The number of characters in the file name are
113      * stored in this field.
114      */
115     private static final ZipField.F2 F_FILE_NAME_LENGTH = new ZipField.F2(
116             F_UNCOMPRESSED_SIZE.endOffset(), "File name length",
117             new ZipFieldInvariantNonNegative());
118 
119     /**
120      * Field in the central directory with the length of the extra field. The extra field is
121      * stored after the file name ({@link #F_FILE_NAME_LENGTH}). The contents of this field are
122      * partially defined in the zip specification but we do not parse it.
123      */
124     private static final ZipField.F2 F_EXTRA_FIELD_LENGTH = new ZipField.F2(
125             F_FILE_NAME_LENGTH.endOffset(), "Extra field length",
126             new ZipFieldInvariantNonNegative());
127 
128     /**
129      * Field in the central directory with the length of the comment. The comment is stored after
130      * the extra field ({@link #F_EXTRA_FIELD_LENGTH}). We do not parse the comment.
131      */
132     private static final ZipField.F2 F_COMMENT_LENGTH = new ZipField.F2(
133             F_EXTRA_FIELD_LENGTH.endOffset(), "Comment length", new ZipFieldInvariantNonNegative());
134 
135     /**
136      * Number of the disk where the central directory starts. Because we do not support multi-file
137      * archives, this field has to have value {@code 0}.
138      */
139     private static final ZipField.F2 F_DISK_NUMBER_START = new ZipField.F2(
140             F_COMMENT_LENGTH.endOffset(), 0, "Disk start");
141 
142     /**
143      * Internal attributes. This field can only contain one bit set, the {@link #ASCII_BIT}.
144      */
145     private static final ZipField.F2 F_INTERNAL_ATTRIBUTES = new ZipField.F2(
146             F_DISK_NUMBER_START.endOffset(), "Int attributes");
147 
148     /**
149      * External attributes. This field is ignored.
150      */
151     private static final ZipField.F4 F_EXTERNAL_ATTRIBUTES = new ZipField.F4(
152             F_INTERNAL_ATTRIBUTES.endOffset(), "Ext attributes");
153 
154     /**
155      * Offset into the archive where the entry starts. This is the offset to the local header
156      * (see {@link StoredEntry} for information on the local header), not to the file data itself.
157      * The file data, if there is any, will be stored after the local header.
158      */
159     private static final ZipField.F4 F_OFFSET = new ZipField.F4(F_EXTERNAL_ATTRIBUTES.endOffset(),
160             "Offset", new ZipFieldInvariantNonNegative());
161 
162     /**
163      * Maximum supported version to extract.
164      */
165     private static final int MAX_VERSION_TO_EXTRACT = 20;
166 
167     /**
168      * Bit that can be set on the internal attributes stating that the file is an ASCII file. We
169      * don't do anything with this information, but we check that nothing unexpected appears in the
170      * internal attributes.
171      */
172     private static final int ASCII_BIT = 1;
173 
174     /**
175      * Contains all entries in the directory mapped from their names.
176      */
177     @Nonnull
178     private final Map<String, StoredEntry> entries;
179 
180     /**
181      * The file where this directory belongs to.
182      */
183     @Nonnull
184     private final ZFile file;
185 
186     /**
187      * Supplier that provides a byte representation of the central directory.
188      */
189     @Nonnull
190     private final CachedSupplier<byte[]> bytesSupplier;
191 
192     /**
193      * Verify log for the central directory.
194      */
195     @Nonnull
196     private final VerifyLog verifyLog;
197 
198     /**
199      * Creates a new, empty, central directory, for a given zip file.
200      *
201      * @param file the file
202      */
CentralDirectory(@onnull ZFile file)203     CentralDirectory(@Nonnull ZFile file) {
204         entries = Maps.newHashMap();
205         this.file = file;
206         bytesSupplier = new CachedSupplier<>(this::computeByteRepresentation);
207         verifyLog = file.getVerifyLog();
208     }
209 
210     /**
211      * Reads the central directory data from a zip file, parses it, and creates the in-memory
212      * structure representing the directory.
213      *
214      * @param bytes the data of the central directory; the directory is read from the buffer's
215      * current position; when this method terminates, the buffer's position is the first byte
216      * after the directory
217      * @param count the number of entries expected in the central directory (usually read from the
218      * {@link Eocd}).
219      * @param file the zip file this central directory belongs to
220      * @return the central directory
221      * @throws IOException failed to read data from the zip, or the central directory is corrupted
222      * or has unsupported features
223      */
makeFromData(@onnull ByteBuffer bytes, int count, @Nonnull ZFile file)224     static CentralDirectory makeFromData(@Nonnull ByteBuffer bytes, int count, @Nonnull ZFile file)
225             throws IOException {
226         Preconditions.checkNotNull(bytes, "bytes == null");
227         Preconditions.checkArgument(count >= 0, "count < 0");
228 
229         CentralDirectory directory = new CentralDirectory(file);
230 
231         for (int i = 0; i < count; i++) {
232             try {
233                 directory.readEntry(bytes);
234             } catch (IOException e) {
235                 throw new IOException(
236                         "Failed to read directory entry index "
237                                 + i
238                                 + " (total "
239                                 + "directory bytes read: "
240                                 + bytes.position()
241                                 + ").",
242                         e);
243             }
244         }
245 
246         return directory;
247     }
248 
249     /**
250      * Creates a new central directory from the entries. This is used to build a new central
251      * directory from entries in the zip file.
252      *
253      * @param entries the entries in the zip file
254      * @param file the zip file itself
255      * @return the created central directory
256      */
makeFromEntries( @onnull Set<StoredEntry> entries, @Nonnull ZFile file)257     static CentralDirectory makeFromEntries(
258             @Nonnull Set<StoredEntry> entries,
259             @Nonnull ZFile file) {
260         CentralDirectory directory = new CentralDirectory(file);
261         for (StoredEntry entry : entries) {
262             CentralDirectoryHeader cdr = entry.getCentralDirectoryHeader();
263             Preconditions.checkArgument(
264                     !directory.entries.containsKey(cdr.getName()),
265                     "Duplicate filename");
266             directory.entries.put(cdr.getName(), entry);
267         }
268 
269         return directory;
270     }
271 
272     /**
273      * Reads the next entry from the central directory and adds it to {@link #entries}.
274      *
275      * @param bytes the central directory's data, positioned starting at the beginning of the next
276      * entry to read; when finished, the buffer's position will be at the first byte after the
277      * entry
278      * @throws IOException failed to read the directory entry, either because of an I/O error,
279      * because it is corrupt or contains unsupported features
280      */
readEntry(@onnull ByteBuffer bytes)281     private void readEntry(@Nonnull ByteBuffer bytes) throws IOException {
282         F_SIGNATURE.verify(bytes);
283         long madeBy = F_MADE_BY.read(bytes);
284 
285         long versionNeededToExtract = F_VERSION_EXTRACT.read(bytes);
286         verifyLog.verify(
287                 versionNeededToExtract <= MAX_VERSION_TO_EXTRACT,
288                 "Ignored unknown version needed to extract in zip directory entry: %s.",
289                 versionNeededToExtract);
290 
291         long gpBit = F_GP_BIT.read(bytes);
292         GPFlags flags = GPFlags.from(gpBit);
293 
294         long methodCode = F_METHOD.read(bytes);
295         CompressionMethod method = CompressionMethod.fromCode(methodCode);
296         verifyLog.verify(method != null, "Unknown method in zip directory entry: %s.", methodCode);
297 
298         long lastModTime;
299         long lastModDate;
300         if (file.areTimestampsIgnored()) {
301             lastModTime = 0;
302             lastModDate = 0;
303             F_LAST_MOD_TIME.skip(bytes);
304             F_LAST_MOD_DATE.skip(bytes);
305         } else {
306             lastModTime = F_LAST_MOD_TIME.read(bytes);
307             lastModDate = F_LAST_MOD_DATE.read(bytes);
308         }
309 
310         long crc32 = F_CRC32.read(bytes);
311         long compressedSize = F_COMPRESSED_SIZE.read(bytes);
312         long uncompressedSize = F_UNCOMPRESSED_SIZE.read(bytes);
313         int fileNameLength = Ints.checkedCast(F_FILE_NAME_LENGTH.read(bytes));
314         int extraFieldLength = Ints.checkedCast(F_EXTRA_FIELD_LENGTH.read(bytes));
315         int fileCommentLength = Ints.checkedCast(F_COMMENT_LENGTH.read(bytes));
316 
317         F_DISK_NUMBER_START.verify(bytes, verifyLog);
318         long internalAttributes = F_INTERNAL_ATTRIBUTES.read(bytes);
319         verifyLog.verify(
320                 (internalAttributes & ~ASCII_BIT) == 0,
321                 "Ignored invalid internal attributes: %s.",
322                 internalAttributes);
323 
324         long externalAttributes = F_EXTERNAL_ATTRIBUTES.read(bytes);
325         long entryOffset = F_OFFSET.read(bytes);
326 
327         long remainingSize = fileNameLength + extraFieldLength + fileCommentLength;
328 
329         if (bytes.remaining() < fileNameLength + extraFieldLength + fileCommentLength) {
330             throw new IOException(
331                     "Directory entry should have "
332                             + remainingSize
333                             + " bytes remaining (name = "
334                             + fileNameLength
335                             + ", extra = "
336                             + extraFieldLength
337                             + ", comment = "
338                             + fileCommentLength
339                             + "), but it has "
340                             + bytes.remaining()
341                             + ".");
342         }
343 
344         byte[] encodedFileName = new byte[fileNameLength];
345         bytes.get(encodedFileName);
346         String fileName = EncodeUtils.decode(encodedFileName, flags);
347 
348         byte[] extraField = new byte[extraFieldLength];
349         bytes.get(extraField);
350 
351         byte[] fileCommentField = new byte[fileCommentLength];
352         bytes.get(fileCommentField);
353 
354         /*
355          * Tricky: to create a CentralDirectoryHeader we need the future that will hold the result
356          * of the compress information. But, to actually create the result of the compress
357          * information we need the CentralDirectoryHeader
358          */
359         ListenableFuture<CentralDirectoryHeaderCompressInfo> compressInfo =
360                 Futures.immediateFuture(
361                         new CentralDirectoryHeaderCompressInfo(
362                                 method,
363                                 compressedSize,
364                                 versionNeededToExtract));
365         CentralDirectoryHeader centralDirectoryHeader =
366                 new CentralDirectoryHeader(
367                         fileName, encodedFileName, uncompressedSize, compressInfo, flags, file);
368         centralDirectoryHeader.setMadeBy(madeBy);
369         centralDirectoryHeader.setLastModTime(lastModTime);
370         centralDirectoryHeader.setLastModDate(lastModDate);
371         centralDirectoryHeader.setCrc32(crc32);
372         centralDirectoryHeader.setInternalAttributes(internalAttributes);
373         centralDirectoryHeader.setExternalAttributes(externalAttributes);
374         centralDirectoryHeader.setOffset(entryOffset);
375         centralDirectoryHeader.setExtraFieldNoNotify(new ExtraField(extraField));
376         centralDirectoryHeader.setComment(fileCommentField);
377 
378         StoredEntry entry;
379 
380         try {
381             entry = new StoredEntry(centralDirectoryHeader, file, null);
382         } catch (IOException e) {
383             throw new IOException("Failed to read stored entry '" + fileName + "'.", e);
384         }
385 
386         if (entries.containsKey(fileName)) {
387             verifyLog.log("File file contains duplicate file '" + fileName + "'.");
388         }
389 
390         entries.put(fileName, entry);
391     }
392 
393     /**
394      * Obtains all the entries in the central directory.
395      *
396      * @return all entries on a non-modifiable map
397      */
398     @Nonnull
getEntries()399     Map<String, StoredEntry> getEntries() {
400         return ImmutableMap.copyOf(entries);
401     }
402 
403     /**
404      * Obtains the byte representation of the central directory.
405      *
406      * @return a byte array containing the whole central directory
407      * @throws IOException failed to write the byte array
408      */
toBytes()409     byte[] toBytes() throws IOException {
410         return bytesSupplier.get();
411     }
412 
413     /**
414      * Computes the byte representation of the central directory.
415      *
416      * @return a byte array containing the whole central directory
417      * @throws UncheckedIOException failed to write the byte array
418      */
computeByteRepresentation()419     private byte[] computeByteRepresentation() {
420 
421         List<StoredEntry> sorted = Lists.newArrayList(entries.values());
422         sorted.sort(StoredEntry.COMPARE_BY_NAME);
423 
424         CentralDirectoryHeader[] cdhs = new CentralDirectoryHeader[entries.size()];
425         CentralDirectoryHeaderCompressInfo[] compressInfos =
426                 new CentralDirectoryHeaderCompressInfo[entries.size()];
427         byte[][] encodedFileNames = new byte[entries.size()][];
428         byte[][] extraFields = new byte[entries.size()][];
429         byte[][] comments = new byte[entries.size()][];
430 
431         try {
432             /*
433              * First collect all the data and compute the total size of the central directory.
434              */
435             int idx = 0;
436             int total = 0;
437             for (StoredEntry entry : sorted) {
438                 cdhs[idx] = entry.getCentralDirectoryHeader();
439                 compressInfos[idx] = cdhs[idx].getCompressionInfoWithWait();
440                 encodedFileNames[idx] = cdhs[idx].getEncodedFileName();
441                 extraFields[idx] = new byte[cdhs[idx].getExtraField().size()];
442                 cdhs[idx].getExtraField().write(ByteBuffer.wrap(extraFields[idx]));
443                 comments[idx] = cdhs[idx].getComment();
444 
445                 total += F_OFFSET.endOffset() + encodedFileNames[idx].length
446                         + extraFields[idx].length + comments[idx].length;
447                 idx++;
448             }
449 
450             ByteBuffer out = ByteBuffer.allocate(total);
451 
452             for (idx = 0; idx < entries.size(); idx++) {
453                 F_SIGNATURE.write(out);
454                 F_MADE_BY.write(out, cdhs[idx].getMadeBy());
455                 F_VERSION_EXTRACT.write(out, compressInfos[idx].getVersionExtract());
456                 F_GP_BIT.write(out, cdhs[idx].getGpBit().getValue());
457                 F_METHOD.write(out, compressInfos[idx].getMethod().methodCode);
458 
459                 if (file.areTimestampsIgnored()) {
460                     F_LAST_MOD_TIME.write(out, 0);
461                     F_LAST_MOD_DATE.write(out, 0);
462                 } else {
463                     F_LAST_MOD_TIME.write(out, cdhs[idx].getLastModTime());
464                     F_LAST_MOD_DATE.write(out, cdhs[idx].getLastModDate());
465                 }
466 
467                 F_CRC32.write(out, cdhs[idx].getCrc32());
468                 F_COMPRESSED_SIZE.write(out, compressInfos[idx].getCompressedSize());
469                 F_UNCOMPRESSED_SIZE.write(out, cdhs[idx].getUncompressedSize());
470 
471                 F_FILE_NAME_LENGTH.write(out, cdhs[idx].getEncodedFileName().length);
472                 F_EXTRA_FIELD_LENGTH.write(out, cdhs[idx].getExtraField().size());
473                 F_COMMENT_LENGTH.write(out, cdhs[idx].getComment().length);
474                 F_DISK_NUMBER_START.write(out);
475                 F_INTERNAL_ATTRIBUTES.write(out, cdhs[idx].getInternalAttributes());
476                 F_EXTERNAL_ATTRIBUTES.write(out, cdhs[idx].getExternalAttributes());
477                 F_OFFSET.write(out, cdhs[idx].getOffset());
478 
479                 out.put(encodedFileNames[idx]);
480                 out.put(extraFields[idx]);
481                 out.put(comments[idx]);
482             }
483 
484             return out.array();
485         } catch (IOException e) {
486             throw new UncheckedIOException(e);
487         }
488     }
489 }
490