    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 &quot;version made
    257         * by&quot; 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    }