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