1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tools.build.apkzlib.zip;
18 
19 import com.android.tools.build.apkzlib.utils.CachedSupplier;
20 import com.google.common.annotations.VisibleForTesting;
21 import com.google.common.base.Preconditions;
22 import com.google.common.base.Verify;
23 import com.google.common.primitives.Ints;
24 import java.io.IOException;
25 import java.io.UncheckedIOException;
26 import java.nio.ByteBuffer;
27 import javax.annotation.Nonnull;
28 
29 /**
30  * End Of Central Directory record in a zip file.
31  */
32 class Eocd {
33     /**
34      * Field in the record: the record signature, fixed at this value by the specification.
35      */
36     private static final ZipField.F4 F_SIGNATURE = new ZipField.F4(0, 0x06054b50, "EOCD signature");
37 
38     /**
39      * Field in the record: the number of the disk where the EOCD is located. It has to be zero
40      * because we do not support multi-file archives.
41      */
42     private static final ZipField.F2 F_NUMBER_OF_DISK = new ZipField.F2(F_SIGNATURE.endOffset(), 0,
43             "Number of this disk");
44 
45     /**
46      * Field in the record: the number of the disk where the Central Directory starts. Has to be
47      * zero because we do not support multi-file archives.
48      */
49     private static final ZipField.F2 F_DISK_CD_START = new ZipField.F2(F_NUMBER_OF_DISK.endOffset(),
50             0, "Disk where CD starts");
51 
52     /**
53      * Field in the record: the number of entries in the Central Directory on this disk. Because
54      * we do not support multi-file archives, this is the same as {@link #F_RECORDS_TOTAL}.
55      */
56     private static final ZipField.F2 F_RECORDS_DISK = new ZipField.F2(F_DISK_CD_START.endOffset(),
57             "Record on disk count", new ZipFieldInvariantNonNegative());
58 
59     /**
60      * Field in the record: the total number of entries in the Central Directory.
61      */
62     private static final ZipField.F2 F_RECORDS_TOTAL = new ZipField.F2(F_RECORDS_DISK.endOffset(),
63             "Total records", new ZipFieldInvariantNonNegative(),
64             new ZipFieldInvariantMaxValue(Integer.MAX_VALUE));
65 
66     /**
67      * Field in the record: number of bytes of the Central Directory.
68      * This is not private because it is required in unit tests.
69      */
70     @VisibleForTesting
71     static final ZipField.F4 F_CD_SIZE = new ZipField.F4(F_RECORDS_TOTAL.endOffset(),
72             "Directory size", new ZipFieldInvariantNonNegative());
73 
74     /**
75      * Field in the record: offset, from the archive start, where the Central Directory starts.
76      * This is not private because it is required in unit tests.
77      */
78     @VisibleForTesting
79     static final ZipField.F4 F_CD_OFFSET = new ZipField.F4(F_CD_SIZE.endOffset(),
80             "Directory offset", new ZipFieldInvariantNonNegative());
81 
82     /**
83      * Field in the record: number of bytes of the file comment (located at the end of the EOCD
84      * record).
85      */
86     private static final ZipField.F2 F_COMMENT_SIZE = new ZipField.F2(F_CD_OFFSET.endOffset(),
87             "File comment size", new ZipFieldInvariantNonNegative());
88 
89     /**
90      * Number of entries in the central directory.
91      */
92     private final int totalRecords;
93 
94     /**
95      * Offset from the beginning of the archive where the Central Directory is located.
96      */
97     private final long directoryOffset;
98 
99     /**
100      * Number of bytes of the Central Directory.
101      */
102     private final long directorySize;
103 
104     /**
105      * Contents of the EOCD comment.
106      */
107     @Nonnull
108     private final byte[] comment;
109 
110     /**
111      * Supplier of the byte representation of the EOCD.
112      */
113     @Nonnull
114     private final CachedSupplier<byte[]> byteSupplier;
115 
116     /**
117      * Creates a new EOCD, reading it from a byte source. This method will parse the byte source
118      * and obtain the EOCD. It will check that the byte source starts with the EOCD signature.
119      *
120      * @param bytes the byte buffer with the EOCD data; when this method finishes, the byte
121      * buffer's position will have moved to the end of the EOCD
122      * @throws IOException failed to read information or the EOCD data is corrupt or invalid
123      */
Eocd(@onnull ByteBuffer bytes)124     Eocd(@Nonnull ByteBuffer bytes) throws IOException {
125 
126         /*
127          * Read the EOCD record.
128          */
129         F_SIGNATURE.verify(bytes);
130         F_NUMBER_OF_DISK.verify(bytes);
131         F_DISK_CD_START.verify(bytes);
132         long totalRecords1 = F_RECORDS_DISK.read(bytes);
133         long totalRecords2 = F_RECORDS_TOTAL.read(bytes);
134         long directorySize = F_CD_SIZE.read(bytes);
135         long directoryOffset = F_CD_OFFSET.read(bytes);
136         int commentSize = Ints.checkedCast(F_COMMENT_SIZE.read(bytes));
137 
138         /*
139          * Some sanity checks.
140          */
141         if (totalRecords1 !=  totalRecords2) {
142             throw new IOException("Zip states records split in multiple disks, which is not "
143                     + "supported.");
144         }
145 
146         Verify.verify(totalRecords1 <= Integer.MAX_VALUE);
147 
148         totalRecords = Ints.checkedCast(totalRecords1);
149         this.directorySize = directorySize;
150         this.directoryOffset = directoryOffset;
151 
152         if (bytes.remaining() < commentSize) {
153             throw new IOException("Corrupt EOCD record: not enough data for comment (comment "
154                     + "size is " + commentSize + ").");
155         }
156 
157         comment = new byte[commentSize];
158         bytes.get(comment);
159         byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
160     }
161 
162     /**
163      * Creates a new EOCD. This is used when generating an EOCD for an Central Directory that has
164      * just been generated. The EOCD will be generated without any comment.
165      *
166      * @param totalRecords total number of records in the directory
167      * @param directoryOffset offset, since beginning of archive, where the Central Directory is
168      * located
169      * @param directorySize number of bytes of the Central Directory
170      * @param comment the EOCD comment
171      */
Eocd(int totalRecords, long directoryOffset, long directorySize, @Nonnull byte[] comment)172     Eocd(int totalRecords, long directoryOffset, long directorySize, @Nonnull byte[] comment) {
173         Preconditions.checkArgument(totalRecords >= 0, "totalRecords < 0");
174         Preconditions.checkArgument(directoryOffset >= 0, "directoryOffset < 0");
175         Preconditions.checkArgument(directorySize >= 0, "directorySize < 0");
176 
177         this.totalRecords = totalRecords;
178         this.directoryOffset = directoryOffset;
179         this.directorySize = directorySize;
180         this.comment = comment;
181         byteSupplier = new CachedSupplier<>(this::computeByteRepresentation);
182     }
183 
184     /**
185      * Obtains the number of records in the Central Directory.
186      *
187      * @return the number of records
188      */
getTotalRecords()189     int getTotalRecords() {
190         return totalRecords;
191     }
192 
193     /**
194      * Obtains the offset since the beginning of the zip archive where the Central Directory is
195      * located.
196      *
197      * @return the offset where the Central Directory is located
198      */
getDirectoryOffset()199     long getDirectoryOffset() {
200         return directoryOffset;
201     }
202 
203     /**
204      * Obtains the size of the Central Directory.
205      *
206      * @return the number of bytes that make up the Central Directory
207      */
getDirectorySize()208     long getDirectorySize() {
209         return directorySize;
210     }
211 
212     /**
213      * Obtains the size of the EOCD.
214      *
215      * @return the size, in bytes, of the EOCD
216      */
getEocdSize()217     long getEocdSize() {
218         return (long) F_COMMENT_SIZE.endOffset() + comment.length;
219     }
220 
221     /**
222      * Generates the EOCD data.
223      *
224      * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes
225      * @throws IOException failed to generate the EOCD data
226      */
227     @Nonnull
toBytes()228     byte[] toBytes() throws IOException {
229         return byteSupplier.get();
230     }
231 
232     /*
233      * Obtains the comment in the EOCD.
234      *
235      * @return the comment exactly as it is represented in the file (no encoding conversion is
236      * done)
237      */
238     @Nonnull
getComment()239     byte[] getComment() {
240         byte[] commentCopy = new byte[comment.length];
241         System.arraycopy(comment, 0, commentCopy, 0, comment.length);
242         return commentCopy;
243     }
244 
245     /**
246      * Computes the byte representation of the EOCD.
247      *
248      * @return a byte representation of the EOCD that has exactly {@link #getEocdSize()} bytes
249      * @throws UncheckedIOException failed to generate the EOCD data
250      */
251     @Nonnull
computeByteRepresentation()252     private byte[] computeByteRepresentation() {
253         ByteBuffer out = ByteBuffer.allocate(F_COMMENT_SIZE.endOffset() + comment.length);
254 
255         try {
256             F_SIGNATURE.write(out);
257             F_NUMBER_OF_DISK.write(out);
258             F_DISK_CD_START.write(out);
259             F_RECORDS_DISK.write(out, totalRecords);
260             F_RECORDS_TOTAL.write(out, totalRecords);
261             F_CD_SIZE.write(out, directorySize);
262             F_CD_OFFSET.write(out, directoryOffset);
263             F_COMMENT_SIZE.write(out, comment.length);
264             out.put(comment);
265 
266             return out.array();
267         } catch (IOException e) {
268             throw new UncheckedIOException(e);
269         }
270     }
271 }
272