001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 *
017 */
018package org.apache.commons.compress.archivers.zip;
019
020import org.apache.commons.compress.archivers.ArchiveEntry;
021import org.apache.commons.compress.archivers.EntryStreamOffsets;
022
023import java.io.File;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Date;
027import java.util.List;
028import java.util.zip.ZipException;
029
030/**
031 * Extension that adds better handling of extra fields and provides
032 * access to the internal and external file attributes.
033 *
034 * <p>The extra data is expected to follow the recommendation of
035 * <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">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 */
051public class ZipArchiveEntry extends java.util.zip.ZipEntry
052    implements ArchiveEntry, EntryStreamOffsets
053{
054
055    public static final int PLATFORM_UNIX = 3;
056    public static final int PLATFORM_FAT  = 0;
057    public static final int CRC_UNKNOWN = -1;
058    private static final int SHORT_MASK = 0xFFFF;
059    private static final int SHORT_SHIFT = 16;
060    private static final byte[] EMPTY = new byte[0];
061
062    /**
063     * The {@link java.util.zip.ZipEntry} base class only supports
064     * the compression methods STORED and DEFLATED. We override the
065     * field so that any compression methods can be used.
066     * <p>
067     * The default value -1 means that the method has not been specified.
068     *
069     * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-93"
070     *        >COMPRESS-93</a>
071     */
072    private int method = ZipMethod.UNKNOWN_CODE;
073
074    /**
075     * The {@link java.util.zip.ZipEntry#setSize} method in the base
076     * class throws an IllegalArgumentException if the size is bigger
077     * than 2GB for Java versions &lt; 7 and even in Java 7+ if the
078     * implementation in java.util.zip doesn't support Zip64 itself
079     * (it is an optional feature).
080     *
081     * <p>We need to keep our own size information for Zip64 support.</p>
082     */
083    private long size = SIZE_UNKNOWN;
084
085    private int internalAttributes = 0;
086    private int versionRequired;
087    private int versionMadeBy;
088    private int platform = PLATFORM_FAT;
089    private int rawFlag;
090    private long externalAttributes = 0;
091    private int alignment = 0;
092    private ZipExtraField[] extraFields;
093    private UnparseableExtraFieldData unparseableExtra = null;
094    private String name = null;
095    private byte[] rawName = null;
096    private GeneralPurposeBit gpb = new GeneralPurposeBit();
097    private static final ZipExtraField[] noExtraFields = new ZipExtraField[0];
098    private long localHeaderOffset = OFFSET_UNKNOWN;
099    private long dataOffset = OFFSET_UNKNOWN;
100    private boolean isStreamContiguous = false;
101
102
103    /**
104     * Creates a new zip entry with the specified name.
105     *
106     * <p>Assumes the entry represents a directory if and only if the
107     * name ends with a forward slash "/".</p>
108     *
109     * @param name the name of the entry
110     */
111    public ZipArchiveEntry(final String name) {
112        super(name);
113        setName(name);
114    }
115
116    /**
117     * Creates a new zip entry with fields taken from the specified zip entry.
118     *
119     * <p>Assumes the entry represents a directory if and only if the
120     * name ends with a forward slash "/".</p>
121     *
122     * @param entry the entry to get fields from
123     * @throws ZipException on error
124     */
125    public ZipArchiveEntry(final java.util.zip.ZipEntry entry) throws ZipException {
126        super(entry);
127        setName(entry.getName());
128        final byte[] extra = entry.getExtra();
129        if (extra != null) {
130            setExtraFields(ExtraFieldUtils.parse(extra, true,
131                                                 ExtraFieldUtils
132                                                 .UnparseableExtraField.READ));
133        } else {
134            // initializes extra data to an empty byte array
135            setExtra();
136        }
137        setMethod(entry.getMethod());
138        this.size = entry.getSize();
139    }
140
141    /**
142     * Creates a new zip entry with fields taken from the specified zip entry.
143     *
144     * <p>Assumes the entry represents a directory if and only if the
145     * name ends with a forward slash "/".</p>
146     *
147     * @param entry the entry to get fields from
148     * @throws ZipException on error
149     */
150    public ZipArchiveEntry(final ZipArchiveEntry entry) throws ZipException {
151        this((java.util.zip.ZipEntry) entry);
152        setInternalAttributes(entry.getInternalAttributes());
153        setExternalAttributes(entry.getExternalAttributes());
154        setExtraFields(getAllExtraFieldsNoCopy());
155        setPlatform(entry.getPlatform());
156        final GeneralPurposeBit other = entry.getGeneralPurposeBit();
157        setGeneralPurposeBit(other == null ? null :
158                             (GeneralPurposeBit) other.clone());
159    }
160
161    /**
162     */
163    protected ZipArchiveEntry() {
164        this("");
165    }
166
167    /**
168     * Creates a new zip entry taking some information from the given
169     * file and using the provided name.
170     *
171     * <p>The name will be adjusted to end with a forward slash "/" if
172     * the file is a directory.  If the file is not a directory a
173     * potential trailing forward slash will be stripped from the
174     * entry name.</p>
175     * @param inputFile file to create the entry from
176     * @param entryName name of the entry
177     */
178    public ZipArchiveEntry(final File inputFile, final String entryName) {
179        this(inputFile.isDirectory() && !entryName.endsWith("/") ? 
180             entryName + "/" : entryName);
181        if (inputFile.isFile()){
182            setSize(inputFile.length());
183        }
184        setTime(inputFile.lastModified());
185        // TODO are there any other fields we can set here?
186    }
187
188    /**
189     * Overwrite clone.
190     * @return a cloned copy of this ZipArchiveEntry
191     */
192    @Override
193    public Object clone() {
194        final ZipArchiveEntry e = (ZipArchiveEntry) super.clone();
195
196        e.setInternalAttributes(getInternalAttributes());
197        e.setExternalAttributes(getExternalAttributes());
198        e.setExtraFields(getAllExtraFieldsNoCopy());
199        return e;
200    }
201
202    /**
203     * Returns the compression method of this entry, or -1 if the
204     * compression method has not been specified.
205     *
206     * @return compression method
207     *
208     * @since 1.1
209     */
210    @Override
211    public int getMethod() {
212        return method;
213    }
214
215    /**
216     * Sets the compression method of this entry.
217     *
218     * @param method compression method
219     *
220     * @since 1.1
221     */
222    @Override
223    public void setMethod(final int method) {
224        if (method < 0) {
225            throw new IllegalArgumentException(
226                    "ZIP compression method can not be negative: " + method);
227        }
228        this.method = method;
229    }
230
231    /**
232     * Retrieves the internal file attributes.
233     *
234     * <p><b>Note</b>: {@link ZipArchiveInputStream} is unable to fill
235     * this field, you must use {@link ZipFile} if you want to read
236     * entries using this attribute.</p>
237     *
238     * @return the internal file attributes
239     */
240    public int getInternalAttributes() {
241        return internalAttributes;
242    }
243
244    /**
245     * Sets the internal file attributes.
246     * @param value an <code>int</code> value
247     */
248    public void setInternalAttributes(final int value) {
249        internalAttributes = value;
250    }
251
252    /**
253     * Retrieves the external file attributes.
254     *
255     * <p><b>Note</b>: {@link ZipArchiveInputStream} is unable to fill
256     * this field, you must use {@link ZipFile} if you want to read
257     * entries using this attribute.</p>
258     *
259     * @return the external file attributes
260     */
261    public long getExternalAttributes() {
262        return externalAttributes;
263    }
264
265    /**
266     * Sets the external file attributes.
267     * @param value an <code>long</code> value
268     */
269    public void setExternalAttributes(final long value) {
270        externalAttributes = value;
271    }
272
273    /**
274     * Sets Unix permissions in a way that is understood by Info-Zip's
275     * unzip command.
276     * @param mode an <code>int</code> value
277     */
278    public void setUnixMode(final int mode) {
279        // CheckStyle:MagicNumberCheck OFF - no point
280        setExternalAttributes((mode << SHORT_SHIFT)
281                              // MS-DOS read-only attribute
282                              | ((mode & 0200) == 0 ? 1 : 0)
283                              // MS-DOS directory flag
284                              | (isDirectory() ? 0x10 : 0));
285        // CheckStyle:MagicNumberCheck ON
286        platform = PLATFORM_UNIX;
287    }
288
289    /**
290     * Unix permission.
291     * @return the unix permissions
292     */
293    public int getUnixMode() {
294        return platform != PLATFORM_UNIX ? 0 :
295            (int) ((getExternalAttributes() >> SHORT_SHIFT) & SHORT_MASK);
296    }
297
298    /**
299     * Returns true if this entry represents a unix symlink,
300     * in which case the entry's content contains the target path
301     * for the symlink.
302     *
303     * @since 1.5
304     * @return true if the entry represents a unix symlink, false otherwise.
305     */
306    public boolean isUnixSymlink() {
307        return (getUnixMode() & UnixStat.FILE_TYPE_FLAG) == UnixStat.LINK_FLAG;
308    }
309
310    /**
311     * Platform specification to put into the &quot;version made
312     * by&quot; part of the central file header.
313     *
314     * @return PLATFORM_FAT unless {@link #setUnixMode setUnixMode}
315     * has been called, in which case PLATFORM_UNIX will be returned.
316     */
317    public int getPlatform() {
318        return platform;
319    }
320
321    /**
322     * Set the platform (UNIX or FAT).
323     * @param platform an <code>int</code> value - 0 is FAT, 3 is UNIX
324     */
325    protected void setPlatform(final int platform) {
326        this.platform = platform;
327    }
328
329    /**
330     * Gets currently configured alignment.
331     *
332     * @return
333     *      alignment for this entry.
334     * @since 1.14
335     */
336    protected int getAlignment() {
337        return this.alignment;
338    }
339
340    /**
341     * Sets alignment for this entry.
342     *
343     * @param alignment
344     *      requested alignment, 0 for default.
345     * @since 1.14
346     */
347    public void setAlignment(int alignment) {
348        if ((alignment & (alignment - 1)) != 0 || alignment > 0xffff) {
349            throw new IllegalArgumentException("Invalid value for alignment, must be power of two and no bigger than "
350                + 0xffff + " but is " + alignment);
351        }
352        this.alignment = alignment;
353    }
354
355    /**
356     * Replaces all currently attached extra fields with the new array.
357     * @param fields an array of extra fields
358     */
359    public void setExtraFields(final ZipExtraField[] fields) {
360        final List<ZipExtraField> newFields = new ArrayList<>();
361        for (final ZipExtraField field : fields) {
362            if (field instanceof UnparseableExtraFieldData) {
363                unparseableExtra = (UnparseableExtraFieldData) field;
364            } else {
365                newFields.add( field);
366            }
367        }
368        extraFields = newFields.toArray(new ZipExtraField[newFields.size()]);
369        setExtra();
370    }
371
372    /**
373     * Retrieves all extra fields that have been parsed successfully.
374     *
375     * <p><b>Note</b>: The set of extra fields may be incomplete when
376     * {@link ZipArchiveInputStream} has been used as some extra
377     * fields use the central directory to store additional
378     * information.</p>
379     *
380     * @return an array of the extra fields
381     */
382    public ZipExtraField[] getExtraFields() {
383        return getParseableExtraFields();
384    }
385
386    /**
387     * Retrieves extra fields.
388     * @param includeUnparseable whether to also return unparseable
389     * extra fields as {@link UnparseableExtraFieldData} if such data
390     * exists.
391     * @return an array of the extra fields
392     *
393     * @since 1.1
394     */
395    public ZipExtraField[] getExtraFields(final boolean includeUnparseable) {
396        return includeUnparseable ?
397                getAllExtraFields() :
398                getParseableExtraFields();
399    }
400
401    private ZipExtraField[] getParseableExtraFieldsNoCopy() {
402        if (extraFields == null) {
403            return noExtraFields;
404        }
405        return extraFields;
406    }
407
408    private ZipExtraField[] getParseableExtraFields() {
409        final ZipExtraField[] parseableExtraFields = getParseableExtraFieldsNoCopy();
410        return (parseableExtraFields == extraFields) ? copyOf(parseableExtraFields) : parseableExtraFields;
411    }
412
413    /**
414     * Get all extra fields, including unparseable ones.
415     * @return An array of all extra fields. Not necessarily a copy of internal data structures, hence private method
416     */
417    private ZipExtraField[] getAllExtraFieldsNoCopy() {
418        if (extraFields == null) {
419            return getUnparseableOnly();
420        }
421        return unparseableExtra != null ? getMergedFields() : extraFields;
422    }
423
424    private ZipExtraField[] copyOf(final ZipExtraField[] src){
425        return copyOf(src, src.length);
426    }
427
428    private ZipExtraField[] copyOf(final ZipExtraField[] src, final int length) {
429        final ZipExtraField[] cpy = new ZipExtraField[length];
430        System.arraycopy(src, 0, cpy, 0, Math.min(src.length, length));
431        return cpy;
432    }
433
434    private ZipExtraField[] getMergedFields() {
435        final ZipExtraField[] zipExtraFields = copyOf(extraFields, extraFields.length + 1);
436        zipExtraFields[extraFields.length] = unparseableExtra;
437        return zipExtraFields;
438    }
439
440    private ZipExtraField[] getUnparseableOnly() {
441        return unparseableExtra == null ? noExtraFields : new ZipExtraField[] { unparseableExtra };
442    }
443
444    private ZipExtraField[] getAllExtraFields() {
445        final ZipExtraField[] allExtraFieldsNoCopy = getAllExtraFieldsNoCopy();
446        return (allExtraFieldsNoCopy == extraFields) ? copyOf( allExtraFieldsNoCopy) : allExtraFieldsNoCopy;
447    }
448    /**
449     * Adds an extra field - replacing an already present extra field
450     * of the same type.
451     *
452     * <p>If no extra field of the same type exists, the field will be
453     * added as last field.</p>
454     * @param ze an extra field
455     */
456    public void addExtraField(final ZipExtraField ze) {
457        if (ze instanceof UnparseableExtraFieldData) {
458            unparseableExtra = (UnparseableExtraFieldData) ze;
459        } else {
460            if (extraFields == null) {
461                extraFields = new ZipExtraField[]{ ze};
462            } else {
463                if (getExtraField(ze.getHeaderId())!= null){
464                    removeExtraField(ze.getHeaderId());
465                }
466                final ZipExtraField[] zipExtraFields = copyOf(extraFields, extraFields.length + 1);
467                zipExtraFields[zipExtraFields.length -1] = ze;
468                extraFields = zipExtraFields;
469            }
470        }
471        setExtra();
472    }
473
474    /**
475     * Adds an extra field - replacing an already present extra field
476     * of the same type.
477     *
478     * <p>The new extra field will be the first one.</p>
479     * @param ze an extra field
480     */
481    public void addAsFirstExtraField(final ZipExtraField ze) {
482        if (ze instanceof UnparseableExtraFieldData) {
483            unparseableExtra = (UnparseableExtraFieldData) ze;
484        } else {
485            if (getExtraField(ze.getHeaderId()) != null){
486                removeExtraField(ze.getHeaderId());
487            }
488            final ZipExtraField[] copy = extraFields;
489            final int newLen = extraFields != null ? extraFields.length + 1: 1;
490            extraFields = new ZipExtraField[newLen];
491            extraFields[0] = ze;
492            if (copy != null){
493                System.arraycopy(copy, 0, extraFields, 1, extraFields.length - 1);
494            }
495        }
496        setExtra();
497    }
498
499    /**
500     * Remove an extra field.
501     * @param type the type of extra field to remove
502     */
503    public void removeExtraField(final ZipShort type) {
504        if (extraFields == null) {
505            throw new java.util.NoSuchElementException();
506        }
507
508        final List<ZipExtraField> newResult = new ArrayList<>();
509        for (final ZipExtraField extraField : extraFields) {
510            if (!type.equals(extraField.getHeaderId())){
511                newResult.add( extraField);
512            }
513        }
514        if (extraFields.length == newResult.size()) {
515            throw new java.util.NoSuchElementException();
516        }
517        extraFields = newResult.toArray(new ZipExtraField[newResult.size()]);
518        setExtra();
519    }
520
521    /**
522     * Removes unparseable extra field data.
523     *
524     * @since 1.1
525     */
526    public void removeUnparseableExtraFieldData() {
527        if (unparseableExtra == null) {
528            throw new java.util.NoSuchElementException();
529        }
530        unparseableExtra = null;
531        setExtra();
532    }
533
534    /**
535     * Looks up an extra field by its header id.
536     *
537     * @param type the header id
538     * @return null if no such field exists.
539     */
540    public ZipExtraField getExtraField(final ZipShort type) {
541        if (extraFields != null) {
542            for (final ZipExtraField extraField : extraFields) {
543                if (type.equals(extraField.getHeaderId())) {
544                    return extraField;
545                }
546            }
547        }
548        return null;
549    }
550
551    /**
552     * Looks up extra field data that couldn't be parsed correctly.
553     *
554     * @return null if no such field exists.
555     *
556     * @since 1.1
557     */
558    public UnparseableExtraFieldData getUnparseableExtraFieldData() {
559        return unparseableExtra;
560    }
561
562    /**
563     * Parses the given bytes as extra field data and consumes any
564     * unparseable data as an {@link UnparseableExtraFieldData}
565     * instance.
566     * @param extra an array of bytes to be parsed into extra fields
567     * @throws RuntimeException if the bytes cannot be parsed
568     * @throws RuntimeException on error
569     */
570    @Override
571    public void setExtra(final byte[] extra) throws RuntimeException {
572        try {
573            final ZipExtraField[] local =
574                ExtraFieldUtils.parse(extra, true,
575                                      ExtraFieldUtils.UnparseableExtraField.READ);
576            mergeExtraFields(local, true);
577        } catch (final ZipException e) {
578            // actually this is not possible as of Commons Compress 1.1
579            throw new RuntimeException("Error parsing extra fields for entry: " //NOSONAR
580                                       + getName() + " - " + e.getMessage(), e);
581        }
582    }
583
584    /**
585     * Unfortunately {@link java.util.zip.ZipOutputStream
586     * java.util.zip.ZipOutputStream} seems to access the extra data
587     * directly, so overriding getExtra doesn't help - we need to
588     * modify super's data directly.
589     */
590    protected void setExtra() {
591        super.setExtra(ExtraFieldUtils.mergeLocalFileDataData(getAllExtraFieldsNoCopy()));
592    }
593
594    /**
595     * Sets the central directory part of extra fields.
596     * @param b an array of bytes to be parsed into extra fields
597     */
598    public void setCentralDirectoryExtra(final byte[] b) {
599        try {
600            final ZipExtraField[] central =
601                ExtraFieldUtils.parse(b, false,
602                                      ExtraFieldUtils.UnparseableExtraField.READ);
603            mergeExtraFields(central, false);
604        } catch (final ZipException e) {
605            throw new RuntimeException(e.getMessage(), e); //NOSONAR
606        }
607    }
608
609    /**
610     * Retrieves the extra data for the local file data.
611     * @return the extra data for local file
612     */
613    public byte[] getLocalFileDataExtra() {
614        final byte[] extra = getExtra();
615        return extra != null ? extra : EMPTY;
616    }
617
618    /**
619     * Retrieves the extra data for the central directory.
620     * @return the central directory extra data
621     */
622    public byte[] getCentralDirectoryExtra() {
623        return ExtraFieldUtils.mergeCentralDirectoryData(getAllExtraFieldsNoCopy());
624    }
625
626    /**
627     * Get the name of the entry.
628     * @return the entry name
629     */
630    @Override
631    public String getName() {
632        return name == null ? super.getName() : name;
633    }
634
635    /**
636     * Is this entry a directory?
637     * @return true if the entry is a directory
638     */
639    @Override
640    public boolean isDirectory() {
641        return getName().endsWith("/");
642    }
643
644    /**
645     * Set the name of the entry.
646     * @param name the name to use
647     */
648    protected void setName(String name) {
649        if (name != null && getPlatform() == PLATFORM_FAT
650            && !name.contains("/")) {
651            name = name.replace('\\', '/');
652        }
653        this.name = name;
654    }
655
656    /**
657     * Gets the uncompressed size of the entry data.
658     *
659     * <p><b>Note</b>: {@link ZipArchiveInputStream} may create
660     * entries that return {@link #SIZE_UNKNOWN SIZE_UNKNOWN} as long
661     * as the entry hasn't been read completely.</p>
662     *
663     * @return the entry size
664     */
665    @Override
666    public long getSize() {
667        return size;
668    }
669
670    /**
671     * Sets the uncompressed size of the entry data.
672     * @param size the uncompressed size in bytes
673     * @throws IllegalArgumentException if the specified size is less
674     *            than 0
675     */
676    @Override
677    public void setSize(final long size) {
678        if (size < 0) {
679            throw new IllegalArgumentException("invalid entry size");
680        }
681        this.size = size;
682    }
683
684    /**
685     * Sets the name using the raw bytes and the string created from
686     * it by guessing or using the configured encoding.
687     * @param name the name to use created from the raw bytes using
688     * the guessed or configured encoding
689     * @param rawName the bytes originally read as name from the
690     * archive
691     * @since 1.2
692     */
693    protected void setName(final String name, final byte[] rawName) {
694        setName(name);
695        this.rawName = rawName;
696    }
697
698    /**
699     * Returns the raw bytes that made up the name before it has been
700     * converted using the configured or guessed encoding.
701     *
702     * <p>This method will return null if this instance has not been
703     * read from an archive.</p>
704     *
705     * @return the raw name bytes
706     * @since 1.2
707     */
708    public byte[] getRawName() {
709        if (rawName != null) {
710            final byte[] b = new byte[rawName.length];
711            System.arraycopy(rawName, 0, b, 0, rawName.length);
712            return b;
713        }
714        return null;
715    }
716
717    protected long getLocalHeaderOffset() {
718        return this.localHeaderOffset;
719    }
720
721    protected void setLocalHeaderOffset(long localHeaderOffset) {
722        this.localHeaderOffset = localHeaderOffset;
723    }
724
725    @Override
726    public long getDataOffset() {
727        return dataOffset;
728    }
729
730    /**
731     * Sets the data offset.
732     *
733     * @param dataOffset
734     *      new value of data offset.
735     */
736    protected void setDataOffset(long dataOffset) {
737        this.dataOffset = dataOffset;
738    }
739
740    @Override
741    public boolean isStreamContiguous() {
742        return isStreamContiguous;
743    }
744
745    protected void setStreamContiguous(boolean isStreamContiguous) {
746        this.isStreamContiguous = isStreamContiguous;
747    }
748
749    /**
750     * Get the hashCode of the entry.
751     * This uses the name as the hashcode.
752     * @return a hashcode.
753     */
754    @Override
755    public int hashCode() {
756        // this method has severe consequences on performance. We cannot rely
757        // on the super.hashCode() method since super.getName() always return
758        // the empty string in the current implemention (there's no setter)
759        // so it is basically draining the performance of a hashmap lookup
760        return getName().hashCode();
761    }
762
763    /**
764     * The "general purpose bit" field.
765     * @return the general purpose bit
766     * @since 1.1
767     */
768    public GeneralPurposeBit getGeneralPurposeBit() {
769        return gpb;
770    }
771
772    /**
773     * The "general purpose bit" field.
774     * @param b the general purpose bit
775     * @since 1.1
776     */
777    public void setGeneralPurposeBit(final GeneralPurposeBit b) {
778        gpb = b;
779    }
780
781    /**
782     * If there are no extra fields, use the given fields as new extra
783     * data - otherwise merge the fields assuming the existing fields
784     * and the new fields stem from different locations inside the
785     * archive.
786     * @param f the extra fields to merge
787     * @param local whether the new fields originate from local data
788     */
789    private void mergeExtraFields(final ZipExtraField[] f, final boolean local)
790        throws ZipException {
791        if (extraFields == null) {
792            setExtraFields(f);
793        } else {
794            for (final ZipExtraField element : f) {
795                ZipExtraField existing;
796                if (element instanceof UnparseableExtraFieldData) {
797                    existing = unparseableExtra;
798                } else {
799                    existing = getExtraField(element.getHeaderId());
800                }
801                if (existing == null) {
802                    addExtraField(element);
803                } else {
804                    if (local) {
805                        final byte[] b = element.getLocalFileDataData();
806                        existing.parseFromLocalFileData(b, 0, b.length);
807                    } else {
808                        final byte[] b = element.getCentralDirectoryData();
809                        existing.parseFromCentralDirectoryData(b, 0, b.length);
810                    }
811                }
812            }
813            setExtra();
814        }
815    }
816
817    /**
818     * Wraps {@link java.util.zip.ZipEntry#getTime} with a {@link Date} as the
819     * entry's last modified date.
820     *
821     * <p>Changes to the implementation of {@link java.util.zip.ZipEntry#getTime}
822     * leak through and the returned value may depend on your local
823     * time zone as well as your version of Java.</p>
824     */
825    @Override
826    public Date getLastModifiedDate() {
827        return new Date(getTime());
828    }
829
830    /* (non-Javadoc)
831     * @see java.lang.Object#equals(java.lang.Object)
832     */
833    @Override
834    public boolean equals(final Object obj) {
835        if (this == obj) {
836            return true;
837        }
838        if (obj == null || getClass() != obj.getClass()) {
839            return false;
840        }
841        final ZipArchiveEntry other = (ZipArchiveEntry) obj;
842        final String myName = getName();
843        final String otherName = other.getName();
844        if (myName == null) {
845            if (otherName != null) {
846                return false;
847            }
848        } else if (!myName.equals(otherName)) {
849            return false;
850        }
851        String myComment = getComment();
852        String otherComment = other.getComment();
853        if (myComment == null) {
854            myComment = "";
855        }
856        if (otherComment == null) {
857            otherComment = "";
858        }
859        return getTime() == other.getTime()
860            && myComment.equals(otherComment)
861            && getInternalAttributes() == other.getInternalAttributes()
862            && getPlatform() == other.getPlatform()
863            && getExternalAttributes() == other.getExternalAttributes()
864            && getMethod() == other.getMethod()
865            && getSize() == other.getSize()
866            && getCrc() == other.getCrc()
867            && getCompressedSize() == other.getCompressedSize()
868            && Arrays.equals(getCentralDirectoryExtra(),
869                             other.getCentralDirectoryExtra())
870            && Arrays.equals(getLocalFileDataExtra(),
871                             other.getLocalFileDataExtra())
872            && localHeaderOffset == other.localHeaderOffset
873            && dataOffset == other.dataOffset
874            && gpb.equals(other.gpb);
875    }
876
877    /**
878     * Sets the "version made by" field.
879     * @param versionMadeBy "version made by" field
880     * @since 1.11
881     */
882    public void setVersionMadeBy(final int versionMadeBy) {
883        this.versionMadeBy = versionMadeBy;
884    }
885
886    /**
887     * Sets the "version required to expand" field.
888     * @param versionRequired "version required to expand" field
889     * @since 1.11
890     */
891    public void setVersionRequired(final int versionRequired) {
892        this.versionRequired = versionRequired;
893    }
894
895    /**
896     * The "version required to expand" field.
897     * @return "version required to expand" field
898     * @since 1.11
899     */
900    public int getVersionRequired() {
901        return versionRequired;
902    }
903
904    /**
905     * The "version made by" field.
906     * @return "version made by" field
907     * @since 1.11
908     */
909    public int getVersionMadeBy() {
910        return versionMadeBy;
911    }
912
913    /**
914     * The content of the flags field.
915     * @return content of the flags field
916     * @since 1.11
917     */
918    public int getRawFlag() {
919        return rawFlag;
920    }
921
922    /**
923     * Sets the content of the flags field.
924     * @param rawFlag content of the flags field
925     * @since 1.11
926     */
927    public void setRawFlag(final int rawFlag) {
928        this.rawFlag = rawFlag;
929    }
930}