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