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 java.io.File;
021    import java.util.ArrayList;
022    import java.util.Arrays;
023    import java.util.Date;
024    import java.util.LinkedHashMap;
025    import java.util.List;
026    import java.util.zip.ZipException;
027    import org.apache.commons.compress.archivers.ArchiveEntry;
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     * {@link <a href="http://www.pkware.com/documents/casestudies/APPNOTE.TXT">
035     * 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     */
051    public class ZipArchiveEntry extends java.util.zip.ZipEntry
052        implements ArchiveEntry, Cloneable {
053    
054        public static final int PLATFORM_UNIX = 3;
055        public static final int PLATFORM_FAT  = 0;
056        private static final int SHORT_MASK = 0xFFFF;
057        private static final int SHORT_SHIFT = 16;
058    
059        /**
060         * The {@link java.util.zip.ZipEntry} base class only supports
061         * the compression methods STORED and DEFLATED. We override the
062         * field so that any compression methods can be used.
063         * <p>
064         * The default value -1 means that the method has not been specified.
065         *
066         * @see <a href="https://issues.apache.org/jira/browse/COMPRESS-93"
067         *        >COMPRESS-93</a>
068         */
069        private int method = -1;
070    
071        private int internalAttributes = 0;
072        private int platform = PLATFORM_FAT;
073        private long externalAttributes = 0;
074        private LinkedHashMap/*<ZipShort, ZipExtraField>*/ extraFields = null;
075        private UnparseableExtraFieldData unparseableExtra = null;
076        private String name = null;
077        private byte[] rawName = null;
078        private GeneralPurposeBit gpb = new GeneralPurposeBit();
079    
080        /**
081         * Creates a new zip entry with the specified name.
082         *
083         * <p>Assumes the entry represents a directory if and only if the
084         * name ends with a forward slash "/".</p>
085         *
086         * @param name the name of the entry
087         */
088        public ZipArchiveEntry(String name) {
089            super(name);
090            setName(name);
091        }
092    
093        /**
094         * Creates a new zip entry with fields taken from the specified zip entry.
095         *
096         * <p>Assumes the entry represents a directory if and only if the
097         * name ends with a forward slash "/".</p>
098         *
099         * @param entry the entry to get fields from
100         * @throws ZipException on error
101         */
102        public ZipArchiveEntry(java.util.zip.ZipEntry entry) throws ZipException {
103            super(entry);
104            setName(entry.getName());
105            byte[] extra = entry.getExtra();
106            if (extra != null) {
107                setExtraFields(ExtraFieldUtils.parse(extra, true,
108                                                     ExtraFieldUtils
109                                                     .UnparseableExtraField.READ));
110            } else {
111                // initializes extra data to an empty byte array
112                setExtra();
113            }
114            setMethod(entry.getMethod());
115        }
116    
117        /**
118         * Creates a new zip entry with fields taken from the specified zip entry.
119         *
120         * <p>Assumes the entry represents a directory if and only if the
121         * name ends with a forward slash "/".</p>
122         *
123         * @param entry the entry to get fields from
124         * @throws ZipException on error
125         */
126        public ZipArchiveEntry(ZipArchiveEntry entry) throws ZipException {
127            this((java.util.zip.ZipEntry) entry);
128            setInternalAttributes(entry.getInternalAttributes());
129            setExternalAttributes(entry.getExternalAttributes());
130            setExtraFields(entry.getExtraFields(true));
131        }
132    
133        /**
134         */
135        protected ZipArchiveEntry() {
136            this("");
137        }
138    
139        /**
140         * Creates a new zip entry taking some information from the given
141         * file and using the provided name.
142         *
143         * <p>The name will be adjusted to end with a forward slash "/" if
144         * the file is a directory.  If the file is not a directory a
145         * potential trailing forward slash will be stripped from the
146         * entry name.</p>
147         */
148        public ZipArchiveEntry(File inputFile, String entryName) {
149            this(inputFile.isDirectory() && !entryName.endsWith("/") ? 
150                 entryName + "/" : entryName);
151            if (inputFile.isFile()){
152                setSize(inputFile.length());
153            }
154            setTime(inputFile.lastModified());
155            // TODO are there any other fields we can set here?
156        }
157    
158        /**
159         * Overwrite clone.
160         * @return a cloned copy of this ZipArchiveEntry
161         */
162        public Object clone() {
163            ZipArchiveEntry e = (ZipArchiveEntry) super.clone();
164    
165            e.setInternalAttributes(getInternalAttributes());
166            e.setExternalAttributes(getExternalAttributes());
167            e.setExtraFields(getExtraFields(true));
168            return e;
169        }
170    
171        /**
172         * Returns the compression method of this entry, or -1 if the
173         * compression method has not been specified.
174         *
175         * @return compression method
176         *
177         * @since Apache Commons Compress 1.1
178         */
179        public int getMethod() {
180            return method;
181        }
182    
183        /**
184         * Sets the compression method of this entry.
185         *
186         * @param method compression method
187         *
188         * @since Apache Commons Compress 1.1
189         */
190        public void setMethod(int method) {
191            if (method < 0) {
192                throw new IllegalArgumentException(
193                        "ZIP compression method can not be negative: " + method);
194            }
195            this.method = method;
196        }
197    
198        /**
199         * Retrieves the internal file attributes.
200         *
201         * @return the internal file attributes
202         */
203        public int getInternalAttributes() {
204            return internalAttributes;
205        }
206    
207        /**
208         * Sets the internal file attributes.
209         * @param value an <code>int</code> value
210         */
211        public void setInternalAttributes(int value) {
212            internalAttributes = value;
213        }
214    
215        /**
216         * Retrieves the external file attributes.
217         * @return the external file attributes
218         */
219        public long getExternalAttributes() {
220            return externalAttributes;
221        }
222    
223        /**
224         * Sets the external file attributes.
225         * @param value an <code>long</code> value
226         */
227        public void setExternalAttributes(long value) {
228            externalAttributes = value;
229        }
230    
231        /**
232         * Sets Unix permissions in a way that is understood by Info-Zip's
233         * unzip command.
234         * @param mode an <code>int</code> value
235         */
236        public void setUnixMode(int mode) {
237            // CheckStyle:MagicNumberCheck OFF - no point
238            setExternalAttributes((mode << SHORT_SHIFT)
239                                  // MS-DOS read-only attribute
240                                  | ((mode & 0200) == 0 ? 1 : 0)
241                                  // MS-DOS directory flag
242                                  | (isDirectory() ? 0x10 : 0));
243            // CheckStyle:MagicNumberCheck ON
244            platform = PLATFORM_UNIX;
245        }
246    
247        /**
248         * Unix permission.
249         * @return the unix permissions
250         */
251        public int getUnixMode() {
252            return platform != PLATFORM_UNIX ? 0 :
253                (int) ((getExternalAttributes() >> SHORT_SHIFT) & SHORT_MASK);
254        }
255    
256        /**
257         * Platform specification to put into the &quot;version made
258         * by&quot; part of the central file header.
259         *
260         * @return PLATFORM_FAT unless {@link #setUnixMode setUnixMode}
261         * has been called, in which case PLATORM_UNIX will be returned.
262         */
263        public int getPlatform() {
264            return platform;
265        }
266    
267        /**
268         * Set the platform (UNIX or FAT).
269         * @param platform an <code>int</code> value - 0 is FAT, 3 is UNIX
270         */
271        protected void setPlatform(int platform) {
272            this.platform = platform;
273        }
274    
275        /**
276         * Replaces all currently attached extra fields with the new array.
277         * @param fields an array of extra fields
278         */
279        public void setExtraFields(ZipExtraField[] fields) {
280            extraFields = new LinkedHashMap();
281            for (int i = 0; i < fields.length; i++) {
282                if (fields[i] instanceof UnparseableExtraFieldData) {
283                    unparseableExtra = (UnparseableExtraFieldData) fields[i];
284                } else {
285                    extraFields.put(fields[i].getHeaderId(), fields[i]);
286                }
287            }
288            setExtra();
289        }
290    
291        /**
292         * Retrieves all extra fields that have been parsed successfully.
293         * @return an array of the extra fields
294         */
295        public ZipExtraField[] getExtraFields() {
296            return getExtraFields(false);
297        }
298    
299        /**
300         * Retrieves extra fields.
301         * @param includeUnparseable whether to also return unparseable
302         * extra fields as {@link UnparseableExtraFieldData} if such data
303         * exists.
304         * @return an array of the extra fields
305         *
306         * @since Apache Commons Compress 1.1
307         */
308        public ZipExtraField[] getExtraFields(boolean includeUnparseable) {
309            if (extraFields == null) {
310                return !includeUnparseable || unparseableExtra == null
311                    ? new ZipExtraField[0]
312                    : new ZipExtraField[] { unparseableExtra };
313            }
314            List result = new ArrayList(extraFields.values());
315            if (includeUnparseable && unparseableExtra != null) {
316                result.add(unparseableExtra);
317            }
318            return (ZipExtraField[]) result.toArray(new ZipExtraField[0]);
319        }
320    
321        /**
322         * Adds an extra field - replacing an already present extra field
323         * of the same type.
324         *
325         * <p>If no extra field of the same type exists, the field will be
326         * added as last field.</p>
327         * @param ze an extra field
328         */
329        public void addExtraField(ZipExtraField ze) {
330            if (ze instanceof UnparseableExtraFieldData) {
331                unparseableExtra = (UnparseableExtraFieldData) ze;
332            } else {
333                if (extraFields == null) {
334                    extraFields = new LinkedHashMap();
335                }
336                extraFields.put(ze.getHeaderId(), ze);
337            }
338            setExtra();
339        }
340    
341        /**
342         * Adds an extra field - replacing an already present extra field
343         * of the same type.
344         *
345         * <p>The new extra field will be the first one.</p>
346         * @param ze an extra field
347         */
348        public void addAsFirstExtraField(ZipExtraField ze) {
349            if (ze instanceof UnparseableExtraFieldData) {
350                unparseableExtra = (UnparseableExtraFieldData) ze;
351            } else {
352                LinkedHashMap copy = extraFields;
353                extraFields = new LinkedHashMap();
354                extraFields.put(ze.getHeaderId(), ze);
355                if (copy != null) {
356                    copy.remove(ze.getHeaderId());
357                    extraFields.putAll(copy);
358                }
359            }
360            setExtra();
361        }
362    
363        /**
364         * Remove an extra field.
365         * @param type the type of extra field to remove
366         */
367        public void removeExtraField(ZipShort type) {
368            if (extraFields == null) {
369                throw new java.util.NoSuchElementException();
370            }
371            if (extraFields.remove(type) == null) {
372                throw new java.util.NoSuchElementException();
373            }
374            setExtra();
375        }
376    
377        /**
378         * Removes unparseable extra field data.
379         *
380         * @since Apache Commons Compress 1.1
381         */
382        public void removeUnparseableExtraFieldData() {
383            if (unparseableExtra == null) {
384                throw new java.util.NoSuchElementException();
385            }
386            unparseableExtra = null;
387            setExtra();
388        }
389    
390        /**
391         * Looks up an extra field by its header id.
392         *
393         * @return null if no such field exists.
394         */
395        public ZipExtraField getExtraField(ZipShort type) {
396            if (extraFields != null) {
397                return (ZipExtraField) extraFields.get(type);
398            }
399            return null;
400        }
401    
402        /**
403         * Looks up extra field data that couldn't be parsed correctly.
404         *
405         * @return null if no such field exists.
406         *
407         * @since Apache Commons Compress 1.1
408         */
409        public UnparseableExtraFieldData getUnparseableExtraFieldData() {
410            return unparseableExtra;
411        }
412    
413        /**
414         * Parses the given bytes as extra field data and consumes any
415         * unparseable data as an {@link UnparseableExtraFieldData}
416         * instance.
417         * @param extra an array of bytes to be parsed into extra fields
418         * @throws RuntimeException if the bytes cannot be parsed
419         * @throws RuntimeException on error
420         */
421        public void setExtra(byte[] extra) throws RuntimeException {
422            try {
423                ZipExtraField[] local =
424                    ExtraFieldUtils.parse(extra, true,
425                                          ExtraFieldUtils.UnparseableExtraField.READ);
426                mergeExtraFields(local, true);
427            } catch (ZipException e) {
428                // actually this is not be possible as of Commons Compress 1.1
429                throw new RuntimeException("Error parsing extra fields for entry: "
430                                           + getName() + " - " + e.getMessage(), e);
431            }
432        }
433    
434        /**
435         * Unfortunately {@link java.util.zip.ZipOutputStream
436         * java.util.zip.ZipOutputStream} seems to access the extra data
437         * directly, so overriding getExtra doesn't help - we need to
438         * modify super's data directly.
439         */
440        protected void setExtra() {
441            super.setExtra(ExtraFieldUtils.mergeLocalFileDataData(getExtraFields(true)));
442        }
443    
444        /**
445         * Sets the central directory part of extra fields.
446         */
447        public void setCentralDirectoryExtra(byte[] b) {
448            try {
449                ZipExtraField[] central =
450                    ExtraFieldUtils.parse(b, false,
451                                          ExtraFieldUtils.UnparseableExtraField.READ);
452                mergeExtraFields(central, false);
453            } catch (ZipException e) {
454                throw new RuntimeException(e.getMessage(), e);
455            }
456        }
457    
458        /**
459         * Retrieves the extra data for the local file data.
460         * @return the extra data for local file
461         */
462        public byte[] getLocalFileDataExtra() {
463            byte[] extra = getExtra();
464            return extra != null ? extra : new byte[0];
465        }
466    
467        /**
468         * Retrieves the extra data for the central directory.
469         * @return the central directory extra data
470         */
471        public byte[] getCentralDirectoryExtra() {
472            return ExtraFieldUtils.mergeCentralDirectoryData(getExtraFields(true));
473        }
474    
475        /**
476         * Get the name of the entry.
477         * @return the entry name
478         */
479        public String getName() {
480            return name == null ? super.getName() : name;
481        }
482    
483        /**
484         * Is this entry a directory?
485         * @return true if the entry is a directory
486         */
487        public boolean isDirectory() {
488            return getName().endsWith("/");
489        }
490    
491        /**
492         * Set the name of the entry.
493         * @param name the name to use
494         */
495        protected void setName(String name) {
496            this.name = name;
497        }
498    
499        /**
500         * Sets the name using the raw bytes and the string created from
501         * it by guessing or using the configured encoding.
502         * @param name the name to use created from the raw bytes using
503         * the guessed or configured encoding
504         * @param rawName the bytes originally read as name from the
505         * archive
506         * @since Apache Commons Compress 1.2
507         */
508        protected void setName(String name, byte[] rawName) {
509            setName(name);
510            this.rawName = rawName;
511        }
512    
513        /**
514         * Returns the raw bytes that made up the name before it has been
515         * converted using the configured or guessed encoding.
516         *
517         * <p>This method will return null if this instance has not been
518         * read from an archive.</p>
519         *
520         * @since Apache Commons Compress 1.2
521         */
522        public byte[] getRawName() {
523            if (rawName != null) {
524                byte[] b = new byte[rawName.length];
525                System.arraycopy(rawName, 0, b, 0, rawName.length);
526                return b;
527            }
528            return null;
529        }
530    
531        /**
532         * Get the hashCode of the entry.
533         * This uses the name as the hashcode.
534         * @return a hashcode.
535         */
536        public int hashCode() {
537            // this method has severe consequences on performance. We cannot rely
538            // on the super.hashCode() method since super.getName() always return
539            // the empty string in the current implemention (there's no setter)
540            // so it is basically draining the performance of a hashmap lookup
541            return getName().hashCode();
542        }
543    
544        /**
545         * The "general purpose bit" field.
546         * @since Apache Commons Compress 1.1
547         */
548        public GeneralPurposeBit getGeneralPurposeBit() {
549            return gpb;
550        }
551    
552        /**
553         * The "general purpose bit" field.
554         * @since Apache Commons Compress 1.1
555         */
556        public void setGeneralPurposeBit(GeneralPurposeBit b) {
557            gpb = b;
558        }
559    
560        /**
561         * If there are no extra fields, use the given fields as new extra
562         * data - otherwise merge the fields assuming the existing fields
563         * and the new fields stem from different locations inside the
564         * archive.
565         * @param f the extra fields to merge
566         * @param local whether the new fields originate from local data
567         */
568        private void mergeExtraFields(ZipExtraField[] f, boolean local)
569            throws ZipException {
570            if (extraFields == null) {
571                setExtraFields(f);
572            } else {
573                for (int i = 0; i < f.length; i++) {
574                    ZipExtraField existing;
575                    if (f[i] instanceof UnparseableExtraFieldData) {
576                        existing = unparseableExtra;
577                    } else {
578                        existing = getExtraField(f[i].getHeaderId());
579                    }
580                    if (existing == null) {
581                        addExtraField(f[i]);
582                    } else {
583                        if (local) {
584                            byte[] b = f[i].getLocalFileDataData();
585                            existing.parseFromLocalFileData(b, 0, b.length);
586                        } else {
587                            byte[] b = f[i].getCentralDirectoryData();
588                            existing.parseFromCentralDirectoryData(b, 0, b.length);
589                        }
590                    }
591                }
592                setExtra();
593            }
594        }
595    
596        /** {@inheritDoc} */
597        public Date getLastModifiedDate() {
598            return new Date(getTime());
599        }
600    
601        /* (non-Javadoc)
602         * @see java.lang.Object#equals(java.lang.Object)
603         */
604        public boolean equals(Object obj) {
605            if (this == obj) {
606                return true;
607            }
608            if (obj == null || getClass() != obj.getClass()) {
609                return false;
610            }
611            ZipArchiveEntry other = (ZipArchiveEntry) obj;
612            String myName = getName();
613            String otherName = other.getName();
614            if (myName == null) {
615                if (otherName != null) {
616                    return false;
617                }
618            } else if (!myName.equals(otherName)) {
619                return false;
620            }
621            String myComment = getComment();
622            String otherComment = other.getComment();
623            if (myComment == null) {
624                if (otherComment != null) {
625                    return false;
626                }
627            } else if (!myComment.equals(otherComment)) {
628                return false;
629            }
630            return getTime() == other.getTime()
631                && getInternalAttributes() == other.getInternalAttributes()
632                && getPlatform() == other.getPlatform()
633                && getExternalAttributes() == other.getExternalAttributes()
634                && getMethod() == other.getMethod()
635                && getSize() == other.getSize()
636                && getCrc() == other.getCrc()
637                && getCompressedSize() == other.getCompressedSize()
638                && Arrays.equals(getCentralDirectoryExtra(),
639                                 other.getCentralDirectoryExtra())
640                && Arrays.equals(getLocalFileDataExtra(),
641                                 other.getLocalFileDataExtra())
642                && gpb.equals(other.gpb);
643        }
644    }