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