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 18 package java.util.zip; 19 20 import dalvik.system.CloseGuard; 21 import java.io.BufferedInputStream; 22 import java.io.EOFException; 23 import java.io.DataInputStream; 24 import java.io.File; 25 import java.io.IOException; 26 import java.io.InputStream; 27 import java.io.RandomAccessFile; 28 import java.nio.ByteOrder; 29 import java.util.Enumeration; 30 import java.util.Iterator; 31 import java.util.LinkedHashMap; 32 import libcore.io.BufferIterator; 33 import libcore.io.HeapBufferIterator; 34 import libcore.io.Streams; 35 36 /** 37 * This class provides random read access to a <i>ZIP-archive</i> file. 38 * <p> 39 * While {@code ZipInputStream} provides stream based read access to a 40 * <i>ZIP-archive</i>, this class implements more efficient (file based) access 41 * and makes use of the <i>central directory</i> within a <i>ZIP-archive</i>. 42 * <p> 43 * Use {@code ZipOutputStream} if you want to create an archive. 44 * <p> 45 * A temporary ZIP file can be marked for automatic deletion upon closing it. 46 * 47 * @see ZipEntry 48 * @see ZipOutputStream 49 */ 50 public class ZipFile implements ZipConstants { 51 /** 52 * General Purpose Bit Flags, Bit 3. 53 * If this bit is set, the fields crc-32, compressed 54 * size and uncompressed size are set to zero in the 55 * local header. The correct values are put in the 56 * data descriptor immediately following the compressed 57 * data. (Note: PKZIP version 2.04g for DOS only 58 * recognizes this bit for method 8 compression, newer 59 * versions of PKZIP recognize this bit for any 60 * compression method.) 61 */ 62 static final int GPBF_DATA_DESCRIPTOR_FLAG = 1 << 3; 63 64 /** 65 * General Purpose Bit Flags, Bit 11. 66 * Language encoding flag (EFS). If this bit is set, 67 * the filename and comment fields for this file 68 * must be encoded using UTF-8. 69 */ 70 static final int GPBF_UTF8_FLAG = 1 << 11; 71 72 /** 73 * Open ZIP file for read. 74 */ 75 public static final int OPEN_READ = 1; 76 77 /** 78 * Delete ZIP file when closed. 79 */ 80 public static final int OPEN_DELETE = 4; 81 82 private final String fileName; 83 84 private File fileToDeleteOnClose; 85 86 private RandomAccessFile mRaf; 87 88 private final LinkedHashMap<String, ZipEntry> mEntries = new LinkedHashMap<String, ZipEntry>(); 89 90 private final CloseGuard guard = CloseGuard.get(); 91 92 /** 93 * Constructs a new {@code ZipFile} with the specified file. 94 * 95 * @param file 96 * the file to read from. 97 * @throws ZipException 98 * if a ZIP error occurs. 99 * @throws IOException 100 * if an {@code IOException} occurs. 101 */ 102 public ZipFile(File file) throws ZipException, IOException { 103 this(file, OPEN_READ); 104 } 105 106 /** 107 * Opens a file as <i>ZIP-archive</i>. "mode" must be {@code OPEN_READ} or 108 * {@code OPEN_DELETE} . The latter sets the "delete on exit" flag through a 109 * file. 110 * 111 * @param file 112 * the ZIP file to read. 113 * @param mode 114 * the mode of the file open operation. 115 * @throws IOException 116 * if an {@code IOException} occurs. 117 */ 118 public ZipFile(File file, int mode) throws IOException { 119 fileName = file.getPath(); 120 if (mode != OPEN_READ && mode != (OPEN_READ | OPEN_DELETE)) { 121 throw new IllegalArgumentException(); 122 } 123 124 if ((mode & OPEN_DELETE) != 0) { 125 fileToDeleteOnClose = file; // file.deleteOnExit(); 126 } else { 127 fileToDeleteOnClose = null; 128 } 129 130 mRaf = new RandomAccessFile(fileName, "r"); 131 132 readCentralDir(); 133 guard.open("close"); 134 } 135 136 /** 137 * Opens a ZIP archived file. 138 * 139 * @param name 140 * the name of the ZIP file. 141 * @throws IOException 142 * if an IOException occurs. 143 */ 144 public ZipFile(String name) throws IOException { 145 this(new File(name), OPEN_READ); 146 } 147 148 @Override protected void finalize() throws IOException { 149 try { 150 if (guard != null) { 151 guard.warnIfOpen(); 152 } 153 } finally { 154 try { 155 super.finalize(); 156 } catch (Throwable t) { 157 throw new AssertionError(t); 158 } 159 } 160 } 161 162 /** 163 * Closes this ZIP file. This method is idempotent. 164 * 165 * @throws IOException 166 * if an IOException occurs. 167 */ 168 public void close() throws IOException { 169 guard.close(); 170 RandomAccessFile raf = mRaf; 171 172 if (raf != null) { // Only close initialized instances 173 synchronized(raf) { 174 mRaf = null; 175 raf.close(); 176 } 177 if (fileToDeleteOnClose != null) { 178 fileToDeleteOnClose.delete(); 179 fileToDeleteOnClose = null; 180 } 181 } 182 } 183 184 private void checkNotClosed() { 185 if (mRaf == null) { 186 throw new IllegalStateException("Zip file closed"); 187 } 188 } 189 190 /** 191 * Returns an enumeration of the entries. The entries are listed in the 192 * order in which they appear in the ZIP archive. 193 * 194 * @return the enumeration of the entries. 195 * @throws IllegalStateException if this ZIP file has been closed. 196 */ 197 public Enumeration<? extends ZipEntry> entries() { 198 checkNotClosed(); 199 final Iterator<ZipEntry> iterator = mEntries.values().iterator(); 200 201 return new Enumeration<ZipEntry>() { 202 public boolean hasMoreElements() { 203 checkNotClosed(); 204 return iterator.hasNext(); 205 } 206 207 public ZipEntry nextElement() { 208 checkNotClosed(); 209 return iterator.next(); 210 } 211 }; 212 } 213 214 /** 215 * Gets the ZIP entry with the specified name from this {@code ZipFile}. 216 * 217 * @param entryName 218 * the name of the entry in the ZIP file. 219 * @return a {@code ZipEntry} or {@code null} if the entry name does not 220 * exist in the ZIP file. 221 * @throws IllegalStateException if this ZIP file has been closed. 222 */ 223 public ZipEntry getEntry(String entryName) { 224 checkNotClosed(); 225 if (entryName == null) { 226 throw new NullPointerException("entryName == null"); 227 } 228 229 ZipEntry ze = mEntries.get(entryName); 230 if (ze == null) { 231 ze = mEntries.get(entryName + "/"); 232 } 233 return ze; 234 } 235 236 /** 237 * Returns an input stream on the data of the specified {@code ZipEntry}. 238 * 239 * @param entry 240 * the ZipEntry. 241 * @return an input stream of the data contained in the {@code ZipEntry}. 242 * @throws IOException 243 * if an {@code IOException} occurs. 244 * @throws IllegalStateException if this ZIP file has been closed. 245 */ 246 public InputStream getInputStream(ZipEntry entry) throws IOException { 247 // Make sure this ZipEntry is in this Zip file. We run it through the name lookup. 248 entry = getEntry(entry.getName()); 249 if (entry == null) { 250 return null; 251 } 252 253 // Create an InputStream at the right part of the file. 254 RandomAccessFile raf = mRaf; 255 synchronized (raf) { 256 // We don't know the entry data's start position. All we have is the 257 // position of the entry's local header. At position 28 we find the 258 // length of the extra data. In some cases this length differs from 259 // the one coming in the central header. 260 RAFStream rafstrm = new RAFStream(raf, entry.mLocalHeaderRelOffset + 28); 261 DataInputStream is = new DataInputStream(rafstrm); 262 int localExtraLenOrWhatever = Short.reverseBytes(is.readShort()); 263 is.close(); 264 265 // Skip the name and this "extra" data or whatever it is: 266 rafstrm.skip(entry.nameLength + localExtraLenOrWhatever); 267 rafstrm.mLength = rafstrm.mOffset + entry.compressedSize; 268 if (entry.compressionMethod == ZipEntry.DEFLATED) { 269 int bufSize = Math.max(1024, (int)Math.min(entry.getSize(), 65535L)); 270 return new ZipInflaterInputStream(rafstrm, new Inflater(true), bufSize, entry); 271 } else { 272 return rafstrm; 273 } 274 } 275 } 276 277 /** 278 * Gets the file name of this {@code ZipFile}. 279 * 280 * @return the file name of this {@code ZipFile}. 281 */ 282 public String getName() { 283 return fileName; 284 } 285 286 /** 287 * Returns the number of {@code ZipEntries} in this {@code ZipFile}. 288 * 289 * @return the number of entries in this file. 290 * @throws IllegalStateException if this ZIP file has been closed. 291 */ 292 public int size() { 293 checkNotClosed(); 294 return mEntries.size(); 295 } 296 297 /** 298 * Find the central directory and read the contents. 299 * 300 * <p>The central directory can be followed by a variable-length comment 301 * field, so we have to scan through it backwards. The comment is at 302 * most 64K, plus we have 18 bytes for the end-of-central-dir stuff 303 * itself, plus apparently sometimes people throw random junk on the end 304 * just for the fun of it. 305 * 306 * <p>This is all a little wobbly. If the wrong value ends up in the EOCD 307 * area, we're hosed. This appears to be the way that everybody handles 308 * it though, so we're in good company if this fails. 309 */ 310 private void readCentralDir() throws IOException { 311 /* 312 * Scan back, looking for the End Of Central Directory field. If 313 * the archive doesn't have a comment, we'll hit it on the first 314 * try. 315 * 316 * No need to synchronize mRaf here -- we only do this when we 317 * first open the Zip file. 318 */ 319 long scanOffset = mRaf.length() - ENDHDR; 320 if (scanOffset < 0) { 321 throw new ZipException("File too short to be a zip file: " + mRaf.length()); 322 } 323 324 long stopOffset = scanOffset - 65536; 325 if (stopOffset < 0) { 326 stopOffset = 0; 327 } 328 329 final int ENDHEADERMAGIC = 0x06054b50; 330 while (true) { 331 mRaf.seek(scanOffset); 332 if (Integer.reverseBytes(mRaf.readInt()) == ENDHEADERMAGIC) { 333 break; 334 } 335 336 scanOffset--; 337 if (scanOffset < stopOffset) { 338 throw new ZipException("EOCD not found; not a Zip archive?"); 339 } 340 } 341 342 // Read the End Of Central Directory. We could use ENDHDR instead of the magic number 18, 343 // but we don't actually need all the header. 344 byte[] eocd = new byte[18]; 345 mRaf.readFully(eocd); 346 347 // Pull out the information we need. 348 BufferIterator it = HeapBufferIterator.iterator(eocd, 0, eocd.length, ByteOrder.LITTLE_ENDIAN); 349 int diskNumber = it.readShort() & 0xffff; 350 int diskWithCentralDir = it.readShort() & 0xffff; 351 int numEntries = it.readShort() & 0xffff; 352 int totalNumEntries = it.readShort() & 0xffff; 353 it.skip(4); // Ignore centralDirSize. 354 int centralDirOffset = it.readInt(); 355 356 if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDir != 0) { 357 throw new ZipException("spanned archives not supported"); 358 } 359 360 // Seek to the first CDE and read all entries. 361 RAFStream rafs = new RAFStream(mRaf, centralDirOffset); 362 BufferedInputStream bin = new BufferedInputStream(rafs, 4096); 363 byte[] hdrBuf = new byte[CENHDR]; // Reuse the same buffer for each entry. 364 for (int i = 0; i < numEntries; ++i) { 365 ZipEntry newEntry = new ZipEntry(hdrBuf, bin); 366 mEntries.put(newEntry.getName(), newEntry); 367 } 368 } 369 370 /** 371 * Wrap a stream around a RandomAccessFile. The RandomAccessFile is shared 372 * among all streams returned by getInputStream(), so we have to synchronize 373 * access to it. (We can optimize this by adding buffering here to reduce 374 * collisions.) 375 * 376 * <p>We could support mark/reset, but we don't currently need them. 377 */ 378 static class RAFStream extends InputStream { 379 380 RandomAccessFile mSharedRaf; 381 long mOffset; 382 long mLength; 383 384 public RAFStream(RandomAccessFile raf, long pos) throws IOException { 385 mSharedRaf = raf; 386 mOffset = pos; 387 mLength = raf.length(); 388 } 389 390 @Override public int available() throws IOException { 391 return (mOffset < mLength ? 1 : 0); 392 } 393 394 @Override public int read() throws IOException { 395 return Streams.readSingleByte(this); 396 } 397 398 @Override public int read(byte[] b, int off, int len) throws IOException { 399 synchronized (mSharedRaf) { 400 mSharedRaf.seek(mOffset); 401 if (len > mLength - mOffset) { 402 len = (int) (mLength - mOffset); 403 } 404 int count = mSharedRaf.read(b, off, len); 405 if (count > 0) { 406 mOffset += count; 407 return count; 408 } else { 409 return -1; 410 } 411 } 412 } 413 414 @Override 415 public long skip(long byteCount) throws IOException { 416 if (byteCount > mLength - mOffset) { 417 byteCount = mLength - mOffset; 418 } 419 mOffset += byteCount; 420 return byteCount; 421 } 422 } 423 424 static class ZipInflaterInputStream extends InflaterInputStream { 425 426 ZipEntry entry; 427 long bytesRead = 0; 428 429 public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) { 430 super(is, inf, bsize); 431 this.entry = entry; 432 } 433 434 @Override 435 public int read(byte[] buffer, int off, int nbytes) throws IOException { 436 int i = super.read(buffer, off, nbytes); 437 if (i != -1) { 438 bytesRead += i; 439 } 440 return i; 441 } 442 443 @Override 444 public int available() throws IOException { 445 if (closed) { 446 // Our superclass will throw an exception, but there's a jtreg test that 447 // explicitly checks that the InputStream returned from ZipFile.getInputStream 448 // returns 0 even when closed. 449 return 0; 450 } 451 return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead); 452 } 453 } 454 } 455