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