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 /**
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 Apache Commons Compress 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 Apache Commons Compress 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 (int i = 0; i < fields.length; i++) {
294 if (fields[i] instanceof UnparseableExtraFieldData) {
295 unparseableExtra = (UnparseableExtraFieldData) fields[i];
296 } else {
297 extraFields.put(fields[i].getHeaderId(), fields[i]);
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 Apache Commons Compress 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 Apache Commons Compress 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 Apache Commons Compress 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 this.name = name;
513 }
514
515 /**
516 * Gets the uncompressed size of the entry data.
517 * @return the entry size
518 */
519 @Override
520 public long getSize() {
521 return size;
522 }
523
524 /**
525 * Sets the uncompressed size of the entry data.
526 * @param size the uncompressed size in bytes
527 * @exception IllegalArgumentException if the specified size is less
528 * than 0
529 */
530 @Override
531 public void setSize(long size) {
532 if (size < 0) {
533 throw new IllegalArgumentException("invalid entry size");
534 }
535 this.size = size;
536 }
537
538 /**
539 * Sets the name using the raw bytes and the string created from
540 * it by guessing or using the configured encoding.
541 * @param name the name to use created from the raw bytes using
542 * the guessed or configured encoding
543 * @param rawName the bytes originally read as name from the
544 * archive
545 * @since Apache Commons Compress 1.2
546 */
547 protected void setName(String name, byte[] rawName) {
548 setName(name);
549 this.rawName = rawName;
550 }
551
552 /**
553 * Returns the raw bytes that made up the name before it has been
554 * converted using the configured or guessed encoding.
555 *
556 * <p>This method will return null if this instance has not been
557 * read from an archive.</p>
558 *
559 * @since Apache Commons Compress 1.2
560 */
561 public byte[] getRawName() {
562 if (rawName != null) {
563 byte[] b = new byte[rawName.length];
564 System.arraycopy(rawName, 0, b, 0, rawName.length);
565 return b;
566 }
567 return null;
568 }
569
570 /**
571 * Get the hashCode of the entry.
572 * This uses the name as the hashcode.
573 * @return a hashcode.
574 */
575 @Override
576 public int hashCode() {
577 // this method has severe consequences on performance. We cannot rely
578 // on the super.hashCode() method since super.getName() always return
579 // the empty string in the current implemention (there's no setter)
580 // so it is basically draining the performance of a hashmap lookup
581 return getName().hashCode();
582 }
583
584 /**
585 * The "general purpose bit" field.
586 * @since Apache Commons Compress 1.1
587 */
588 public GeneralPurposeBit getGeneralPurposeBit() {
589 return gpb;
590 }
591
592 /**
593 * The "general purpose bit" field.
594 * @since Apache Commons Compress 1.1
595 */
596 public void setGeneralPurposeBit(GeneralPurposeBit b) {
597 gpb = b;
598 }
599
600 /**
601 * If there are no extra fields, use the given fields as new extra
602 * data - otherwise merge the fields assuming the existing fields
603 * and the new fields stem from different locations inside the
604 * archive.
605 * @param f the extra fields to merge
606 * @param local whether the new fields originate from local data
607 */
608 private void mergeExtraFields(ZipExtraField[] f, boolean local)
609 throws ZipException {
610 if (extraFields == null) {
611 setExtraFields(f);
612 } else {
613 for (int i = 0; i < f.length; i++) {
614 ZipExtraField existing;
615 if (f[i] instanceof UnparseableExtraFieldData) {
616 existing = unparseableExtra;
617 } else {
618 existing = getExtraField(f[i].getHeaderId());
619 }
620 if (existing == null) {
621 addExtraField(f[i]);
622 } else {
623 if (local) {
624 byte[] b = f[i].getLocalFileDataData();
625 existing.parseFromLocalFileData(b, 0, b.length);
626 } else {
627 byte[] b = f[i].getCentralDirectoryData();
628 existing.parseFromCentralDirectoryData(b, 0, b.length);
629 }
630 }
631 }
632 setExtra();
633 }
634 }
635
636 /** {@inheritDoc} */
637 public Date getLastModifiedDate() {
638 return new Date(getTime());
639 }
640
641 /* (non-Javadoc)
642 * @see java.lang.Object#equals(java.lang.Object)
643 */
644 @Override
645 public boolean equals(Object obj) {
646 if (this == obj) {
647 return true;
648 }
649 if (obj == null || getClass() != obj.getClass()) {
650 return false;
651 }
652 ZipArchiveEntry other = (ZipArchiveEntry) obj;
653 String myName = getName();
654 String otherName = other.getName();
655 if (myName == null) {
656 if (otherName != null) {
657 return false;
658 }
659 } else if (!myName.equals(otherName)) {
660 return false;
661 }
662 String myComment = getComment();
663 String otherComment = other.getComment();
664 if (myComment == null) {
665 if (otherComment != null) {
666 return false;
667 }
668 } else if (!myComment.equals(otherComment)) {
669 return false;
670 }
671 return getTime() == other.getTime()
672 && getInternalAttributes() == other.getInternalAttributes()
673 && getPlatform() == other.getPlatform()
674 && getExternalAttributes() == other.getExternalAttributes()
675 && getMethod() == other.getMethod()
676 && getSize() == other.getSize()
677 && getCrc() == other.getCrc()
678 && getCompressedSize() == other.getCompressedSize()
679 && Arrays.equals(getCentralDirectoryExtra(),
680 other.getCentralDirectoryExtra())
681 && Arrays.equals(getLocalFileDataExtra(),
682 other.getLocalFileDataExtra())
683 && gpb.equals(other.gpb);
684 }
685 }