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 }