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