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 }