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