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