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