    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 java.io.File;
    021import java.io.IOException;
    022import java.nio.file.Files;
    023import java.nio.file.LinkOption;
    024import java.nio.file.Path;
    025import java.nio.file.attribute.BasicFileAttributes;
    026import java.nio.file.attribute.FileTime;
    027import java.util.ArrayList;
    028import java.util.Arrays;
    029import java.util.Date;
    030import java.util.List;
    031import java.util.NoSuchElementException;
    032import java.util.Objects;
    033import java.util.zip.ZipEntry;
    034import java.util.zip.ZipException;
    035
    036import org.apache.commons.compress.archivers.ArchiveEntry;
    037import org.apache.commons.compress.archivers.EntryStreamOffsets;
    038import org.apache.commons.compress.utils.ByteUtils;
    039import org.apache.commons.compress.utils.TimeUtils;
    040
    041/**
    042 * Extension that adds better handling of extra fields and provides
    043 * access to the internal and external file attributes.
    044 *
    045 * <p>The extra data is expected to follow the recommendation of
    046 * <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">APPNOTE.TXT</a>:</p>
    047 * <ul>
    048 *   <li>the extra byte array consists of a sequence of extra fields</li>
    049 *   <li>each extra fields starts by a two byte header id followed by
    050 *   a two byte sequence holding the length of the remainder of
    051 *   data.</li>
    052 * </ul>
    053 *
    054 * <p>Any extra data that cannot be parsed by the rules above will be
    055 * consumed as "unparseable" extra data and treated differently by the
    056 * methods of this class.  Versions prior to Apache Commons Compress
    057 * 1.1 would have thrown an exception if any attempt was made to read
    058 * or write extra data not conforming to the recommendation.</p>
    059 *
    060 * @NotThreadSafe
    061 */
    062public class ZipArchiveEntry extends java.util.zip.ZipEntry implements ArchiveEntry, EntryStreamOffsets {
    063
    064    /**
    065     * Indicates how the comment of this entry has been determined.
    066     * @since 1.16
    067     */
    068    public enum CommentSource {
    069        /**
    070         * The comment has been read from the archive using the encoding
    071         * of the archive specified when creating the {@link
    072         * ZipArchiveInputStream} or {@link ZipFile} (defaults to the
    073         * platform's default encoding).
    074         */
    075        COMMENT,
    076        /**
    077         * The comment has been read from an {@link UnicodeCommentExtraField
    078         * Unicode Extra Field}.
    079         */
    080        UNICODE_EXTRA_FIELD
    081    }
    082
    083    /**
    084     * How to try to parse the extra fields.
    085     *
    086     * <p>Configures the behavior for:</p>
    087     * <ul>
    088     *   <li>What shall happen if the extra field content doesn't
    089     *   follow the recommended pattern of two-byte id followed by a
    090     *   two-byte length?</li>
    091     *  <li>What shall happen if an extra field is generally supported
    092     *  by Commons Compress but its content cannot be parsed
    093     *  correctly? This may for example happen if the archive is
    094     *  corrupt, it triggers a bug in Commons Compress or the extra
    095     *  field uses a version not (yet) supported by Commons
    096     *  Compress.</li>
    097     * </ul>
    098     *
    099     * @since 1.19
    100     */
    101    public enum ExtraFieldParsingMode implements ExtraFieldParsingBehavior {
    102        /**
    103         * Try to parse as many extra fields as possible and wrap
    104         * unknown extra fields as well as supported extra fields that
    105         * cannot be parsed in {@link UnrecognizedExtraField}.
    106         *
    107         * <p>Wrap extra data that doesn't follow the recommended
    108         * pattern in an {@link UnparseableExtraFieldData}
    109         * instance.</p>
    110         *
    111         * <p>This is the default behavior starting with Commons Compress 1.19.</p>
    112         */
    113        BEST_EFFORT(ExtraFieldUtils.UnparseableExtraField.READ) {
    114            @Override
    115            public ZipExtraField fill(final ZipExtraField field, final byte[] data, final int off, final int len, final boolean local) {
    116                return fillAndMakeUnrecognizedOnError(field, data, off, len, local);
    117            }
    118        },
    119        /**
    120         * Try to parse as many extra fields as possible and wrap
    121         * unknown extra fields in {@link UnrecognizedExtraField}.
    122         *
    123         * <p>Wrap extra data that doesn't follow the recommended
    124         * pattern in an {@link UnparseableExtraFieldData}
    125         * instance.</p>
    126         *
    127         * <p>Throw an exception if an extra field that is generally
    128         * supported cannot be parsed.</p>
    129         *
    130         * <p>This used to be the default behavior prior to Commons
    131         * Compress 1.19.</p>
    132         */
    133        STRICT_FOR_KNOW_EXTRA_FIELDS(ExtraFieldUtils.UnparseableExtraField.READ),
    134        /**
    135         * Try to parse as many extra fields as possible and wrap
    136         * unknown extra fields as well as supported extra fields that
    137         * cannot be parsed in {@link UnrecognizedExtraField}.
    138         *
    139         * <p>Ignore extra data that doesn't follow the recommended
    140         * pattern.</p>
    141         */
    142        ONLY_PARSEABLE_LENIENT(ExtraFieldUtils.UnparseableExtraField.SKIP) {
    143            @Override
    144            public ZipExtraField fill(final ZipExtraField field, final byte[] data, final int off, final int len, final boolean local) {
    145                return fillAndMakeUnrecognizedOnError(field, data, off, len, local);
    146            }
    147        },
    148        /**
    149         * Try to parse as many extra fields as possible and wrap
    150         * unknown extra fields in {@link UnrecognizedExtraField}.
    151         *
    152         * <p>Ignore extra data that doesn't follow the recommended
    153         * pattern.</p>
    154         *
    155         * <p>Throw an exception if an extra field that is generally
    156         * supported cannot be parsed.</p>
    157         */
    158        ONLY_PARSEABLE_STRICT(ExtraFieldUtils.UnparseableExtraField.SKIP),
    159        /**
    160         * Throw an exception if any of the recognized extra fields
    161         * cannot be parsed or any extra field violates the
    162         * recommended pattern.
    163         */
    164        DRACONIC(ExtraFieldUtils.UnparseableExtraField.THROW);
    165
    166        private static ZipExtraField fillAndMakeUnrecognizedOnError(final ZipExtraField field, final byte[] data, final int off,
    167            final int len, final boolean local) {
    168            try {
    169                return ExtraFieldUtils.fillExtraField(field, data, off, len, local);
    170            } catch (final ZipException ex) {
    171                final UnrecognizedExtraField u = new UnrecognizedExtraField();
    172                u.setHeaderId(field.getHeaderId());
    173                if (local) {
    174                    u.setLocalFileDataData(Arrays.copyOfRange(data, off, off + len));
    175                } else {
    176                    u.setCentralDirectoryData(Arrays.copyOfRange(data, off, off + len));
    177                }
    178                return u;
    179            }
    180        }
    181
    182        private final ExtraFieldUtils.UnparseableExtraField onUnparseableData;
    183
    184        ExtraFieldParsingMode(final ExtraFieldUtils.UnparseableExtraField onUnparseableData) {
    185            this.onUnparseableData = onUnparseableData;
    186        }
    187
    188        @Override
    189        public ZipExtraField createExtraField(final ZipShort headerId)
    190            throws ZipException, InstantiationException, IllegalAccessException {
    191            return ExtraFieldUtils.createExtraField(headerId);
    192        }
    193
    194        @Override
    195        public ZipExtraField fill(final ZipExtraField field, final byte[] data, final int off, final int len, final boolean local)
    196            throws ZipException {
    197            return ExtraFieldUtils.fillExtraField(field, data, off, len, local);
    198        }
    199
    200        @Override
    201        public ZipExtraField onUnparseableExtraField(final byte[] data, final int off, final int len, final boolean local,
    202            final int claimedLength) throws ZipException {
    203            return onUnparseableData.onUnparseableExtraField(data, off, len, local, claimedLength);
    204        }
    205    }
    206    /**
    207     * Indicates how the name of this entry has been determined.
    208     * @since 1.16
    209     */
    210    public enum NameSource {
    211        /**
    212         * The name has been read from the archive using the encoding
    213         * of the archive specified when creating the {@link
    214         * ZipArchiveInputStream} or {@link ZipFile} (defaults to the
    215         * platform's default encoding).
    216         */
    217        NAME,
    218        /**
    219         * The name has been read from the archive and the archive
    220         * specified the EFS flag which indicates the name has been
    221         * encoded as UTF-8.
    222         */
    223        NAME_WITH_EFS_FLAG,
    224        /**
    225         * The name has been read from an {@link UnicodePathExtraField
    226         * Unicode Extra Field}.
    227         */
    228        UNICODE_EXTRA_FIELD
    229    }
    230    static final ZipArchiveEntry[] EMPTY_ARRAY = {};
    231    public static final int PLATFORM_UNIX = 3;
    232    public static final int PLATFORM_FAT  = 0;
    233    public static final int CRC_UNKNOWN = -1;
    234
    235    private static final int SHORT_MASK = 0xFFFF;
    236
    237    private static final int SHORT_SHIFT = 16;
    238
    239    private static boolean canConvertToInfoZipExtendedTimestamp(
    240            final FileTime lastModifiedTime,
    241            final FileTime lastAccessTime,
    242            final FileTime creationTime) {
    243        return TimeUtils.isUnixTime(lastModifiedTime)
    244                && TimeUtils.isUnixTime(lastAccessTime)
    245                && TimeUtils.isUnixTime(creationTime);
    246    }
    247
    248    /**
    249     * The {@link java.util.zip.ZipEntry} base class only supports
    250     * the compression methods STORED and DEFLATED. We override the
    251     * field so that any compression methods can be used.
    252     * <p>
    253     * The default value -1 means that the method has not been specified.
    254     *
    255     * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-93"
    256     *        >COMPRESS-93</a>
    257     */
    258    private int method = ZipMethod.UNKNOWN_CODE;
    259    /**
    260     * The {@link java.util.zip.ZipEntry#setSize} method in the base
    261     * class throws an IllegalArgumentException if the size is bigger
    262     * than 2GB for Java versions &lt; 7 and even in Java 7+ if the
    263     * implementation in java.util.zip doesn't support Zip64 itself
    264     * (it is an optional feature).
    265     *
    266     * <p>We need to keep our own size information for Zip64 support.</p>
    267     */
    268    private long size = SIZE_UNKNOWN;
    269    private int internalAttributes;
    270    private int versionRequired;
    271    private int versionMadeBy;
    272    private int platform = PLATFORM_FAT;
    273    private int rawFlag;
    274    private long externalAttributes;
    275    private int alignment;
    276    private ZipExtraField[] extraFields;
    277    private UnparseableExtraFieldData unparseableExtra;
    278    private String name;
    279    private byte[] rawName;
    280    private GeneralPurposeBit gpb = new GeneralPurposeBit();
    281    private long localHeaderOffset = OFFSET_UNKNOWN;
    282    private long dataOffset = OFFSET_UNKNOWN;
    283    private boolean isStreamContiguous;
    284    private NameSource nameSource = NameSource.NAME;
    285
    286    private CommentSource commentSource = CommentSource.COMMENT;
    287
    288    private long diskNumberStart;
    289
    290    private boolean lastModifiedDateSet = false;
    291
    292    private long time = -1;
    293
    294    /**
    295     */
    296    protected ZipArchiveEntry() {
    297        this("");
    298    }
    299
    300    /**
    301     * Creates a new ZIP entry taking some information from the given
    302     * file and using the provided name.
    303     *
    304     * <p>The name will be adjusted to end with a forward slash "/" if
    305     * the file is a directory.  If the file is not a directory a
    306     * potential trailing forward slash will be stripped from the
    307     * entry name.</p>
    308     * @param inputFile file to create the entry from
    309     * @param entryName name of the entry
    310     */
    311    public ZipArchiveEntry(final File inputFile, final String entryName) {
    312        this(inputFile.isDirectory() && !entryName.endsWith("/") ?
    313             entryName + "/" : entryName);
    314        try {
    315            setAttributes(inputFile.toPath());
    316        } catch (IOException e) { // NOSONAR
    317            if (inputFile.isFile()){
    318                setSize(inputFile.length());
    319            }
    320            setTime(inputFile.lastModified());
    321        }
    322    }
    323
    324    /**
    325     * Creates a new ZIP entry with fields taken from the specified ZIP entry.
    326     *
    327     * <p>Assumes the entry represents a directory if and only if the
    328     * name ends with a forward slash "/".</p>
    329     *
    330     * @param entry the entry to get fields from
    331     * @throws ZipException on error
    332     */
    333    public ZipArchiveEntry(final java.util.zip.ZipEntry entry) throws ZipException {
    334        super(entry);
    335        setName(entry.getName());
    336        final byte[] extra = entry.getExtra();
    337        if (extra != null) {
    338            setExtraFields(ExtraFieldUtils.parse(extra, true, ExtraFieldParsingMode.BEST_EFFORT));
    339        } else {
    340            // initializes extra data to an empty byte array
    341            setExtra();
    342        }
    343        setMethod(entry.getMethod());
    344        this.size = entry.getSize();
    345    }
    346
    347    /**
    348     * Creates a new ZIP entry taking some information from the given
    349     * path and using the provided name.
    350     *
    351     * <p>The name will be adjusted to end with a forward slash "/" if
    352     * the file is a directory.  If the file is not a directory a
    353     * potential trailing forward slash will be stripped from the
    354     * entry name.</p>
    355     * @param inputPath path to create the entry from.
    356     * @param entryName name of the entry.
    357     * @param options options indicating how symbolic links are handled.
    358     * @throws IOException if an I/O error occurs.
    359     * @since 1.21
    360     */
    361    public ZipArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
    362        this(Files.isDirectory(inputPath, options) && !entryName.endsWith("/") ?
    363             entryName + "/" : entryName);
    364        setAttributes(inputPath, options);
    365    }
    366
    367    /**
    368     * Creates a new ZIP entry with the specified name.
    369     *
    370     * <p>Assumes the entry represents a directory if and only if the
    371     * name ends with a forward slash "/".</p>
    372     *
    373     * @param name the name of the entry
    374     */
    375    public ZipArchiveEntry(final String name) {
    376        super(name);
    377        setName(name);
    378    }
    379
    380    /**
    381     * Creates a new ZIP entry with fields taken from the specified ZIP entry.
    382     *
    383     * <p>Assumes the entry represents a directory if and only if the
    384     * name ends with a forward slash "/".</p>
    385     *
    386     * @param entry the entry to get fields from
    387     * @throws ZipException on error
    388     */
    389    public ZipArchiveEntry(final ZipArchiveEntry entry) throws ZipException {
    390        this((java.util.zip.ZipEntry) entry);
    391        setInternalAttributes(entry.getInternalAttributes());
    392        setExternalAttributes(entry.getExternalAttributes());
    393        setExtraFields(entry.getAllExtraFieldsNoCopy());
    394        setPlatform(entry.getPlatform());
    395        final GeneralPurposeBit other = entry.getGeneralPurposeBit();
    396        setGeneralPurposeBit(other == null ? null :
    397                             (GeneralPurposeBit) other.clone());
    398    }
    399
    400    /**
    401     * Adds an extra field - replacing an already present extra field
    402     * of the same type.
    403     *
    404     * <p>The new extra field will be the first one.</p>
    405     * @param ze an extra field
    406     */
    407    public void addAsFirstExtraField(final ZipExtraField ze) {
    408        if (ze instanceof UnparseableExtraFieldData) {
    409            unparseableExtra = (UnparseableExtraFieldData) ze;
    410        } else {
    411            if (getExtraField(ze.getHeaderId()) != null) {
    412                internalRemoveExtraField(ze.getHeaderId());
    413            }
    414            final ZipExtraField[] copy = extraFields;
    415            final int newLen = extraFields != null ? extraFields.length + 1 : 1;
    416            extraFields = new ZipExtraField[newLen];
    417            extraFields[0] = ze;
    418            if (copy != null){
    419                System.arraycopy(copy, 0, extraFields, 1, extraFields.length - 1);
    420            }
    421        }
    422        setExtra();
    423    }
    424
    425    /**
    426     * Adds an extra field - replacing an already present extra field
    427     * of the same type.
    428     *
    429     * <p>If no extra field of the same type exists, the field will be
    430     * added as last field.</p>
    431     * @param ze an extra field
    432     */
    433    public void addExtraField(final ZipExtraField ze) {
    434        internalAddExtraField(ze);
    435        setExtra();
    436    }
    437
    438    private void addInfoZipExtendedTimestamp(
    439            final FileTime lastModifiedTime,
    440            final FileTime lastAccessTime,
    441            final FileTime creationTime) {
    442        final X5455_ExtendedTimestamp infoZipTimestamp = new X5455_ExtendedTimestamp();
    443        if (lastModifiedTime != null) {
    444            infoZipTimestamp.setModifyFileTime(lastModifiedTime);
    445        }
    446        if (lastAccessTime != null) {
    447            infoZipTimestamp.setAccessFileTime(lastAccessTime);
    448        }
    449        if (creationTime != null) {
    450            infoZipTimestamp.setCreateFileTime(creationTime);
    451        }
    452        internalAddExtraField(infoZipTimestamp);
    453    }
    454
    455    private void addNTFSTimestamp(
    456            final FileTime lastModifiedTime,
    457            final FileTime lastAccessTime,
    458            final FileTime creationTime) {
    459        final X000A_NTFS ntfsTimestamp = new X000A_NTFS();
    460        if (lastModifiedTime != null) {
    461            ntfsTimestamp.setModifyFileTime(lastModifiedTime);
    462        }
    463        if (lastAccessTime != null) {
    464            ntfsTimestamp.setAccessFileTime(lastAccessTime);
    465        }
    466        if (creationTime != null) {
    467            ntfsTimestamp.setCreateFileTime(creationTime);
    468        }
    469        internalAddExtraField(ntfsTimestamp);
    470    }
    471
    472    /**
    473     * Overwrite clone.
    474     * @return a cloned copy of this ZipArchiveEntry
    475     */
    476    @Override
    477    public Object clone() {
    478        final ZipArchiveEntry e = (ZipArchiveEntry) super.clone();
    479
    480        e.setInternalAttributes(getInternalAttributes());
    481        e.setExternalAttributes(getExternalAttributes());
    482        e.setExtraFields(getAllExtraFieldsNoCopy());
    483        return e;
    484    }
    485
    486    private ZipExtraField[] copyOf(final ZipExtraField[] src, final int length) {
    487        return Arrays.copyOf(src, length);
    488    }
    489
    490    @Override
    491    public boolean equals(final Object obj) {
    492        if (this == obj) {
    493            return true;
    494        }
    495        if (obj == null || getClass() != obj.getClass()) {
    496            return false;
    497        }
    498        final ZipArchiveEntry other = (ZipArchiveEntry) obj;
    499        final String myName = getName();
    500        final String otherName = other.getName();
    501        if (!Objects.equals(myName, otherName)) {
    502            return false;
    503        }
    504        String myComment = getComment();
    505        String otherComment = other.getComment();
    506        if (myComment == null) {
    507            myComment = "";
    508        }
    509        if (otherComment == null) {
    510            otherComment = "";
    511        }
    512        return Objects.equals(getLastModifiedTime(), other.getLastModifiedTime())
    513            && Objects.equals(getLastAccessTime(), other.getLastAccessTime())
    514            && Objects.equals(getCreationTime(), other.getCreationTime())
    515            && myComment.equals(otherComment)
    516            && getInternalAttributes() == other.getInternalAttributes()
    517            && getPlatform() == other.getPlatform()
    518            && getExternalAttributes() == other.getExternalAttributes()
    519            && getMethod() == other.getMethod()
    520            && getSize() == other.getSize()
    521            && getCrc() == other.getCrc()
    522            && getCompressedSize() == other.getCompressedSize()
    523            && Arrays.equals(getCentralDirectoryExtra(),
    524                             other.getCentralDirectoryExtra())
    525            && Arrays.equals(getLocalFileDataExtra(),
    526                             other.getLocalFileDataExtra())
    527            && localHeaderOffset == other.localHeaderOffset
    528            && dataOffset == other.dataOffset
    529            && gpb.equals(other.gpb);
    530    }
    531
    532    private ZipExtraField findMatching(final ZipShort headerId, final List<ZipExtraField> fs) {
    533        return fs.stream().filter(f -> headerId.equals(f.getHeaderId())).findFirst().orElse(null);
    534    }
    535
    536    private ZipExtraField findUnparseable(final List<ZipExtraField> fs) {
    537        return fs.stream().filter(UnparseableExtraFieldData.class::isInstance).findFirst().orElse(null);
    538    }
    539
    540    /**
    541     * Gets currently configured alignment.
    542     *
    543     * @return
    544     *      alignment for this entry.
    545     * @since 1.14
    546     */
    547    protected int getAlignment() {
    548        return this.alignment;
    549    }
    550
    551    private ZipExtraField[] getAllExtraFields() {
    552        final ZipExtraField[] allExtraFieldsNoCopy = getAllExtraFieldsNoCopy();
    553        return (allExtraFieldsNoCopy == extraFields) ? copyOf(allExtraFieldsNoCopy, allExtraFieldsNoCopy.length)
    554            : allExtraFieldsNoCopy;
    555    }
    556
    557    /**
    558     * Get all extra fields, including unparseable ones.
    559     * @return An array of all extra fields. Not necessarily a copy of internal data structures, hence private method
    560     */
    561    private ZipExtraField[] getAllExtraFieldsNoCopy() {
    562        if (extraFields == null) {
    563            return getUnparseableOnly();
    564        }
    565        return unparseableExtra != null ? getMergedFields() : extraFields;
    566    }
    567
    568    /**
    569     * Retrieves the extra data for the central directory.
    570     * @return the central directory extra data
    571     */
    572    public byte[] getCentralDirectoryExtra() {
    573        return ExtraFieldUtils.mergeCentralDirectoryData(getAllExtraFieldsNoCopy());
    574    }
    575
    576    /**
    577     * The source of the comment field value.
    578     * @return source of the comment field value
    579     * @since 1.16
    580     */
    581    public CommentSource getCommentSource() {
    582        return commentSource;
    583    }
    584
    585    @Override
    586    public long getDataOffset() {
    587        return dataOffset;
    588    }
    589
    590    /**
    591     * The number of the split segment this entry starts at.
    592     *
    593     * @return the number of the split segment this entry starts at.
    594     * @since 1.20
    595     */
    596    public long getDiskNumberStart() {
    597        return diskNumberStart;
    598    }
    599
    600    /**
    601     * Retrieves the external file attributes.
    602     *
    603     * <p><b>Note</b>: {@link ZipArchiveInputStream} is unable to fill
    604     * this field, you must use {@link ZipFile} if you want to read
    605     * entries using this attribute.</p>
    606     *
    607     * @return the external file attributes
    608     */
    609    public long getExternalAttributes() {
    610        return externalAttributes;
    611    }
    612
    613    /**
    614     * Looks up an extra field by its header id.
    615     *
    616     * @param type the header id
    617     * @return null if no such field exists.
    618     */
    619    public ZipExtraField getExtraField(final ZipShort type) {
    620        if (extraFields != null) {
    621            for (final ZipExtraField extraField : extraFields) {
    622                if (type.equals(extraField.getHeaderId())) {
    623                    return extraField;
    624                }
    625            }
    626        }
    627        return null;
    628    }
    629
    630    /**
    631     * Retrieves all extra fields that have been parsed successfully.
    632     *
    633     * <p><b>Note</b>: The set of extra fields may be incomplete when
    634     * {@link ZipArchiveInputStream} has been used as some extra
    635     * fields use the central directory to store additional
    636     * information.</p>
    637     *
    638     * @return an array of the extra fields
    639     */
    640    public ZipExtraField[] getExtraFields() {
    641        return getParseableExtraFields();
    642    }
    643
    644    /**
    645     * Retrieves extra fields.
    646     * @param includeUnparseable whether to also return unparseable
    647     * extra fields as {@link UnparseableExtraFieldData} if such data
    648     * exists.
    649     * @return an array of the extra fields
    650     *
    651     * @since 1.1
    652     */
    653    public ZipExtraField[] getExtraFields(final boolean includeUnparseable) {
    654        return includeUnparseable ?
    655                getAllExtraFields() :
    656                getParseableExtraFields();
    657    }
    658
    659    /**
    660     * Retrieves extra fields.
    661     * @param parsingBehavior controls parsing of extra fields.
    662     * @return an array of the extra fields
    663     *
    664     * @throws ZipException if parsing fails, can not happen if {@code
    665     * parsingBehavior} is {@link ExtraFieldParsingMode#BEST_EFFORT}.
    666     *
    667     * @since 1.19
    668     */
    669    public ZipExtraField[] getExtraFields(final ExtraFieldParsingBehavior parsingBehavior)
    670        throws ZipException {
    671        if (parsingBehavior == ExtraFieldParsingMode.BEST_EFFORT) {
    672            return getExtraFields(true);
    673        }
    674        if (parsingBehavior == ExtraFieldParsingMode.ONLY_PARSEABLE_LENIENT) {
    675            return getExtraFields(false);
    676        }
    677        final byte[] local = getExtra();
    678        final List<ZipExtraField> localFields = new ArrayList<>(Arrays.asList(ExtraFieldUtils.parse(local, true,
    679            parsingBehavior)));
    680        final byte[] central = getCentralDirectoryExtra();
    681        final List<ZipExtraField> centralFields = new ArrayList<>(Arrays.asList(ExtraFieldUtils.parse(central, false,
    682            parsingBehavior)));
    683        final List<ZipExtraField> merged = new ArrayList<>();
    684        for (final ZipExtraField l : localFields) {
    685            ZipExtraField c = null;
    686            if (l instanceof UnparseableExtraFieldData) {
    687                c = findUnparseable(centralFields);
    688            } else {
    689                c = findMatching(l.getHeaderId(), centralFields);
    690            }
    691            if (c != null) {
    692                final byte[] cd = c.getCentralDirectoryData();
    693                if (cd != null && cd.length > 0) {
    694                    l.parseFromCentralDirectoryData(cd, 0, cd.length);
    695                }
    696                centralFields.remove(c);
    697            }
    698            merged.add(l);
    699        }
    700        merged.addAll(centralFields);
    701        return merged.toArray(ExtraFieldUtils.EMPTY_ZIP_EXTRA_FIELD_ARRAY);
    702    }
    703
    704    /**
    705     * The "general purpose bit" field.
    706     * @return the general purpose bit
    707     * @since 1.1
    708     */
    709    public GeneralPurposeBit getGeneralPurposeBit() {
    710        return gpb;
    711    }
    712
    713    /**
    714     * Retrieves the internal file attributes.
    715     *
    716     * <p><b>Note</b>: {@link ZipArchiveInputStream} is unable to fill
    717     * this field, you must use {@link ZipFile} if you want to read
    718     * entries using this attribute.</p>
    719     *
    720     * @return the internal file attributes
    721     */
    722    public int getInternalAttributes() {
    723        return internalAttributes;
    724    }
    725
    726    /**
    727     * Wraps {@link java.util.zip.ZipEntry#getTime} with a {@link Date} as the
    728     * entry's last modified date.
    729     *
    730     * <p>Changes to the implementation of {@link java.util.zip.ZipEntry#getTime}
    731     * leak through and the returned value may depend on your local
    732     * time zone as well as your version of Java.</p>
    733     */
    734    @Override
    735    public Date getLastModifiedDate() {
    736        return new Date(getTime());
    737    }
    738
    739    /**
    740     * Retrieves the extra data for the local file data.
    741     * @return the extra data for local file
    742     */
    743    public byte[] getLocalFileDataExtra() {
    744        final byte[] extra = getExtra();
    745        return extra != null ? extra : ByteUtils.EMPTY_BYTE_ARRAY;
    746    }
    747
    748    protected long getLocalHeaderOffset() {
    749        return this.localHeaderOffset;
    750    }
    751
    752    private ZipExtraField[] getMergedFields() {
    753        final ZipExtraField[] zipExtraFields = copyOf(extraFields, extraFields.length + 1);
    754        zipExtraFields[extraFields.length] = unparseableExtra;
    755        return zipExtraFields;
    756    }
    757
    758    /**
    759     * Returns the compression method of this entry, or -1 if the
    760     * compression method has not been specified.
    761     *
    762     * @return compression method
    763     *
    764     * @since 1.1
    765     */
    766    @Override
    767    public int getMethod() {
    768        return method;
    769    }
    770
    771    /**
    772     * Get the name of the entry.
    773     *
    774     * <p>This method returns the raw name as it is stored inside of the archive.</p>
    775     *
    776     * @return the entry name
    777     */
    778    @Override
    779    public String getName() {
    780        return name == null ? super.getName() : name;
    781    }
    782
    783    /**
    784     * The source of the name field value.
    785     * @return source of the name field value
    786     * @since 1.16
    787     */
    788    public NameSource getNameSource() {
    789        return nameSource;
    790    }
    791
    792    private ZipExtraField[] getParseableExtraFields() {
    793        final ZipExtraField[] parseableExtraFields = getParseableExtraFieldsNoCopy();
    794        return (parseableExtraFields == extraFields) ? copyOf(parseableExtraFields, parseableExtraFields.length)
    795            : parseableExtraFields;
    796    }
    797
    798    private ZipExtraField[] getParseableExtraFieldsNoCopy() {
    799        if (extraFields == null) {
    800            return ExtraFieldUtils.EMPTY_ZIP_EXTRA_FIELD_ARRAY;
    801        }
    802        return extraFields;
    803    }
    804
    805    /**
    806     * Platform specification to put into the &quot;version made
    807     * by&quot; part of the central file header.
    808     *
    809     * @return PLATFORM_FAT unless {@link #setUnixMode setUnixMode}
    810     * has been called, in which case PLATFORM_UNIX will be returned.
    811     */
    812    public int getPlatform() {
    813        return platform;
    814    }
    815
    816    /**
    817     * The content of the flags field.
    818     * @return content of the flags field
    819     * @since 1.11
    820     */
    821    public int getRawFlag() {
    822        return rawFlag;
    823    }
    824
    825    /**
    826     * Returns the raw bytes that made up the name before it has been
    827     * converted using the configured or guessed encoding.
    828     *
    829     * <p>This method will return null if this instance has not been
    830     * read from an archive.</p>
    831     *
    832     * @return the raw name bytes
    833     * @since 1.2
    834     */
    835    public byte[] getRawName() {
    836        if (rawName != null) {
    837            return Arrays.copyOf(rawName, rawName.length);
    838        }
    839        return null;
    840    }
    841
    842    /**
    843     * Gets the uncompressed size of the entry data.
    844     *
    845     * <p><b>Note</b>: {@link ZipArchiveInputStream} may create
    846     * entries that return {@link #SIZE_UNKNOWN SIZE_UNKNOWN} as long
    847     * as the entry hasn't been read completely.</p>
    848     *
    849     * @return the entry size
    850     */
    851    @Override
    852    public long getSize() {
    853        return size;
    854    }
    855
    856    /**
    857     * {@inheritDoc}
    858     *
    859     * <p>Override to work around bug <a href="https://bugs.openjdk.org/browse/JDK-8130914">JDK-8130914</a></p>
    860     *
    861     * @return  The last modification time of the entry in milliseconds
    862     *          since the epoch, or -1 if not specified
    863     *
    864     * @see #setTime(long)
    865     * @see #setLastModifiedTime(FileTime)
    866     */
    867    @Override
    868    public long getTime() {
    869        if (lastModifiedDateSet) {
    870            return getLastModifiedTime().toMillis();
    871        }
    872        return time != -1 ? time : super.getTime();
    873    }
    874
    875    /**
    876     * Unix permission.
    877     * @return the unix permissions
    878     */
    879    public int getUnixMode() {
    880        return platform != PLATFORM_UNIX ? 0 :
    881            (int) ((getExternalAttributes() >> SHORT_SHIFT) & SHORT_MASK);
    882    }
    883
    884    /**
    885     * Looks up extra field data that couldn't be parsed correctly.
    886     *
    887     * @return null if no such field exists.
    888     *
    889     * @since 1.1
    890     */
    891    public UnparseableExtraFieldData getUnparseableExtraFieldData() {
    892        return unparseableExtra;
    893    }
    894
    895    private ZipExtraField[] getUnparseableOnly() {
    896        return unparseableExtra == null ? ExtraFieldUtils.EMPTY_ZIP_EXTRA_FIELD_ARRAY : new ZipExtraField[] { unparseableExtra };
    897    }
    898
    899    /**
    900     * The "version made by" field.
    901     * @return "version made by" field
    902     * @since 1.11
    903     */
    904    public int getVersionMadeBy() {
    905        return versionMadeBy;
    906    }
    907
    908    /**
    909     * The "version required to expand" field.
    910     * @return "version required to expand" field
    911     * @since 1.11
    912     */
    913    public int getVersionRequired() {
    914        return versionRequired;
    915    }
    916
    917    /**
    918     * Get the hash code of the entry.
    919     * This uses the name as the hash code.
    920     * @return a hash code.
    921     */
    922    @Override
    923    public int hashCode() {
    924        // this method has severe consequences on performance. We cannot rely
    925        // on the super.hashCode() method since super.getName() always return
    926        // the empty string in the current implementation (there's no setter)
    927        // so it is basically draining the performance of a hashmap lookup
    928        return getName().hashCode();
    929    }
    930
    931    private void internalAddExtraField(final ZipExtraField ze) {
    932        if (ze instanceof UnparseableExtraFieldData) {
    933            unparseableExtra = (UnparseableExtraFieldData) ze;
    934        } else if (extraFields == null) {
    935            extraFields = new ZipExtraField[]{ze};
    936        } else {
    937            if (getExtraField(ze.getHeaderId()) != null) {
    938                internalRemoveExtraField(ze.getHeaderId());
    939            }
    940            final ZipExtraField[] zipExtraFields = copyOf(extraFields, extraFields.length + 1);
    941            zipExtraFields[zipExtraFields.length - 1] = ze;
    942            extraFields = zipExtraFields;
    943        }
    944    }
    945
    946    private void internalRemoveExtraField(final ZipShort type) {
    947        if (extraFields == null) {
    948            return;
    949        }
    950        final List<ZipExtraField> newResult = new ArrayList<>();
    951        for (final ZipExtraField extraField : extraFields) {
    952            if (!type.equals(extraField.getHeaderId())) {
    953                newResult.add(extraField);
    954            }
    955        }
    956        if (extraFields.length == newResult.size()) {
    957            return;
    958        }
    959        extraFields = newResult.toArray(ExtraFieldUtils.EMPTY_ZIP_EXTRA_FIELD_ARRAY);
    960    }
    961
    962    private void internalSetLastModifiedTime(final FileTime time) {
    963        super.setLastModifiedTime(time);
    964        this.time = time.toMillis();
    965        lastModifiedDateSet = true;
    966    }
    967
    968    /**
    969     * Is this entry a directory?
    970     * @return true if the entry is a directory
    971     */
    972    @Override
    973    public boolean isDirectory() {
    974        return getName().endsWith("/");
    975    }
    976
    977    @Override
    978    public boolean isStreamContiguous() {
    979        return isStreamContiguous;
    980    }
    981
    982    /**
    983     * Returns true if this entry represents a unix symlink,
    984     * in which case the entry's content contains the target path
    985     * for the symlink.
    986     *
    987     * @since 1.5
    988     * @return true if the entry represents a unix symlink, false otherwise.
    989     */
    990    public boolean isUnixSymlink() {
    991        return (getUnixMode() & UnixStat.FILE_TYPE_FLAG) == UnixStat.LINK_FLAG;
    992    }
    993
    994    /**
    995     * If there are no extra fields, use the given fields as new extra
    996     * data - otherwise merge the fields assuming the existing fields
    997     * and the new fields stem from different locations inside the
    998     * archive.
    999     * @param f the extra fields to merge
    1000     * @param local whether the new fields originate from local data
    1001     */
    1002    private void mergeExtraFields(final ZipExtraField[] f, final boolean local) {
    1003        if (extraFields == null) {
    1004            setExtraFields(f);
    1005        } else {
    1006            for (final ZipExtraField element : f) {
    1007                final ZipExtraField existing;
    1008                if (element instanceof UnparseableExtraFieldData) {
    1009                    existing = unparseableExtra;
    1010                } else {
    1011                    existing = getExtraField(element.getHeaderId());
    1012                }
    1013                if (existing == null) {
    1014                    internalAddExtraField(element);
    1015                } else {
    1016                    final byte[] b = local ? element.getLocalFileDataData()
    1017                        : element.getCentralDirectoryData();
    1018                    try {
    1019                        if (local) {
    1020                            existing.parseFromLocalFileData(b, 0, b.length);
    1021                        } else {
    1022                            existing.parseFromCentralDirectoryData(b, 0, b.length);
    1023                        }
    1024                    } catch (final ZipException ex) {
    1025                        // emulate ExtraFieldParsingMode.fillAndMakeUnrecognizedOnError
    1026                        final UnrecognizedExtraField u = new UnrecognizedExtraField();
    1027                        u.setHeaderId(existing.getHeaderId());
    1028                        if (local) {
    1029                            u.setLocalFileDataData(b);
    1030                            u.setCentralDirectoryData(existing.getCentralDirectoryData());
    1031                        } else {
    1032                            u.setLocalFileDataData(existing.getLocalFileDataData());
    1033                            u.setCentralDirectoryData(b);
    1034                        }
    1035                        internalRemoveExtraField(existing.getHeaderId());
    1036                        internalAddExtraField(u);
    1037                    }
    1038                }
    1039            }
    1040            setExtra();
    1041        }
    1042    }
    1043
    1044    /**
    1045     * Remove an extra field.
    1046     * @param type the type of extra field to remove
    1047     */
    1048    public void removeExtraField(final ZipShort type) {
    1049        if (getExtraField(type) == null) {
    1050            throw new NoSuchElementException();
    1051        }
    1052        internalRemoveExtraField(type);
    1053        setExtra();
    1054    }
    1055
    1056    /**
    1057     * Removes unparseable extra field data.
    1058     *
    1059     * @since 1.1
    1060     */
    1061    public void removeUnparseableExtraFieldData() {
    1062        if (unparseableExtra == null) {
    1063            throw new NoSuchElementException();
    1064        }
    1065        unparseableExtra = null;
    1066        setExtra();
    1067    }
    1068
    1069    private boolean requiresExtraTimeFields() {
    1070        if (getLastAccessTime() != null || getCreationTime() != null) {
    1071            return true;
    1072        }
    1073        return lastModifiedDateSet;
    1074    }
    1075
    1076    /**
    1077     * Sets alignment for this entry.
    1078     *
    1079     * @param alignment
    1080     *      requested alignment, 0 for default.
    1081     * @since 1.14
    1082     */
    1083    public void setAlignment(final int alignment) {
    1084        if ((alignment & (alignment - 1)) != 0 || alignment > 0xffff) {
    1085            throw new IllegalArgumentException("Invalid value for alignment, must be power of two and no bigger than "
    1086                + 0xffff + " but is " + alignment);
    1087        }
    1088        this.alignment = alignment;
    1089    }
    1090
    1091    private void setAttributes(final Path inputPath, final LinkOption... options) throws IOException {
    1092        final BasicFileAttributes attributes = Files.readAttributes(inputPath, BasicFileAttributes.class, options);
    1093        if (attributes.isRegularFile()) {
    1094            setSize(attributes.size());
    1095        }
    1096        super.setLastModifiedTime(attributes.lastModifiedTime());
    1097        super.setCreationTime(attributes.creationTime());
    1098        super.setLastAccessTime(attributes.lastAccessTime());
    1099        setExtraTimeFields();
    1100    }
    1101
    1102    /**
    1103     * Sets the central directory part of extra fields.
    1104     * @param b an array of bytes to be parsed into extra fields
    1105     */
    1106        public void setCentralDirectoryExtra(final byte[] b) {
    1107                try {
    1108                        mergeExtraFields(ExtraFieldUtils.parse(b, false, ExtraFieldParsingMode.BEST_EFFORT), false);
    1109                } catch (final ZipException e) {
    1110                        // actually this is not possible as of Commons Compress 1.19
    1111                        throw new IllegalArgumentException(e.getMessage(), e); // NOSONAR
    1112                }
    1113        }
    1114
    1115    /**
    1116     * Sets the source of the comment field value.
    1117     * @param commentSource source of the comment field value
    1118     * @since 1.16
    1119     */
    1120    public void setCommentSource(final CommentSource commentSource) {
    1121        this.commentSource = commentSource;
    1122    }
    1123
    1124    @Override
    1125    public ZipEntry setCreationTime(final FileTime time) {
    1126        super.setCreationTime(time);
    1127        setExtraTimeFields();
    1128        return this;
    1129    }
    1130    /* (non-Javadoc)
    1131     * @see Object#equals(Object)
    1132     */
    1133
    1134    /**
    1135     * Sets the data offset.
    1136     *
    1137     * @param dataOffset
    1138     *      new value of data offset.
    1139     */
    1140    protected void setDataOffset(final long dataOffset) {
    1141        this.dataOffset = dataOffset;
    1142    }
    1143
    1144    /**
    1145     * The number of the split segment this entry starts at.
    1146     *
    1147     * @param diskNumberStart the number of the split segment this entry starts at.
    1148     * @since 1.20
    1149     */
    1150    public void setDiskNumberStart(final long diskNumberStart) {
    1151        this.diskNumberStart = diskNumberStart;
    1152    }
    1153
    1154    /**
    1155     * Sets the external file attributes.
    1156     * @param value an {@code long} value
    1157     */
    1158    public void setExternalAttributes(final long value) {
    1159        externalAttributes = value;
    1160    }
    1161
    1162    /**
    1163     * Unfortunately {@link java.util.zip.ZipOutputStream} seems to
    1164     * access the extra data directly, so overriding getExtra doesn't
    1165     * help - we need to modify super's data directly and on every update.
    1166     */
    1167    protected void setExtra() {
    1168        // ZipEntry will update the time fields here, so we need to reprocess them afterwards
    1169        super.setExtra(ExtraFieldUtils.mergeLocalFileDataData(getAllExtraFieldsNoCopy()));
    1170        // Reprocess and overwrite the modifications made by ZipEntry#setExtra(byte[])
    1171        updateTimeFieldsFromExtraFields();
    1172    }
    1173
    1174    /**
    1175     * Parses the given bytes as extra field data and consumes any
    1176     * unparseable data as an {@link UnparseableExtraFieldData}
    1177     * instance.
    1178     * @param extra an array of bytes to be parsed into extra fields
    1179     * @throws RuntimeException if the bytes cannot be parsed
    1180     * @throws RuntimeException on error
    1181     */
    1182    @Override
    1183        public void setExtra(final byte[] extra) throws RuntimeException {
    1184                try {
    1185                        mergeExtraFields(ExtraFieldUtils.parse(extra, true, ExtraFieldParsingMode.BEST_EFFORT), true);
    1186                } catch (final ZipException e) {
    1187                        // actually this is not possible as of Commons Compress 1.1
    1188                        throw new IllegalArgumentException("Error parsing extra fields for entry: " // NOSONAR
    1189                                        + getName() + " - " + e.getMessage(), e);
    1190                }
    1191        }
    1192
    1193    /**
    1194     * Replaces all currently attached extra fields with the new array.
    1195     * @param fields an array of extra fields
    1196     */
    1197    public void setExtraFields(final ZipExtraField[] fields) {
    1198        unparseableExtra = null;
    1199        final List<ZipExtraField> newFields = new ArrayList<>();
    1200        if (fields != null) {
    1201            for (final ZipExtraField field : fields) {
    1202                if (field instanceof UnparseableExtraFieldData) {
    1203                    unparseableExtra = (UnparseableExtraFieldData) field;
    1204                } else {
    1205                    newFields.add(field);
    1206                }
    1207            }
    1208        }
    1209        extraFields = newFields.toArray(ExtraFieldUtils.EMPTY_ZIP_EXTRA_FIELD_ARRAY);
    1210        setExtra();
    1211    }
    1212
    1213    private void setExtraTimeFields() {
    1214        if (getExtraField(X5455_ExtendedTimestamp.HEADER_ID) != null) {
    1215            internalRemoveExtraField(X5455_ExtendedTimestamp.HEADER_ID);
    1216        }
    1217        if (getExtraField(X000A_NTFS.HEADER_ID) != null) {
    1218            internalRemoveExtraField(X000A_NTFS.HEADER_ID);
    1219        }
    1220        if (requiresExtraTimeFields()) {
    1221            final FileTime lastModifiedTime = getLastModifiedTime();
    1222            final FileTime lastAccessTime = getLastAccessTime();
    1223            final FileTime creationTime = getCreationTime();
    1224            if (canConvertToInfoZipExtendedTimestamp(lastModifiedTime, lastAccessTime, creationTime)) {
    1225                addInfoZipExtendedTimestamp(lastModifiedTime, lastAccessTime, creationTime);
    1226            }
    1227            addNTFSTimestamp(lastModifiedTime, lastAccessTime, creationTime);
    1228        }
    1229        setExtra();
    1230    }
    1231
    1232    /**
    1233     * The "general purpose bit" field.
    1234     * @param b the general purpose bit
    1235     * @since 1.1
    1236     */
    1237    public void setGeneralPurposeBit(final GeneralPurposeBit b) {
    1238        gpb = b;
    1239    }
    1240
    1241    /**
    1242     * Sets the internal file attributes.
    1243     * @param value an {@code int} value
    1244     */
    1245    public void setInternalAttributes(final int value) {
    1246        internalAttributes = value;
    1247    }
    1248
    1249    @Override
    1250    public ZipEntry setLastAccessTime(final FileTime time) {
    1251        super.setLastAccessTime(time);
    1252        setExtraTimeFields();
    1253        return this;
    1254    }
    1255
    1256    @Override
    1257    public ZipEntry setLastModifiedTime(final FileTime time) {
    1258        internalSetLastModifiedTime(time);
    1259        setExtraTimeFields();
    1260        return this;
    1261    }
    1262
    1263    protected void setLocalHeaderOffset(final long localHeaderOffset) {
    1264        this.localHeaderOffset = localHeaderOffset;
    1265    }
    1266
    1267    /**
    1268     * Sets the compression method of this entry.
    1269     *
    1270     * @param method compression method
    1271     *
    1272     * @since 1.1
    1273     */
    1274    @Override
    1275    public void setMethod(final int method) {
    1276        if (method < 0) {
    1277            throw new IllegalArgumentException(
    1278                    "ZIP compression method can not be negative: " + method);
    1279        }
    1280        this.method = method;
    1281    }
    1282
    1283    /**
    1284     * Set the name of the entry.
    1285     * @param name the name to use
    1286     */
    1287    protected void setName(String name) {
    1288        if (name != null && getPlatform() == PLATFORM_FAT
    1289            && !name.contains("/")) {
    1290            name = name.replace('\\', '/');
    1291        }
    1292        this.name = name;
    1293    }
    1294
    1295    /**
    1296     * Sets the name using the raw bytes and the string created from
    1297     * it by guessing or using the configured encoding.
    1298     * @param name the name to use created from the raw bytes using
    1299     * the guessed or configured encoding
    1300     * @param rawName the bytes originally read as name from the
    1301     * archive
    1302     * @since 1.2
    1303     */
    1304    protected void setName(final String name, final byte[] rawName) {
    1305        setName(name);
    1306        this.rawName = rawName;
    1307    }
    1308
    1309    /**
    1310     * Sets the source of the name field value.
    1311     * @param nameSource source of the name field value
    1312     * @since 1.16
    1313     */
    1314    public void setNameSource(final NameSource nameSource) {
    1315        this.nameSource = nameSource;
    1316    }
    1317
    1318    /**
    1319     * Set the platform (UNIX or FAT).
    1320     * @param platform an {@code int} value - 0 is FAT, 3 is UNIX
    1321     */
    1322    protected void setPlatform(final int platform) {
    1323        this.platform = platform;
    1324    }
    1325
    1326    /**
    1327     * Sets the content of the flags field.
    1328     * @param rawFlag content of the flags field
    1329     * @since 1.11
    1330     */
    1331    public void setRawFlag(final int rawFlag) {
    1332        this.rawFlag = rawFlag;
    1333    }
    1334
    1335    /**
    1336     * Sets the uncompressed size of the entry data.
    1337     * @param size the uncompressed size in bytes
    1338     * @throws IllegalArgumentException if the specified size is less
    1339     *            than 0
    1340     */
    1341    @Override
    1342    public void setSize(final long size) {
    1343        if (size < 0) {
    1344            throw new IllegalArgumentException("Invalid entry size");
    1345        }
    1346        this.size = size;
    1347    }
    1348
    1349    protected void setStreamContiguous(final boolean isStreamContiguous) {
    1350        this.isStreamContiguous = isStreamContiguous;
    1351    }
    1352
    1353    /**
    1354     * Sets the modification time of the entry.
    1355     * @param fileTime the entry modification time.
    1356     * @since 1.21
    1357     */
    1358    public void setTime(final FileTime fileTime) {
    1359        setTime(fileTime.toMillis());
    1360    }
    1361
    1362    /**
    1363     *
    1364     * {@inheritDoc}
    1365     *
    1366     * <p>Override to work around bug <a href="https://bugs.openjdk.org/browse/JDK-8130914">JDK-8130914</a></p>
    1367     *
    1368     * @param time
    1369     *         The last modification time of the entry in milliseconds
    1370     *         since the epoch
    1371     * @see #getTime()
    1372     * @see #getLastModifiedTime()
    1373     */
    1374    @Override
    1375    public void setTime(final long time) {
    1376        if (ZipUtil.isDosTime(time)) {
    1377            super.setTime(time);
    1378            this.time = time;
    1379            lastModifiedDateSet = false;
    1380            setExtraTimeFields();
    1381        } else {
    1382            setLastModifiedTime(FileTime.fromMillis(time));
    1383        }
    1384    }
    1385
    1386    /**
    1387     * Sets Unix permissions in a way that is understood by Info-Zip's
    1388     * unzip command.
    1389     * @param mode an {@code int} value
    1390     */
    1391    public void setUnixMode(final int mode) {
    1392        // CheckStyle:MagicNumberCheck OFF - no point
    1393        setExternalAttributes((mode << SHORT_SHIFT)
    1394                              // MS-DOS read-only attribute
    1395                              | ((mode & 0200) == 0 ? 1 : 0)
    1396                              // MS-DOS directory flag
    1397                              | (isDirectory() ? 0x10 : 0));
    1398        // CheckStyle:MagicNumberCheck ON
    1399        platform = PLATFORM_UNIX;
    1400    }
    1401
    1402    /**
    1403     * Sets the "version made by" field.
    1404     * @param versionMadeBy "version made by" field
    1405     * @since 1.11
    1406     */
    1407    public void setVersionMadeBy(final int versionMadeBy) {
    1408        this.versionMadeBy = versionMadeBy;
    1409    }
    1410
    1411    /**
    1412     * Sets the "version required to expand" field.
    1413     * @param versionRequired "version required to expand" field
    1414     * @since 1.11
    1415     */
    1416    public void setVersionRequired(final int versionRequired) {
    1417        this.versionRequired = versionRequired;
    1418    }
    1419
    1420    private void updateTimeFieldsFromExtraFields() {
    1421        // Update times from X5455_ExtendedTimestamp field
    1422        updateTimeFromExtendedTimestampField();
    1423        // Update times from X000A_NTFS field, overriding X5455_ExtendedTimestamp if both are present
    1424        updateTimeFromNtfsField();
    1425    }
    1426
    1427    /**
    1428     * Workaround for the fact that, as of Java 17, {@link java.util.zip.ZipEntry} does not properly modify
    1429     * the entry's {@code xdostime} field, only setting {@code mtime}. While this is not strictly necessary,
    1430     * it's better to maintain the same behavior between this and the NTFS field.
    1431     */
    1432    private void updateTimeFromExtendedTimestampField() {
    1433        final ZipExtraField extraField = getExtraField(X5455_ExtendedTimestamp.HEADER_ID);
    1434        if (extraField instanceof X5455_ExtendedTimestamp) {
    1435            final X5455_ExtendedTimestamp extendedTimestamp = (X5455_ExtendedTimestamp) extraField;
    1436            if (extendedTimestamp.isBit0_modifyTimePresent()) {
    1437                final FileTime modifyTime = extendedTimestamp.getModifyFileTime();
    1438                if (modifyTime != null) {
    1439                    internalSetLastModifiedTime(modifyTime);
    1440                }
    1441            }
    1442            if (extendedTimestamp.isBit1_accessTimePresent()) {
    1443                final FileTime accessTime = extendedTimestamp.getAccessFileTime();
    1444                if (accessTime != null) {
    1445                    super.setLastAccessTime(accessTime);
    1446                }
    1447            }
    1448            if (extendedTimestamp.isBit2_createTimePresent()) {
    1449                final FileTime creationTime = extendedTimestamp.getCreateFileTime();
    1450                if (creationTime != null) {
    1451                    super.setCreationTime(creationTime);
    1452                }
    1453            }
    1454        }
    1455    }
    1456
    1457    /**
    1458     * Workaround for the fact that, as of Java 17, {@link java.util.zip.ZipEntry} parses NTFS
    1459     * timestamps with a maximum precision of microseconds, which is lower than the 100ns precision
    1460     * provided by this extra field.
    1461     */
    1462    private void updateTimeFromNtfsField() {
    1463        final ZipExtraField extraField = getExtraField(X000A_NTFS.HEADER_ID);
    1464        if (extraField instanceof X000A_NTFS) {
    1465            final X000A_NTFS ntfsTimestamp = (X000A_NTFS) extraField;
    1466            final FileTime modifyTime = ntfsTimestamp.getModifyFileTime();
    1467            if (modifyTime != null) {
    1468                internalSetLastModifiedTime(modifyTime);
    1469            }
    1470            final FileTime accessTime = ntfsTimestamp.getAccessFileTime();
    1471            if (accessTime != null) {
    1472                super.setLastAccessTime(accessTime);
    1473            }
    1474            final FileTime creationTime = ntfsTimestamp.getCreateFileTime();
    1475            if (creationTime != null) {
    1476                super.setCreationTime(creationTime);
    1477            }
    1478        }
    1479    }
    1480}