1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tools.build.apkzlib.zip;
18 
19 import com.android.tools.build.apkzlib.zip.utils.CloseableByteSource;
20 import com.android.tools.build.apkzlib.zip.utils.CloseableDelegateByteSource;
21 import com.google.common.annotations.VisibleForTesting;
22 import com.google.common.base.Preconditions;
23 import com.google.common.base.Verify;
24 import com.google.common.io.ByteSource;
25 import com.google.common.io.ByteStreams;
26 import com.google.common.primitives.Ints;
27 import java.io.BufferedInputStream;
28 import java.io.IOException;
29 import java.io.InputStream;
30 import java.nio.ByteBuffer;
31 import java.util.Comparator;
32 import javax.annotation.Nonnull;
33 import javax.annotation.Nullable;
34 
35 /**
36  * A stored entry represents a file in the zip. The entry may or may not be written to the zip
37  * file.
38  *
39  * <p>Stored entries provide the operations that are related to the files themselves, not to the
40  * zip. It is through the {@code StoredEntry} class that entries can be deleted ({@link #delete()},
41  * open ({@link #open()}) or realigned ({@link #realign()}).
42  *
43  * <p>Entries are not created directly. They are created using
44  * {@link ZFile#add(String, InputStream, boolean)} and obtained from the zip file
45  * using {@link ZFile#get(String)} or {@link ZFile#entries()}.
46  *
47  * <p>Most of the data in the an entry is in the Central Directory Header. This includes the name,
48  * compression method, file compressed and uncompressed sizes, CRC32 checksum, etc. The CDH can
49  * be obtained using the {@link #getCentralDirectoryHeader()} method.
50  */
51 public class StoredEntry {
52 
53     /**
54      * Comparator that compares instances of {@link StoredEntry} by their names.
55      */
56     static final Comparator<StoredEntry> COMPARE_BY_NAME =
57             (o1, o2) -> {
58                 if (o1 == null && o2 == null) {
59                     return 0;
60                 }
61 
62                 if (o1 == null) {
63                     return -1;
64                 }
65 
66                 if (o2 == null) {
67                     return 1;
68                 }
69 
70                 String name1 = o1.getCentralDirectoryHeader().getName();
71                 String name2 = o2.getCentralDirectoryHeader().getName();
72                 return name1.compareTo(name2);
73             };
74 
75     /**
76      * Signature of the data descriptor.
77      */
78     private static final int DATA_DESC_SIGNATURE = 0x08074b50;
79 
80     /**
81      * Local header field: signature.
82      */
83     private static final ZipField.F4 F_LOCAL_SIGNATURE = new ZipField.F4(0, 0x04034b50,
84             "Signature");
85 
86     /**
87      * Local header field: version to extract, should match the CDH's.
88      */
89     @VisibleForTesting
90     static final ZipField.F2 F_VERSION_EXTRACT = new ZipField.F2(
91             F_LOCAL_SIGNATURE.endOffset(), "Version to extract",
92             new ZipFieldInvariantNonNegative());
93 
94     /**
95      * Local header field: GP bit flag, should match the CDH's.
96      */
97     private static final ZipField.F2 F_GP_BIT = new ZipField.F2(F_VERSION_EXTRACT.endOffset(),
98             "GP bit flag");
99 
100     /**
101      * Local header field: compression method, should match the CDH's.
102      */
103     private static final ZipField.F2 F_METHOD = new ZipField.F2(F_GP_BIT.endOffset(),
104             "Compression method", new ZipFieldInvariantNonNegative());
105 
106     /**
107      * Local header field: last modification time, should match the CDH's.
108      */
109     private static final ZipField.F2 F_LAST_MOD_TIME = new ZipField.F2(F_METHOD.endOffset(),
110             "Last modification time");
111 
112     /**
113      * Local header field: last modification time, should match the CDH's.
114      */
115     private static final ZipField.F2 F_LAST_MOD_DATE = new ZipField.F2(F_LAST_MOD_TIME.endOffset(),
116             "Last modification date");
117 
118     /**
119      * Local header field: CRC32 checksum, should match the CDH's. 0 if there is no data.
120      */
121     private static final ZipField.F4 F_CRC32 = new ZipField.F4(F_LAST_MOD_DATE.endOffset(),
122             "CRC32");
123 
124     /**
125      * Local header field: compressed size, size the data takes in the zip file.
126      */
127     private static final ZipField.F4 F_COMPRESSED_SIZE = new ZipField.F4(F_CRC32.endOffset(),
128             "Compressed size", new ZipFieldInvariantNonNegative());
129 
130     /**
131      * Local header field: uncompressed size, size the data takes after extraction.
132      */
133     private static final ZipField.F4 F_UNCOMPRESSED_SIZE = new ZipField.F4(
134             F_COMPRESSED_SIZE.endOffset(), "Uncompressed size", new ZipFieldInvariantNonNegative());
135 
136     /**
137      * Local header field: length of the file name.
138      */
139     private static final ZipField.F2 F_FILE_NAME_LENGTH = new ZipField.F2(
140             F_UNCOMPRESSED_SIZE.endOffset(), "@File name length",
141             new ZipFieldInvariantNonNegative());
142 
143     /**
144      * Local header filed: length of the extra field.
145      */
146     private static final ZipField.F2 F_EXTRA_LENGTH = new ZipField.F2(
147             F_FILE_NAME_LENGTH.endOffset(), "Extra length", new ZipFieldInvariantNonNegative());
148 
149     /**
150      * Local header size (fixed part, not counting file name or extra field).
151      */
152     static final int FIXED_LOCAL_FILE_HEADER_SIZE = F_EXTRA_LENGTH.endOffset();
153 
154     /**
155      * Type of entry.
156      */
157     @Nonnull
158     private StoredEntryType type;
159 
160     /**
161      * The central directory header with information about the file.
162      */
163     @Nonnull
164     private CentralDirectoryHeader cdh;
165 
166     /**
167      * The file this entry is associated with
168      */
169     @Nonnull
170     private ZFile file;
171 
172     /**
173      * Has this entry been deleted?
174      */
175     private boolean deleted;
176 
177     /**
178      * Extra field specified in the local directory.
179      */
180     @Nonnull
181     private ExtraField localExtra;
182 
183     /**
184      * Type of data descriptor associated with the entry.
185      */
186     @Nonnull
187     private DataDescriptorType dataDescriptorType;
188 
189     /**
190      * Source for this entry's data. If this entry is a directory, this source has to have zero
191      * size.
192      */
193     @Nonnull
194     private ProcessedAndRawByteSources source;
195 
196     /**
197      * Verify log for the entry.
198      */
199     @Nonnull
200     private final VerifyLog verifyLog;
201 
202     /**
203      * Creates a new stored entry.
204      *
205      * @param header the header with the entry information; if the header does not contain an
206      * offset it means that this entry is not yet written in the zip file
207      * @param file the zip file containing the entry
208      * @param source the entry's data source; it can be {@code null} only if the source can be
209      * read from the zip file, that is, if {@code header.getOffset()} is non-negative
210      * @throws IOException failed to create the entry
211      */
StoredEntry( @onnull CentralDirectoryHeader header, @Nonnull ZFile file, @Nullable ProcessedAndRawByteSources source)212     StoredEntry(
213             @Nonnull CentralDirectoryHeader header,
214             @Nonnull ZFile file,
215             @Nullable ProcessedAndRawByteSources source)
216             throws IOException {
217         cdh = header;
218         this.file = file;
219         deleted = false;
220         verifyLog = file.makeVerifyLog();
221 
222         if (header.getOffset() >= 0) {
223             /*
224              * This will be overwritten during readLocalHeader. However, IJ complains if we don't
225              * assign a value to localExtra because of the @Nonnull annotation.
226              */
227             localExtra = new ExtraField();
228 
229             readLocalHeader();
230 
231             Preconditions.checkArgument(
232                     source == null,
233                     "Source was defined but contents already exist on file.");
234 
235             /*
236              * Since the file is already in the zip, dynamically create a source that will read
237              * the file from the zip when needed. The assignment is not really needed, but we
238              * would get a warning because of the @NotNull otherwise.
239              */
240             this.source = createSourceFromZip(cdh.getOffset());
241         } else {
242             /*
243              * There is no local extra data for new files.
244              */
245             localExtra = new ExtraField();
246 
247             Preconditions.checkNotNull(
248                     source,
249                     "Source was not defined, but contents are not on file.");
250             this.source = source;
251         }
252 
253         /*
254          * It seems that zip utilities store directories as names ending with "/".
255          * This seems to be respected by all zip utilities although I could not find there anywhere
256          * in the specification.
257          */
258         if (cdh.getName().endsWith(Character.toString(ZFile.SEPARATOR))) {
259             type = StoredEntryType.DIRECTORY;
260             verifyLog.verify(
261                     this.source.getProcessedByteSource().isEmpty(),
262                     "Directory source is not empty.");
263             verifyLog.verify(cdh.getCrc32() == 0, "Directory has CRC32 = %s.", cdh.getCrc32());
264             verifyLog.verify(
265                     cdh.getUncompressedSize() == 0,
266                     "Directory has uncompressed size = %s.",
267                     cdh.getUncompressedSize());
268 
269             /*
270              * Some clever (OMG!) tools, like jar will actually try to compress the directory
271              * contents and generate a 2 byte compressed data. Of course, the uncompressed size is
272              * zero and we're just wasting space.
273              */
274             long compressedSize = cdh.getCompressionInfoWithWait().getCompressedSize();
275             verifyLog.verify(
276                     compressedSize == 0 || compressedSize == 2,
277                     "Directory has compressed size = %s.", compressedSize);
278         } else {
279             type = StoredEntryType.FILE;
280         }
281 
282         /*
283          * By default we assume there is no data descriptor unless the CRC is marked as deferred
284          * in the header's GP Bit.
285          */
286         dataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR;
287         if (header.getGpBit().isDeferredCrc()) {
288             /*
289              * If the deferred CRC bit exists, then we have an extra descriptor field. This extra
290              * field may have a signature.
291              */
292             Verify.verify(header.getOffset() >= 0, "Files that are not on disk cannot have the "
293                     + "deferred CRC bit set.");
294 
295             try {
296                 readDataDescriptorRecord();
297             } catch (IOException e) {
298                 throw new IOException("Failed to read data descriptor record.", e);
299             }
300         }
301     }
302 
303     /**
304      * Obtains the size of the local header of this entry.
305      *
306      * @return the local header size in bytes
307      */
getLocalHeaderSize()308     public int getLocalHeaderSize() {
309         Preconditions.checkState(!deleted, "deleted");
310         return FIXED_LOCAL_FILE_HEADER_SIZE + cdh.getEncodedFileName().length + localExtra.size();
311     }
312 
313     /**
314      * Obtains the size of the whole entry on disk, including local header and data descriptor.
315      * This method will wait until compression information is complete, if needed.
316      *
317      * @return the number of bytes
318      * @throws IOException failed to get compression information
319      */
getInFileSize()320     long getInFileSize() throws IOException {
321         Preconditions.checkState(!deleted, "deleted");
322         return cdh.getCompressionInfoWithWait().getCompressedSize() + getLocalHeaderSize()
323                 + dataDescriptorType.size;
324     }
325 
326     /**
327      * Obtains a stream that allows reading from the entry.
328      *
329      * @return a stream that will return as many bytes as the uncompressed entry size
330      * @throws IOException failed to open the stream
331      */
332     @Nonnull
open()333     public InputStream open() throws IOException {
334         return source.getProcessedByteSource().openStream();
335     }
336 
337     /**
338      * Obtains the contents of the file.
339      *
340      * @return a byte array with the contents of the file (uncompressed if the file was compressed)
341      * @throws IOException failed to read the file
342      */
343     @Nonnull
read()344     public byte[] read() throws IOException {
345         try (InputStream is = open()) {
346             return ByteStreams.toByteArray(is);
347         }
348     }
349 
350     /**
351      * Obtains the contents of the file in an existing buffer.
352      *
353      * @param bytes buffer to read the file contents in.
354      * @return the number of bytes read
355      * @throws IOException failed to read the file.
356      */
read(byte[] bytes)357     public int read(byte[] bytes) throws IOException {
358         if (bytes.length < getCentralDirectoryHeader().getUncompressedSize()) {
359             throw new RuntimeException(
360                     "Buffer to small while reading {}" + getCentralDirectoryHeader().getName());
361         }
362         try (InputStream is = new BufferedInputStream(open())) {
363             return ByteStreams.read(is, bytes, 0, bytes.length);
364         }
365     }
366 
367     /**
368      * Obtains the type of entry.
369      *
370      * @return the type of entry
371      */
372     @Nonnull
getType()373     public StoredEntryType getType() {
374         Preconditions.checkState(!deleted, "deleted");
375         return type;
376     }
377 
378     /**
379      * Deletes this entry from the zip file. Invoking this method doesn't update the zip itself.
380      * To eventually write updates to disk, {@link ZFile#update()} must be called.
381      *
382      * @throws IOException failed to delete the entry
383      * @throws IllegalStateException if the zip file was open in read-only mode
384      */
delete()385     public void delete() throws IOException {
386         delete(true);
387     }
388 
389     /**
390      * Deletes this entry from the zip file. Invoking this method doesn't update the zip itself.
391      * To eventually write updates to disk, {@link ZFile#update()} must be called.
392      *
393      * @param notify should listeners be notified of the deletion? This will only be
394      * {@code false} if the entry is being removed as part of a replacement
395      * @throws IOException failed to delete the entry
396      * @throws IllegalStateException if the zip file was open in read-only mode
397      */
delete(boolean notify)398     void delete(boolean notify) throws IOException {
399         Preconditions.checkState(!deleted, "deleted");
400         file.delete(this, notify);
401         deleted = true;
402         source.close();
403     }
404 
405     /**
406      * Returns {@code true} if this entry has been deleted/replaced.
407      */
isDeleted()408     public boolean isDeleted() {
409         return deleted;
410     }
411 
412     /**
413      * Obtains the CDH associated with this entry.
414      *
415      * @return the CDH
416      */
417     @Nonnull
getCentralDirectoryHeader()418     public CentralDirectoryHeader getCentralDirectoryHeader() {
419         return cdh;
420     }
421 
422     /**
423      * Reads the file's local header and verifies that it matches the Central Directory
424      * Header provided in the constructor. This method should only be called if the entry already
425      * exists on disk; new entries do not have local headers.
426      * <p>
427      * This method will define the {@link #localExtra} field that is only defined in the
428      * local descriptor.
429      *
430      * @throws IOException failed to read the local header
431      */
readLocalHeader()432     private void readLocalHeader() throws IOException {
433         byte[] localHeader = new byte[FIXED_LOCAL_FILE_HEADER_SIZE];
434         file.directFullyRead(cdh.getOffset(), localHeader);
435 
436         CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
437 
438         ByteBuffer bytes = ByteBuffer.wrap(localHeader);
439         F_LOCAL_SIGNATURE.verify(bytes);
440         F_VERSION_EXTRACT.verify(bytes, compressInfo.getVersionExtract(), verifyLog);
441         F_GP_BIT.verify(bytes, cdh.getGpBit().getValue(), verifyLog);
442         F_METHOD.verify(bytes, compressInfo.getMethod().methodCode, verifyLog);
443 
444         if (file.areTimestampsIgnored()) {
445             F_LAST_MOD_TIME.skip(bytes);
446             F_LAST_MOD_DATE.skip(bytes);
447         } else {
448             F_LAST_MOD_TIME.verify(bytes, cdh.getLastModTime(), verifyLog);
449             F_LAST_MOD_DATE.verify(bytes, cdh.getLastModDate(), verifyLog);
450         }
451 
452         /*
453          * If CRC-32, compressed size and uncompressed size are deferred, their values in Local
454          * File Header must be ignored and their actual values must be read from the Data
455          * Descriptor following the contents of this entry. See readDataDescriptorRecord().
456          */
457         if (cdh.getGpBit().isDeferredCrc()) {
458             F_CRC32.skip(bytes);
459             F_COMPRESSED_SIZE.skip(bytes);
460             F_UNCOMPRESSED_SIZE.skip(bytes);
461         } else {
462             F_CRC32.verify(bytes, cdh.getCrc32(), verifyLog);
463             F_COMPRESSED_SIZE.verify(bytes, compressInfo.getCompressedSize(), verifyLog);
464             F_UNCOMPRESSED_SIZE.verify(bytes, cdh.getUncompressedSize(), verifyLog);
465         }
466 
467         F_FILE_NAME_LENGTH.verify(bytes, cdh.getEncodedFileName().length);
468         long extraLength = F_EXTRA_LENGTH.read(bytes);
469         long fileNameStart = cdh.getOffset() + F_EXTRA_LENGTH.endOffset();
470         byte[] fileNameData = new byte[cdh.getEncodedFileName().length];
471         file.directFullyRead(fileNameStart, fileNameData);
472 
473         String fileName = EncodeUtils.decode(fileNameData, cdh.getGpBit());
474         if (!fileName.equals(cdh.getName())) {
475             verifyLog.log(
476                     String.format(
477                             "Central directory reports file as being named '%s' but local header"
478                                     + "reports file being named '%s'.",
479                     cdh.getName(),
480                     fileName));
481         }
482 
483         long localExtraStart = fileNameStart + cdh.getEncodedFileName().length;
484         byte[] localExtraRaw = new byte[Ints.checkedCast(extraLength)];
485         file.directFullyRead(localExtraStart, localExtraRaw);
486         localExtra = new ExtraField(localExtraRaw);
487     }
488 
489     /**
490      * Reads the data descriptor record. This method can only be invoked once it is established
491      * that a data descriptor does exist. It will read the data descriptor and check that the data
492      * described there matches the data provided in the Central Directory.
493      * <p>
494      * This method will set the {@link #dataDescriptorType} field to the appropriate type of
495      * data descriptor record.
496      *
497      * @throws IOException failed to read the data descriptor record
498      */
readDataDescriptorRecord()499     private void readDataDescriptorRecord() throws IOException {
500         CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
501 
502         long ddStart = cdh.getOffset() + FIXED_LOCAL_FILE_HEADER_SIZE
503                 + cdh.getName().length() + localExtra.size() + compressInfo.getCompressedSize();
504         byte[] ddData = new byte[DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE.size];
505         file.directFullyRead(ddStart, ddData);
506 
507         ByteBuffer ddBytes = ByteBuffer.wrap(ddData);
508 
509         ZipField.F4 signatureField = new ZipField.F4(0, "Data descriptor signature");
510         int cpos = ddBytes.position();
511         long sig = signatureField.read(ddBytes);
512         if (sig == DATA_DESC_SIGNATURE) {
513             dataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITH_SIGNATURE;
514         } else {
515             dataDescriptorType = DataDescriptorType.DATA_DESCRIPTOR_WITHOUT_SIGNATURE;
516             ddBytes.position(cpos);
517         }
518 
519         ZipField.F4 crc32Field = new ZipField.F4(0, "CRC32");
520         ZipField.F4 compressedField = new ZipField.F4(crc32Field.endOffset(), "Compressed size");
521         ZipField.F4 uncompressedField = new ZipField.F4(compressedField.endOffset(),
522                 "Uncompressed size");
523 
524         crc32Field.verify(ddBytes, cdh.getCrc32(), verifyLog);
525         compressedField.verify(ddBytes, compressInfo.getCompressedSize(), verifyLog);
526         uncompressedField.verify(ddBytes, cdh.getUncompressedSize(), verifyLog);
527     }
528 
529     /**
530      * Creates a new source that reads data from the zip.
531      *
532      * @param zipOffset the offset into the zip file where the data is, must be non-negative
533      * @throws IOException failed to close the old source
534      * @return the created source
535      */
536     @Nonnull
createSourceFromZip(final long zipOffset)537     private ProcessedAndRawByteSources createSourceFromZip(final long zipOffset)
538             throws IOException {
539         Preconditions.checkArgument(zipOffset >= 0, "zipOffset < 0");
540 
541         final CentralDirectoryHeaderCompressInfo compressInfo;
542         try {
543             compressInfo = cdh.getCompressionInfoWithWait();
544         } catch (IOException e) {
545             throw new RuntimeException("IOException should never occur here because compression "
546                     + "information should be immediately available if reading from zip.", e);
547         }
548 
549         /*
550          * Create a source that will return whatever is on the zip file.
551          */
552         CloseableByteSource rawContents = new CloseableByteSource() {
553             @Override
554             public long size() throws IOException {
555                 return compressInfo.getCompressedSize();
556             }
557 
558             @Nonnull
559             @Override
560             public InputStream openStream() throws IOException {
561                 Preconditions.checkState(!deleted, "deleted");
562 
563                 long dataStart = zipOffset + getLocalHeaderSize();
564                 long dataEnd = dataStart + compressInfo.getCompressedSize();
565 
566                 file.openReadOnly();
567                 return file.directOpen(dataStart, dataEnd);
568             }
569 
570             @Override
571             protected void innerClose() throws IOException {
572                 /*
573                  * Nothing to do here.
574                  */
575             }
576         };
577 
578         return createSourcesFromRawContents(rawContents);
579     }
580 
581     /**
582      * Creates a {@link ProcessedAndRawByteSources} from the raw data source . The processed source
583      * will either inflate or do nothing depending on the compression information that, at this
584      * point, should already be available
585      *
586      * @param rawContents the raw data to create the source from
587      * @return the sources for this entry
588      */
589     @Nonnull
createSourcesFromRawContents( @onnull CloseableByteSource rawContents)590     private ProcessedAndRawByteSources createSourcesFromRawContents(
591             @Nonnull CloseableByteSource rawContents) {
592         CentralDirectoryHeaderCompressInfo compressInfo;
593         try {
594             compressInfo = cdh.getCompressionInfoWithWait();
595         } catch (IOException e) {
596             throw new RuntimeException("IOException should never occur here because compression "
597                     + "information should be immediately available if creating from raw "
598                     + "contents.", e);
599         }
600 
601         CloseableByteSource contents;
602 
603         /*
604          * If the contents are deflated, wrap that source in an inflater source so we get the
605          * uncompressed data.
606          */
607         if (compressInfo.getMethod() == CompressionMethod.DEFLATE) {
608             contents = new InflaterByteSource(rawContents);
609         } else {
610             contents = rawContents;
611         }
612 
613         return new ProcessedAndRawByteSources(contents, rawContents);
614     }
615 
616     /**
617      * Replaces {@link #source} with one that reads file data from the zip file.
618      *
619      * @param zipFileOffset the offset in the zip file where data is written; must be non-negative
620      * @throws IOException failed to replace the source
621      */
replaceSourceFromZip(long zipFileOffset)622     void replaceSourceFromZip(long zipFileOffset) throws IOException {
623         Preconditions.checkArgument(zipFileOffset >= 0, "zipFileOffset < 0");
624 
625         ProcessedAndRawByteSources oldSource = source;
626         source = createSourceFromZip(zipFileOffset);
627         cdh.setOffset(zipFileOffset);
628         oldSource.close();
629     }
630 
631     /**
632      * Loads all data in memory and replaces {@link #source} with one that contains all the data
633      * in memory.
634      *
635      * <p>If the entry's contents are already in memory, this call does nothing.
636      *
637      * @throws IOException failed to replace the source
638      */
loadSourceIntoMemory()639     void loadSourceIntoMemory() throws IOException {
640         if (cdh.getOffset() == -1) {
641             /*
642              * No offset in the CDR means data has not been written to disk which, in turn,
643              * means data is already loaded into memory.
644              */
645             return;
646         }
647 
648         ProcessedAndRawByteSources oldSource = source;
649         byte[] rawContents = oldSource.getRawByteSource().read();
650         source = createSourcesFromRawContents(new CloseableDelegateByteSource(
651                 ByteSource.wrap(rawContents), rawContents.length));
652         cdh.setOffset(-1);
653         oldSource.close();
654     }
655 
656     /**
657      * Obtains the source data for this entry. This method can only be called for files, it
658      * cannot be called for directories.
659      *
660      * @return the entry source
661      */
662     @Nonnull
getSource()663     ProcessedAndRawByteSources getSource() {
664         return source;
665     }
666 
667     /**
668      * Obtains the type of data descriptor used in the entry.
669      *
670      * @return the type of data descriptor
671      */
672     @Nonnull
getDataDescriptorType()673     public DataDescriptorType getDataDescriptorType() {
674         return dataDescriptorType;
675     }
676 
677     /**
678      * Removes the data descriptor, if it has one and resets the data descriptor bit in the
679      * central directory header.
680      *
681      * @return was the data descriptor remove?
682      */
removeDataDescriptor()683     boolean removeDataDescriptor() {
684         if (dataDescriptorType == DataDescriptorType.NO_DATA_DESCRIPTOR) {
685             return false;
686         }
687 
688         dataDescriptorType = DataDescriptorType.NO_DATA_DESCRIPTOR;
689         cdh.resetDeferredCrc();
690         return true;
691     }
692 
693     /**
694      * Obtains the local header data.
695      *
696      * @return the header data
697      * @throws IOException failed to get header byte data
698      */
699     @Nonnull
toHeaderData()700     byte[] toHeaderData() throws IOException {
701 
702         byte[] encodedFileName = cdh.getEncodedFileName();
703 
704         ByteBuffer out =
705                 ByteBuffer.allocate(
706                         F_EXTRA_LENGTH.endOffset() + encodedFileName.length + localExtra.size());
707 
708         CentralDirectoryHeaderCompressInfo compressInfo = cdh.getCompressionInfoWithWait();
709 
710         F_LOCAL_SIGNATURE.write(out);
711         F_VERSION_EXTRACT.write(out, compressInfo.getVersionExtract());
712         F_GP_BIT.write(out, cdh.getGpBit().getValue());
713         F_METHOD.write(out, compressInfo.getMethod().methodCode);
714 
715         if (file.areTimestampsIgnored()) {
716             F_LAST_MOD_TIME.write(out, 0);
717             F_LAST_MOD_DATE.write(out, 0);
718         } else {
719             F_LAST_MOD_TIME.write(out, cdh.getLastModTime());
720             F_LAST_MOD_DATE.write(out, cdh.getLastModDate());
721         }
722 
723         F_CRC32.write(out, cdh.getCrc32());
724         F_COMPRESSED_SIZE.write(out, compressInfo.getCompressedSize());
725         F_UNCOMPRESSED_SIZE.write(out, cdh.getUncompressedSize());
726         F_FILE_NAME_LENGTH.write(out, cdh.getEncodedFileName().length);
727         F_EXTRA_LENGTH.write(out, localExtra.size());
728 
729         out.put(cdh.getEncodedFileName());
730         localExtra.write(out);
731 
732         return out.array();
733     }
734 
735     /**
736      * Requests that this entry be realigned. If this entry is already aligned according to the
737      * rules in {@link ZFile} then this method does nothing. Otherwise it will move the file's data
738      * into memory and place it in a different area of the zip.
739      *
740      * @return has this file been changed? Note that if the entry has not yet been written on the
741      * file, realignment does not count as a change as nothing needs to be updated in the file;
742      * also, if the entry has been changed, this object may have been marked as deleted and a new
743      * stored entry may need to be fetched from the file
744      * @throws IOException failed to realign the entry; the entry may no longer exist in the zip
745      * file
746      */
realign()747     public boolean realign() throws IOException {
748         Preconditions.checkState(!deleted, "Entry has been deleted.");
749 
750         return file.realign(this);
751     }
752 
753     /**
754      * Obtains the contents of the local extra field.
755      *
756      * @return the contents of the local extra field
757      */
758     @Nonnull
getLocalExtra()759     public ExtraField getLocalExtra() {
760         return localExtra;
761     }
762 
763     /**
764      * Sets the contents of the local extra field.
765      *
766      * @param localExtra the contents of the local extra field
767      * @throws IOException failed to update the zip file
768      */
setLocalExtra(@onnull ExtraField localExtra)769     public void setLocalExtra(@Nonnull ExtraField localExtra) throws IOException {
770         boolean resized = setLocalExtraNoNotify(localExtra);
771         file.localHeaderChanged(this, resized);
772     }
773 
774     /**
775      * Sets the contents of the local extra field, does not notify the {@link ZFile} of the change.
776      * This is used internally when the {@link ZFile} itself wants to change the local extra and
777      * doesn't need the callback.
778      *
779      * @param localExtra the contents of the local extra field
780      * @return has the local header size changed?
781      * @throws IOException failed to load the file
782      */
setLocalExtraNoNotify(@onnull ExtraField localExtra)783     boolean setLocalExtraNoNotify(@Nonnull ExtraField localExtra) throws IOException {
784         boolean sizeChanged;
785 
786         /*
787          * Make sure we load into memory.
788          *
789          * If we change the size of the local header, the actual start of the file changes
790          * according to our in-memory structures so, if we don't read the file now, we won't be
791          * able to load it later :)
792          *
793          * But, even if the size doesn't change, we need to read it force the entry to be
794          * rewritten otherwise the changes in the local header aren't written. Of course this case
795          * may be optimized with some extra complexity added :)
796          */
797         loadSourceIntoMemory();
798 
799         if (this.localExtra.size() != localExtra.size()) {
800             sizeChanged = true;
801         } else {
802             sizeChanged = false;
803         }
804 
805         this.localExtra = localExtra;
806         return sizeChanged;
807     }
808 
809     /**
810      * Obtains the verify log for the entry.
811      *
812      * @return the verify log
813      */
814     @Nonnull
getVerifyLog()815     public VerifyLog getVerifyLog() {
816         return verifyLog;
817     }
818 }
819