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 quick 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