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.Date;
022    import java.util.LinkedHashMap;
023    import java.util.zip.ZipException;
024    import org.apache.commons.compress.archivers.ArchiveEntry;
025    
026    /**
027     * Extension that adds better handling of extra fields and provides
028     * access to the internal and external file attributes.
029     *
030     * @NotThreadSafe
031     */
032    public class ZipArchiveEntry extends java.util.zip.ZipEntry
033        implements ArchiveEntry, Cloneable {
034    
035        public static final int PLATFORM_UNIX = 3;
036        public static final int PLATFORM_FAT  = 0;
037        private static final int SHORT_MASK = 0xFFFF;
038        private static final int SHORT_SHIFT = 16;
039    
040        private int internalAttributes = 0;
041        private int platform = PLATFORM_FAT;
042        private long externalAttributes = 0;
043        private LinkedHashMap/*<ZipShort, ZipExtraField>*/ extraFields = null;
044        private String name = null;
045    
046        /**
047         * Creates a new zip entry with the specified name.
048         * @param name the name of the entry
049         */
050        public ZipArchiveEntry(String name) {
051            super(name);
052        }
053    
054        /**
055         * Creates a new zip entry with fields taken from the specified zip entry.
056         * @param entry the entry to get fields from
057         * @throws ZipException on error
058         */
059        public ZipArchiveEntry(java.util.zip.ZipEntry entry) throws ZipException {
060            super(entry);
061            setName(entry.getName());
062            byte[] extra = entry.getExtra();
063            if (extra != null) {
064                setExtraFields(ExtraFieldUtils.parse(extra));
065            } else {
066                // initializes extra data to an empty byte array
067                setExtra();
068            }
069        }
070    
071        /**
072         * Creates a new zip entry with fields taken from the specified zip entry.
073         * @param entry the entry to get fields from
074         * @throws ZipException on error
075         */
076        public ZipArchiveEntry(ZipArchiveEntry entry) throws ZipException {
077            this((java.util.zip.ZipEntry) entry);
078            setInternalAttributes(entry.getInternalAttributes());
079            setExternalAttributes(entry.getExternalAttributes());
080            setExtraFields(entry.getExtraFields());
081        }
082    
083        /**
084         */
085        protected ZipArchiveEntry() {
086            super("");
087        }
088    
089        public ZipArchiveEntry(File inputFile, String entryName) {
090            this(inputFile.isDirectory() && !entryName.endsWith("/") ? 
091                 entryName + "/" : entryName);
092            if (inputFile.isFile()){
093                setSize(inputFile.length());
094            }
095            setTime(inputFile.lastModified());
096            // TODO are there any other fields we can set here?
097        }
098    
099        /**
100         * Overwrite clone.
101         * @return a cloned copy of this ZipArchiveEntry
102         */
103        public Object clone() {
104            ZipArchiveEntry e = (ZipArchiveEntry) super.clone();
105    
106            e.extraFields = extraFields != null ? (LinkedHashMap) extraFields.clone() : null;
107            e.setInternalAttributes(getInternalAttributes());
108            e.setExternalAttributes(getExternalAttributes());
109            e.setExtraFields(getExtraFields());
110            return e;
111        }
112    
113        /**
114         * Retrieves the internal file attributes.
115         *
116         * @return the internal file attributes
117         */
118        public int getInternalAttributes() {
119            return internalAttributes;
120        }
121    
122        /**
123         * Sets the internal file attributes.
124         * @param value an <code>int</code> value
125         */
126        public void setInternalAttributes(int value) {
127            internalAttributes = value;
128        }
129    
130        /**
131         * Retrieves the external file attributes.
132         * @return the external file attributes
133         */
134        public long getExternalAttributes() {
135            return externalAttributes;
136        }
137    
138        /**
139         * Sets the external file attributes.
140         * @param value an <code>long</code> value
141         */
142        public void setExternalAttributes(long value) {
143            externalAttributes = value;
144        }
145    
146        /**
147         * Sets Unix permissions in a way that is understood by Info-Zip's
148         * unzip command.
149         * @param mode an <code>int</code> value
150         */
151        public void setUnixMode(int mode) {
152            // CheckStyle:MagicNumberCheck OFF - no point
153            setExternalAttributes((mode << SHORT_SHIFT)
154                                  // MS-DOS read-only attribute
155                                  | ((mode & 0200) == 0 ? 1 : 0)
156                                  // MS-DOS directory flag
157                                  | (isDirectory() ? 0x10 : 0));
158            // CheckStyle:MagicNumberCheck ON
159            platform = PLATFORM_UNIX;
160        }
161    
162        /**
163         * Unix permission.
164         * @return the unix permissions
165         */
166        public int getUnixMode() {
167            return platform != PLATFORM_UNIX ? 0 :
168                (int) ((getExternalAttributes() >> SHORT_SHIFT) & SHORT_MASK);
169        }
170    
171        /**
172         * Platform specification to put into the &quot;version made
173         * by&quot; part of the central file header.
174         *
175         * @return PLATFORM_FAT unless {@link #setUnixMode setUnixMode}
176         * has been called, in which case PLATORM_UNIX will be returned.
177         */
178        public int getPlatform() {
179            return platform;
180        }
181    
182        /**
183         * Set the platform (UNIX or FAT).
184         * @param platform an <code>int</code> value - 0 is FAT, 3 is UNIX
185         */
186        protected void setPlatform(int platform) {
187            this.platform = platform;
188        }
189    
190        /**
191         * Replaces all currently attached extra fields with the new array.
192         * @param fields an array of extra fields
193         */
194        public void setExtraFields(ZipExtraField[] fields) {
195            extraFields = new LinkedHashMap();
196            for (int i = 0; i < fields.length; i++) {
197                extraFields.put(fields[i].getHeaderId(), fields[i]);
198            }
199            setExtra();
200        }
201    
202        /**
203         * Retrieves extra fields.
204         * @return an array of the extra fields
205         */
206        public ZipExtraField[] getExtraFields() {
207            if (extraFields == null) {
208                return new ZipExtraField[0];
209            }
210            ZipExtraField[] result = new ZipExtraField[extraFields.size()];
211            return (ZipExtraField[]) extraFields.values().toArray(result);
212        }
213    
214        /**
215         * Adds an extra fields - replacing an already present extra field
216         * of the same type.
217         *
218         * <p>If no extra field of the same type exists, the field will be
219         * added as last field.</p>
220         * @param ze an extra field
221         */
222        public void addExtraField(ZipExtraField ze) {
223            if (extraFields == null) {
224                extraFields = new LinkedHashMap();
225            }
226            extraFields.put(ze.getHeaderId(), ze);
227            setExtra();
228        }
229    
230        /**
231         * Adds an extra fields - replacing an already present extra field
232         * of the same type.
233         *
234         * <p>The new extra field will be the first one.</p>
235         * @param ze an extra field
236         */
237        public void addAsFirstExtraField(ZipExtraField ze) {
238            LinkedHashMap copy = extraFields;
239            extraFields = new LinkedHashMap();
240            extraFields.put(ze.getHeaderId(), ze);
241            if (copy != null) {
242                copy.remove(ze.getHeaderId());
243                extraFields.putAll(copy);
244            }
245            setExtra();
246        }
247    
248        /**
249         * Remove an extra fields.
250         * @param type the type of extra field to remove
251         */
252        public void removeExtraField(ZipShort type) {
253            if (extraFields == null) {
254                throw new java.util.NoSuchElementException();
255            }
256            if (extraFields.remove(type) == null) {
257                throw new java.util.NoSuchElementException();
258            }
259            setExtra();
260        }
261    
262        /**
263         * Looks up an extra field by its header id.
264         *
265         * @return null if no such field exists.
266         */
267        public ZipExtraField getExtraField(ZipShort type) {
268            if (extraFields != null) {
269                return (ZipExtraField) extraFields.get(type);
270            }
271            return null;
272        }
273    
274        /**
275         * Throws an Exception if extra data cannot be parsed into extra fields.
276         * @param extra an array of bytes to be parsed into extra fields
277         * @throws RuntimeException if the bytes cannot be parsed
278         * @throws RuntimeException on error
279         */
280        public void setExtra(byte[] extra) throws RuntimeException {
281            try {
282                ZipExtraField[] local = ExtraFieldUtils.parse(extra, true);
283                mergeExtraFields(local, true);
284            } catch (ZipException e) {
285                throw new RuntimeException(e.getMessage(), e);
286            }
287        }
288    
289        /**
290         * Unfortunately {@link java.util.zip.ZipOutputStream
291         * java.util.zip.ZipOutputStream} seems to access the extra data
292         * directly, so overriding getExtra doesn't help - we need to
293         * modify super's data directly.
294         */
295        protected void setExtra() {
296            super.setExtra(ExtraFieldUtils.mergeLocalFileDataData(getExtraFields()));
297        }
298    
299        /**
300         * Sets the central directory part of extra fields.
301         */
302        public void setCentralDirectoryExtra(byte[] b) {
303            try {
304                ZipExtraField[] central = ExtraFieldUtils.parse(b, false);
305                mergeExtraFields(central, false);
306            } catch (ZipException e) {
307                throw new RuntimeException(e.getMessage(), e);
308            }
309        }
310    
311        /**
312         * Retrieves the extra data for the local file data.
313         * @return the extra data for local file
314         */
315        public byte[] getLocalFileDataExtra() {
316            byte[] extra = getExtra();
317            return extra != null ? extra : new byte[0];
318        }
319    
320        /**
321         * Retrieves the extra data for the central directory.
322         * @return the central directory extra data
323         */
324        public byte[] getCentralDirectoryExtra() {
325            return ExtraFieldUtils.mergeCentralDirectoryData(getExtraFields());
326        }
327    
328        /**
329         * Get the name of the entry.
330         * @return the entry name
331         */
332        public String getName() {
333            return name == null ? super.getName() : name;
334        }
335    
336        /**
337         * Is this entry a directory?
338         * @return true if the entry is a directory
339         */
340        public boolean isDirectory() {
341            return getName().endsWith("/");
342        }
343    
344        /**
345         * Set the name of the entry.
346         * @param name the name to use
347         */
348        protected void setName(String name) {
349            this.name = name;
350        }
351    
352        /**
353         * Get the hashCode of the entry.
354         * This uses the name as the hashcode.
355         * @return a hashcode.
356         */
357        public int hashCode() {
358            // this method has severe consequences on performance. We cannot rely
359            // on the super.hashCode() method since super.getName() always return
360            // the empty string in the current implemention (there's no setter)
361            // so it is basically draining the performance of a hashmap lookup
362            return getName().hashCode();
363        }
364    
365        /**
366         * If there are no extra fields, use the given fields as new extra
367         * data - otherwise merge the fields assuming the existing fields
368         * and the new fields stem from different locations inside the
369         * archive.
370         * @param f the extra fields to merge
371         * @param local whether the new fields originate from local data
372         */
373        private void mergeExtraFields(ZipExtraField[] f, boolean local)
374            throws ZipException {
375            if (extraFields == null) {
376                setExtraFields(f);
377            } else {
378                for (int i = 0; i < f.length; i++) {
379                    ZipExtraField existing = getExtraField(f[i].getHeaderId());
380                    if (existing == null) {
381                        addExtraField(f[i]);
382                    } else {
383                        if (local) {
384                            byte[] b = f[i].getLocalFileDataData();
385                            existing.parseFromLocalFileData(b, 0, b.length);
386                        } else {
387                            byte[] b = f[i].getCentralDirectoryData();
388                            existing.parseFromCentralDirectoryData(b, 0, b.length);
389                        }
390                    }
391                }
392                setExtra();
393            }
394        }
395    
396        /** {@inheritDocs} */
397        public Date getLastModifiedDate() {
398            return new Date(getTime());
399        }
400    
401        /* (non-Javadoc)
402         * @see java.lang.Object#equals(java.lang.Object)
403         */
404        public boolean equals(Object obj) {
405            if (this == obj) {
406                return true;
407            }
408            if (obj == null || getClass() != obj.getClass()) {
409                return false;
410            }
411            ZipArchiveEntry other = (ZipArchiveEntry) obj;
412            if (name == null) {
413                if (other.name != null) {
414                    return false;
415                }
416            } else if (!name.equals(other.name)) {
417                return false;
418            }
419            return true;
420        }
421    }