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