1/* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package java.util.zip; 19 20import dalvik.system.CloseGuard; 21import java.io.BufferedInputStream; 22import java.io.Closeable; 23import java.io.DataInputStream; 24import java.io.File; 25import java.io.IOException; 26import java.io.InputStream; 27import java.io.RandomAccessFile; 28import java.nio.ByteOrder; 29import java.nio.charset.StandardCharsets; 30import java.util.Enumeration; 31import java.util.Iterator; 32import java.util.LinkedHashMap; 33import libcore.io.BufferIterator; 34import libcore.io.HeapBufferIterator; 35import libcore.io.Streams; 36 37/** 38 * This class provides random read access to a zip file. You pay more to read 39 * the zip file's central directory up front (from the constructor), but if you're using 40 * {@link #getEntry} to look up multiple files by name, you get the benefit of this index. 41 * 42 * <p>If you only want to iterate through all the files (using {@link #entries()}, you should 43 * consider {@link ZipInputStream}, which provides stream-like read access to a zip file and 44 * has a lower up-front cost because you don't pay to build an in-memory index. 45 * 46 * <p>If you want to create a zip file, use {@link ZipOutputStream}. There is no API for updating 47 * an existing zip file. 48 */ 49public class ZipFile implements Closeable, ZipConstants { 50 /** 51 * General Purpose Bit Flags, Bit 0. 52 * If set, indicates that the file is encrypted. 53 */ 54 static final int GPBF_ENCRYPTED_FLAG = 1 << 0; 55 56 /** 57 * General Purpose Bit Flags, Bit 3. 58 * If this bit is set, the fields crc-32, compressed 59 * size and uncompressed size are set to zero in the 60 * local header. The correct values are put in the 61 * data descriptor immediately following the compressed 62 * data. (Note: PKZIP version 2.04g for DOS only 63 * recognizes this bit for method 8 compression, newer 64 * versions of PKZIP recognize this bit for any 65 * compression method.) 66 */ 67 static final int GPBF_DATA_DESCRIPTOR_FLAG = 1 << 3; 68 69 /** 70 * General Purpose Bit Flags, Bit 11. 71 * Language encoding flag (EFS). If this bit is set, 72 * the filename and comment fields for this file 73 * must be encoded using UTF-8. 74 */ 75 static final int GPBF_UTF8_FLAG = 1 << 11; 76 77 /** 78 * Supported General Purpose Bit Flags Mask. 79 * Bit mask of bits not supported. 80 * Note: The only bit that we will enforce at this time 81 * is the encrypted bit. Although other bits are not supported, 82 * we must not enforce them as this could break some legitimate 83 * use cases (See http://b/8617715). 84 */ 85 static final int GPBF_UNSUPPORTED_MASK = GPBF_ENCRYPTED_FLAG; 86 87 /** 88 * Open zip file for reading. 89 */ 90 public static final int OPEN_READ = 1; 91 92 /** 93 * Delete zip file when closed. 94 */ 95 public static final int OPEN_DELETE = 4; 96 97 private final String filename; 98 99 private File fileToDeleteOnClose; 100 101 private RandomAccessFile raf; 102 103 private final LinkedHashMap<String, ZipEntry> entries = new LinkedHashMap<String, ZipEntry>(); 104 105 private String comment; 106 107 private final CloseGuard guard = CloseGuard.get(); 108 109 /** 110 * Constructs a new {@code ZipFile} allowing read access to the contents of the given file. 111 * @throws ZipException if a zip error occurs. 112 * @throws IOException if an {@code IOException} occurs. 113 */ 114 public ZipFile(File file) throws ZipException, IOException { 115 this(file, OPEN_READ); 116 } 117 118 /** 119 * Constructs a new {@code ZipFile} allowing read access to the contents of the given file. 120 * @throws IOException if an IOException occurs. 121 */ 122 public ZipFile(String name) throws IOException { 123 this(new File(name), OPEN_READ); 124 } 125 126 /** 127 * Constructs a new {@code ZipFile} allowing access to the given file. 128 * The {@code mode} must be either {@code OPEN_READ} or {@code OPEN_READ|OPEN_DELETE}. 129 * 130 * <p>If the {@code OPEN_DELETE} flag is supplied, the file will be deleted at or before the 131 * time that the {@code ZipFile} is closed (the contents will remain accessible until 132 * this {@code ZipFile} is closed); it also calls {@code File.deleteOnExit}. 133 * 134 * @throws IOException if an {@code IOException} occurs. 135 */ 136 public ZipFile(File file, int mode) throws IOException { 137 filename = file.getPath(); 138 if (mode != OPEN_READ && mode != (OPEN_READ | OPEN_DELETE)) { 139 throw new IllegalArgumentException("Bad mode: " + mode); 140 } 141 142 if ((mode & OPEN_DELETE) != 0) { 143 fileToDeleteOnClose = file; 144 fileToDeleteOnClose.deleteOnExit(); 145 } else { 146 fileToDeleteOnClose = null; 147 } 148 149 raf = new RandomAccessFile(filename, "r"); 150 151 readCentralDir(); 152 guard.open("close"); 153 } 154 155 @Override protected void finalize() throws IOException { 156 try { 157 if (guard != null) { 158 guard.warnIfOpen(); 159 } 160 } finally { 161 try { 162 super.finalize(); 163 } catch (Throwable t) { 164 throw new AssertionError(t); 165 } 166 } 167 } 168 169 /** 170 * Closes this zip file. This method is idempotent. This method may cause I/O if the 171 * zip file needs to be deleted. 172 * 173 * @throws IOException 174 * if an IOException occurs. 175 */ 176 public void close() throws IOException { 177 guard.close(); 178 179 RandomAccessFile localRaf = raf; 180 if (localRaf != null) { // Only close initialized instances 181 synchronized (localRaf) { 182 raf = null; 183 localRaf.close(); 184 } 185 if (fileToDeleteOnClose != null) { 186 fileToDeleteOnClose.delete(); 187 fileToDeleteOnClose = null; 188 } 189 } 190 } 191 192 private void checkNotClosed() { 193 if (raf == null) { 194 throw new IllegalStateException("Zip file closed"); 195 } 196 } 197 198 /** 199 * Returns an enumeration of the entries. The entries are listed in the 200 * order in which they appear in the zip file. 201 * 202 * <p>If you only need to iterate over the entries in a zip file, and don't 203 * need random-access entry lookup by name, you should probably use {@link ZipInputStream} 204 * instead, to avoid paying to construct the in-memory index. 205 * 206 * @throws IllegalStateException if this zip file has been closed. 207 */ 208 public Enumeration<? extends ZipEntry> entries() { 209 checkNotClosed(); 210 final Iterator<ZipEntry> iterator = entries.values().iterator(); 211 212 return new Enumeration<ZipEntry>() { 213 public boolean hasMoreElements() { 214 checkNotClosed(); 215 return iterator.hasNext(); 216 } 217 218 public ZipEntry nextElement() { 219 checkNotClosed(); 220 return iterator.next(); 221 } 222 }; 223 } 224 225 /** 226 * Returns this file's comment, or null if it doesn't have one. 227 * See {@link ZipOutputStream#setComment}. 228 * 229 * @throws IllegalStateException if this zip file has been closed. 230 * @since 1.7 231 */ 232 public String getComment() { 233 checkNotClosed(); 234 return comment; 235 } 236 237 /** 238 * Returns the zip entry with the given name, or null if there is no such entry. 239 * 240 * @throws IllegalStateException if this zip file has been closed. 241 */ 242 public ZipEntry getEntry(String entryName) { 243 checkNotClosed(); 244 if (entryName == null) { 245 throw new NullPointerException("entryName == null"); 246 } 247 248 ZipEntry ze = entries.get(entryName); 249 if (ze == null) { 250 ze = entries.get(entryName + "/"); 251 } 252 return ze; 253 } 254 255 /** 256 * Returns an input stream on the data of the specified {@code ZipEntry}. 257 * 258 * @param entry 259 * the ZipEntry. 260 * @return an input stream of the data contained in the {@code ZipEntry}. 261 * @throws IOException 262 * if an {@code IOException} occurs. 263 * @throws IllegalStateException if this zip file has been closed. 264 */ 265 public InputStream getInputStream(ZipEntry entry) throws IOException { 266 // Make sure this ZipEntry is in this Zip file. We run it through the name lookup. 267 entry = getEntry(entry.getName()); 268 if (entry == null) { 269 return null; 270 } 271 272 // Create an InputStream at the right part of the file. 273 RandomAccessFile localRaf = raf; 274 synchronized (localRaf) { 275 // We don't know the entry data's start position. All we have is the 276 // position of the entry's local header. 277 // http://www.pkware.com/documents/casestudies/APPNOTE.TXT 278 RAFStream rafStream = new RAFStream(localRaf, entry.localHeaderRelOffset); 279 DataInputStream is = new DataInputStream(rafStream); 280 281 final int localMagic = Integer.reverseBytes(is.readInt()); 282 if (localMagic != LOCSIG) { 283 throwZipException("Local File Header", localMagic); 284 } 285 286 is.skipBytes(2); 287 288 // At position 6 we find the General Purpose Bit Flag. 289 int gpbf = Short.reverseBytes(is.readShort()) & 0xffff; 290 if ((gpbf & ZipFile.GPBF_UNSUPPORTED_MASK) != 0) { 291 throw new ZipException("Invalid General Purpose Bit Flag: " + gpbf); 292 } 293 294 // Offset 26 has the file name length, and offset 28 has the extra field length. 295 // These lengths can differ from the ones in the central header. 296 is.skipBytes(18); 297 int fileNameLength = Short.reverseBytes(is.readShort()) & 0xffff; 298 int extraFieldLength = Short.reverseBytes(is.readShort()) & 0xffff; 299 is.close(); 300 301 // Skip the variable-size file name and extra field data. 302 rafStream.skip(fileNameLength + extraFieldLength); 303 304 if (entry.compressionMethod == ZipEntry.STORED) { 305 rafStream.endOffset = rafStream.offset + entry.size; 306 return rafStream; 307 } else { 308 rafStream.endOffset = rafStream.offset + entry.compressedSize; 309 int bufSize = Math.max(1024, (int) Math.min(entry.getSize(), 65535L)); 310 return new ZipInflaterInputStream(rafStream, new Inflater(true), bufSize, entry); 311 } 312 } 313 } 314 315 /** 316 * Gets the file name of this {@code ZipFile}. 317 * 318 * @return the file name of this {@code ZipFile}. 319 */ 320 public String getName() { 321 return filename; 322 } 323 324 /** 325 * Returns the number of {@code ZipEntries} in this {@code ZipFile}. 326 * 327 * @return the number of entries in this file. 328 * @throws IllegalStateException if this zip file has been closed. 329 */ 330 public int size() { 331 checkNotClosed(); 332 return entries.size(); 333 } 334 335 /** 336 * Find the central directory and read the contents. 337 * 338 * <p>The central directory can be followed by a variable-length comment 339 * field, so we have to scan through it backwards. The comment is at 340 * most 64K, plus we have 18 bytes for the end-of-central-dir stuff 341 * itself, plus apparently sometimes people throw random junk on the end 342 * just for the fun of it. 343 * 344 * <p>This is all a little wobbly. If the wrong value ends up in the EOCD 345 * area, we're hosed. This appears to be the way that everybody handles 346 * it though, so we're in good company if this fails. 347 */ 348 private void readCentralDir() throws IOException { 349 // Scan back, looking for the End Of Central Directory field. If the zip file doesn't 350 // have an overall comment (unrelated to any per-entry comments), we'll hit the EOCD 351 // on the first try. 352 // No need to synchronize raf here -- we only do this when we first open the zip file. 353 long scanOffset = raf.length() - ENDHDR; 354 if (scanOffset < 0) { 355 throw new ZipException("File too short to be a zip file: " + raf.length()); 356 } 357 358 raf.seek(0); 359 final int headerMagic = Integer.reverseBytes(raf.readInt()); 360 if (headerMagic != LOCSIG) { 361 throw new ZipException("Not a zip archive"); 362 } 363 364 long stopOffset = scanOffset - 65536; 365 if (stopOffset < 0) { 366 stopOffset = 0; 367 } 368 369 while (true) { 370 raf.seek(scanOffset); 371 if (Integer.reverseBytes(raf.readInt()) == ENDSIG) { 372 break; 373 } 374 375 scanOffset--; 376 if (scanOffset < stopOffset) { 377 throw new ZipException("End Of Central Directory signature not found"); 378 } 379 } 380 381 // Read the End Of Central Directory. ENDHDR includes the signature bytes, 382 // which we've already read. 383 byte[] eocd = new byte[ENDHDR - 4]; 384 raf.readFully(eocd); 385 386 // Pull out the information we need. 387 BufferIterator it = HeapBufferIterator.iterator(eocd, 0, eocd.length, ByteOrder.LITTLE_ENDIAN); 388 int diskNumber = it.readShort() & 0xffff; 389 int diskWithCentralDir = it.readShort() & 0xffff; 390 int numEntries = it.readShort() & 0xffff; 391 int totalNumEntries = it.readShort() & 0xffff; 392 it.skip(4); // Ignore centralDirSize. 393 long centralDirOffset = ((long) it.readInt()) & 0xffffffffL; 394 int commentLength = it.readShort() & 0xffff; 395 396 if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDir != 0) { 397 throw new ZipException("Spanned archives not supported"); 398 } 399 400 if (commentLength > 0) { 401 byte[] commentBytes = new byte[commentLength]; 402 raf.readFully(commentBytes); 403 comment = new String(commentBytes, 0, commentBytes.length, StandardCharsets.UTF_8); 404 } 405 406 // Seek to the first CDE and read all entries. 407 // We have to do this now (from the constructor) rather than lazily because the 408 // public API doesn't allow us to throw IOException except from the constructor 409 // or from getInputStream. 410 RAFStream rafStream = new RAFStream(raf, centralDirOffset); 411 BufferedInputStream bufferedStream = new BufferedInputStream(rafStream, 4096); 412 byte[] hdrBuf = new byte[CENHDR]; // Reuse the same buffer for each entry. 413 for (int i = 0; i < numEntries; ++i) { 414 ZipEntry newEntry = new ZipEntry(hdrBuf, bufferedStream); 415 if (newEntry.localHeaderRelOffset >= centralDirOffset) { 416 throw new ZipException("Local file header offset is after central directory"); 417 } 418 String entryName = newEntry.getName(); 419 if (entries.put(entryName, newEntry) != null) { 420 throw new ZipException("Duplicate entry name: " + entryName); 421 } 422 } 423 } 424 425 static void throwZipException(String msg, int magic) throws ZipException { 426 final String hexString = IntegralToString.intToHexString(magic, true, 8); 427 throw new ZipException(msg + " signature not found; was " + hexString); 428 } 429 430 /** 431 * Wrap a stream around a RandomAccessFile. The RandomAccessFile is shared 432 * among all streams returned by getInputStream(), so we have to synchronize 433 * access to it. (We can optimize this by adding buffering here to reduce 434 * collisions.) 435 * 436 * <p>We could support mark/reset, but we don't currently need them. 437 */ 438 static class RAFStream extends InputStream { 439 private final RandomAccessFile sharedRaf; 440 private long endOffset; 441 private long offset; 442 443 public RAFStream(RandomAccessFile raf, long initialOffset) throws IOException { 444 sharedRaf = raf; 445 offset = initialOffset; 446 endOffset = raf.length(); 447 } 448 449 @Override public int available() throws IOException { 450 return (offset < endOffset ? 1 : 0); 451 } 452 453 @Override public int read() throws IOException { 454 return Streams.readSingleByte(this); 455 } 456 457 @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { 458 synchronized (sharedRaf) { 459 final long length = endOffset - offset; 460 if (byteCount > length) { 461 byteCount = (int) length; 462 } 463 sharedRaf.seek(offset); 464 int count = sharedRaf.read(buffer, byteOffset, byteCount); 465 if (count > 0) { 466 offset += count; 467 return count; 468 } else { 469 return -1; 470 } 471 } 472 } 473 474 @Override public long skip(long byteCount) throws IOException { 475 if (byteCount > endOffset - offset) { 476 byteCount = endOffset - offset; 477 } 478 offset += byteCount; 479 return byteCount; 480 } 481 482 public int fill(Inflater inflater, int nativeEndBufSize) throws IOException { 483 synchronized (sharedRaf) { 484 int len = Math.min((int) (endOffset - offset), nativeEndBufSize); 485 int cnt = inflater.setFileInput(sharedRaf.getFD(), offset, nativeEndBufSize); 486 // setFileInput read from the file, so we need to get the OS and RAFStream back 487 // in sync... 488 skip(cnt); 489 return len; 490 } 491 } 492 } 493 494 static class ZipInflaterInputStream extends InflaterInputStream { 495 private final ZipEntry entry; 496 private long bytesRead = 0; 497 498 public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) { 499 super(is, inf, bsize); 500 this.entry = entry; 501 } 502 503 @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException { 504 final int i; 505 try { 506 i = super.read(buffer, byteOffset, byteCount); 507 } catch (IOException e) { 508 throw new IOException("Error reading data for " + entry.getName() + " near offset " 509 + bytesRead, e); 510 } 511 if (i == -1) { 512 if (entry.size != bytesRead) { 513 throw new IOException("Size mismatch on inflated file: " + bytesRead + " vs " 514 + entry.size); 515 } 516 } else { 517 bytesRead += i; 518 } 519 return i; 520 } 521 522 @Override public int available() throws IOException { 523 if (closed) { 524 // Our superclass will throw an exception, but there's a jtreg test that 525 // explicitly checks that the InputStream returned from ZipFile.getInputStream 526 // returns 0 even when closed. 527 return 0; 528 } 529 return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead); 530 } 531 } 532} 533