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