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