001    /*
002     *  Licensed to the Apache Software Foundation (ASF) under one or more
003     *  contributor license agreements.  See the NOTICE file distributed with
004     *  this work for additional information regarding copyright ownership.
005     *  The ASF licenses this file to You under the Apache License, Version 2.0
006     *  (the "License"); you may not use this file except in compliance with
007     *  the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     *
017     */
018    package org.apache.commons.compress.archivers.zip;
019    
020    import org.apache.commons.compress.archivers.ArchiveEntry;
021    
022    import java.io.File;
023    import java.util.ArrayList;
024    import java.util.Arrays;
025    import java.util.Date;
026    import java.util.LinkedHashMap;
027    import java.util.List;
028    import 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     * {@link <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">
036     * APPNOTE.txt</a>}:</p>
037     * <ul>
038     *   <li>the extra byte array consists of a sequence of extra fields</li>
039     *   <li>each extra fields starts by a two byte header id followed by
040     *   a two byte sequence holding the length of the remainder of
041     *   data.</li>
042     * </ul>
043     *
044     * <p>Any extra data that cannot be parsed by the rules above will be
045     * consumed as "unparseable" extra data and treated differently by the
046     * methods of this class.  Versions prior to Apache Commons Compress
047     * 1.1 would have thrown an exception if any attempt was made to read
048     * or write extra data not conforming to the recommendation.</p>
049     *
050     * @NotThreadSafe
051     */
052    public class ZipArchiveEntry extends java.util.zip.ZipEntry
053        implements ArchiveEntry {
054    
055        public static final int PLATFORM_UNIX = 3;
056        public static final int PLATFORM_FAT  = 0;
057        private static final int SHORT_MASK = 0xFFFF;
058        private static final int SHORT_SHIFT = 16;
059        private static final byte[] EMPTY = new byte[0];
060    
061        /**
062         * The {@link java.util.zip.ZipEntry} base class only supports
063         * the compression methods STORED and DEFLATED. We override the
064         * field so that any compression methods can be used.
065         * <p>
066         * The default value -1 means that the method has not been specified.
067         *
068         * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-93"
069         *        >COMPRESS-93</a>
070         */
071        private int method = -1;
072    
073        /**
074         * The {@link java.util.zip.ZipEntry#setSize} method in the base
075         * class throws an IllegalArgumentException if the size is bigger
076         * than 2GB for Java versions < 7.  Need to keep our own size
077         * information for Zip64 support.
078         */
079        private long size = SIZE_UNKNOWN;
080    
081        private int internalAttributes = 0;
082        private int platform = PLATFORM_FAT;
083        private long externalAttributes = 0;
084        private LinkedHashMap<ZipShort, ZipExtraField> extraFields = null;
085        private UnparseableExtraFieldData unparseableExtra = null;
086        private String name = null;
087        private byte[] rawName = null;
088        private GeneralPurposeBit gpb = new GeneralPurposeBit();
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(entry.getExtraFields(true));
142        }
143    
144        /**
145         */
146        protected ZipArchiveEntry() {
147            this("");
148        }
149    
150        /**
151         * Creates a new zip entry taking some information from the given
152         * file and using the provided name.
153         *
154         * <p>The name will be adjusted to end with a forward slash "/" if
155         * the file is a directory.  If the file is not a directory a
156         * potential trailing forward slash will be stripped from the
157         * entry name.</p>
158         */
159        public ZipArchiveEntry(File inputFile, String entryName) {
160            this(inputFile.isDirectory() && !entryName.endsWith("/") ? 
161                 entryName + "/" : entryName);
162            if (inputFile.isFile()){
163                setSize(inputFile.length());
164            }
165            setTime(inputFile.lastModified());
166            // TODO are there any other fields we can set here?
167        }
168    
169        /**
170         * Overwrite clone.
171         * @return a cloned copy of this ZipArchiveEntry
172         */
173        @Override
174        public Object clone() {
175            ZipArchiveEntry e = (ZipArchiveEntry) super.clone();
176    
177            e.setInternalAttributes(getInternalAttributes());
178            e.setExternalAttributes(getExternalAttributes());
179            e.setExtraFields(getExtraFields(true));
180            return e;
181        }
182    
183        /**
184         * Returns the compression method of this entry, or -1 if the
185         * compression method has not been specified.
186         *
187         * @return compression method
188         *
189         * @since 1.1
190         */
191        @Override
192        public int getMethod() {
193            return method;
194        }
195    
196        /**
197         * Sets the compression method of this entry.
198         *
199         * @param method compression method
200         *
201         * @since 1.1
202         */
203        @Override
204        public void setMethod(int method) {
205            if (method < 0) {
206                throw new IllegalArgumentException(
207                        "ZIP compression method can not be negative: " + method);
208            }
209            this.method = method;
210        }
211    
212        /**
213         * Retrieves the internal file attributes.
214         *
215         * @return the internal file attributes
216         */
217        public int getInternalAttributes() {
218            return internalAttributes;
219        }
220    
221        /**
222         * Sets the internal file attributes.
223         * @param value an <code>int</code> value
224         */
225        public void setInternalAttributes(int value) {
226            internalAttributes = value;
227        }
228    
229        /**
230         * Retrieves the external file attributes.
231         * @return the external file attributes
232         */
233        public long getExternalAttributes() {
234            return externalAttributes;
235        }
236    
237        /**
238         * Sets the external file attributes.
239         * @param value an <code>long</code> value
240         */
241        public void setExternalAttributes(long value) {
242            externalAttributes = value;
243        }
244    
245        /**
246         * Sets Unix permissions in a way that is understood by Info-Zip's
247         * unzip command.
248         * @param mode an <code>int</code> value
249         */
250        public void setUnixMode(int mode) {
251            // CheckStyle:MagicNumberCheck OFF - no point
252            setExternalAttributes((mode << SHORT_SHIFT)
253                                  // MS-DOS read-only attribute
254                                  | ((mode & 0200) == 0 ? 1 : 0)
255                                  // MS-DOS directory flag
256                                  | (isDirectory() ? 0x10 : 0));
257            // CheckStyle:MagicNumberCheck ON
258            platform = PLATFORM_UNIX;
259        }
260    
261        /**
262         * Unix permission.
263         * @return the unix permissions
264         */
265        public int getUnixMode() {
266            return platform != PLATFORM_UNIX ? 0 :
267                (int) ((getExternalAttributes() >> SHORT_SHIFT) & SHORT_MASK);
268        }
269    
270        /**
271         * Returns true if this entry represents a unix symlink,
272         * in which case the entry's content contains the target path
273         * for the symlink.
274         *
275         * @since 1.5
276         * @return true if the entry represents a unix symlink, false otherwise.
277         */
278        public boolean isUnixSymlink() {
279            return (getUnixMode() & UnixStat.LINK_FLAG) == UnixStat.LINK_FLAG;
280        }
281    
282        /**
283         * Platform specification to put into the &quot;version made
284         * by&quot; part of the central file header.
285         *
286         * @return PLATFORM_FAT unless {@link #setUnixMode setUnixMode}
287         * has been called, in which case PLATFORM_UNIX will be returned.
288         */
289        public int getPlatform() {
290            return platform;
291        }
292    
293        /**
294         * Set the platform (UNIX or FAT).
295         * @param platform an <code>int</code> value - 0 is FAT, 3 is UNIX
296         */
297        protected void setPlatform(int platform) {
298            this.platform = platform;
299        }
300    
301        /**
302         * Replaces all currently attached extra fields with the new array.
303         * @param fields an array of extra fields
304         */
305        public void setExtraFields(ZipExtraField[] fields) {
306            extraFields = new LinkedHashMap<ZipShort, ZipExtraField>();
307            for (ZipExtraField field : fields) {
308                if (field instanceof UnparseableExtraFieldData) {
309                    unparseableExtra = (UnparseableExtraFieldData) field;
310                } else {
311                    extraFields.put(field.getHeaderId(), field);
312                }
313            }
314            setExtra();
315        }
316    
317        /**
318         * Retrieves all extra fields that have been parsed successfully.
319         * @return an array of the extra fields
320         */
321        public ZipExtraField[] getExtraFields() {
322            return getExtraFields(false);
323        }
324    
325        /**
326         * Retrieves extra fields.
327         * @param includeUnparseable whether to also return unparseable
328         * extra fields as {@link UnparseableExtraFieldData} if such data
329         * exists.
330         * @return an array of the extra fields
331         *
332         * @since 1.1
333         */
334        public ZipExtraField[] getExtraFields(boolean includeUnparseable) {
335            if (extraFields == null) {
336                return !includeUnparseable || unparseableExtra == null
337                    ? new ZipExtraField[0]
338                    : new ZipExtraField[] { unparseableExtra };
339            }
340            List<ZipExtraField> result =
341                new ArrayList<ZipExtraField>(extraFields.values());
342            if (includeUnparseable && unparseableExtra != null) {
343                result.add(unparseableExtra);
344            }
345            return result.toArray(new ZipExtraField[0]);
346        }
347    
348        /**
349         * Adds an extra field - replacing an already present extra field
350         * of the same type.
351         *
352         * <p>If no extra field of the same type exists, the field will be
353         * added as last field.</p>
354         * @param ze an extra field
355         */
356        public void addExtraField(ZipExtraField ze) {
357            if (ze instanceof UnparseableExtraFieldData) {
358                unparseableExtra = (UnparseableExtraFieldData) ze;
359            } else {
360                if (extraFields == null) {
361                    extraFields = new LinkedHashMap<ZipShort, ZipExtraField>();
362                }
363                extraFields.put(ze.getHeaderId(), ze);
364            }
365            setExtra();
366        }
367    
368        /**
369         * Adds an extra field - replacing an already present extra field
370         * of the same type.
371         *
372         * <p>The new extra field will be the first one.</p>
373         * @param ze an extra field
374         */
375        public void addAsFirstExtraField(ZipExtraField ze) {
376            if (ze instanceof UnparseableExtraFieldData) {
377                unparseableExtra = (UnparseableExtraFieldData) ze;
378            } else {
379                LinkedHashMap<ZipShort, ZipExtraField> copy = extraFields;
380                extraFields = new LinkedHashMap<ZipShort, ZipExtraField>();
381                extraFields.put(ze.getHeaderId(), ze);
382                if (copy != null) {
383                    copy.remove(ze.getHeaderId());
384                    extraFields.putAll(copy);
385                }
386            }
387            setExtra();
388        }
389    
390        /**
391         * Remove an extra field.
392         * @param type the type of extra field to remove
393         */
394        public void removeExtraField(ZipShort type) {
395            if (extraFields == null) {
396                throw new java.util.NoSuchElementException();
397            }
398            if (extraFields.remove(type) == null) {
399                throw new java.util.NoSuchElementException();
400            }
401            setExtra();
402        }
403    
404        /**
405         * Removes unparseable extra field data.
406         *
407         * @since 1.1
408         */
409        public void removeUnparseableExtraFieldData() {
410            if (unparseableExtra == null) {
411                throw new java.util.NoSuchElementException();
412            }
413            unparseableExtra = null;
414            setExtra();
415        }
416    
417        /**
418         * Looks up an extra field by its header id.
419         *
420         * @return null if no such field exists.
421         */
422        public ZipExtraField getExtraField(ZipShort type) {
423            if (extraFields != null) {
424                return extraFields.get(type);
425            }
426            return null;
427        }
428    
429        /**
430         * Looks up extra field data that couldn't be parsed correctly.
431         *
432         * @return null if no such field exists.
433         *
434         * @since 1.1
435         */
436        public UnparseableExtraFieldData getUnparseableExtraFieldData() {
437            return unparseableExtra;
438        }
439    
440        /**
441         * Parses the given bytes as extra field data and consumes any
442         * unparseable data as an {@link UnparseableExtraFieldData}
443         * instance.
444         * @param extra an array of bytes to be parsed into extra fields
445         * @throws RuntimeException if the bytes cannot be parsed
446         * @throws RuntimeException on error
447         */
448        @Override
449        public void setExtra(byte[] extra) throws RuntimeException {
450            try {
451                ZipExtraField[] local =
452                    ExtraFieldUtils.parse(extra, true,
453                                          ExtraFieldUtils.UnparseableExtraField.READ);
454                mergeExtraFields(local, true);
455            } catch (ZipException e) {
456                // actually this is not possible as of Commons Compress 1.1
457                throw new RuntimeException("Error parsing extra fields for entry: "
458                                           + getName() + " - " + e.getMessage(), e);
459            }
460        }
461    
462        /**
463         * Unfortunately {@link java.util.zip.ZipOutputStream
464         * java.util.zip.ZipOutputStream} seems to access the extra data
465         * directly, so overriding getExtra doesn't help - we need to
466         * modify super's data directly.
467         */
468        protected void setExtra() {
469            super.setExtra(ExtraFieldUtils.mergeLocalFileDataData(getExtraFields(true)));
470        }
471    
472        /**
473         * Sets the central directory part of extra fields.
474         */
475        public void setCentralDirectoryExtra(byte[] b) {
476            try {
477                ZipExtraField[] central =
478                    ExtraFieldUtils.parse(b, false,
479                                          ExtraFieldUtils.UnparseableExtraField.READ);
480                mergeExtraFields(central, false);
481            } catch (ZipException e) {
482                throw new RuntimeException(e.getMessage(), e);
483            }
484        }
485    
486        /**
487         * Retrieves the extra data for the local file data.
488         * @return the extra data for local file
489         */
490        public byte[] getLocalFileDataExtra() {
491            byte[] extra = getExtra();
492            return extra != null ? extra : EMPTY;
493        }
494    
495        /**
496         * Retrieves the extra data for the central directory.
497         * @return the central directory extra data
498         */
499        public byte[] getCentralDirectoryExtra() {
500            return ExtraFieldUtils.mergeCentralDirectoryData(getExtraFields(true));
501        }
502    
503        /**
504         * Get the name of the entry.
505         * @return the entry name
506         */
507        @Override
508        public String getName() {
509            return name == null ? super.getName() : name;
510        }
511    
512        /**
513         * Is this entry a directory?
514         * @return true if the entry is a directory
515         */
516        @Override
517        public boolean isDirectory() {
518            return getName().endsWith("/");
519        }
520    
521        /**
522         * Set the name of the entry.
523         * @param name the name to use
524         */
525        protected void setName(String name) {
526            if (name != null && getPlatform() == PLATFORM_FAT
527                && name.indexOf("/") == -1) {
528                name = name.replace('\\', '/');
529            }
530            this.name = name;
531        }
532    
533        /**
534         * Gets the uncompressed size of the entry data.
535         * @return the entry size
536         */
537        @Override
538        public long getSize() {
539            return size;
540        }
541    
542        /**
543         * Sets the uncompressed size of the entry data.
544         * @param size the uncompressed size in bytes
545         * @exception IllegalArgumentException if the specified size is less
546         *            than 0
547         */
548        @Override
549        public void setSize(long size) {
550            if (size < 0) {
551                throw new IllegalArgumentException("invalid entry size");
552            }
553            this.size = size;
554        }
555    
556        /**
557         * Sets the name using the raw bytes and the string created from
558         * it by guessing or using the configured encoding.
559         * @param name the name to use created from the raw bytes using
560         * the guessed or configured encoding
561         * @param rawName the bytes originally read as name from the
562         * archive
563         * @since 1.2
564         */
565        protected void setName(String name, byte[] rawName) {
566            setName(name);
567            this.rawName = rawName;
568        }
569    
570        /**
571         * Returns the raw bytes that made up the name before it has been
572         * converted using the configured or guessed encoding.
573         *
574         * <p>This method will return null if this instance has not been
575         * read from an archive.</p>
576         *
577         * @since 1.2
578         */
579        public byte[] getRawName() {
580            if (rawName != null) {
581                byte[] b = new byte[rawName.length];
582                System.arraycopy(rawName, 0, b, 0, rawName.length);
583                return b;
584            }
585            return null;
586        }
587    
588        /**
589         * Get the hashCode of the entry.
590         * This uses the name as the hashcode.
591         * @return a hashcode.
592         */
593        @Override
594        public int hashCode() {
595            // this method has severe consequences on performance. We cannot rely
596            // on the super.hashCode() method since super.getName() always return
597            // the empty string in the current implemention (there's no setter)
598            // so it is basically draining the performance of a hashmap lookup
599            return getName().hashCode();
600        }
601    
602        /**
603         * The "general purpose bit" field.
604         * @since 1.1
605         */
606        public GeneralPurposeBit getGeneralPurposeBit() {
607            return gpb;
608        }
609    
610        /**
611         * The "general purpose bit" field.
612         * @since 1.1
613         */
614        public void setGeneralPurposeBit(GeneralPurposeBit b) {
615            gpb = b;
616        }
617    
618        /**
619         * If there are no extra fields, use the given fields as new extra
620         * data - otherwise merge the fields assuming the existing fields
621         * and the new fields stem from different locations inside the
622         * archive.
623         * @param f the extra fields to merge
624         * @param local whether the new fields originate from local data
625         */
626        private void mergeExtraFields(ZipExtraField[] f, boolean local)
627            throws ZipException {
628            if (extraFields == null) {
629                setExtraFields(f);
630            } else {
631                for (ZipExtraField element : f) {
632                    ZipExtraField existing;
633                    if (element instanceof UnparseableExtraFieldData) {
634                        existing = unparseableExtra;
635                    } else {
636                        existing = getExtraField(element.getHeaderId());
637                    }
638                    if (existing == null) {
639                        addExtraField(element);
640                    } else {
641                        if (local) {
642                            byte[] b = element.getLocalFileDataData();
643                            existing.parseFromLocalFileData(b, 0, b.length);
644                        } else {
645                            byte[] b = element.getCentralDirectoryData();
646                            existing.parseFromCentralDirectoryData(b, 0, b.length);
647                        }
648                    }
649                }
650                setExtra();
651            }
652        }
653    
654        /** {@inheritDoc} */
655        public Date getLastModifiedDate() {
656            return new Date(getTime());
657        }
658    
659        /* (non-Javadoc)
660         * @see java.lang.Object#equals(java.lang.Object)
661         */
662        @Override
663        public boolean equals(Object obj) {
664            if (this == obj) {
665                return true;
666            }
667            if (obj == null || getClass() != obj.getClass()) {
668                return false;
669            }
670            ZipArchiveEntry other = (ZipArchiveEntry) obj;
671            String myName = getName();
672            String otherName = other.getName();
673            if (myName == null) {
674                if (otherName != null) {
675                    return false;
676                }
677            } else if (!myName.equals(otherName)) {
678                return false;
679            }
680            String myComment = getComment();
681            String otherComment = other.getComment();
682            if (myComment == null) {
683                myComment = "";
684            }
685            if (otherComment == null) {
686                otherComment = "";
687            }
688            return getTime() == other.getTime()
689                && myComment.equals(otherComment)
690                && getInternalAttributes() == other.getInternalAttributes()
691                && getPlatform() == other.getPlatform()
692                && getExternalAttributes() == other.getExternalAttributes()
693                && getMethod() == other.getMethod()
694                && getSize() == other.getSize()
695                && getCrc() == other.getCrc()
696                && getCompressedSize() == other.getCompressedSize()
697                && Arrays.equals(getCentralDirectoryExtra(),
698                                 other.getCentralDirectoryExtra())
699                && Arrays.equals(getLocalFileDataExtra(),
700                                 other.getLocalFileDataExtra())
701                && gpb.equals(other.gpb);
702        }
703    }