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.nio.ByteOrder; 23 import java.nio.charset.Charset; 24 import java.nio.charset.StandardCharsets; 25 import java.util.Arrays; 26 import java.util.Calendar; 27 import java.util.Date; 28 import java.util.GregorianCalendar; 29 import libcore.io.BufferIterator; 30 import libcore.io.HeapBufferIterator; 31 import libcore.io.Streams; 32 33 /** 34 * An entry within a zip file. 35 * An entry has attributes such as its name (which is actually a path) and the uncompressed size 36 * of the corresponding data. An entry does not contain the data itself, but can be used as a key 37 * with {@link ZipFile#getInputStream}. The class documentation for {@link ZipInputStream} and 38 * {@link ZipOutputStream} shows how {@code ZipEntry} is used in conjunction with those two classes. 39 */ 40 public class ZipEntry implements ZipConstants, Cloneable { 41 String name; 42 String comment; 43 44 long crc = -1; // Needs to be a long to distinguish -1 ("not set") from the 0xffffffff CRC32. 45 46 long compressedSize = -1; 47 long size = -1; 48 49 int compressionMethod = -1; 50 int time = -1; 51 int modDate = -1; 52 53 byte[] extra; 54 55 int nameLength = -1; 56 long localHeaderRelOffset = -1; 57 58 long dataOffset = -1; 59 60 /** 61 * Zip entry state: Deflated. 62 */ 63 public static final int DEFLATED = 8; 64 65 /** 66 * Zip entry state: Stored. 67 */ 68 public static final int STORED = 0; 69 ZipEntry(String name, String comment, long crc, long compressedSize, long size, int compressionMethod, int time, int modDate, byte[] extra, int nameLength, long localHeaderRelOffset, long dataOffset)70 ZipEntry(String name, String comment, long crc, long compressedSize, 71 long size, int compressionMethod, int time, int modDate, byte[] extra, 72 int nameLength, long localHeaderRelOffset, long dataOffset) { 73 this.name = name; 74 this.comment = comment; 75 this.crc = crc; 76 this.compressedSize = compressedSize; 77 this.size = size; 78 this.compressionMethod = compressionMethod; 79 this.time = time; 80 this.modDate = modDate; 81 this.extra = extra; 82 this.nameLength = nameLength; 83 this.localHeaderRelOffset = localHeaderRelOffset; 84 this.dataOffset = dataOffset; 85 } 86 87 /** 88 * Constructs a new {@code ZipEntry} with the specified name. The name is actually a path, 89 * and may contain {@code /} characters. 90 * 91 * @throws IllegalArgumentException 92 * if the name length is outside the range (> 0xFFFF). 93 */ ZipEntry(String name)94 public ZipEntry(String name) { 95 if (name == null) { 96 throw new NullPointerException("name == null"); 97 } 98 validateStringLength("Name", name); 99 this.name = name; 100 } 101 102 /** 103 * Returns the comment for this {@code ZipEntry}, or {@code null} if there is no comment. 104 * If we're reading a zip file using {@code ZipInputStream}, the comment is not available. 105 */ getComment()106 public String getComment() { 107 return comment; 108 } 109 110 /** 111 * Gets the compressed size of this {@code ZipEntry}. 112 * 113 * @return the compressed size, or -1 if the compressed size has not been 114 * set. 115 */ getCompressedSize()116 public long getCompressedSize() { 117 return compressedSize; 118 } 119 120 /** 121 * Gets the checksum for this {@code ZipEntry}. 122 * 123 * @return the checksum, or -1 if the checksum has not been set. 124 */ getCrc()125 public long getCrc() { 126 return crc; 127 } 128 129 /** 130 * Gets the extra information for this {@code ZipEntry}. 131 * 132 * @return a byte array containing the extra information, or {@code null} if 133 * there is none. 134 */ getExtra()135 public byte[] getExtra() { 136 return extra; 137 } 138 139 /** 140 * Gets the compression method for this {@code ZipEntry}. 141 * 142 * @return the compression method, either {@code DEFLATED}, {@code STORED} 143 * or -1 if the compression method has not been set. 144 */ getMethod()145 public int getMethod() { 146 return compressionMethod; 147 } 148 149 /** 150 * Gets the name of this {@code ZipEntry}. 151 * 152 * @return the entry name. 153 */ getName()154 public String getName() { 155 return name; 156 } 157 158 /** 159 * Gets the uncompressed size of this {@code ZipEntry}. 160 * 161 * @return the uncompressed size, or {@code -1} if the size has not been 162 * set. 163 */ getSize()164 public long getSize() { 165 return size; 166 } 167 168 /** 169 * Gets the last modification time of this {@code ZipEntry}. 170 * 171 * @return the last modification time as the number of milliseconds since 172 * Jan. 1, 1970. 173 */ getTime()174 public long getTime() { 175 if (time != -1) { 176 GregorianCalendar cal = new GregorianCalendar(); 177 cal.set(Calendar.MILLISECOND, 0); 178 cal.set(1980 + ((modDate >> 9) & 0x7f), ((modDate >> 5) & 0xf) - 1, 179 modDate & 0x1f, (time >> 11) & 0x1f, (time >> 5) & 0x3f, 180 (time & 0x1f) << 1); 181 return cal.getTime().getTime(); 182 } 183 return -1; 184 } 185 186 /** 187 * Determine whether or not this {@code ZipEntry} is a directory. 188 * 189 * @return {@code true} when this {@code ZipEntry} is a directory, {@code 190 * false} otherwise. 191 */ isDirectory()192 public boolean isDirectory() { 193 return name.charAt(name.length() - 1) == '/'; 194 } 195 196 /** 197 * Sets the comment for this {@code ZipEntry}. 198 * @throws IllegalArgumentException if the comment is >= 64 Ki UTF-8 bytes. 199 */ setComment(String comment)200 public void setComment(String comment) { 201 if (comment == null) { 202 this.comment = null; 203 return; 204 } 205 validateStringLength("Comment", comment); 206 207 this.comment = comment; 208 } 209 210 /** 211 * Sets the compressed size for this {@code ZipEntry}. 212 * 213 * @param value 214 * the compressed size (in bytes). 215 */ setCompressedSize(long value)216 public void setCompressedSize(long value) { 217 compressedSize = value; 218 } 219 220 /** 221 * Sets the checksum for this {@code ZipEntry}. 222 * 223 * @param value 224 * the checksum for this entry. 225 * @throws IllegalArgumentException 226 * if {@code value} is < 0 or > 0xFFFFFFFFL. 227 */ setCrc(long value)228 public void setCrc(long value) { 229 if (value >= 0 && value <= 0xFFFFFFFFL) { 230 crc = value; 231 } else { 232 throw new IllegalArgumentException("Bad CRC32: " + value); 233 } 234 } 235 236 /** 237 * Sets the extra information for this {@code ZipEntry}. 238 * 239 * @throws IllegalArgumentException if the data length >= 64 KiB. 240 */ setExtra(byte[] data)241 public void setExtra(byte[] data) { 242 if (data != null && data.length > 0xffff) { 243 throw new IllegalArgumentException("Extra data too long: " + data.length); 244 } 245 extra = data; 246 } 247 248 /** 249 * Sets the compression method for this entry to either {@code DEFLATED} or {@code STORED}. 250 * The default is {@code DEFLATED}, which will cause the size, compressed size, and CRC to be 251 * set automatically, and the entry's data to be compressed. If you switch to {@code STORED} 252 * note that you'll have to set the size (or compressed size; they must be the same, but it's 253 * okay to only set one) and CRC yourself because they must appear <i>before</i> the user data 254 * in the resulting zip file. See {@link #setSize} and {@link #setCrc}. 255 * @throws IllegalArgumentException 256 * when value is not {@code DEFLATED} or {@code STORED}. 257 */ setMethod(int value)258 public void setMethod(int value) { 259 if (value != STORED && value != DEFLATED) { 260 throw new IllegalArgumentException("Bad method: " + value); 261 } 262 compressionMethod = value; 263 } 264 265 /** 266 * Sets the uncompressed size of this {@code ZipEntry}. 267 * 268 * @param value 269 * the uncompressed size for this entry. 270 * @throws IllegalArgumentException 271 * if {@code value} < 0 or {@code value} > 0xFFFFFFFFL. 272 */ setSize(long value)273 public void setSize(long value) { 274 if (value >= 0 && value <= 0xFFFFFFFFL) { 275 size = value; 276 } else { 277 throw new IllegalArgumentException("Bad size: " + value); 278 } 279 } 280 281 /** 282 * Sets the modification time of this {@code ZipEntry}. 283 * 284 * @param value 285 * the modification time as the number of milliseconds since Jan. 286 * 1, 1970. 287 */ setTime(long value)288 public void setTime(long value) { 289 GregorianCalendar cal = new GregorianCalendar(); 290 cal.setTime(new Date(value)); 291 int year = cal.get(Calendar.YEAR); 292 if (year < 1980) { 293 modDate = 0x21; 294 time = 0; 295 } else { 296 modDate = cal.get(Calendar.DATE); 297 modDate = (cal.get(Calendar.MONTH) + 1 << 5) | modDate; 298 modDate = ((cal.get(Calendar.YEAR) - 1980) << 9) | modDate; 299 time = cal.get(Calendar.SECOND) >> 1; 300 time = (cal.get(Calendar.MINUTE) << 5) | time; 301 time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time; 302 } 303 } 304 305 306 /** @hide */ setDataOffset(long value)307 public void setDataOffset(long value) { 308 dataOffset = value; 309 } 310 311 /** @hide */ getDataOffset()312 public long getDataOffset() { 313 return dataOffset; 314 } 315 316 /** 317 * Returns the string representation of this {@code ZipEntry}. 318 * 319 * @return the string representation of this {@code ZipEntry}. 320 */ 321 @Override toString()322 public String toString() { 323 return name; 324 } 325 326 /** 327 * Constructs a new {@code ZipEntry} using the values obtained from {@code 328 * ze}. 329 * 330 * @param ze 331 * the {@code ZipEntry} from which to obtain values. 332 */ ZipEntry(ZipEntry ze)333 public ZipEntry(ZipEntry ze) { 334 name = ze.name; 335 comment = ze.comment; 336 time = ze.time; 337 size = ze.size; 338 compressedSize = ze.compressedSize; 339 crc = ze.crc; 340 compressionMethod = ze.compressionMethod; 341 modDate = ze.modDate; 342 extra = ze.extra; 343 nameLength = ze.nameLength; 344 localHeaderRelOffset = ze.localHeaderRelOffset; 345 dataOffset = ze.dataOffset; 346 } 347 348 /** 349 * Returns a deep copy of this zip entry. 350 */ clone()351 @Override public Object clone() { 352 try { 353 ZipEntry result = (ZipEntry) super.clone(); 354 result.extra = extra != null ? extra.clone() : null; 355 return result; 356 } catch (CloneNotSupportedException e) { 357 throw new AssertionError(e); 358 } 359 } 360 361 /** 362 * Returns the hash code for this {@code ZipEntry}. 363 * 364 * @return the hash code of the entry. 365 */ 366 @Override hashCode()367 public int hashCode() { 368 return name.hashCode(); 369 } 370 371 /* 372 * Internal constructor. Creates a new ZipEntry by reading the 373 * Central Directory Entry (CDE) from "in", which must be positioned 374 * at the CDE signature. If the GPBF_UTF8_FLAG is set in the CDE then 375 * UTF-8 is used to decode the string information, otherwise the 376 * defaultCharset is used. 377 * 378 * On exit, "in" will be positioned at the start of the next entry 379 * in the Central Directory. 380 */ ZipEntry(byte[] cdeHdrBuf, InputStream cdStream, Charset defaultCharset)381 ZipEntry(byte[] cdeHdrBuf, InputStream cdStream, Charset defaultCharset) throws IOException { 382 Streams.readFully(cdStream, cdeHdrBuf, 0, cdeHdrBuf.length); 383 384 BufferIterator it = HeapBufferIterator.iterator(cdeHdrBuf, 0, cdeHdrBuf.length, 385 ByteOrder.LITTLE_ENDIAN); 386 387 int sig = it.readInt(); 388 if (sig != CENSIG) { 389 ZipFile.throwZipException("Central Directory Entry", sig); 390 } 391 392 it.seek(8); 393 int gpbf = it.readShort() & 0xffff; 394 395 if ((gpbf & ZipFile.GPBF_UNSUPPORTED_MASK) != 0) { 396 throw new ZipException("Invalid General Purpose Bit Flag: " + gpbf); 397 } 398 399 // If the GPBF_UTF8_FLAG is set then the character encoding is UTF-8 whatever the default 400 // provided. 401 Charset charset = defaultCharset; 402 if ((gpbf & ZipFile.GPBF_UTF8_FLAG) != 0) { 403 charset = StandardCharsets.UTF_8; 404 } 405 406 compressionMethod = it.readShort() & 0xffff; 407 time = it.readShort() & 0xffff; 408 modDate = it.readShort() & 0xffff; 409 410 // These are 32-bit values in the file, but 64-bit fields in this object. 411 crc = ((long) it.readInt()) & 0xffffffffL; 412 compressedSize = ((long) it.readInt()) & 0xffffffffL; 413 size = ((long) it.readInt()) & 0xffffffffL; 414 415 nameLength = it.readShort() & 0xffff; 416 int extraLength = it.readShort() & 0xffff; 417 int commentByteCount = it.readShort() & 0xffff; 418 419 // This is a 32-bit value in the file, but a 64-bit field in this object. 420 it.seek(42); 421 localHeaderRelOffset = ((long) it.readInt()) & 0xffffffffL; 422 423 byte[] nameBytes = new byte[nameLength]; 424 Streams.readFully(cdStream, nameBytes, 0, nameBytes.length); 425 if (containsNulByte(nameBytes)) { 426 throw new ZipException("Filename contains NUL byte: " + Arrays.toString(nameBytes)); 427 } 428 name = new String(nameBytes, 0, nameBytes.length, charset); 429 430 if (extraLength > 0) { 431 extra = new byte[extraLength]; 432 Streams.readFully(cdStream, extra, 0, extraLength); 433 } 434 435 if (commentByteCount > 0) { 436 byte[] commentBytes = new byte[commentByteCount]; 437 Streams.readFully(cdStream, commentBytes, 0, commentByteCount); 438 comment = new String(commentBytes, 0, commentBytes.length, charset); 439 } 440 } 441 containsNulByte(byte[] bytes)442 private static boolean containsNulByte(byte[] bytes) { 443 for (byte b : bytes) { 444 if (b == 0) { 445 return true; 446 } 447 } 448 return false; 449 } 450 validateStringLength(String argument, String string)451 private static void validateStringLength(String argument, String string) { 452 // This check is not perfect: the character encoding is determined when the entry is 453 // written out. UTF-8 is probably a worst-case: most alternatives should be single byte per 454 // character. 455 byte[] bytes = string.getBytes(StandardCharsets.UTF_8); 456 if (bytes.length > 0xffff) { 457 throw new IllegalArgumentException(argument + " too long: " + bytes.length); 458 } 459 } 460 } 461