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