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.tools.build.apkzlib.zip; 18 19 import com.android.tools.build.apkzlib.utils.CachedFileContents; 20 import com.android.tools.build.apkzlib.utils.IOExceptionFunction; 21 import com.android.tools.build.apkzlib.utils.IOExceptionRunnable; 22 import com.android.tools.build.apkzlib.zip.compress.Zip64NotSupportedException; 23 import com.android.tools.build.apkzlib.zip.utils.ByteTracker; 24 import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource; 25 import com.android.tools.build.apkzlib.zip.utils.LittleEndianUtils; 26 import com.google.common.base.Preconditions; 27 import com.google.common.base.Verify; 28 import com.google.common.base.VerifyException; 29 import com.google.common.collect.ImmutableList; 30 import com.google.common.collect.Lists; 31 import com.google.common.collect.Maps; 32 import com.google.common.collect.Sets; 33 import com.google.common.hash.Hashing; 34 import com.google.common.io.ByteSource; 35 import com.google.common.io.Closer; 36 import com.google.common.io.Files; 37 import com.google.common.primitives.Ints; 38 import com.google.common.util.concurrent.FutureCallback; 39 import com.google.common.util.concurrent.Futures; 40 import com.google.common.util.concurrent.ListenableFuture; 41 import com.google.common.util.concurrent.MoreExecutors; 42 import com.google.common.util.concurrent.SettableFuture; 43 import java.io.ByteArrayInputStream; 44 import java.io.Closeable; 45 import java.io.EOFException; 46 import java.io.File; 47 import java.io.FileInputStream; 48 import java.io.IOException; 49 import java.io.InputStream; 50 import java.io.RandomAccessFile; 51 import java.nio.ByteBuffer; 52 import java.nio.channels.FileChannel; 53 import java.util.ArrayList; 54 import java.util.HashSet; 55 import java.util.List; 56 import java.util.Map; 57 import java.util.Set; 58 import java.util.SortedSet; 59 import java.util.TreeMap; 60 import java.util.TreeSet; 61 import java.util.concurrent.ExecutionException; 62 import java.util.concurrent.Future; 63 import java.util.function.Function; 64 import java.util.function.Predicate; 65 import java.util.function.Supplier; 66 import javax.annotation.Nonnull; 67 import javax.annotation.Nullable; 68 69 /** 70 * The {@code ZFile} provides the main interface for interacting with zip files. A {@code ZFile} 71 * can be created on a new file or in an existing file. Once created, files can be added or removed 72 * from the zip file. 73 * 74 * <p>Changes in the zip file are always deferred. Any change requested is made in memory and 75 * written to disk only when {@link #update()} or {@link #close()} is invoked. 76 * 77 * <p>Zip files are open initially in read-only mode and will switch to read-write when needed. This 78 * is done automatically. Because modifications to the file are done in-memory, the zip file can 79 * be manipulated when closed. When invoking {@link #update()} or {@link #close()} the zip file 80 * will be reopen and changes will be written. However, the zip file cannot be modified outside 81 * the control of {@code ZFile}. So, if a {@code ZFile} is closed, modified outside and then a file 82 * is added or removed from the zip file, when reopening the zip file, {@link ZFile} will detect 83 * the outside modification and will fail. 84 * 85 * <p>In memory manipulation means that files added to the zip file are kept in memory until written 86 * to disk. This provides much faster operation and allows better zip file allocation (see below). 87 * It may, however, increase the memory footprint of the application. When adding large files, if 88 * memory consumption is a concern, a call to {@link #update()} will actually write the file to 89 * disk and discard the memory buffer. Information about allocation can be obtained from a 90 * {@link ByteTracker} that can be given to the file on creation. 91 * 92 * <p>{@code ZFile} keeps track of allocation inside of the zip file. If a file is deleted, its 93 * space is marked as freed and will be reused for an added file if it fits in the space. 94 * Allocation of files to empty areas is done using a <em>best fit</em> algorithm. When adding a 95 * file, if it doesn't fit in any free area, the zip file will be extended. 96 * 97 * <p>{@code ZFile} provides a fast way to merge data from another zip file 98 * (see {@link #mergeFrom(ZFile, Predicate)}) avoiding recompression and copying of equal files. 99 * When merging, patterns of files may be provided that are ignored. This allows handling special 100 * files in the merging process, such as files in {@code META-INF}. 101 * 102 * <p>When adding files to the zip file, unless files are explicitly required to be stored, files 103 * will be deflated. However, deflating will not occur if the deflated file is larger then the 104 * stored file, <em>e.g.</em> if compression would yield a bigger file. See {@link Compressor} for 105 * details on how compression works. 106 * 107 * <p>Because {@code ZFile} was designed to be used in a build system and not as general-purpose 108 * zip utility, it is very strict (and unforgiving) about the zip format and unsupported features. 109 * 110 * <p>{@code ZFile} supports <em>alignment</em>. Alignment means that file data (not entries -- the 111 * local header must be discounted) must start at offsets that are multiple of a number -- the 112 * alignment. Alignment is defined by an alignment rules ({@link AlignmentRule} in the 113 * {@link ZFileOptions} object used to create the {@link ZFile}. 114 * 115 * <p>When a file is added to the zip, the alignment rules will be checked and alignment will be 116 * honored when positioning the file in the zip. This means that unused spaces in the zip may 117 * be generated as a result. However, alignment of existing entries will not be changed. 118 * 119 * <p>Entries can be realigned individually (see {@link StoredEntry#realign()} or the full zip file 120 * may be realigned (see {@link #realign()}). When realigning the full zip entries that are already 121 * aligned will not be affected. 122 * 123 * <p>Because realignment may cause files to move in the zip, realignment is done in-memory meaning 124 * that files that need to change location will moved to memory and will only be flushed when 125 * either {@link #update()} or {@link #close()} are called. 126 * 127 * <p>Alignment only applies to filed that are forced to be uncompressed. This is because alignment 128 * is used to allow mapping files in the archive directly into memory and compressing defeats the 129 * purpose of alignment. 130 * 131 * <p>Manipulating zip files with {@link ZFile} may yield zip files with empty spaces between files. 132 * This happens in two situations: (1) if alignment is required, files may be shifted to conform to 133 * the request alignment leaving an empty space before the previous file, and (2) if a file is 134 * removed or replaced with a file that does not fit the space it was in. By default, {@link ZFile} 135 * does not do any special processing in these situations. Files are indexed by their offsets from 136 * the central directory and empty spaces can exist in the zip file. 137 * 138 * <p>However, it is possible to tell {@link ZFile} to use the extra field in the local header 139 * to do cover the empty spaces. This is done by setting 140 * {@link ZFileOptions#setCoverEmptySpaceUsingExtraField(boolean)} to {@code true}. This has the 141 * advantage of leaving no gaps between entries in the zip, as required by some tools like Oracle's 142 * {code jar} tool. However, setting this option will destroy the contents of the file's extra 143 * field. 144 * 145 * <p>Activating {@link ZFileOptions#setCoverEmptySpaceUsingExtraField(boolean)} may lead to 146 * <i>virtual files</i> being added to the zip file. Since extra field is limited to 64k, it is not 147 * possible to cover any space bigger than that using the extra field. In those cases, <i>virtual 148 * files</i> are added to the file. A virtual file is a file that exists in the actual zip data, 149 * but is not referenced from the central directory. A zip-compliant utility should ignore these 150 * files. However, zip utilities that expect the zip to be a stream, such as Oracle's jar, will 151 * find these files instead of considering the zip to be corrupt. 152 * 153 * <p>{@code ZFile} support sorting zip files. Sorting (done through the {@link #sortZipContents()} 154 * method) is a process by which all files are re-read into memory, if not already in memory, 155 * removed from the zip and re-added in alphabetical order, respecting alignment rules. So, in 156 * general, file {@code b} will come after file {@code a} unless file {@code a} is subject to 157 * alignment that forces an empty space before that can be occupied by {@code b}. Sorting can be 158 * used to minimize the changes between two zips. 159 * 160 * <p>Sorting in {@code ZFile} can be done manually or automatically. Manual sorting is done by 161 * invoking {@link #sortZipContents()}. Automatic sorting is done by setting the 162 * {@link ZFileOptions#getAutoSortFiles()} option when creating the {@code ZFile}. Automatic 163 * sorting invokes {@link #sortZipContents()} immediately when doing an {@link #update()} after 164 * all extensions have processed the {@link ZFileExtension#beforeUpdate()}. This has the guarantee 165 * that files added by extensions will be sorted, something that does not happen if the invocation 166 * is sequential, <i>i.e.</i>, {@link #sortZipContents()} called before {@link #update()}. The 167 * drawback of automatic sorting is that sorting will happen every time {@link #update()} is 168 * called and the file is dirty having a possible penalty in performance. 169 * 170 * <p>To allow whole-apk signing, the {@code ZFile} allows the central directory location to be 171 * offset by a fixed amount. This amount can be set using the {@link #setExtraDirectoryOffset(long)} 172 * method. Setting a non-zero value will add extra (unused) space in the zip file before the 173 * central directory. This value can be changed at any time and it will force the central directory 174 * rewritten when the file is updated or closed. 175 * 176 * <p>{@code ZFile} provides an extension mechanism to allow objects to register with the file 177 * and be notified when changes to the file happen. This should be used 178 * to add extra features to the zip file while providing strong decoupling. See 179 * {@link ZFileExtension}, {@link ZFile#addZFileExtension(ZFileExtension)} and 180 * {@link ZFile#removeZFileExtension(ZFileExtension)}. 181 * 182 * <p>This class is <strong>not</strong> thread-safe. Neither are any of the classes associated with 183 * it in this package, except when otherwise noticed. 184 */ 185 public class ZFile implements Closeable { 186 187 /** 188 * The file separator in paths in the zip file. This is fixed by the zip specification 189 * (section 4.4.17). 190 */ 191 public static final char SEPARATOR = '/'; 192 193 /** 194 * Minimum size the EOCD can have. 195 */ 196 private static final int MIN_EOCD_SIZE = 22; 197 198 /** 199 * Number of bytes of the Zip64 EOCD locator record. 200 */ 201 private static final int ZIP64_EOCD_LOCATOR_SIZE = 20; 202 203 /** 204 * Maximum size for the EOCD. 205 */ 206 private static final int MAX_EOCD_COMMENT_SIZE = 65535; 207 208 /** 209 * How many bytes to look back from the end of the file to look for the EOCD signature. 210 */ 211 private static final int LAST_BYTES_TO_READ = MIN_EOCD_SIZE + MAX_EOCD_COMMENT_SIZE; 212 213 /** 214 * Signature of the Zip64 EOCD locator record. 215 */ 216 private static final int ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50; 217 218 /** 219 * Signature of the EOCD record. 220 */ 221 private static final byte[] EOCD_SIGNATURE = new byte[] { 0x06, 0x05, 0x4b, 0x50 }; 222 223 /** 224 * Size of buffer for I/O operations. 225 */ 226 private static final int IO_BUFFER_SIZE = 1024 * 1024; 227 228 /** 229 * When extensions request re-runs, we do maximum number of cycles until we decide to stop and 230 * flag a infinite recursion problem. 231 */ 232 private static final int MAXIMUM_EXTENSION_CYCLE_COUNT = 10; 233 234 /** 235 * Minimum size for the extra field when we have to add one. We rely on the alignment segment 236 * to do that so the minimum size for the extra field is the minimum size of an alignment 237 * segment. 238 */ 239 private static final int MINIMUM_EXTRA_FIELD_SIZE = ExtraField.AlignmentSegment.MINIMUM_SIZE; 240 241 /** 242 * Maximum size of the extra field. 243 * 244 * <p>Theoretically, this is (1 << 16) - 1 = 65535 and not (1 < 15) -1 = 32767. However, due to 245 * http://b.android.com/221703, we need to keep this limited. 246 */ 247 private static final int MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE = (1 << 15) - 1; 248 249 /** 250 * File zip file. 251 */ 252 @Nonnull 253 private final File file; 254 255 /** 256 * The random access file used to access the zip file. This will be {@code null} if and only 257 * if {@link #state} is {@link ZipFileState#CLOSED}. 258 */ 259 @Nullable 260 private RandomAccessFile raf; 261 262 /** 263 * The map containing the in-memory contents of the zip file. It keeps track of which parts of 264 * the zip file are used and which are not. 265 */ 266 @Nonnull 267 private final FileUseMap map; 268 269 /** 270 * The EOCD entry. Will be {@code null} if there is no EOCD (because the zip is new) or the 271 * one that exists on disk is no longer valid (because the zip has been changed). 272 * 273 * <p>If the EOCD is deleted because the zip has been changed and the old EOCD was no longer 274 * valid, then {@link #eocdComment} will contain the comment saved from the EOCD. 275 */ 276 @Nullable 277 private FileUseMapEntry<Eocd> eocdEntry; 278 279 /** 280 * The Central Directory entry. Will be {@code null} if there is no Central Directory (because 281 * the zip is new) or because the one that exists on disk is no longer valid (because the zip 282 * has been changed). 283 */ 284 @Nullable 285 private FileUseMapEntry<CentralDirectory> directoryEntry; 286 287 /** 288 * All entries in the zip file. It includes in-memory changes and may not reflect what is 289 * written on disk. Only entries that have been compressed are in this list. 290 */ 291 @Nonnull 292 private final Map<String, FileUseMapEntry<StoredEntry>> entries; 293 294 /** 295 * Entries added to the zip file, but that are not yet compressed. When compression is done, 296 * these entries are eventually moved to {@link #entries}. uncompressedEntries is a list 297 * because entries need to be kept in the order by which they were added. It allows adding 298 * multiple files with the same name and getting the right notifications on which files replaced 299 * which. 300 * 301 * <p>Files are placed in this list in {@link #add(StoredEntry)} method. This method will 302 * keep files here temporarily and move then to {@link #entries} when the data is 303 * available. 304 * 305 * <p>Moving files out of this list to {@link #entries} is done by 306 * {@link #processAllReadyEntries()}. 307 */ 308 @Nonnull 309 private final List<StoredEntry> uncompressedEntries; 310 311 /** 312 * Current state of the zip file. 313 */ 314 @Nonnull 315 private ZipFileState state; 316 317 /** 318 * Are the in-memory changes that have not been written to the zip file? 319 * 320 * <p>This might be false, but will become true after {@link #processAllReadyEntriesWithWait()} 321 * is called if there are {@link #uncompressedEntries} compressing in the background. 322 */ 323 private boolean dirty; 324 325 /** 326 * Non-{@code null} only if the file is currently closed. Used to detect if the zip is 327 * modified outside this object's control. If the file has never been written, this will 328 * be {@code null} even if it is closed. 329 */ 330 @Nullable 331 private CachedFileContents<Object> closedControl; 332 333 /** 334 * The alignment rule. 335 */ 336 @Nonnull 337 private final AlignmentRule alignmentRule; 338 339 /** 340 * Extensions registered with the file. 341 */ 342 @Nonnull 343 private final List<ZFileExtension> extensions; 344 345 /** 346 * When notifying extensions, extensions may request that some runnables are executed. This 347 * list collects all runnables by the order they were requested. Together with 348 * {@link #isNotifying}, it is used to avoid reordering notifications. 349 */ 350 @Nonnull 351 private final List<IOExceptionRunnable> toRun; 352 353 /** 354 * {@code true} when {@link #notify(com.android.tools.build.apkzlib.utils.IOExceptionFunction)} 355 * is notifying extensions. Used to avoid reordering notifications. 356 */ 357 private boolean isNotifying; 358 359 /** 360 * An extra offset for the central directory location. {@code 0} if the central directory 361 * should be written in its standard location. 362 */ 363 private long extraDirectoryOffset; 364 365 /** 366 * Should all timestamps be zeroed when reading / writing the zip? 367 */ 368 private boolean noTimestamps; 369 370 /** 371 * Compressor to use. 372 */ 373 @Nonnull 374 private Compressor compressor; 375 376 /** 377 * Byte tracker to use. 378 */ 379 @Nonnull 380 private final ByteTracker tracker; 381 382 /** 383 * Use the zip entry's "extra field" field to cover empty space in the zip file? 384 */ 385 private boolean coverEmptySpaceUsingExtraField; 386 387 /** 388 * Should files be automatically sorted when updating? 389 */ 390 private boolean autoSortFiles; 391 392 /** 393 * Verify log factory to use. 394 */ 395 @Nonnull 396 private final Supplier<VerifyLog> verifyLogFactory; 397 398 /** 399 * Verify log to use. 400 */ 401 @Nonnull 402 private final VerifyLog verifyLog; 403 404 /** 405 * This field contains the comment in the zip's EOCD if there is no in-memory EOCD structure. 406 * This may happen, for example, if the zip has been changed and the Central Directory and 407 * EOCD have been deleted (in-memory). In that case, this field will save the comment to place 408 * on the EOCD once it is created. 409 * 410 * <p>This field will only be non-{@code null} if there is no in-memory EOCD structure 411 * (<i>i.e.</i>, {@link #eocdEntry} is {@code null}). If there is an {@link #eocdEntry}, then 412 * the comment will be there instead of being in this field. 413 */ 414 @Nullable 415 private byte[] eocdComment; 416 417 /** 418 * Is the file in read-only mode? In read-only mode no changes are allowed. 419 */ 420 private boolean readOnly; 421 422 423 /** 424 * Creates a new zip file. If the zip file does not exist, then no file is created at this 425 * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will 426 * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists, 427 * it will be parsed and read. 428 * 429 * @param file the zip file 430 * @throws IOException some file exists but could not be read 431 */ ZFile(@onnull File file)432 public ZFile(@Nonnull File file) throws IOException { 433 this(file, new ZFileOptions()); 434 } 435 436 /** 437 * Creates a new zip file. If the zip file does not exist, then no file is created at this 438 * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will 439 * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists, 440 * it will be parsed and read. 441 * 442 * @param file the zip file 443 * @param options configuration options 444 * @throws IOException some file exists but could not be read 445 */ ZFile(@onnull File file, @Nonnull ZFileOptions options)446 public ZFile(@Nonnull File file, @Nonnull ZFileOptions options) throws IOException { 447 this(file, options, false); 448 } 449 450 /** 451 * Creates a new zip file. If the zip file does not exist, then no file is created at this 452 * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will 453 * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists, 454 * it will be parsed and read. 455 * 456 * @param file the zip file 457 * @param options configuration options 458 * @param readOnly should the file be open in read-only mode? If {@code true} then the file must 459 * exist and no methods can be invoked that could potentially change the file 460 * @throws IOException some file exists but could not be read 461 */ ZFile(@onnull File file, @Nonnull ZFileOptions options, boolean readOnly)462 public ZFile(@Nonnull File file, @Nonnull ZFileOptions options, boolean readOnly) 463 throws IOException { 464 this.file = file; 465 map = new FileUseMap( 466 0, 467 options.getCoverEmptySpaceUsingExtraField() 468 ? MINIMUM_EXTRA_FIELD_SIZE 469 : 0); 470 this.readOnly = readOnly; 471 dirty = false; 472 closedControl = null; 473 alignmentRule = options.getAlignmentRule(); 474 extensions = Lists.newArrayList(); 475 toRun = Lists.newArrayList(); 476 noTimestamps = options.getNoTimestamps(); 477 tracker = options.getTracker(); 478 compressor = options.getCompressor(); 479 coverEmptySpaceUsingExtraField = options.getCoverEmptySpaceUsingExtraField(); 480 autoSortFiles = options.getAutoSortFiles(); 481 verifyLogFactory = options.getVerifyLogFactory(); 482 verifyLog = verifyLogFactory.get(); 483 484 /* 485 * These two values will be overwritten by openReadOnly() below if the file exists. 486 */ 487 state = ZipFileState.CLOSED; 488 raf = null; 489 490 if (file.exists()) { 491 openReadOnly(); 492 } else if (readOnly) { 493 throw new IOException("File does not exist but read-only mode requested"); 494 } else { 495 dirty = true; 496 } 497 498 entries = Maps.newHashMap(); 499 uncompressedEntries = Lists.newArrayList(); 500 extraDirectoryOffset = 0; 501 502 try { 503 if (state != ZipFileState.CLOSED) { 504 long rafSize = raf.length(); 505 if (rafSize > Integer.MAX_VALUE) { 506 throw new IOException("File exceeds size limit of " + Integer.MAX_VALUE + "."); 507 } 508 509 map.extend(Ints.checkedCast(rafSize)); 510 readData(); 511 } 512 513 // If we don't have an EOCD entry, set the comment to empty. 514 if (eocdEntry == null) { 515 eocdComment = new byte[0]; 516 } 517 518 // Notify the extensions if the zip file has been open. 519 if (state != ZipFileState.CLOSED) { 520 notify(ZFileExtension::open); 521 } 522 } catch (Zip64NotSupportedException e) { 523 throw e; 524 } catch (IOException e) { 525 throw new IOException("Failed to read zip file '" + file.getAbsolutePath() + "'.", e); 526 } catch (IllegalStateException | IllegalArgumentException | VerifyException e) { 527 throw new RuntimeException( 528 "Internal error when trying to read zip file '" + file.getAbsolutePath() + "'.", 529 e); 530 } 531 } 532 533 /** 534 * Obtains all entries in the file. Entries themselves may be or not written in disk. However, 535 * all of them can be open for reading. 536 * 537 * @return all entries in the zip 538 */ 539 @Nonnull entries()540 public Set<StoredEntry> entries() { 541 Map<String, StoredEntry> entries = Maps.newHashMap(); 542 543 for (FileUseMapEntry<StoredEntry> mapEntry : this.entries.values()) { 544 StoredEntry entry = mapEntry.getStore(); 545 assert entry != null; 546 entries.put(entry.getCentralDirectoryHeader().getName(), entry); 547 } 548 549 /* 550 * mUncompressed may override mEntriesReady as we may not have yet processed all 551 * entries. 552 */ 553 for (StoredEntry uncompressed : uncompressedEntries) { 554 entries.put(uncompressed.getCentralDirectoryHeader().getName(), uncompressed); 555 } 556 557 return Sets.newHashSet(entries.values()); 558 } 559 560 /** 561 * Obtains an entry at a given path in the zip. 562 * 563 * @param path the path 564 * @return the entry at the path or {@code null} if none exists 565 */ 566 @Nullable get(@onnull String path)567 public StoredEntry get(@Nonnull String path) { 568 /* 569 * The latest entries are the last ones in uncompressed and they may eventually override 570 * files in entries. 571 */ 572 for (StoredEntry stillUncompressed : Lists.reverse(uncompressedEntries)) { 573 if (stillUncompressed.getCentralDirectoryHeader().getName().equals(path)) { 574 return stillUncompressed; 575 } 576 } 577 578 FileUseMapEntry<StoredEntry> found = entries.get(path); 579 if (found == null) { 580 return null; 581 } 582 583 return found.getStore(); 584 } 585 586 /** 587 * Reads all the data in the zip file, except the contents of the entries themselves. This 588 * method will populate the directory and maps in the instance variables. 589 * 590 * @throws IOException failed to read the zip file 591 */ readData()592 private void readData() throws IOException { 593 Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); 594 Preconditions.checkState(raf != null, "raf == null"); 595 596 readEocd(); 597 readCentralDirectory(); 598 599 /* 600 * Go over all files and create the usage map, verifying there is no overlap in the files. 601 */ 602 long entryEndOffset; 603 long directoryStartOffset; 604 605 if (directoryEntry != null) { 606 CentralDirectory directory = directoryEntry.getStore(); 607 assert directory != null; 608 609 entryEndOffset = 0; 610 611 for (StoredEntry entry : directory.getEntries().values()) { 612 long start = entry.getCentralDirectoryHeader().getOffset(); 613 long end = start + entry.getInFileSize(); 614 615 /* 616 * If isExtraAlignmentBlock(entry.getLocalExtra()) is true, we know the entry 617 * has an extra field that is solely used for alignment. This means the 618 * actual entry could start at start + extra.length and leave space before. 619 * 620 * But, if we did this here, we would be modifying the zip file and that is 621 * weird because we're just opening it for reading. 622 * 623 * The downside is that we will never reuse that space. Maybe one day ZFile 624 * can be clever enough to remove the local extra when we start modifying the zip 625 * file. 626 */ 627 628 Verify.verify(start >= 0, "start < 0"); 629 Verify.verify(end < map.size(), "end >= map.size()"); 630 631 FileUseMapEntry<?> found = map.at(start); 632 Verify.verifyNotNull(found); 633 634 // We've got a problem if the found entry is not free or is a free entry but 635 // doesn't cover the whole file. 636 if (!found.isFree() || found.getEnd() < end) { 637 if (found.isFree()) { 638 found = map.after(found); 639 Verify.verify(found != null && !found.isFree()); 640 } 641 642 Object foundEntry = found.getStore(); 643 Verify.verify(foundEntry != null); 644 645 // Obtains a custom description of an entry. 646 IOExceptionFunction<StoredEntry, String> describe = 647 e -> 648 String.format( 649 "'%s' (offset: %d, size: %d)", 650 e.getCentralDirectoryHeader().getName(), 651 e.getCentralDirectoryHeader().getOffset(), 652 e.getInFileSize()); 653 654 String overlappingEntryDescription; 655 if (foundEntry instanceof StoredEntry) { 656 StoredEntry foundStored = (StoredEntry) foundEntry; 657 overlappingEntryDescription = describe.apply((StoredEntry) foundEntry); 658 } else { 659 overlappingEntryDescription = 660 "Central Directory / EOCD: " 661 + found.getStart() 662 + " - " 663 + found.getEnd(); 664 } 665 666 throw new IOException( 667 "Cannot read entry " 668 + describe.apply(entry) 669 + " because it overlaps with " 670 + overlappingEntryDescription); 671 } 672 673 FileUseMapEntry<StoredEntry> mapEntry = map.add(start, end, entry); 674 entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry); 675 676 if (end > entryEndOffset) { 677 entryEndOffset = end; 678 } 679 } 680 681 directoryStartOffset = directoryEntry.getStart(); 682 } else { 683 /* 684 * No directory means an empty zip file. Use the start of the EOCD to compute 685 * an existing offset. 686 */ 687 Verify.verifyNotNull(eocdEntry); 688 assert eocdEntry != null; 689 directoryStartOffset = eocdEntry.getStart(); 690 entryEndOffset = 0; 691 } 692 693 /* 694 * Check if there is an extra central directory offset. If there is, save it. Note that 695 * we can't call extraDirectoryOffset() because that would mark the file as dirty. 696 */ 697 long extraOffset = directoryStartOffset - entryEndOffset; 698 Verify.verify(extraOffset >= 0, "extraOffset (%s) < 0", extraOffset); 699 extraDirectoryOffset = extraOffset; 700 } 701 702 /** 703 * Finds the EOCD marker and reads it. It will populate the {@link #eocdEntry} variable. 704 * 705 * @throws IOException failed to read the EOCD 706 */ readEocd()707 private void readEocd() throws IOException { 708 Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); 709 Preconditions.checkState(raf != null, "raf == null"); 710 711 /* 712 * Read the last part of the zip into memory. If we don't find the EOCD signature by then, 713 * the file is corrupt. 714 */ 715 int lastToRead = LAST_BYTES_TO_READ; 716 if (lastToRead > raf.length()) { 717 lastToRead = Ints.checkedCast(raf.length()); 718 } 719 720 byte[] last = new byte[lastToRead]; 721 directFullyRead(raf.length() - lastToRead, last); 722 723 724 /* 725 * Start endIdx at the first possible location where the signature can be located and then 726 * move backwards. Because the EOCD must have at least MIN_EOCD size, the first byte of the 727 * signature (and first byte of the EOCD) must be located at last.length - MIN_EOCD_SIZE. 728 * 729 * Because the EOCD signature may exist in the file comment, when we find a signature we 730 * will try to read the Eocd. If we fail, we continue searching for the signature. However, 731 * we will keep the last exception in case we don't find any signature. 732 */ 733 Eocd eocd = null; 734 int foundEocdSignature = -1; 735 IOException errorFindingSignature = null; 736 int eocdStart = -1; 737 738 for (int endIdx = last.length - MIN_EOCD_SIZE; endIdx >= 0 && foundEocdSignature == -1; 739 endIdx--) { 740 /* 741 * Remember: little endian... 742 */ 743 if (last[endIdx] == EOCD_SIGNATURE[3] 744 && last[endIdx + 1] == EOCD_SIGNATURE[2] 745 && last[endIdx + 2] == EOCD_SIGNATURE[1] 746 && last[endIdx + 3] == EOCD_SIGNATURE[0]) { 747 748 /* 749 * We found a signature. Try to read the EOCD record. 750 */ 751 752 foundEocdSignature = endIdx; 753 ByteBuffer eocdBytes = 754 ByteBuffer.wrap(last, foundEocdSignature, last.length - foundEocdSignature); 755 756 try { 757 eocd = new Eocd(eocdBytes); 758 eocdStart = Ints.checkedCast(raf.length() - lastToRead + foundEocdSignature); 759 760 /* 761 * Make sure the EOCD takes the whole file up to the end. Log an error if it 762 * doesn't. 763 */ 764 if (eocdStart + eocd.getEocdSize() != raf.length()) { 765 verifyLog.log("EOCD starts at " 766 + eocdStart 767 + " and has " 768 + eocd.getEocdSize() 769 + " bytes, but file ends at " 770 + raf.length() 771 + "."); 772 } 773 } catch (IOException e) { 774 if (errorFindingSignature != null) { 775 e.addSuppressed(errorFindingSignature); 776 } 777 778 errorFindingSignature = e; 779 foundEocdSignature = -1; 780 eocd = null; 781 } 782 } 783 } 784 785 if (foundEocdSignature == -1) { 786 throw new IOException("EOCD signature not found in the last " 787 + lastToRead + " bytes of the file.", errorFindingSignature); 788 } 789 790 Verify.verify(eocdStart >= 0); 791 792 /* 793 * Look for the Zip64 central directory locator. If we find it, then this file is a Zip64 794 * file and we do not support it. 795 */ 796 int zip64LocatorStart = eocdStart - ZIP64_EOCD_LOCATOR_SIZE; 797 if (zip64LocatorStart >= 0) { 798 byte[] possibleZip64Locator = new byte[4]; 799 directFullyRead(zip64LocatorStart, possibleZip64Locator); 800 if (LittleEndianUtils.readUnsigned4Le(ByteBuffer.wrap(possibleZip64Locator)) == 801 ZIP64_EOCD_LOCATOR_SIGNATURE) { 802 throw new Zip64NotSupportedException( 803 "Zip64 EOCD locator found but Zip64 format is not supported."); 804 } 805 } 806 807 eocdEntry = map.add(eocdStart, eocdStart + eocd.getEocdSize(), eocd); 808 } 809 810 /** 811 * Reads the zip's central directory and populates the {@link #directoryEntry} variable. This 812 * method can only be called after the EOCD has been read. If the central directory is empty 813 * (if there are no files on the zip archive), then {@link #directoryEntry} will be set to 814 * {@code null}. 815 * 816 * @throws IOException failed to read the central directory 817 */ readCentralDirectory()818 private void readCentralDirectory() throws IOException { 819 Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); 820 Preconditions.checkNotNull(eocdEntry.getStore(), "eocdEntry.getStore() == null"); 821 Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); 822 Preconditions.checkState(raf != null, "raf == null"); 823 Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); 824 825 Eocd eocd = eocdEntry.getStore(); 826 827 long dirSize = eocd.getDirectorySize(); 828 if (dirSize > Integer.MAX_VALUE) { 829 throw new IOException("Cannot read central directory with size " + dirSize + "."); 830 } 831 832 long centralDirectoryEnd = eocd.getDirectoryOffset() + dirSize; 833 if (centralDirectoryEnd != eocdEntry.getStart()) { 834 String msg = "Central directory is stored in [" 835 + eocd.getDirectoryOffset() 836 + " - " 837 + (centralDirectoryEnd - 1) 838 + "] and EOCD starts at " 839 + eocdEntry.getStart() 840 + "."; 841 842 /* 843 * If there is an empty space between the central directory and the EOCD, we proceed 844 * logging an error. If the central directory ends after the start of the EOCD (and 845 * therefore, they overlap), throw an exception. 846 */ 847 if (centralDirectoryEnd > eocdEntry.getSize()) { 848 throw new IOException(msg); 849 } else { 850 verifyLog.log(msg); 851 } 852 } 853 854 byte[] directoryData = new byte[Ints.checkedCast(dirSize)]; 855 directFullyRead(eocd.getDirectoryOffset(), directoryData); 856 857 CentralDirectory directory = 858 CentralDirectory.makeFromData( 859 ByteBuffer.wrap(directoryData), 860 eocd.getTotalRecords(), 861 this); 862 if (eocd.getDirectorySize() > 0) { 863 directoryEntry = map.add( 864 eocd.getDirectoryOffset(), 865 eocd.getDirectoryOffset() + eocd.getDirectorySize(), 866 directory); 867 } 868 } 869 870 /** 871 * Opens a portion of the zip for reading. The zip must be open for this method to be invoked. 872 * Note that if the zip has not been updated, the individual zip entries may not have been 873 * written yet. 874 * 875 * @param start the index within the zip file to start reading 876 * @param end the index within the zip file to end reading (the actual byte pointed by 877 * <em>end</em> will not be read) 878 * @return a stream that will read the portion of the file; no decompression is done, data is 879 * returned <em>as is</em> 880 * @throws IOException failed to open the zip file 881 */ 882 @Nonnull directOpen(final long start, final long end)883 public InputStream directOpen(final long start, final long end) throws IOException { 884 Preconditions.checkState(state != ZipFileState.CLOSED, "state == ZipFileState.CLOSED"); 885 Preconditions.checkState(raf != null, "raf == null"); 886 Preconditions.checkArgument(start >= 0, "start < 0"); 887 Preconditions.checkArgument(end >= start, "end < start"); 888 Preconditions.checkArgument(end <= raf.length(), "end > raf.length()"); 889 890 return new InputStream() { 891 private long mCurr = start; 892 893 @Override 894 public int read() throws IOException { 895 if (mCurr == end) { 896 return -1; 897 } 898 899 byte[] b = new byte[1]; 900 int r = directRead(mCurr, b); 901 if (r > 0) { 902 mCurr++; 903 return b[0]; 904 } else { 905 return -1; 906 } 907 } 908 909 @Override 910 public int read(@Nonnull byte[] b, int off, int len) throws IOException { 911 Preconditions.checkNotNull(b, "b == null"); 912 Preconditions.checkArgument(off >= 0, "off < 0"); 913 Preconditions.checkArgument(off <= b.length, "off > b.length"); 914 Preconditions.checkArgument(len >= 0, "len < 0"); 915 Preconditions.checkArgument(off + len <= b.length, "off + len > b.length"); 916 917 long availableToRead = end - mCurr; 918 long toRead = Math.min(len, availableToRead); 919 920 if (toRead == 0) { 921 return -1; 922 } 923 924 if (toRead > Integer.MAX_VALUE) { 925 throw new IOException("Cannot read " + toRead + " bytes."); 926 } 927 928 int r = directRead(mCurr, b, off, Ints.checkedCast(toRead)); 929 if (r > 0) { 930 mCurr += r; 931 } 932 933 return r; 934 } 935 }; 936 } 937 938 /** 939 * Deletes an entry from the zip. This method does not actually delete anything on disk. It 940 * just changes in-memory structures. Use {@link #update()} to update the contents on disk. 941 * 942 * @param entry the entry to delete 943 * @param notify should listeners be notified of the deletion? This will only be 944 * {@code false} if the entry is being removed as part of a replacement 945 * @throws IOException failed to delete the entry 946 * @throws IllegalStateException if open in read-only mode 947 */ delete(@onnull final StoredEntry entry, boolean notify)948 void delete(@Nonnull final StoredEntry entry, boolean notify) throws IOException { 949 checkNotInReadOnlyMode(); 950 951 String path = entry.getCentralDirectoryHeader().getName(); 952 FileUseMapEntry<StoredEntry> mapEntry = entries.get(path); 953 Preconditions.checkNotNull(mapEntry, "mapEntry == null"); 954 Preconditions.checkArgument(entry == mapEntry.getStore(), "entry != mapEntry.getStore()"); 955 956 dirty = true; 957 958 map.remove(mapEntry); 959 entries.remove(path); 960 961 if (notify) { 962 notify(ext -> ext.removed(entry)); 963 } 964 } 965 966 /** 967 * Checks that the file is not in read-only mode. 968 * 969 * @throws IllegalStateException if the file is in read-only mode 970 */ checkNotInReadOnlyMode()971 private void checkNotInReadOnlyMode() { 972 if (readOnly) { 973 throw new IllegalStateException("Illegal operation in read only model"); 974 } 975 } 976 977 /** 978 * Updates the file writing new entries and removing deleted entries. This will force 979 * reopening the file as read/write if the file wasn't open in read/write mode. 980 * 981 * @throws IOException failed to update the file; this exception may have been thrown by 982 * the compressor but only reported here 983 */ update()984 public void update() throws IOException { 985 checkNotInReadOnlyMode(); 986 987 /* 988 * Process all background stuff before calling in the extensions. 989 */ 990 processAllReadyEntriesWithWait(); 991 992 notify(ZFileExtension::beforeUpdate); 993 994 /* 995 * Process all background stuff that may be leftover by the extensions. 996 */ 997 processAllReadyEntriesWithWait(); 998 999 1000 if (!dirty) { 1001 return; 1002 } 1003 1004 reopenRw(); 1005 1006 /* 1007 * At this point, no more files can be added. We may need to repack to remove extra 1008 * empty spaces or sort. If we sort, we don't need to repack as sorting forces the 1009 * zip file to be as compact as possible. 1010 */ 1011 if (autoSortFiles) { 1012 sortZipContents(); 1013 } else { 1014 packIfNecessary(); 1015 } 1016 1017 /* 1018 * We're going to change the file so delete the central directory and the EOCD as they 1019 * will have to be rewritten. 1020 */ 1021 deleteDirectoryAndEocd(); 1022 map.truncate(); 1023 1024 /* 1025 * If we need to use the extra field to cover empty spaces, we do the processing here. 1026 */ 1027 if (coverEmptySpaceUsingExtraField) { 1028 1029 /* We will go over all files in the zip and check whether there is empty space before 1030 * them. If there is, then we will move the entry to the beginning of the empty space 1031 * (covering it) and extend the extra field with the size of the empty space. 1032 */ 1033 for (FileUseMapEntry<StoredEntry> entry : new HashSet<>(entries.values())) { 1034 StoredEntry storedEntry = entry.getStore(); 1035 assert storedEntry != null; 1036 1037 FileUseMapEntry<?> before = map.before(entry); 1038 if (before == null || !before.isFree()) { 1039 continue; 1040 } 1041 1042 /* 1043 * We have free space before the current entry. However, we do know that it can 1044 * be covered by the extra field, because both sortZipContents() and 1045 * packIfNecessary() guarantee it. 1046 */ 1047 int localExtraSize = 1048 storedEntry.getLocalExtra().size() + Ints.checkedCast(before.getSize()); 1049 Verify.verify(localExtraSize <= MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE); 1050 1051 /* 1052 * Move file back in the zip. 1053 */ 1054 storedEntry.loadSourceIntoMemory(); 1055 1056 long newStart = before.getStart(); 1057 long newSize = entry.getSize() + before.getSize(); 1058 1059 /* 1060 * Remove the entry. 1061 */ 1062 String name = storedEntry.getCentralDirectoryHeader().getName(); 1063 map.remove(entry); 1064 Verify.verify(entry == entries.remove(name)); 1065 1066 /* 1067 * Make a list will all existing segments in the entry's extra field, but remove 1068 * the alignment field, if it exists. Also, sum the size of all kept extra field 1069 * segments. 1070 */ 1071 ImmutableList<ExtraField.Segment> currentSegments; 1072 try { 1073 currentSegments = storedEntry.getLocalExtra().getSegments(); 1074 } catch (IOException e) { 1075 /* 1076 * Parsing current segments has failed. This means the contents of the extra 1077 * field are not valid. We'll continue discarding the existing segments. 1078 */ 1079 currentSegments = ImmutableList.of(); 1080 } 1081 1082 List<ExtraField.Segment> extraFieldSegments = new ArrayList<>(); 1083 int newExtraFieldSize = currentSegments.stream() 1084 .filter(s -> s.getHeaderId() 1085 != ExtraField.ALIGNMENT_ZIP_EXTRA_DATA_FIELD_HEADER_ID) 1086 .peek(extraFieldSegments::add) 1087 .map(ExtraField.Segment::size) 1088 .reduce(0, Integer::sum); 1089 1090 int spaceToFill = 1091 Ints.checkedCast( 1092 before.getSize() 1093 + storedEntry.getLocalExtra().size() 1094 - newExtraFieldSize); 1095 1096 extraFieldSegments.add( 1097 new ExtraField.AlignmentSegment(chooseAlignment(storedEntry),spaceToFill)); 1098 1099 storedEntry.setLocalExtraNoNotify( 1100 new ExtraField(ImmutableList.copyOf(extraFieldSegments))); 1101 entries.put(name, map.add(newStart, newStart + newSize, storedEntry)); 1102 1103 /* 1104 * Reset the offset to force the file to be rewritten. 1105 */ 1106 storedEntry.getCentralDirectoryHeader().setOffset(-1); 1107 } 1108 } 1109 1110 /* 1111 * Write new files in the zip. We identify new files because they don't have an offset 1112 * in the zip where they are written although we already know, by their location in the 1113 * file map, where they will be written to. 1114 * 1115 * Before writing the files, we sort them in the order they are written in the file so that 1116 * writes are made in order on disk. 1117 * This is, however, unlikely to optimize anything relevant given the way the Operating 1118 * System does caching, but it certainly won't hurt :) 1119 */ 1120 TreeMap<FileUseMapEntry<?>, StoredEntry> toWriteToStore = 1121 new TreeMap<>(FileUseMapEntry.COMPARE_BY_START); 1122 1123 for (FileUseMapEntry<StoredEntry> entry : entries.values()) { 1124 StoredEntry entryStore = entry.getStore(); 1125 assert entryStore != null; 1126 if (entryStore.getCentralDirectoryHeader().getOffset() == -1) { 1127 toWriteToStore.put(entry, entryStore); 1128 } 1129 } 1130 1131 /* 1132 * Add all free entries to the set. 1133 */ 1134 for(FileUseMapEntry<?> freeArea : map.getFreeAreas()) { 1135 toWriteToStore.put(freeArea, null); 1136 } 1137 1138 /* 1139 * Write everything to file. 1140 */ 1141 for (FileUseMapEntry<?> fileUseMapEntry : toWriteToStore.keySet()) { 1142 StoredEntry entry = toWriteToStore.get(fileUseMapEntry); 1143 if (entry == null) { 1144 int size = Ints.checkedCast(fileUseMapEntry.getSize()); 1145 directWrite(fileUseMapEntry.getStart(), new byte[size]); 1146 } else { 1147 writeEntry(entry, fileUseMapEntry.getStart()); 1148 } 1149 } 1150 1151 boolean hasCentralDirectory; 1152 int extensionBugDetector = MAXIMUM_EXTENSION_CYCLE_COUNT; 1153 do { 1154 computeCentralDirectory(); 1155 computeEocd(); 1156 1157 hasCentralDirectory = (directoryEntry != null); 1158 1159 notify(ext -> { 1160 ext.entriesWritten(); 1161 return null; 1162 }); 1163 1164 if ((--extensionBugDetector) == 0) { 1165 throw new IOException("Extensions keep resetting the central directory. This is " 1166 + "probably a bug."); 1167 } 1168 } while (hasCentralDirectory && directoryEntry == null); 1169 1170 appendCentralDirectory(); 1171 appendEocd(); 1172 1173 Verify.verifyNotNull(raf); 1174 raf.setLength(map.size()); 1175 1176 dirty = false; 1177 1178 notify(ext -> { 1179 ext.updated(); 1180 return null; 1181 }); 1182 } 1183 1184 /** 1185 * Reorganizes the zip so that there are no gaps between files bigger than 1186 * {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE} if {@link #coverEmptySpaceUsingExtraField} 1187 * is set to {@code true}. 1188 * 1189 * <p>Essentially, this makes sure we can cover any empty space with the extra field, given 1190 * that the local extra field is limited to {@link #MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE}. If 1191 * an entry is too far from the previous one, it is removed and re-added. 1192 * 1193 * @throws IOException failed to repack 1194 */ packIfNecessary()1195 private void packIfNecessary() throws IOException { 1196 if (!coverEmptySpaceUsingExtraField) { 1197 return; 1198 } 1199 1200 SortedSet<FileUseMapEntry<StoredEntry>> entriesByLocation = 1201 new TreeSet<>(FileUseMapEntry.COMPARE_BY_START); 1202 entriesByLocation.addAll(entries.values()); 1203 1204 for (FileUseMapEntry<StoredEntry> entry : entriesByLocation) { 1205 StoredEntry storedEntry = entry.getStore(); 1206 assert storedEntry != null; 1207 1208 FileUseMapEntry<?> before = map.before(entry); 1209 if (before == null || !before.isFree()) { 1210 continue; 1211 } 1212 1213 int localExtraSize = 1214 storedEntry.getLocalExtra().size() + Ints.checkedCast(before.getSize()); 1215 if (localExtraSize > MAX_LOCAL_EXTRA_FIELD_CONTENTS_SIZE) { 1216 /* 1217 * This entry is too far from the previous one. Remove it and re-add it to the 1218 * zip file. 1219 */ 1220 reAdd(storedEntry, PositionHint.LOWEST_OFFSET); 1221 } 1222 } 1223 } 1224 1225 /** 1226 * Removes a stored entry from the zip and adds it back again. This will force the entry to be 1227 * loaded into memory and repositioned in the zip file. It will also mark the archive as 1228 * being dirty. 1229 * 1230 * @param entry the entry 1231 * @param positionHint hint to where the file should be positioned when re-adding 1232 * @throws IOException failed to load the entry into memory 1233 */ reAdd(@onnull StoredEntry entry, @Nonnull PositionHint positionHint)1234 private void reAdd(@Nonnull StoredEntry entry, @Nonnull PositionHint positionHint) 1235 throws IOException { 1236 String name = entry.getCentralDirectoryHeader().getName(); 1237 FileUseMapEntry<StoredEntry> mapEntry = entries.get(name); 1238 Preconditions.checkNotNull(mapEntry); 1239 Preconditions.checkState(mapEntry.getStore() == entry); 1240 1241 entry.loadSourceIntoMemory(); 1242 1243 map.remove(mapEntry); 1244 entries.remove(name); 1245 FileUseMapEntry<StoredEntry> positioned = positionInFile(entry, positionHint); 1246 entries.put(name, positioned); 1247 dirty = true; 1248 } 1249 1250 /** 1251 * Invoked from {@link StoredEntry} when entry has changed in a way that forces the local 1252 * header to be rewritten 1253 * 1254 * @param entry the entry that changed 1255 * @param resized was the local header resized? 1256 * @throws IOException failed to load the entry into memory 1257 */ localHeaderChanged(@onnull StoredEntry entry, boolean resized)1258 void localHeaderChanged(@Nonnull StoredEntry entry, boolean resized) throws IOException { 1259 dirty = true; 1260 1261 if (resized) { 1262 reAdd(entry, PositionHint.ANYWHERE); 1263 } 1264 } 1265 1266 /** 1267 * Invoked when the central directory has changed and needs to be rewritten. 1268 */ centralDirectoryChanged()1269 void centralDirectoryChanged() { 1270 dirty = true; 1271 deleteDirectoryAndEocd(); 1272 } 1273 1274 /** 1275 * Updates the file and closes it. 1276 */ 1277 @Override close()1278 public void close() throws IOException { 1279 // We need to make sure to release raf, otherwise we end up locking the file on 1280 // Windows. Use try-with-resources to handle exception suppressing. 1281 try (Closeable ignored = this::innerClose) { 1282 if (!readOnly) { 1283 update(); 1284 } 1285 } 1286 1287 notify(ext -> { 1288 ext.closed(); 1289 return null; 1290 }); 1291 } 1292 1293 /** 1294 * Removes the Central Directory and EOCD from the file. This will free space for new entries 1295 * as well as allowing the zip file to be truncated if files have been removed. 1296 * 1297 * <p>This method does not mark the zip as dirty. 1298 */ deleteDirectoryAndEocd()1299 private void deleteDirectoryAndEocd() { 1300 if (directoryEntry != null) { 1301 map.remove(directoryEntry); 1302 directoryEntry = null; 1303 } 1304 1305 if (eocdEntry != null) { 1306 map.remove(eocdEntry); 1307 1308 Eocd eocd = eocdEntry.getStore(); 1309 Verify.verify(eocd != null); 1310 eocdComment = eocd.getComment(); 1311 eocdEntry = null; 1312 } 1313 } 1314 1315 /** 1316 * Writes an entry's data in the zip file. This includes everything: the local header and 1317 * the data itself. After writing, the entry is updated with the offset and its source replaced 1318 * with a source that reads from the zip file. 1319 * 1320 * @param entry the entry to write 1321 * @param offset the offset at which the entry should be written 1322 * @throws IOException failed to write the entry 1323 */ writeEntry(@onnull StoredEntry entry, long offset)1324 private void writeEntry(@Nonnull StoredEntry entry, long offset) throws IOException { 1325 Preconditions.checkArgument(entry.getDataDescriptorType() 1326 == DataDescriptorType. NO_DATA_DESCRIPTOR, "Cannot write entries with a data " 1327 + "descriptor."); 1328 Preconditions.checkNotNull(raf, "raf == null"); 1329 Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); 1330 1331 /* 1332 * Place the cursor and write the local header. 1333 */ 1334 byte[] headerData = entry.toHeaderData(); 1335 directWrite(offset, headerData); 1336 1337 /* 1338 * Get the raw source data to write. 1339 */ 1340 ProcessedAndRawByteSources source = entry.getSource(); 1341 ByteSource rawContents = source.getRawByteSource(); 1342 1343 /* 1344 * Write the source data. 1345 */ 1346 byte[] chunk = new byte[IO_BUFFER_SIZE]; 1347 int r; 1348 long writeOffset = offset + headerData.length; 1349 InputStream is = rawContents.openStream(); 1350 while ((r = is.read(chunk)) >= 0) { 1351 directWrite(writeOffset, chunk, 0, r); 1352 writeOffset += r; 1353 } 1354 1355 is.close(); 1356 1357 /* 1358 * Set the entry's offset and create the entry source. 1359 */ 1360 entry.replaceSourceFromZip(offset); 1361 } 1362 1363 /** 1364 * Computes the central directory. The central directory must not have been computed yet. When 1365 * this method finishes, the central directory has been computed {@link #directoryEntry}, 1366 * unless the directory is empty in which case {@link #directoryEntry} 1367 * is left as {@code null}. Nothing is written to disk as a result of this method's invocation. 1368 * 1369 * @throws IOException failed to append the central directory 1370 */ computeCentralDirectory()1371 private void computeCentralDirectory() throws IOException { 1372 Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); 1373 Preconditions.checkNotNull(raf, "raf == null"); 1374 Preconditions.checkState(directoryEntry == null, "directoryEntry == null"); 1375 1376 Set<StoredEntry> newStored = Sets.newHashSet(); 1377 for (FileUseMapEntry<StoredEntry> mapEntry : entries.values()) { 1378 newStored.add(mapEntry.getStore()); 1379 } 1380 1381 /* 1382 * Make sure we truncate the map before computing the central directory's location since 1383 * the central directory is the last part of the file. 1384 */ 1385 map.truncate(); 1386 1387 CentralDirectory newDirectory = CentralDirectory.makeFromEntries(newStored, this); 1388 byte[] newDirectoryBytes = newDirectory.toBytes(); 1389 long directoryOffset = map.size() + extraDirectoryOffset; 1390 1391 map.extend(directoryOffset + newDirectoryBytes.length); 1392 1393 if (newDirectoryBytes.length > 0) { 1394 directoryEntry = map.add(directoryOffset, directoryOffset + newDirectoryBytes.length, 1395 newDirectory); 1396 } 1397 } 1398 1399 /** 1400 * Writes the central directory to the end of the zip file. {@link #directoryEntry} may be 1401 * {@code null} only if there are no files in the archive. 1402 * 1403 * @throws IOException failed to append the central directory 1404 */ appendCentralDirectory()1405 private void appendCentralDirectory() throws IOException { 1406 Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); 1407 Preconditions.checkNotNull(raf, "raf == null"); 1408 1409 if (entries.isEmpty()) { 1410 Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); 1411 return; 1412 } 1413 1414 Preconditions.checkNotNull(directoryEntry, "directoryEntry != null"); 1415 1416 CentralDirectory newDirectory = directoryEntry.getStore(); 1417 Preconditions.checkNotNull(newDirectory, "newDirectory != null"); 1418 1419 byte[] newDirectoryBytes = newDirectory.toBytes(); 1420 long directoryOffset = directoryEntry.getStart(); 1421 1422 /* 1423 * It is fine to seek beyond the end of file. Seeking beyond the end of file will not extend 1424 * the file. Even if we do not have any directory data to write, the extend() call below 1425 * will force the file to be extended leaving exactly extraDirectoryOffset bytes empty at 1426 * the beginning. 1427 */ 1428 directWrite(directoryOffset, newDirectoryBytes); 1429 } 1430 1431 /** 1432 * Obtains the byte array representation of the central directory. The central directory must 1433 * have been already computed. If there are no entries in the zip, the central directory will be 1434 * empty. 1435 * 1436 * @return the byte representation, or an empty array if there are no entries in the zip 1437 * @throws IOException failed to compute the central directory byte representation 1438 */ 1439 @Nonnull getCentralDirectoryBytes()1440 public byte[] getCentralDirectoryBytes() throws IOException { 1441 if (entries.isEmpty()) { 1442 Preconditions.checkState(directoryEntry == null, "directoryEntry != null"); 1443 return new byte[0]; 1444 } 1445 1446 Preconditions.checkNotNull(directoryEntry, "directoryEntry == null"); 1447 1448 CentralDirectory cd = directoryEntry.getStore(); 1449 Preconditions.checkNotNull(cd, "cd == null"); 1450 return cd.toBytes(); 1451 } 1452 1453 /** 1454 * Computes the EOCD. This creates a new {@link #eocdEntry}. The 1455 * central directory must already be written. If {@link #directoryEntry} is {@code null}, then 1456 * the zip file must not have any entries. 1457 * 1458 * @throws IOException failed to write the EOCD 1459 */ computeEocd()1460 private void computeEocd() throws IOException { 1461 Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); 1462 Preconditions.checkNotNull(raf, "raf == null"); 1463 if (directoryEntry == null) { 1464 Preconditions.checkState(entries.isEmpty(), 1465 "directoryEntry == null && !entries.isEmpty()"); 1466 } 1467 1468 long dirStart; 1469 long dirSize = 0; 1470 1471 if (directoryEntry != null) { 1472 CentralDirectory directory = directoryEntry.getStore(); 1473 assert directory != null; 1474 1475 dirStart = directoryEntry.getStart(); 1476 dirSize = directoryEntry.getSize(); 1477 Verify.verify(directory.getEntries().size() == entries.size()); 1478 } else { 1479 /* 1480 * If we do not have a directory, then we must leave any requested offset empty. 1481 */ 1482 dirStart = extraDirectoryOffset; 1483 } 1484 1485 Verify.verify(eocdComment != null); 1486 Eocd eocd = new Eocd(entries.size(), dirStart, dirSize, eocdComment); 1487 eocdComment = null; 1488 1489 byte[] eocdBytes = eocd.toBytes(); 1490 long eocdOffset = map.size(); 1491 1492 map.extend(eocdOffset + eocdBytes.length); 1493 1494 eocdEntry = map.add(eocdOffset, eocdOffset + eocdBytes.length, eocd); 1495 } 1496 1497 /** 1498 * Writes the EOCD to the end of the zip file. This creates a new {@link #eocdEntry}. The 1499 * central directory must already be written. If {@link #directoryEntry} is {@code null}, then 1500 * the zip file must not have any entries. 1501 * 1502 * @throws IOException failed to write the EOCD 1503 */ appendEocd()1504 private void appendEocd() throws IOException { 1505 Preconditions.checkState(state == ZipFileState.OPEN_RW, "state != ZipFileState.OPEN_RW"); 1506 Preconditions.checkNotNull(raf, "raf == null"); 1507 Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); 1508 1509 Eocd eocd = eocdEntry.getStore(); 1510 Preconditions.checkNotNull(eocd, "eocd == null"); 1511 1512 byte[] eocdBytes = eocd.toBytes(); 1513 long eocdOffset = eocdEntry.getStart(); 1514 1515 directWrite(eocdOffset, eocdBytes); 1516 } 1517 1518 /** 1519 * Obtains the byte array representation of the EOCD. The EOCD must have already been computed 1520 * for this method to be invoked. 1521 * 1522 * @return the byte representation of the EOCD 1523 * @throws IOException failed to obtain the byte representation of the EOCD 1524 */ 1525 @Nonnull getEocdBytes()1526 public byte[] getEocdBytes() throws IOException { 1527 Preconditions.checkNotNull(eocdEntry, "eocdEntry == null"); 1528 1529 Eocd eocd = eocdEntry.getStore(); 1530 Preconditions.checkNotNull(eocd, "eocd == null"); 1531 return eocd.toBytes(); 1532 } 1533 1534 /** 1535 * Closes the file, if it is open. 1536 * 1537 * @throws IOException failed to close the file 1538 */ innerClose()1539 private void innerClose() throws IOException { 1540 if (state == ZipFileState.CLOSED) { 1541 return; 1542 } 1543 1544 Verify.verifyNotNull(raf, "raf == null"); 1545 1546 raf.close(); 1547 raf = null; 1548 state = ZipFileState.CLOSED; 1549 if (closedControl == null) { 1550 closedControl = new CachedFileContents<>(file); 1551 } 1552 1553 closedControl.closed(null); 1554 } 1555 1556 /** 1557 * If the zip file is closed, opens it in read-only mode. If it is already open, does nothing. 1558 * In general, it is not necessary to directly invoke this method. However, if directly 1559 * reading the zip file using, for example {@link #directRead(long, byte[])}, then this 1560 * method needs to be called. 1561 * @throws IOException failed to open the file 1562 */ openReadOnly()1563 public void openReadOnly() throws IOException { 1564 if (state != ZipFileState.CLOSED) { 1565 return; 1566 } 1567 1568 state = ZipFileState.OPEN_RO; 1569 raf = new RandomAccessFile(file, "r"); 1570 } 1571 1572 /** 1573 * Opens (or reopens) the zip file as read-write. This method will ensure that 1574 * {@link #raf} is not null and open for writing. 1575 * 1576 * @throws IOException failed to open the file, failed to close it or the file was closed and 1577 * has been modified outside the control of this object 1578 */ reopenRw()1579 private void reopenRw() throws IOException { 1580 // We an never open a file RW in read-only mode. We should never get this far, though. 1581 Verify.verify(!readOnly); 1582 1583 if (state == ZipFileState.OPEN_RW) { 1584 return; 1585 } 1586 1587 boolean wasClosed; 1588 if (state == ZipFileState.OPEN_RO) { 1589 /* 1590 * ReadAccessFile does not have a way to reopen as RW so we have to close it and 1591 * open it again. 1592 */ 1593 innerClose(); 1594 wasClosed = false; 1595 } else { 1596 wasClosed = true; 1597 } 1598 1599 Verify.verify(state == ZipFileState.CLOSED, "state != ZpiFileState.CLOSED"); 1600 Verify.verify(raf == null, "raf != null"); 1601 1602 if (closedControl != null && !closedControl.isValid()) { 1603 throw new IOException("File '" + file.getAbsolutePath() + "' has been modified " 1604 + "by an external application."); 1605 } 1606 1607 raf = new RandomAccessFile(file, "rw"); 1608 state = ZipFileState.OPEN_RW; 1609 1610 /* 1611 * Now that we've open the zip and are ready to write, clear out any data descriptors 1612 * in the zip since we don't need them and they take space in the archive. 1613 */ 1614 for (StoredEntry entry : entries()) { 1615 dirty |= entry.removeDataDescriptor(); 1616 } 1617 1618 if (wasClosed) { 1619 notify(ZFileExtension::open); 1620 } 1621 } 1622 1623 /** 1624 * Equivalent to call {@link #add(String, InputStream, boolean)} using 1625 * {@code true} as {@code mayCompress}. 1626 * 1627 * @param name the file name (<em>i.e.</em>, path); paths should be defined using slashes 1628 * and the name should not end in slash 1629 * @param stream the source for the file's data 1630 * @throws IOException failed to read the source data 1631 * @throws IllegalStateException if the file is in read-only mode 1632 */ add(@onnull String name, @Nonnull InputStream stream)1633 public void add(@Nonnull String name, @Nonnull InputStream stream) throws IOException { 1634 checkNotInReadOnlyMode(); 1635 add(name, stream, true); 1636 } 1637 1638 /** 1639 * Creates a stored entry. This does not add the entry to the zip file, it just creates the 1640 * {@link StoredEntry} object. 1641 * 1642 * @param name the name of the entry 1643 * @param stream the input stream with the entry's data 1644 * @param mayCompress can the entry be compressed? 1645 * @return the created entry 1646 * @throws IOException failed to create the entry 1647 */ 1648 @Nonnull makeStoredEntry( @onnull String name, @Nonnull InputStream stream, boolean mayCompress)1649 private StoredEntry makeStoredEntry( 1650 @Nonnull String name, 1651 @Nonnull InputStream stream, 1652 boolean mayCompress) 1653 throws IOException { 1654 CloseableByteSource source = tracker.fromStream(stream); 1655 long crc32 = source.hash(Hashing.crc32()).padToLong(); 1656 1657 boolean encodeWithUtf8 = !EncodeUtils.canAsciiEncode(name); 1658 1659 SettableFuture<CentralDirectoryHeaderCompressInfo> compressInfo = 1660 SettableFuture.create(); 1661 GPFlags flags = GPFlags.make(encodeWithUtf8); 1662 CentralDirectoryHeader newFileData = 1663 new CentralDirectoryHeader( 1664 name, 1665 EncodeUtils.encode(name, flags), 1666 source.size(), 1667 compressInfo, 1668 flags, 1669 this); 1670 newFileData.setCrc32(crc32); 1671 1672 /* 1673 * Create the new entry and sets its data source. Offset should be set to -1 automatically 1674 * because this is a new file. With offset set to -1, StoredEntry does not try to verify the 1675 * local header. Since this is a new file, there is no local header and not checking it is 1676 * what we want to happen. 1677 */ 1678 Verify.verify(newFileData.getOffset() == -1); 1679 return new StoredEntry( 1680 newFileData, 1681 this, 1682 createSources(mayCompress, source, compressInfo, newFileData)); 1683 } 1684 1685 /** 1686 * Creates the processed and raw sources for an entry. 1687 * 1688 * @param mayCompress can the entry be compressed? 1689 * @param source the entry's data (uncompressed) 1690 * @param compressInfo the compression info future that will be set when the raw entry is 1691 * created and the {@link CentralDirectoryHeaderCompressInfo} object can be created 1692 * @param newFileData the central directory header for the new file 1693 * @return the sources whose data may or may not be already defined 1694 * @throws IOException failed to create the raw sources 1695 */ 1696 @Nonnull createSources( boolean mayCompress, @Nonnull CloseableByteSource source, @Nonnull SettableFuture<CentralDirectoryHeaderCompressInfo> compressInfo, @Nonnull CentralDirectoryHeader newFileData)1697 private ProcessedAndRawByteSources createSources( 1698 boolean mayCompress, 1699 @Nonnull CloseableByteSource source, 1700 @Nonnull SettableFuture<CentralDirectoryHeaderCompressInfo> compressInfo, 1701 @Nonnull CentralDirectoryHeader newFileData) 1702 throws IOException { 1703 if (mayCompress) { 1704 ListenableFuture<CompressionResult> result = compressor.compress(source); 1705 Futures.addCallback( 1706 result, 1707 new FutureCallback<CompressionResult>() { 1708 @Override 1709 public void onSuccess(CompressionResult result) { 1710 compressInfo.set( 1711 new CentralDirectoryHeaderCompressInfo( 1712 newFileData, 1713 result.getCompressionMethod(), 1714 result.getSize())); 1715 } 1716 1717 @Override 1718 public void onFailure(@Nonnull Throwable t) { 1719 compressInfo.setException(t); 1720 } 1721 }, 1722 MoreExecutors.directExecutor()); 1723 1724 ListenableFuture<CloseableByteSource> compressedByteSourceFuture = 1725 Futures.transform( 1726 result, CompressionResult::getSource, MoreExecutors.directExecutor()); 1727 LazyDelegateByteSource compressedByteSource = new LazyDelegateByteSource( 1728 compressedByteSourceFuture); 1729 return new ProcessedAndRawByteSources(source, compressedByteSource); 1730 } else { 1731 compressInfo.set(new CentralDirectoryHeaderCompressInfo(newFileData, 1732 CompressionMethod.STORE, source.size())); 1733 return new ProcessedAndRawByteSources(source, source); 1734 } 1735 } 1736 1737 /** 1738 * Adds a file to the archive. 1739 * 1740 * <p>Adding the file will not update the archive immediately. Updating will only happen 1741 * when the {@link #update()} method is invoked. 1742 * 1743 * <p>Adding a file with the same name as an existing file will replace that file in the 1744 * archive. 1745 * 1746 * @param name the file name (<em>i.e.</em>, path); paths should be defined using slashes 1747 * and the name should not end in slash 1748 * @param stream the source for the file's data 1749 * @param mayCompress can the file be compressed? This flag will be ignored if the alignment 1750 * rules force the file to be aligned, in which case the file will not be compressed. 1751 * @throws IOException failed to read the source data 1752 * @throws IllegalStateException if the file is in read-only mode 1753 */ add(@onnull String name, @Nonnull InputStream stream, boolean mayCompress)1754 public void add(@Nonnull String name, @Nonnull InputStream stream, boolean mayCompress) 1755 throws IOException { 1756 checkNotInReadOnlyMode(); 1757 1758 /* 1759 * Clean pending background work, if needed. 1760 */ 1761 processAllReadyEntries(); 1762 1763 add(makeStoredEntry(name, stream, mayCompress)); 1764 } 1765 1766 /** 1767 * Adds a {@link StoredEntry} to the zip. The entry is not immediately added to 1768 * {@link #entries} because data may not yet be available. Instead, it is placed under 1769 * {@link #uncompressedEntries} and later moved to {@link #processAllReadyEntries()} when 1770 * done. 1771 * 1772 * <p>This method invokes {@link #processAllReadyEntries()} to move the entry if it has already 1773 * been computed so, if there is no delay in compression, and no more files are in waiting 1774 * queue, then the entry is added to {@link #entries} immediately. 1775 * 1776 * @param newEntry the entry to add 1777 * @throws IOException failed to process this entry (or a previous one whose future only 1778 * completed now) 1779 */ add(@onnull final StoredEntry newEntry)1780 private void add(@Nonnull final StoredEntry newEntry) throws IOException { 1781 uncompressedEntries.add(newEntry); 1782 processAllReadyEntries(); 1783 } 1784 1785 /** 1786 * Moves all ready entries from {@link #uncompressedEntries} to {@link #entries}. It will 1787 * stop as soon as entry whose future has not been completed is found. 1788 * 1789 * @throws IOException the exception reported in the future computation, if any, or failed 1790 * to add a file to the archive 1791 */ processAllReadyEntries()1792 private void processAllReadyEntries() throws IOException { 1793 /* 1794 * Many things can happen during addToEntries(). Because addToEntries() fires 1795 * notifications to extensions, other files can be added, removed, etc. Ee are *not* 1796 * guaranteed that new stuff does not get into uncompressedEntries: add() will still work 1797 * and will add new entries in there. 1798 * 1799 * However -- important -- processReadyEntries() may be invoked during addToEntries() 1800 * because of the extension mechanism. This means that stuff *can* be removed from 1801 * uncompressedEntries and moved to entries during addToEntries(). 1802 */ 1803 while (!uncompressedEntries.isEmpty()) { 1804 StoredEntry next = uncompressedEntries.get(0); 1805 CentralDirectoryHeader cdh = next.getCentralDirectoryHeader(); 1806 Future<CentralDirectoryHeaderCompressInfo> compressionInfo = cdh.getCompressionInfo(); 1807 if (!compressionInfo.isDone()) { 1808 /* 1809 * First entry in queue is not yet complete. We can't do anything else. 1810 */ 1811 return; 1812 } 1813 1814 uncompressedEntries.remove(0); 1815 1816 try { 1817 compressionInfo.get(); 1818 } catch (InterruptedException e) { 1819 throw new IOException("Impossible I/O exception: get for already computed " 1820 + "future throws InterruptedException", e); 1821 } catch (ExecutionException e) { 1822 throw new IOException("Failed to obtain compression information for entry", e); 1823 } 1824 1825 addToEntries(next); 1826 } 1827 } 1828 1829 /** 1830 * Waits until {@link #uncompressedEntries} is empty. 1831 * 1832 * @throws IOException the exception reported in the future computation, if any, or failed 1833 * to add a file to the archive 1834 */ processAllReadyEntriesWithWait()1835 private void processAllReadyEntriesWithWait() throws IOException { 1836 processAllReadyEntries(); 1837 while (!uncompressedEntries.isEmpty()) { 1838 /* 1839 * Wait for the first future to complete and then try again. Keep looping until we're 1840 * done. 1841 */ 1842 StoredEntry first = uncompressedEntries.get(0); 1843 CentralDirectoryHeader cdh = first.getCentralDirectoryHeader(); 1844 cdh.getCompressionInfoWithWait(); 1845 1846 processAllReadyEntries(); 1847 } 1848 } 1849 1850 /** 1851 * Adds a new file to {@link #entries}. This is actually added to the zip and its space 1852 * allocated in the {@link #map}. 1853 * 1854 * @param newEntry the new entry to add 1855 * @throws IOException failed to add the file 1856 */ addToEntries(@onnull final StoredEntry newEntry)1857 private void addToEntries(@Nonnull final StoredEntry newEntry) throws IOException { 1858 Preconditions.checkArgument(newEntry.getDataDescriptorType() == 1859 DataDescriptorType.NO_DATA_DESCRIPTOR, "newEntry has data descriptor"); 1860 1861 /* 1862 * If there is a file with the same name in the archive, remove it. We remove it by 1863 * calling delete() on the entry (this is the public API to remove a file from the archive). 1864 * StoredEntry.delete() will call {@link ZFile#delete(StoredEntry, boolean)} to perform 1865 * data structure cleanup. 1866 */ 1867 FileUseMapEntry<StoredEntry> toReplace = entries.get( 1868 newEntry.getCentralDirectoryHeader().getName()); 1869 final StoredEntry replaceStore; 1870 if (toReplace != null) { 1871 replaceStore = toReplace.getStore(); 1872 assert replaceStore != null; 1873 replaceStore.delete(false); 1874 } else { 1875 replaceStore = null; 1876 } 1877 1878 FileUseMapEntry<StoredEntry> fileUseMapEntry = 1879 positionInFile(newEntry, PositionHint.ANYWHERE); 1880 entries.put(newEntry.getCentralDirectoryHeader().getName(), fileUseMapEntry); 1881 1882 dirty = true; 1883 1884 notify(ext -> ext.added(newEntry, replaceStore)); 1885 } 1886 1887 /** 1888 * Finds a location in the zip where this entry will be added to and create the map entry. 1889 * This method cannot be called if there is already a map entry for the given entry (if you 1890 * do that, then you're doing something wrong somewhere). 1891 * 1892 * <p>This may delete the central directory and EOCD (if it deletes one, it deletes the other) 1893 * if there is no space before the central directory. Otherwise, the file would be added 1894 * after the central directory. This would force a new central directory to be written 1895 * when updating the file and would create a hole in the zip. Me no like holes. Holes are evil. 1896 * 1897 * @param entry the entry to place in the zip 1898 * @param positionHint hint to where the file should be positioned 1899 * @return the position in the file where the entry should be placed 1900 */ 1901 @Nonnull positionInFile( @onnull StoredEntry entry, @Nonnull PositionHint positionHint)1902 private FileUseMapEntry<StoredEntry> positionInFile( 1903 @Nonnull StoredEntry entry, 1904 @Nonnull PositionHint positionHint) 1905 throws IOException { 1906 deleteDirectoryAndEocd(); 1907 long size = entry.getInFileSize(); 1908 int localHeaderSize = entry.getLocalHeaderSize(); 1909 int alignment = chooseAlignment(entry); 1910 1911 FileUseMap.PositionAlgorithm algorithm; 1912 1913 switch (positionHint) { 1914 case LOWEST_OFFSET: 1915 algorithm = FileUseMap.PositionAlgorithm.FIRST_FIT; 1916 break; 1917 case ANYWHERE: 1918 algorithm = FileUseMap.PositionAlgorithm.BEST_FIT; 1919 break; 1920 default: 1921 throw new AssertionError(); 1922 } 1923 1924 long newOffset = map.locateFree(size, localHeaderSize, alignment, algorithm); 1925 long newEnd = newOffset + entry.getInFileSize(); 1926 if (newEnd > map.size()) { 1927 map.extend(newEnd); 1928 } 1929 1930 return map.add(newOffset, newEnd, entry); 1931 } 1932 1933 /** 1934 * Determines what is the alignment value of an entry. 1935 * 1936 * @param entry the entry 1937 * @return the alignment value, {@link AlignmentRule#NO_ALIGNMENT} if there is no alignment 1938 * required for the entry 1939 * @throws IOException failed to determine the alignment 1940 */ chooseAlignment(@onnull StoredEntry entry)1941 private int chooseAlignment(@Nonnull StoredEntry entry) throws IOException { 1942 CentralDirectoryHeader cdh = entry.getCentralDirectoryHeader(); 1943 CentralDirectoryHeaderCompressInfo compressionInfo = cdh.getCompressionInfoWithWait(); 1944 1945 boolean isCompressed = compressionInfo.getMethod() != CompressionMethod.STORE; 1946 if (isCompressed) { 1947 return AlignmentRule.NO_ALIGNMENT; 1948 } else { 1949 return alignmentRule.alignment(cdh.getName()); 1950 } 1951 } 1952 1953 /** 1954 * Adds all files from another zip file, maintaining their compression. Files specified in 1955 * <em>src</em> that are already on this file will replace the ones in this file. However, if 1956 * their sizes and checksums are equal, they will be ignored. 1957 * 1958 * <p> This method will not perform any changes in itself, it will only update in-memory data 1959 * structures. To actually write the zip file, invoke either {@link #update()} or 1960 * {@link #close()}. 1961 * 1962 * @param src the source archive 1963 * @param ignoreFilter predicate that, if {@code true}, identifies files in <em>src</em> that 1964 * should be ignored by merging; merging will behave as if these files were not there 1965 * @throws IOException failed to read from <em>src</em> or write on the output 1966 * @throws IllegalStateException if the file is in read-only mode 1967 */ mergeFrom(@onnull ZFile src, @Nonnull Predicate<String> ignoreFilter)1968 public void mergeFrom(@Nonnull ZFile src, @Nonnull Predicate<String> ignoreFilter) 1969 throws IOException { 1970 checkNotInReadOnlyMode(); 1971 1972 for (StoredEntry fromEntry : src.entries()) { 1973 if (ignoreFilter.test(fromEntry.getCentralDirectoryHeader().getName())) { 1974 continue; 1975 } 1976 1977 boolean replaceCurrent = true; 1978 String path = fromEntry.getCentralDirectoryHeader().getName(); 1979 FileUseMapEntry<StoredEntry> currentEntry = entries.get(path); 1980 1981 if (currentEntry != null) { 1982 long fromSize = fromEntry.getCentralDirectoryHeader().getUncompressedSize(); 1983 long fromCrc = fromEntry.getCentralDirectoryHeader().getCrc32(); 1984 1985 StoredEntry currentStore = currentEntry.getStore(); 1986 assert currentStore != null; 1987 1988 long currentSize = currentStore.getCentralDirectoryHeader().getUncompressedSize(); 1989 long currentCrc = currentStore.getCentralDirectoryHeader().getCrc32(); 1990 1991 if (fromSize == currentSize && fromCrc == currentCrc) { 1992 replaceCurrent = false; 1993 } 1994 } 1995 1996 if (replaceCurrent) { 1997 CentralDirectoryHeader fromCdr = fromEntry.getCentralDirectoryHeader(); 1998 CentralDirectoryHeaderCompressInfo fromCompressInfo = 1999 fromCdr.getCompressionInfoWithWait(); 2000 CentralDirectoryHeader newFileData; 2001 try { 2002 /* 2003 * We make two changes in the central directory from the file to merge: 2004 * we reset the offset to force the entry to be written and we reset the 2005 * deferred CRC bit as we don't need the extra stuff after the file. It takes 2006 * space and is totally useless. 2007 */ 2008 newFileData = fromCdr.clone(); 2009 newFileData.setOffset(-1); 2010 newFileData.resetDeferredCrc(); 2011 } catch (CloneNotSupportedException e) { 2012 throw new IOException("Failed to clone CDR.", e); 2013 } 2014 2015 /* 2016 * Read the data (read directly the compressed source if there is one). 2017 */ 2018 ProcessedAndRawByteSources fromSource = fromEntry.getSource(); 2019 InputStream fromInput = fromSource.getRawByteSource().openStream(); 2020 long sourceSize = fromSource.getRawByteSource().size(); 2021 if (sourceSize > Integer.MAX_VALUE) { 2022 throw new IOException("Cannot read source with " + sourceSize + " bytes."); 2023 } 2024 2025 byte[] data = new byte[Ints.checkedCast(sourceSize)]; 2026 int read = 0; 2027 while (read < data.length) { 2028 int r = fromInput.read(data, read, data.length - read); 2029 Verify.verify(r >= 0, "There should be at least 'size' bytes in the stream."); 2030 read += r; 2031 } 2032 2033 /* 2034 * Build the new source and wrap it around an inflater source if data came from 2035 * a compressed source. 2036 */ 2037 CloseableByteSource rawContents = tracker.fromSource(fromSource.getRawByteSource()); 2038 CloseableByteSource processedContents; 2039 if (fromCompressInfo.getMethod() == CompressionMethod.DEFLATE) { 2040 //noinspection IOResourceOpenedButNotSafelyClosed 2041 processedContents = new InflaterByteSource(rawContents); 2042 } else { 2043 processedContents = rawContents; 2044 } 2045 2046 ProcessedAndRawByteSources newSource = new ProcessedAndRawByteSources( 2047 processedContents, rawContents); 2048 2049 /* 2050 * Add will replace any current entry with the same name. 2051 */ 2052 StoredEntry newEntry = new StoredEntry(newFileData, this, newSource); 2053 add(newEntry); 2054 } 2055 } 2056 } 2057 2058 /** 2059 * Forcibly marks this zip file as touched, forcing it to be updated when {@link #update()} 2060 * or {@link #close()} are invoked. 2061 * 2062 * @throws IllegalStateException if the file is in read-only mode 2063 */ touch()2064 public void touch() { 2065 checkNotInReadOnlyMode(); 2066 dirty = true; 2067 } 2068 2069 /** 2070 * Wait for any background tasks to finish and report any errors. In general this method does 2071 * not need to be invoked directly as errors from background tasks are reported during 2072 * {@link #add(String, InputStream, boolean)}, {@link #update()} and {@link #close()}. 2073 * However, if required for some purposes, <em>e.g.</em>, ensuring all notifications have been 2074 * done to extensions, then this method may be called. It will wait for all background tasks 2075 * to complete. 2076 * @throws IOException some background work failed 2077 */ finishAllBackgroundTasks()2078 public void finishAllBackgroundTasks() throws IOException { 2079 processAllReadyEntriesWithWait(); 2080 } 2081 2082 /** 2083 * Realigns all entries in the zip. This is equivalent to call {@link StoredEntry#realign()} 2084 * for all entries in the zip file. 2085 * 2086 * @return has any entry been changed? Note that for entries that have not yet been written on 2087 * the file, realignment does not count as a change as nothing needs to be updated in the file; 2088 * entries that have been updated may have been recreated and the existing references outside 2089 * of {@code ZFile} may refer to {@link StoredEntry}s that are no longer valid 2090 * @throws IOException failed to realign the zip; some entries in the zip may have been lost 2091 * due to the I/O error 2092 * @throws IllegalStateException if the file is in read-only mode 2093 */ realign()2094 public boolean realign() throws IOException { 2095 checkNotInReadOnlyMode(); 2096 2097 boolean anyChanges = false; 2098 for (StoredEntry entry : entries()) { 2099 anyChanges |= entry.realign(); 2100 } 2101 2102 if (anyChanges) { 2103 dirty = true; 2104 } 2105 2106 return anyChanges; 2107 } 2108 2109 /** 2110 * Realigns a stored entry, if necessary. Realignment is done by removing and re-adding the file 2111 * if it was not aligned. 2112 * 2113 * @param entry the entry to realign 2114 * @return has the entry been changed? Note that if the entry has not yet been written on the 2115 * file, realignment does not count as a change as nothing needs to be updated in the file 2116 * @throws IOException failed to read/write an entry; the entry may no longer exist in the 2117 * file 2118 */ realign(@onnull StoredEntry entry)2119 boolean realign(@Nonnull StoredEntry entry) throws IOException { 2120 FileUseMapEntry<StoredEntry> mapEntry = 2121 entries.get(entry.getCentralDirectoryHeader().getName()); 2122 Verify.verify(entry == mapEntry.getStore()); 2123 long currentDataOffset = mapEntry.getStart() + entry.getLocalHeaderSize(); 2124 2125 int expectedAlignment = chooseAlignment(entry); 2126 long misalignment = currentDataOffset % expectedAlignment; 2127 if (misalignment == 0) { 2128 /* 2129 * Good. File is aligned properly. 2130 */ 2131 return false; 2132 } 2133 2134 if (entry.getCentralDirectoryHeader().getOffset() == -1) { 2135 /* 2136 * File is not aligned but it is not written. We do not really need to do much other 2137 * than find another place in the map. 2138 */ 2139 map.remove(mapEntry); 2140 long newStart = 2141 map.locateFree( 2142 mapEntry.getSize(), 2143 entry.getLocalHeaderSize(), 2144 expectedAlignment, 2145 FileUseMap.PositionAlgorithm.BEST_FIT); 2146 mapEntry = map.add(newStart, newStart + entry.getInFileSize(), entry); 2147 entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry); 2148 2149 /* 2150 * Just for safety. We're modifying the in-memory structures but the file should 2151 * already be marked as dirty. 2152 */ 2153 Verify.verify(dirty); 2154 2155 return false; 2156 2157 } 2158 2159 /* 2160 * Get the entry data source, but check if we have a compressed one (we don't want to 2161 * inflate and deflate). 2162 */ 2163 CentralDirectoryHeaderCompressInfo compressInfo = 2164 entry.getCentralDirectoryHeader().getCompressionInfoWithWait(); 2165 2166 ProcessedAndRawByteSources source = entry.getSource(); 2167 2168 CentralDirectoryHeader clonedCdh; 2169 try { 2170 clonedCdh = entry.getCentralDirectoryHeader().clone(); 2171 } catch (CloneNotSupportedException e) { 2172 Verify.verify(false); 2173 return false; 2174 } 2175 2176 /* 2177 * We make two changes in the central directory when realigning: 2178 * we reset the offset to force the entry to be written and we reset the 2179 * deferred CRC bit as we don't need the extra stuff after the file. It takes 2180 * space and is totally useless and we may need the extra space to realign the entry... 2181 */ 2182 clonedCdh.setOffset(-1); 2183 clonedCdh.resetDeferredCrc(); 2184 2185 CloseableByteSource rawContents = tracker.fromSource(source.getRawByteSource()); 2186 CloseableByteSource processedContents; 2187 2188 if (compressInfo.getMethod() == CompressionMethod.DEFLATE) { 2189 //noinspection IOResourceOpenedButNotSafelyClosed 2190 processedContents = new InflaterByteSource(rawContents); 2191 } else { 2192 processedContents = rawContents; 2193 } 2194 2195 ProcessedAndRawByteSources newSource = new ProcessedAndRawByteSources(processedContents, 2196 rawContents); 2197 2198 /* 2199 * Add the new file. This will replace the existing one. 2200 */ 2201 StoredEntry newEntry = new StoredEntry(clonedCdh, this, newSource); 2202 add(newEntry); 2203 return true; 2204 } 2205 2206 /** 2207 * Adds an extension to this zip file. 2208 * 2209 * @param extension the listener to add 2210 * @throws IllegalStateException if the file is in read-only mode 2211 */ addZFileExtension(@onnull ZFileExtension extension)2212 public void addZFileExtension(@Nonnull ZFileExtension extension) { 2213 checkNotInReadOnlyMode(); 2214 extensions.add(extension); 2215 } 2216 2217 /** 2218 * Removes an extension from this zip file. 2219 * 2220 * @param extension the listener to remove 2221 * @throws IllegalStateException if the file is in read-only mode 2222 */ removeZFileExtension(@onnull ZFileExtension extension)2223 public void removeZFileExtension(@Nonnull ZFileExtension extension) { 2224 checkNotInReadOnlyMode(); 2225 extensions.remove(extension); 2226 } 2227 2228 /** 2229 * Notifies all extensions, collecting their execution requests and running them. 2230 * 2231 * @param function the function to apply to all listeners, it will generally invoke the 2232 * notification method on the listener and return the result of that invocation 2233 * @throws IOException failed to process some extensions 2234 */ notify(@onnull IOExceptionFunction<ZFileExtension, IOExceptionRunnable> function)2235 private void notify(@Nonnull IOExceptionFunction<ZFileExtension, IOExceptionRunnable> function) 2236 throws IOException { 2237 for (ZFileExtension fl : Lists.newArrayList(extensions)) { 2238 IOExceptionRunnable r = function.apply(fl); 2239 if (r != null) { 2240 toRun.add(r); 2241 } 2242 } 2243 2244 if (!isNotifying) { 2245 isNotifying = true; 2246 2247 try { 2248 while (!toRun.isEmpty()) { 2249 IOExceptionRunnable r = toRun.remove(0); 2250 r.run(); 2251 } 2252 } finally { 2253 isNotifying = false; 2254 } 2255 } 2256 } 2257 2258 /** 2259 * Directly writes data in the zip file. <strong>Incorrect use of this method may corrupt the 2260 * zip file</strong>. Invoking this method may force the zip to be reopened in read/write 2261 * mode. 2262 * 2263 * @param offset the offset at which data should be written 2264 * @param data the data to write, may be an empty array 2265 * @param start start offset in {@code data} where data to write is located 2266 * @param count number of bytes of data to write 2267 * @throws IOException failed to write the data 2268 * @throws IllegalStateException if the file is in read-only mode 2269 */ directWrite(long offset, @Nonnull byte[] data, int start, int count)2270 public void directWrite(long offset, @Nonnull byte[] data, int start, int count) 2271 throws IOException { 2272 checkNotInReadOnlyMode(); 2273 2274 Preconditions.checkArgument(offset >= 0, "offset < 0"); 2275 Preconditions.checkArgument(start >= 0, "start >= 0"); 2276 Preconditions.checkArgument(count >= 0, "count >= 0"); 2277 2278 if (data.length == 0) { 2279 return; 2280 } 2281 2282 Preconditions.checkArgument(start <= data.length, "start > data.length"); 2283 Preconditions.checkArgument(start + count <= data.length, "start + count > data.length"); 2284 2285 reopenRw(); 2286 assert raf != null; 2287 2288 raf.seek(offset); 2289 raf.write(data, start, count); 2290 } 2291 2292 /** 2293 * Same as {@code directWrite(offset, data, 0, data.length)}. 2294 * 2295 * @param offset the offset at which data should be written 2296 * @param data the data to write, may be an empty array 2297 * @throws IOException failed to write the data 2298 * @throws IllegalStateException if the file is in read-only mode 2299 */ directWrite(long offset, @Nonnull byte[] data)2300 public void directWrite(long offset, @Nonnull byte[] data) throws IOException { 2301 checkNotInReadOnlyMode(); 2302 directWrite(offset, data, 0, data.length); 2303 } 2304 2305 /** 2306 * Returns the current size (in bytes) of the underlying file. 2307 * 2308 * @throws IOException if an I/O error occurs 2309 */ directSize()2310 public long directSize() throws IOException { 2311 /* 2312 * Only force a reopen if the file is closed. 2313 */ 2314 if (raf == null) { 2315 reopenRw(); 2316 assert raf != null; 2317 } 2318 return raf.length(); 2319 } 2320 2321 /** 2322 * Directly reads data from the zip file. Invoking this method may force the zip to be reopened 2323 * in read/write mode. 2324 * 2325 * @param offset the offset at which data should be written 2326 * @param data the array where read data should be stored 2327 * @param start start position in the array where to write data to 2328 * @param count how many bytes of data can be written 2329 * @return how many bytes of data have been written or {@code -1} if there are no more bytes 2330 * to be read 2331 * @throws IOException failed to write the data 2332 */ directRead(long offset, @Nonnull byte[] data, int start, int count)2333 public int directRead(long offset, @Nonnull byte[] data, int start, int count) 2334 throws IOException { 2335 Preconditions.checkArgument(start >= 0, "start >= 0"); 2336 Preconditions.checkArgument(count >= 0, "count >= 0"); 2337 Preconditions.checkArgument(start <= data.length, "start > data.length"); 2338 Preconditions.checkArgument(start + count <= data.length, "start + count > data.length"); 2339 return directRead(offset, ByteBuffer.wrap(data, start, count)); 2340 } 2341 2342 /** 2343 * Directly reads data from the zip file. Invoking this method may force the zip to be reopened 2344 * in read/write mode. 2345 * 2346 * @param offset the offset from which data should be read 2347 * @param dest the output buffer to fill with data from the {@code offset}. 2348 * @return how many bytes of data have been written or {@code -1} if there are no more bytes 2349 * to be read 2350 * @throws IOException failed to write the data 2351 */ directRead(long offset, @Nonnull ByteBuffer dest)2352 public int directRead(long offset, @Nonnull ByteBuffer dest) throws IOException { 2353 Preconditions.checkArgument(offset >= 0, "offset < 0"); 2354 2355 if (!dest.hasRemaining()) { 2356 return 0; 2357 } 2358 2359 /* 2360 * Only force a reopen if the file is closed. 2361 */ 2362 if (raf == null) { 2363 reopenRw(); 2364 assert raf != null; 2365 } 2366 2367 raf.seek(offset); 2368 return raf.getChannel().read(dest); 2369 } 2370 2371 /** 2372 * Same as {@code directRead(offset, data, 0, data.length)}. 2373 * 2374 * @param offset the offset at which data should be read 2375 * @param data receives the read data, may be an empty array 2376 * @throws IOException failed to read the data 2377 */ directRead(long offset, @Nonnull byte[] data)2378 public int directRead(long offset, @Nonnull byte[] data) throws IOException { 2379 return directRead(offset, data, 0, data.length); 2380 } 2381 2382 /** 2383 * Reads exactly {@code data.length} bytes of data, failing if it was not possible to read all 2384 * the requested data. 2385 * 2386 * @param offset the offset at which to start reading 2387 * @param data the array that receives the data read 2388 * @throws IOException failed to read some data or there is not enough data to read 2389 */ directFullyRead(long offset, @Nonnull byte[] data)2390 public void directFullyRead(long offset, @Nonnull byte[] data) throws IOException { 2391 directFullyRead(offset, ByteBuffer.wrap(data)); 2392 } 2393 2394 /** 2395 * Reads exactly {@code dest.remaining()} bytes of data, failing if it was not possible to read 2396 * all the requested data. 2397 * 2398 * @param offset the offset at which to start reading 2399 * @param dest the output buffer to fill with data 2400 * @throws IOException failed to read some data or there is not enough data to read 2401 */ directFullyRead(long offset, @Nonnull ByteBuffer dest)2402 public void directFullyRead(long offset, @Nonnull ByteBuffer dest) throws IOException { 2403 Preconditions.checkArgument(offset >= 0, "offset < 0"); 2404 2405 if (!dest.hasRemaining()) { 2406 return; 2407 } 2408 2409 /* 2410 * Only force a reopen if the file is closed. 2411 */ 2412 if (raf == null) { 2413 reopenRw(); 2414 assert raf != null; 2415 } 2416 2417 FileChannel fileChannel = raf.getChannel(); 2418 while (dest.hasRemaining()) { 2419 fileChannel.position(offset); 2420 int chunkSize = fileChannel.read(dest); 2421 if (chunkSize == -1) { 2422 throw new EOFException( 2423 "Failed to read " + dest.remaining() + " more bytes: premature EOF"); 2424 } 2425 offset += chunkSize; 2426 } 2427 } 2428 2429 /** 2430 * Adds all files and directories recursively. 2431 * <p> 2432 * Equivalent to calling {@link #addAllRecursively(File, Function)} using a function that 2433 * always returns {@code true} 2434 * 2435 * @param file a file or directory; if it is a directory, all files and directories will be 2436 * added recursively 2437 * @throws IOException failed to some (or all ) of the files 2438 * @throws IllegalStateException if the file is in read-only mode 2439 */ addAllRecursively(@onnull File file)2440 public void addAllRecursively(@Nonnull File file) throws IOException { 2441 checkNotInReadOnlyMode(); 2442 addAllRecursively(file, f -> true); 2443 } 2444 2445 /** 2446 * Adds all files and directories recursively. 2447 * 2448 * @param file a file or directory; if it is a directory, all files and directories will be 2449 * added recursively 2450 * @param mayCompress a function that decides whether files may be compressed 2451 * @throws IOException failed to some (or all ) of the files 2452 * @throws IllegalStateException if the file is in read-only mode 2453 */ addAllRecursively( @onnull File file, @Nonnull Function<? super File, Boolean> mayCompress)2454 public void addAllRecursively( 2455 @Nonnull File file, 2456 @Nonnull Function<? super File, Boolean> mayCompress) throws IOException { 2457 checkNotInReadOnlyMode(); 2458 2459 /* 2460 * The case of file.isFile() is different because if file.isFile() we will add it to the 2461 * zip in the root. However, if file.isDirectory() we won't add it and add its children. 2462 */ 2463 if (file.isFile()) { 2464 boolean mayCompressFile = Verify.verifyNotNull(mayCompress.apply(file), 2465 "mayCompress.apply() returned null"); 2466 2467 try (Closer closer = Closer.create()) { 2468 FileInputStream fileInput = closer.register(new FileInputStream(file)); 2469 add(file.getName(), fileInput, mayCompressFile); 2470 } 2471 2472 return; 2473 } 2474 2475 for (File f : Files.fileTreeTraverser().preOrderTraversal(file).skip(1)) { 2476 String path = file.toURI().relativize(f.toURI()).getPath(); 2477 2478 InputStream stream; 2479 try (Closer closer = Closer.create()) { 2480 boolean mayCompressFile; 2481 if (f.isDirectory()) { 2482 stream = closer.register(new ByteArrayInputStream(new byte[0])); 2483 mayCompressFile = false; 2484 } else { 2485 stream = closer.register(new FileInputStream(f)); 2486 mayCompressFile = Verify.verifyNotNull(mayCompress.apply(f), 2487 "mayCompress.apply() returned null"); 2488 } 2489 2490 add(path, stream, mayCompressFile); 2491 } 2492 } 2493 } 2494 2495 /** 2496 * Obtains the offset at which the central directory exists, or at which it will be written 2497 * if the zip file were to be flushed immediately. 2498 * 2499 * @return the offset, in bytes, where the central directory is or will be written; this value 2500 * includes any extra offset for the central directory 2501 */ getCentralDirectoryOffset()2502 public long getCentralDirectoryOffset() { 2503 if (directoryEntry != null) { 2504 return directoryEntry.getStart(); 2505 } 2506 2507 /* 2508 * If there are no entries, the central directory is written at the start of the file. 2509 */ 2510 if (entries.isEmpty()) { 2511 return extraDirectoryOffset; 2512 } 2513 2514 /* 2515 * The Central Directory is written after all entries. This will be at the end of the file 2516 * if the 2517 */ 2518 return map.usedSize() + extraDirectoryOffset; 2519 } 2520 2521 /** 2522 * Obtains the size of the central directory, if the central directory is written in the zip 2523 * file. 2524 * 2525 * @return the size of the central directory or {@code -1} if the central directory has not 2526 * been computed 2527 */ getCentralDirectorySize()2528 public long getCentralDirectorySize() { 2529 if (directoryEntry != null) { 2530 return directoryEntry.getSize(); 2531 } 2532 2533 if (entries.isEmpty()) { 2534 return 0; 2535 } 2536 2537 return 1; 2538 } 2539 2540 /** 2541 * Obtains the offset of the EOCD record, if the EOCD has been written to the file. 2542 * 2543 * @return the offset of the EOCD or {@code -1} if none exists yet 2544 */ getEocdOffset()2545 public long getEocdOffset() { 2546 if (eocdEntry == null) { 2547 return -1; 2548 } 2549 2550 return eocdEntry.getStart(); 2551 } 2552 2553 /** 2554 * Obtains the size of the EOCD record, if the EOCD has been written to the file. 2555 * 2556 * @return the size of the EOCD of {@code -1} it none exists yet 2557 */ getEocdSize()2558 public long getEocdSize() { 2559 if (eocdEntry == null) { 2560 return -1; 2561 } 2562 2563 return eocdEntry.getSize(); 2564 } 2565 2566 /** 2567 * Obtains the comment in the EOCD. 2568 * 2569 * @return the comment exactly as it was encoded in the EOCD, no encoding conversion is done 2570 */ 2571 @Nonnull getEocdComment()2572 public byte[] getEocdComment() { 2573 if (eocdEntry == null) { 2574 Verify.verify(eocdComment != null); 2575 byte[] eocdCommentCopy = new byte[eocdComment.length]; 2576 System.arraycopy(eocdComment, 0, eocdCommentCopy, 0, eocdComment.length); 2577 return eocdCommentCopy; 2578 } 2579 2580 Eocd eocd = eocdEntry.getStore(); 2581 Verify.verify(eocd != null); 2582 return eocd.getComment(); 2583 } 2584 2585 /** 2586 * Sets the comment in the EOCD. 2587 * 2588 * @param comment the new comment; no conversion is done, these exact bytes will be placed in 2589 * the EOCD comment 2590 * @throws IllegalStateException if file is in read-only mode 2591 */ setEocdComment(@onnull byte[] comment)2592 public void setEocdComment(@Nonnull byte[] comment) { 2593 checkNotInReadOnlyMode(); 2594 2595 if (comment.length > MAX_EOCD_COMMENT_SIZE) { 2596 throw new IllegalArgumentException( 2597 "EOCD comment size (" 2598 + comment.length 2599 + ") is larger than the maximum allowed (" 2600 + MAX_EOCD_COMMENT_SIZE 2601 + ")"); 2602 } 2603 2604 // Check if the EOCD signature appears anywhere in the comment we need to check if it 2605 // is valid. 2606 for (int i = 0; i < comment.length - MIN_EOCD_SIZE; i++) { 2607 // Remember: little endian... 2608 if (comment[i] == EOCD_SIGNATURE[3] 2609 && comment[i + 1] == EOCD_SIGNATURE[2] 2610 && comment[i + 2] == EOCD_SIGNATURE[1] 2611 && comment[i + 3] == EOCD_SIGNATURE[0]) { 2612 // We found a possible EOCD signature at position i. Try to read it. 2613 ByteBuffer bytes = ByteBuffer.wrap(comment, i, comment.length - i); 2614 try { 2615 new Eocd(bytes); 2616 throw new IllegalArgumentException( 2617 "Position " 2618 + i 2619 + " of the comment contains a valid EOCD record."); 2620 } catch (IOException e) { 2621 // Fine, this is an invalid record. Move along... 2622 } 2623 } 2624 } 2625 2626 deleteDirectoryAndEocd(); 2627 eocdComment = new byte[comment.length]; 2628 System.arraycopy(comment, 0, eocdComment, 0, comment.length); 2629 dirty = true; 2630 } 2631 2632 /** 2633 * Sets an extra offset for the central directory. See class description for details. Changing 2634 * this value will mark the file as dirty and force a rewrite of the central directory when 2635 * updated. 2636 * 2637 * @param offset the offset or {@code 0} to write the central directory at its current location 2638 * @throws IllegalStateException if file is in read-only mode 2639 */ setExtraDirectoryOffset(long offset)2640 public void setExtraDirectoryOffset(long offset) { 2641 checkNotInReadOnlyMode(); 2642 Preconditions.checkArgument(offset >= 0, "offset < 0"); 2643 2644 if (extraDirectoryOffset != offset) { 2645 extraDirectoryOffset = offset; 2646 deleteDirectoryAndEocd(); 2647 dirty = true; 2648 } 2649 } 2650 2651 /** 2652 * Obtains the extra offset for the central directory. See class description for details. 2653 * 2654 * @return the offset or {@code 0} if no offset is set 2655 */ getExtraDirectoryOffset()2656 public long getExtraDirectoryOffset() { 2657 return extraDirectoryOffset; 2658 } 2659 2660 /** 2661 * Obtains whether this {@code ZFile} is ignoring timestamps. 2662 * 2663 * @return are the timestamps being ignored? 2664 */ areTimestampsIgnored()2665 public boolean areTimestampsIgnored() { 2666 return noTimestamps; 2667 } 2668 2669 /** 2670 * Sorts all files in the zip. This will force all files to be loaded and will wait for all 2671 * background tasks to complete. Sorting files is never done implicitly and will operate in 2672 * memory only (maybe reading files from the zip disk into memory, if needed). It will leave 2673 * the zip in dirty state, requiring a call to {@link #update()} to force the entries to be 2674 * written to disk. 2675 * 2676 * @throws IOException failed to load or move a file in the zip 2677 * @throws IllegalStateException if file is in read-only mode 2678 */ sortZipContents()2679 public void sortZipContents() throws IOException { 2680 checkNotInReadOnlyMode(); 2681 reopenRw(); 2682 2683 processAllReadyEntriesWithWait(); 2684 2685 Verify.verify(uncompressedEntries.isEmpty()); 2686 2687 SortedSet<StoredEntry> sortedEntries = Sets.newTreeSet(StoredEntry.COMPARE_BY_NAME); 2688 for (FileUseMapEntry<StoredEntry> fmEntry : entries.values()) { 2689 StoredEntry entry = fmEntry.getStore(); 2690 Preconditions.checkNotNull(entry); 2691 sortedEntries.add(entry); 2692 entry.loadSourceIntoMemory(); 2693 2694 map.remove(fmEntry); 2695 } 2696 2697 entries.clear(); 2698 for (StoredEntry entry : sortedEntries) { 2699 String name = entry.getCentralDirectoryHeader().getName(); 2700 FileUseMapEntry<StoredEntry> positioned = 2701 positionInFile(entry, PositionHint.LOWEST_OFFSET); 2702 2703 entries.put(name, positioned); 2704 } 2705 2706 dirty = true; 2707 } 2708 2709 /** 2710 * Obtains the filesystem path to the zip file. 2711 * 2712 * @return the file that may or may not exist (depending on whether something existed there 2713 * before the zip was created and on whether the zip has been updated or not) 2714 */ 2715 @Nonnull getFile()2716 public File getFile() { 2717 return file; 2718 } 2719 2720 /** 2721 * Creates a new verify log. 2722 * 2723 * @return the new verify log 2724 */ 2725 @Nonnull makeVerifyLog()2726 VerifyLog makeVerifyLog() { 2727 VerifyLog log = verifyLogFactory.get(); 2728 assert log != null; 2729 return log; 2730 } 2731 2732 /** 2733 * Obtains the zip file's verify log. 2734 * 2735 * @return the verify log 2736 */ 2737 @Nonnull getVerifyLog()2738 VerifyLog getVerifyLog() { 2739 return verifyLog; 2740 } 2741 2742 /** 2743 * Are there in-memory changes that have not been written to the zip file? 2744 * 2745 * <p>Waits for all pending processing which may make changes. 2746 */ hasPendingChangesWithWait()2747 public boolean hasPendingChangesWithWait() throws IOException { 2748 processAllReadyEntriesWithWait(); 2749 return dirty; 2750 } 2751 2752 /** Hint to where files should be positioned. */ 2753 enum PositionHint { 2754 /** 2755 * File may be positioned anywhere, caller doesn't care. 2756 */ 2757 ANYWHERE, 2758 2759 /** 2760 * File should be positioned at the lowest offset possible. 2761 */ 2762 LOWEST_OFFSET 2763 } 2764 } 2765