001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 * 017 */ 018 package org.apache.commons.compress.archivers.zip; 019 020 import org.apache.commons.compress.archivers.ArchiveEntry; 021 022 import java.io.File; 023 import java.util.ArrayList; 024 import java.util.Arrays; 025 import java.util.Date; 026 import java.util.LinkedHashMap; 027 import java.util.List; 028 import java.util.zip.ZipException; 029 030 /** 031 * Extension that adds better handling of extra fields and provides 032 * access to the internal and external file attributes. 033 * 034 * <p>The extra data is expected to follow the recommendation of 035 * {@link <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT"> 036 * APPNOTE.txt</a>}:</p> 037 * <ul> 038 * <li>the extra byte array consists of a sequence of extra fields</li> 039 * <li>each extra fields starts by a two byte header id followed by 040 * a two byte sequence holding the length of the remainder of 041 * data.</li> 042 * </ul> 043 * 044 * <p>Any extra data that cannot be parsed by the rules above will be 045 * consumed as "unparseable" extra data and treated differently by the 046 * methods of this class. Versions prior to Apache Commons Compress 047 * 1.1 would have thrown an exception if any attempt was made to read 048 * or write extra data not conforming to the recommendation.</p> 049 * 050 * @NotThreadSafe 051 */ 052 public class ZipArchiveEntry extends java.util.zip.ZipEntry 053 implements ArchiveEntry { 054 055 public static final int PLATFORM_UNIX = 3; 056 public static final int PLATFORM_FAT = 0; 057 private static final int SHORT_MASK = 0xFFFF; 058 private static final int SHORT_SHIFT = 16; 059 private static final byte[] EMPTY = new byte[0]; 060 061 /** 062 * The {@link java.util.zip.ZipEntry} base class only supports 063 * the compression methods STORED and DEFLATED. We override the 064 * field so that any compression methods can be used. 065 * <p> 066 * The default value -1 means that the method has not been specified. 067 * 068 * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-93" 069 * >COMPRESS-93</a> 070 */ 071 private int method = -1; 072 073 /** 074 * The {@link java.util.zip.ZipEntry#setSize} method in the base 075 * class throws an IllegalArgumentException if the size is bigger 076 * than 2GB for Java versions < 7. Need to keep our own size 077 * information for Zip64 support. 078 */ 079 private long size = SIZE_UNKNOWN; 080 081 private int internalAttributes = 0; 082 private int platform = PLATFORM_FAT; 083 private long externalAttributes = 0; 084 private LinkedHashMap<ZipShort, ZipExtraField> extraFields = null; 085 private UnparseableExtraFieldData unparseableExtra = null; 086 private String name = null; 087 private byte[] rawName = null; 088 private GeneralPurposeBit gpb = new GeneralPurposeBit(); 089 090 /** 091 * Creates a new zip entry with the specified name. 092 * 093 * <p>Assumes the entry represents a directory if and only if the 094 * name ends with a forward slash "/".</p> 095 * 096 * @param name the name of the entry 097 */ 098 public ZipArchiveEntry(String name) { 099 super(name); 100 setName(name); 101 } 102 103 /** 104 * Creates a new zip entry with fields taken from the specified zip entry. 105 * 106 * <p>Assumes the entry represents a directory if and only if the 107 * name ends with a forward slash "/".</p> 108 * 109 * @param entry the entry to get fields from 110 * @throws ZipException on error 111 */ 112 public ZipArchiveEntry(java.util.zip.ZipEntry entry) throws ZipException { 113 super(entry); 114 setName(entry.getName()); 115 byte[] extra = entry.getExtra(); 116 if (extra != null) { 117 setExtraFields(ExtraFieldUtils.parse(extra, true, 118 ExtraFieldUtils 119 .UnparseableExtraField.READ)); 120 } else { 121 // initializes extra data to an empty byte array 122 setExtra(); 123 } 124 setMethod(entry.getMethod()); 125 this.size = entry.getSize(); 126 } 127 128 /** 129 * Creates a new zip entry with fields taken from the specified zip entry. 130 * 131 * <p>Assumes the entry represents a directory if and only if the 132 * name ends with a forward slash "/".</p> 133 * 134 * @param entry the entry to get fields from 135 * @throws ZipException on error 136 */ 137 public ZipArchiveEntry(ZipArchiveEntry entry) throws ZipException { 138 this((java.util.zip.ZipEntry) entry); 139 setInternalAttributes(entry.getInternalAttributes()); 140 setExternalAttributes(entry.getExternalAttributes()); 141 setExtraFields(entry.getExtraFields(true)); 142 } 143 144 /** 145 */ 146 protected ZipArchiveEntry() { 147 this(""); 148 } 149 150 /** 151 * Creates a new zip entry taking some information from the given 152 * file and using the provided name. 153 * 154 * <p>The name will be adjusted to end with a forward slash "/" if 155 * the file is a directory. If the file is not a directory a 156 * potential trailing forward slash will be stripped from the 157 * entry name.</p> 158 */ 159 public ZipArchiveEntry(File inputFile, String entryName) { 160 this(inputFile.isDirectory() && !entryName.endsWith("/") ? 161 entryName + "/" : entryName); 162 if (inputFile.isFile()){ 163 setSize(inputFile.length()); 164 } 165 setTime(inputFile.lastModified()); 166 // TODO are there any other fields we can set here? 167 } 168 169 /** 170 * Overwrite clone. 171 * @return a cloned copy of this ZipArchiveEntry 172 */ 173 @Override 174 public Object clone() { 175 ZipArchiveEntry e = (ZipArchiveEntry) super.clone(); 176 177 e.setInternalAttributes(getInternalAttributes()); 178 e.setExternalAttributes(getExternalAttributes()); 179 e.setExtraFields(getExtraFields(true)); 180 return e; 181 } 182 183 /** 184 * Returns the compression method of this entry, or -1 if the 185 * compression method has not been specified. 186 * 187 * @return compression method 188 * 189 * @since 1.1 190 */ 191 @Override 192 public int getMethod() { 193 return method; 194 } 195 196 /** 197 * Sets the compression method of this entry. 198 * 199 * @param method compression method 200 * 201 * @since 1.1 202 */ 203 @Override 204 public void setMethod(int method) { 205 if (method < 0) { 206 throw new IllegalArgumentException( 207 "ZIP compression method can not be negative: " + method); 208 } 209 this.method = method; 210 } 211 212 /** 213 * Retrieves the internal file attributes. 214 * 215 * @return the internal file attributes 216 */ 217 public int getInternalAttributes() { 218 return internalAttributes; 219 } 220 221 /** 222 * Sets the internal file attributes. 223 * @param value an <code>int</code> value 224 */ 225 public void setInternalAttributes(int value) { 226 internalAttributes = value; 227 } 228 229 /** 230 * Retrieves the external file attributes. 231 * @return the external file attributes 232 */ 233 public long getExternalAttributes() { 234 return externalAttributes; 235 } 236 237 /** 238 * Sets the external file attributes. 239 * @param value an <code>long</code> value 240 */ 241 public void setExternalAttributes(long value) { 242 externalAttributes = value; 243 } 244 245 /** 246 * Sets Unix permissions in a way that is understood by Info-Zip's 247 * unzip command. 248 * @param mode an <code>int</code> value 249 */ 250 public void setUnixMode(int mode) { 251 // CheckStyle:MagicNumberCheck OFF - no point 252 setExternalAttributes((mode << SHORT_SHIFT) 253 // MS-DOS read-only attribute 254 | ((mode & 0200) == 0 ? 1 : 0) 255 // MS-DOS directory flag 256 | (isDirectory() ? 0x10 : 0)); 257 // CheckStyle:MagicNumberCheck ON 258 platform = PLATFORM_UNIX; 259 } 260 261 /** 262 * Unix permission. 263 * @return the unix permissions 264 */ 265 public int getUnixMode() { 266 return platform != PLATFORM_UNIX ? 0 : 267 (int) ((getExternalAttributes() >> SHORT_SHIFT) & SHORT_MASK); 268 } 269 270 /** 271 * Returns true if this entry represents a unix symlink, 272 * in which case the entry's content contains the target path 273 * for the symlink. 274 * 275 * @since 1.5 276 * @return true if the entry represents a unix symlink, false otherwise. 277 */ 278 public boolean isUnixSymlink() { 279 return (getUnixMode() & UnixStat.LINK_FLAG) == UnixStat.LINK_FLAG; 280 } 281 282 /** 283 * Platform specification to put into the "version made 284 * by" part of the central file header. 285 * 286 * @return PLATFORM_FAT unless {@link #setUnixMode setUnixMode} 287 * has been called, in which case PLATFORM_UNIX will be returned. 288 */ 289 public int getPlatform() { 290 return platform; 291 } 292 293 /** 294 * Set the platform (UNIX or FAT). 295 * @param platform an <code>int</code> value - 0 is FAT, 3 is UNIX 296 */ 297 protected void setPlatform(int platform) { 298 this.platform = platform; 299 } 300 301 /** 302 * Replaces all currently attached extra fields with the new array. 303 * @param fields an array of extra fields 304 */ 305 public void setExtraFields(ZipExtraField[] fields) { 306 extraFields = new LinkedHashMap<ZipShort, ZipExtraField>(); 307 for (ZipExtraField field : fields) { 308 if (field instanceof UnparseableExtraFieldData) { 309 unparseableExtra = (UnparseableExtraFieldData) field; 310 } else { 311 extraFields.put(field.getHeaderId(), field); 312 } 313 } 314 setExtra(); 315 } 316 317 /** 318 * Retrieves all extra fields that have been parsed successfully. 319 * @return an array of the extra fields 320 */ 321 public ZipExtraField[] getExtraFields() { 322 return getExtraFields(false); 323 } 324 325 /** 326 * Retrieves extra fields. 327 * @param includeUnparseable whether to also return unparseable 328 * extra fields as {@link UnparseableExtraFieldData} if such data 329 * exists. 330 * @return an array of the extra fields 331 * 332 * @since 1.1 333 */ 334 public ZipExtraField[] getExtraFields(boolean includeUnparseable) { 335 if (extraFields == null) { 336 return !includeUnparseable || unparseableExtra == null 337 ? new ZipExtraField[0] 338 : new ZipExtraField[] { unparseableExtra }; 339 } 340 List<ZipExtraField> result = 341 new ArrayList<ZipExtraField>(extraFields.values()); 342 if (includeUnparseable && unparseableExtra != null) { 343 result.add(unparseableExtra); 344 } 345 return result.toArray(new ZipExtraField[0]); 346 } 347 348 /** 349 * Adds an extra field - replacing an already present extra field 350 * of the same type. 351 * 352 * <p>If no extra field of the same type exists, the field will be 353 * added as last field.</p> 354 * @param ze an extra field 355 */ 356 public void addExtraField(ZipExtraField ze) { 357 if (ze instanceof UnparseableExtraFieldData) { 358 unparseableExtra = (UnparseableExtraFieldData) ze; 359 } else { 360 if (extraFields == null) { 361 extraFields = new LinkedHashMap<ZipShort, ZipExtraField>(); 362 } 363 extraFields.put(ze.getHeaderId(), ze); 364 } 365 setExtra(); 366 } 367 368 /** 369 * Adds an extra field - replacing an already present extra field 370 * of the same type. 371 * 372 * <p>The new extra field will be the first one.</p> 373 * @param ze an extra field 374 */ 375 public void addAsFirstExtraField(ZipExtraField ze) { 376 if (ze instanceof UnparseableExtraFieldData) { 377 unparseableExtra = (UnparseableExtraFieldData) ze; 378 } else { 379 LinkedHashMap<ZipShort, ZipExtraField> copy = extraFields; 380 extraFields = new LinkedHashMap<ZipShort, ZipExtraField>(); 381 extraFields.put(ze.getHeaderId(), ze); 382 if (copy != null) { 383 copy.remove(ze.getHeaderId()); 384 extraFields.putAll(copy); 385 } 386 } 387 setExtra(); 388 } 389 390 /** 391 * Remove an extra field. 392 * @param type the type of extra field to remove 393 */ 394 public void removeExtraField(ZipShort type) { 395 if (extraFields == null) { 396 throw new java.util.NoSuchElementException(); 397 } 398 if (extraFields.remove(type) == null) { 399 throw new java.util.NoSuchElementException(); 400 } 401 setExtra(); 402 } 403 404 /** 405 * Removes unparseable extra field data. 406 * 407 * @since 1.1 408 */ 409 public void removeUnparseableExtraFieldData() { 410 if (unparseableExtra == null) { 411 throw new java.util.NoSuchElementException(); 412 } 413 unparseableExtra = null; 414 setExtra(); 415 } 416 417 /** 418 * Looks up an extra field by its header id. 419 * 420 * @return null if no such field exists. 421 */ 422 public ZipExtraField getExtraField(ZipShort type) { 423 if (extraFields != null) { 424 return extraFields.get(type); 425 } 426 return null; 427 } 428 429 /** 430 * Looks up extra field data that couldn't be parsed correctly. 431 * 432 * @return null if no such field exists. 433 * 434 * @since 1.1 435 */ 436 public UnparseableExtraFieldData getUnparseableExtraFieldData() { 437 return unparseableExtra; 438 } 439 440 /** 441 * Parses the given bytes as extra field data and consumes any 442 * unparseable data as an {@link UnparseableExtraFieldData} 443 * instance. 444 * @param extra an array of bytes to be parsed into extra fields 445 * @throws RuntimeException if the bytes cannot be parsed 446 * @throws RuntimeException on error 447 */ 448 @Override 449 public void setExtra(byte[] extra) throws RuntimeException { 450 try { 451 ZipExtraField[] local = 452 ExtraFieldUtils.parse(extra, true, 453 ExtraFieldUtils.UnparseableExtraField.READ); 454 mergeExtraFields(local, true); 455 } catch (ZipException e) { 456 // actually this is not possible as of Commons Compress 1.1 457 throw new RuntimeException("Error parsing extra fields for entry: " 458 + getName() + " - " + e.getMessage(), e); 459 } 460 } 461 462 /** 463 * Unfortunately {@link java.util.zip.ZipOutputStream 464 * java.util.zip.ZipOutputStream} seems to access the extra data 465 * directly, so overriding getExtra doesn't help - we need to 466 * modify super's data directly. 467 */ 468 protected void setExtra() { 469 super.setExtra(ExtraFieldUtils.mergeLocalFileDataData(getExtraFields(true))); 470 } 471 472 /** 473 * Sets the central directory part of extra fields. 474 */ 475 public void setCentralDirectoryExtra(byte[] b) { 476 try { 477 ZipExtraField[] central = 478 ExtraFieldUtils.parse(b, false, 479 ExtraFieldUtils.UnparseableExtraField.READ); 480 mergeExtraFields(central, false); 481 } catch (ZipException e) { 482 throw new RuntimeException(e.getMessage(), e); 483 } 484 } 485 486 /** 487 * Retrieves the extra data for the local file data. 488 * @return the extra data for local file 489 */ 490 public byte[] getLocalFileDataExtra() { 491 byte[] extra = getExtra(); 492 return extra != null ? extra : EMPTY; 493 } 494 495 /** 496 * Retrieves the extra data for the central directory. 497 * @return the central directory extra data 498 */ 499 public byte[] getCentralDirectoryExtra() { 500 return ExtraFieldUtils.mergeCentralDirectoryData(getExtraFields(true)); 501 } 502 503 /** 504 * Get the name of the entry. 505 * @return the entry name 506 */ 507 @Override 508 public String getName() { 509 return name == null ? super.getName() : name; 510 } 511 512 /** 513 * Is this entry a directory? 514 * @return true if the entry is a directory 515 */ 516 @Override 517 public boolean isDirectory() { 518 return getName().endsWith("/"); 519 } 520 521 /** 522 * Set the name of the entry. 523 * @param name the name to use 524 */ 525 protected void setName(String name) { 526 if (name != null && getPlatform() == PLATFORM_FAT 527 && name.indexOf("/") == -1) { 528 name = name.replace('\\', '/'); 529 } 530 this.name = name; 531 } 532 533 /** 534 * Gets the uncompressed size of the entry data. 535 * @return the entry size 536 */ 537 @Override 538 public long getSize() { 539 return size; 540 } 541 542 /** 543 * Sets the uncompressed size of the entry data. 544 * @param size the uncompressed size in bytes 545 * @exception IllegalArgumentException if the specified size is less 546 * than 0 547 */ 548 @Override 549 public void setSize(long size) { 550 if (size < 0) { 551 throw new IllegalArgumentException("invalid entry size"); 552 } 553 this.size = size; 554 } 555 556 /** 557 * Sets the name using the raw bytes and the string created from 558 * it by guessing or using the configured encoding. 559 * @param name the name to use created from the raw bytes using 560 * the guessed or configured encoding 561 * @param rawName the bytes originally read as name from the 562 * archive 563 * @since 1.2 564 */ 565 protected void setName(String name, byte[] rawName) { 566 setName(name); 567 this.rawName = rawName; 568 } 569 570 /** 571 * Returns the raw bytes that made up the name before it has been 572 * converted using the configured or guessed encoding. 573 * 574 * <p>This method will return null if this instance has not been 575 * read from an archive.</p> 576 * 577 * @since 1.2 578 */ 579 public byte[] getRawName() { 580 if (rawName != null) { 581 byte[] b = new byte[rawName.length]; 582 System.arraycopy(rawName, 0, b, 0, rawName.length); 583 return b; 584 } 585 return null; 586 } 587 588 /** 589 * Get the hashCode of the entry. 590 * This uses the name as the hashcode. 591 * @return a hashcode. 592 */ 593 @Override 594 public int hashCode() { 595 // this method has severe consequences on performance. We cannot rely 596 // on the super.hashCode() method since super.getName() always return 597 // the empty string in the current implemention (there's no setter) 598 // so it is basically draining the performance of a hashmap lookup 599 return getName().hashCode(); 600 } 601 602 /** 603 * The "general purpose bit" field. 604 * @since 1.1 605 */ 606 public GeneralPurposeBit getGeneralPurposeBit() { 607 return gpb; 608 } 609 610 /** 611 * The "general purpose bit" field. 612 * @since 1.1 613 */ 614 public void setGeneralPurposeBit(GeneralPurposeBit b) { 615 gpb = b; 616 } 617 618 /** 619 * If there are no extra fields, use the given fields as new extra 620 * data - otherwise merge the fields assuming the existing fields 621 * and the new fields stem from different locations inside the 622 * archive. 623 * @param f the extra fields to merge 624 * @param local whether the new fields originate from local data 625 */ 626 private void mergeExtraFields(ZipExtraField[] f, boolean local) 627 throws ZipException { 628 if (extraFields == null) { 629 setExtraFields(f); 630 } else { 631 for (ZipExtraField element : f) { 632 ZipExtraField existing; 633 if (element instanceof UnparseableExtraFieldData) { 634 existing = unparseableExtra; 635 } else { 636 existing = getExtraField(element.getHeaderId()); 637 } 638 if (existing == null) { 639 addExtraField(element); 640 } else { 641 if (local) { 642 byte[] b = element.getLocalFileDataData(); 643 existing.parseFromLocalFileData(b, 0, b.length); 644 } else { 645 byte[] b = element.getCentralDirectoryData(); 646 existing.parseFromCentralDirectoryData(b, 0, b.length); 647 } 648 } 649 } 650 setExtra(); 651 } 652 } 653 654 /** {@inheritDoc} */ 655 public Date getLastModifiedDate() { 656 return new Date(getTime()); 657 } 658 659 /* (non-Javadoc) 660 * @see java.lang.Object#equals(java.lang.Object) 661 */ 662 @Override 663 public boolean equals(Object obj) { 664 if (this == obj) { 665 return true; 666 } 667 if (obj == null || getClass() != obj.getClass()) { 668 return false; 669 } 670 ZipArchiveEntry other = (ZipArchiveEntry) obj; 671 String myName = getName(); 672 String otherName = other.getName(); 673 if (myName == null) { 674 if (otherName != null) { 675 return false; 676 } 677 } else if (!myName.equals(otherName)) { 678 return false; 679 } 680 String myComment = getComment(); 681 String otherComment = other.getComment(); 682 if (myComment == null) { 683 myComment = ""; 684 } 685 if (otherComment == null) { 686 otherComment = ""; 687 } 688 return getTime() == other.getTime() 689 && myComment.equals(otherComment) 690 && getInternalAttributes() == other.getInternalAttributes() 691 && getPlatform() == other.getPlatform() 692 && getExternalAttributes() == other.getExternalAttributes() 693 && getMethod() == other.getMethod() 694 && getSize() == other.getSize() 695 && getCrc() == other.getCrc() 696 && getCompressedSize() == other.getCompressedSize() 697 && Arrays.equals(getCentralDirectoryExtra(), 698 other.getCentralDirectoryExtra()) 699 && Arrays.equals(getLocalFileDataExtra(), 700 other.getLocalFileDataExtra()) 701 && gpb.equals(other.gpb); 702 } 703 }