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