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 java.io.IOException; 21 import java.io.InputStream; 22 import java.io.PushbackInputStream; 23 import java.nio.ByteOrder; 24 import java.nio.charset.ModifiedUtf8; 25 import java.util.jar.Attributes; 26 import java.util.jar.JarEntry; 27 import java.util.Arrays; 28 import libcore.io.Memory; 29 import libcore.io.Streams; 30 31 /** 32 * This class provides an implementation of {@code FilterInputStream} that 33 * decompresses data from an {@code InputStream} containing a ZIP archive. 34 * 35 * <p>A ZIP archive is a collection of (possibly) compressed files. 36 * When reading from a {@code ZipInputStream}, you retrieve the 37 * entry's metadata with {@code getNextEntry} before you can read the userdata. 38 * 39 * <p>Although {@code InflaterInputStream} can only read compressed ZIP archive 40 * entries, this class can read non-compressed entries as well. 41 * 42 * <p>Use {@code ZipFile} if you can access the archive as a file directly, 43 * especially if you want random access to entries, rather than needing to 44 * iterate over all entries. 45 * 46 * <h3>Example</h3> 47 * <p>Using {@code ZipInputStream} is a little more complicated than {@link GZIPInputStream} 48 * because ZIP archives are containers that can contain multiple files. This code pulls all the 49 * files out of a ZIP archive, similar to the {@code unzip(1)} utility. 50 * <pre> 51 * InputStream is = ... 52 * ZipInputStream zis = new ZipInputStream(new BufferedInputStream(is)); 53 * try { 54 * ZipEntry ze; 55 * while ((ze = zis.getNextEntry()) != null) { 56 * ByteArrayOutputStream baos = new ByteArrayOutputStream(); 57 * byte[] buffer = new byte[1024]; 58 * int count; 59 * while ((count = zis.read(buffer)) != -1) { 60 * baos.write(buffer, 0, count); 61 * } 62 * String filename = ze.getName(); 63 * byte[] bytes = baos.toByteArray(); 64 * // do something with 'filename' and 'bytes'... 65 * } 66 * } finally { 67 * zis.close(); 68 * } 69 * </pre> 70 * 71 * @see ZipEntry 72 * @see ZipFile 73 */ 74 public class ZipInputStream extends InflaterInputStream implements ZipConstants { 75 private static final int ZIPLocalHeaderVersionNeeded = 20; 76 77 private boolean entriesEnd = false; 78 79 private boolean hasDD = false; 80 81 private int entryIn = 0; 82 83 private int inRead, lastRead = 0; 84 85 private ZipEntry currentEntry; 86 87 private final byte[] hdrBuf = new byte[LOCHDR - LOCVER]; 88 89 private final CRC32 crc = new CRC32(); 90 91 private byte[] nameBuf = new byte[256]; 92 93 private char[] charBuf = new char[256]; 94 95 /** 96 * Constructs a new {@code ZipInputStream} from the specified input stream. 97 * 98 * @param stream 99 * the input stream to representing a ZIP archive. 100 */ 101 public ZipInputStream(InputStream stream) { 102 super(new PushbackInputStream(stream, BUF_SIZE), new Inflater(true)); 103 if (stream == null) { 104 throw new NullPointerException("stream == null"); 105 } 106 } 107 108 /** 109 * Closes this {@code ZipInputStream}. 110 * 111 * @throws IOException 112 * if an {@code IOException} occurs. 113 */ 114 @Override 115 public void close() throws IOException { 116 if (!closed) { 117 closeEntry(); // Close the current entry 118 super.close(); 119 } 120 } 121 122 /** 123 * Closes the current ZIP entry and positions to read the next entry. 124 * 125 * @throws IOException 126 * if an {@code IOException} occurs. 127 */ 128 public void closeEntry() throws IOException { 129 checkClosed(); 130 if (currentEntry == null) { 131 return; 132 } 133 if (currentEntry instanceof java.util.jar.JarEntry) { 134 Attributes temp = ((JarEntry) currentEntry).getAttributes(); 135 if (temp != null && temp.containsKey("hidden")) { 136 return; 137 } 138 } 139 140 /* 141 * The following code is careful to leave the ZipInputStream in a 142 * consistent state, even when close() results in an exception. It does 143 * so by: 144 * - pushing bytes back into the source stream 145 * - reading a data descriptor footer from the source stream 146 * - resetting fields that manage the entry being closed 147 */ 148 149 // Ensure all entry bytes are read 150 Exception failure = null; 151 try { 152 Streams.skipAll(this); 153 } catch (Exception e) { 154 failure = e; 155 } 156 157 int inB, out; 158 if (currentEntry.compressionMethod == ZipEntry.DEFLATED) { 159 inB = inf.getTotalIn(); 160 out = inf.getTotalOut(); 161 } else { 162 inB = inRead; 163 out = inRead; 164 } 165 int diff = entryIn - inB; 166 // Pushback any required bytes 167 if (diff != 0) { 168 ((PushbackInputStream) in).unread(buf, len - diff, diff); 169 } 170 171 try { 172 readAndVerifyDataDescriptor(inB, out); 173 } catch (Exception e) { 174 if (failure == null) { // otherwise we're already going to throw 175 failure = e; 176 } 177 } 178 179 inf.reset(); 180 lastRead = inRead = entryIn = len = 0; 181 crc.reset(); 182 currentEntry = null; 183 184 if (failure != null) { 185 if (failure instanceof IOException) { 186 throw (IOException) failure; 187 } else if (failure instanceof RuntimeException) { 188 throw (RuntimeException) failure; 189 } 190 AssertionError error = new AssertionError(); 191 error.initCause(failure); 192 throw error; 193 } 194 } 195 196 private void readAndVerifyDataDescriptor(int inB, int out) throws IOException { 197 if (hasDD) { 198 Streams.readFully(in, hdrBuf, 0, EXTHDR); 199 int sig = Memory.peekInt(hdrBuf, 0, ByteOrder.LITTLE_ENDIAN); 200 if (sig != (int) EXTSIG) { 201 throw new ZipException(String.format("unknown format (EXTSIG=%x)", sig)); 202 } 203 currentEntry.crc = ((long) Memory.peekInt(hdrBuf, EXTCRC, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL; 204 currentEntry.compressedSize = ((long) Memory.peekInt(hdrBuf, EXTSIZ, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL; 205 currentEntry.size = ((long) Memory.peekInt(hdrBuf, EXTLEN, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL; 206 } 207 if (currentEntry.crc != crc.getValue()) { 208 throw new ZipException("CRC mismatch"); 209 } 210 if (currentEntry.compressedSize != inB || currentEntry.size != out) { 211 throw new ZipException("Size mismatch"); 212 } 213 } 214 215 /** 216 * Reads the next entry from this {@code ZipInputStream} or {@code null} if 217 * no more entries are present. 218 * 219 * @return the next {@code ZipEntry} contained in the input stream. 220 * @throws IOException 221 * if an {@code IOException} occurs. 222 * @see ZipEntry 223 */ 224 public ZipEntry getNextEntry() throws IOException { 225 closeEntry(); 226 if (entriesEnd) { 227 return null; 228 } 229 230 // Read the signature to see whether there's another local file header. 231 Streams.readFully(in, hdrBuf, 0, 4); 232 int hdr = Memory.peekInt(hdrBuf, 0, ByteOrder.LITTLE_ENDIAN); 233 if (hdr == CENSIG) { 234 entriesEnd = true; 235 return null; 236 } 237 if (hdr != LOCSIG) { 238 return null; 239 } 240 241 // Read the local file header. 242 Streams.readFully(in, hdrBuf, 0, (LOCHDR - LOCVER)); 243 int version = peekShort(0) & 0xff; 244 if (version > ZIPLocalHeaderVersionNeeded) { 245 throw new ZipException("Cannot read local header version " + version); 246 } 247 int flags = peekShort(LOCFLG - LOCVER); 248 hasDD = ((flags & ZipFile.GPBF_DATA_DESCRIPTOR_FLAG) != 0); 249 int ceLastModifiedTime = peekShort(LOCTIM - LOCVER); 250 int ceLastModifiedDate = peekShort(LOCTIM - LOCVER + 2); 251 int ceCompressionMethod = peekShort(LOCHOW - LOCVER); 252 long ceCrc = 0, ceCompressedSize = 0, ceSize = -1; 253 if (!hasDD) { 254 ceCrc = ((long) Memory.peekInt(hdrBuf, LOCCRC - LOCVER, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL; 255 ceCompressedSize = ((long) Memory.peekInt(hdrBuf, LOCSIZ - LOCVER, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL; 256 ceSize = ((long) Memory.peekInt(hdrBuf, LOCLEN - LOCVER, ByteOrder.LITTLE_ENDIAN)) & 0xffffffffL; 257 } 258 int nameLength = peekShort(LOCNAM - LOCVER); 259 if (nameLength == 0) { 260 throw new ZipException("Entry is not named"); 261 } 262 int extraLength = peekShort(LOCEXT - LOCVER); 263 264 if (nameLength > nameBuf.length) { 265 nameBuf = new byte[nameLength]; 266 // The bytes are modified UTF-8, so the number of chars will always be less than or 267 // equal to the number of bytes. It's fine if this buffer is too long. 268 charBuf = new char[nameLength]; 269 } 270 Streams.readFully(in, nameBuf, 0, nameLength); 271 currentEntry = createZipEntry(ModifiedUtf8.decode(nameBuf, charBuf, 0, nameLength)); 272 currentEntry.time = ceLastModifiedTime; 273 currentEntry.modDate = ceLastModifiedDate; 274 currentEntry.setMethod(ceCompressionMethod); 275 if (ceSize != -1) { 276 currentEntry.setCrc(ceCrc); 277 currentEntry.setSize(ceSize); 278 currentEntry.setCompressedSize(ceCompressedSize); 279 } 280 if (extraLength > 0) { 281 byte[] extraData = new byte[extraLength]; 282 Streams.readFully(in, extraData, 0, extraLength); 283 currentEntry.setExtra(extraData); 284 } 285 return currentEntry; 286 } 287 288 private int peekShort(int offset) { 289 return Memory.peekShort(hdrBuf, offset, ByteOrder.LITTLE_ENDIAN) & 0xffff; 290 } 291 292 /** 293 * Reads up to the specified number of uncompressed bytes into the buffer 294 * starting at the offset. 295 * 296 * @return the number of bytes read 297 */ 298 @Override 299 public int read(byte[] buffer, int offset, int byteCount) throws IOException { 300 checkClosed(); 301 Arrays.checkOffsetAndCount(buffer.length, offset, byteCount); 302 303 if (inf.finished() || currentEntry == null) { 304 return -1; 305 } 306 307 if (currentEntry.compressionMethod == ZipEntry.STORED) { 308 int csize = (int) currentEntry.size; 309 if (inRead >= csize) { 310 return -1; 311 } 312 if (lastRead >= len) { 313 lastRead = 0; 314 if ((len = in.read(buf)) == -1) { 315 eof = true; 316 return -1; 317 } 318 entryIn += len; 319 } 320 int toRead = byteCount > (len - lastRead) ? len - lastRead : byteCount; 321 if ((csize - inRead) < toRead) { 322 toRead = csize - inRead; 323 } 324 System.arraycopy(buf, lastRead, buffer, offset, toRead); 325 lastRead += toRead; 326 inRead += toRead; 327 crc.update(buffer, offset, toRead); 328 return toRead; 329 } 330 if (inf.needsInput()) { 331 fill(); 332 if (len > 0) { 333 entryIn += len; 334 } 335 } 336 int read; 337 try { 338 read = inf.inflate(buffer, offset, byteCount); 339 } catch (DataFormatException e) { 340 throw new ZipException(e.getMessage()); 341 } 342 if (read == 0 && inf.finished()) { 343 return -1; 344 } 345 crc.update(buffer, offset, read); 346 return read; 347 } 348 349 @Override 350 public int available() throws IOException { 351 checkClosed(); 352 // The InflaterInputStream contract says we must only return 0 or 1. 353 return (currentEntry == null || inRead < currentEntry.size) ? 1 : 0; 354 } 355 356 /** 357 * creates a {@link ZipEntry } with the given name. 358 * 359 * @param name 360 * the name of the entry. 361 * @return the created {@code ZipEntry}. 362 */ 363 protected ZipEntry createZipEntry(String name) { 364 return new ZipEntry(name); 365 } 366 367 private void checkClosed() throws IOException { 368 if (closed) { 369 throw new IOException("Stream is closed"); 370 } 371 } 372 } 373