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