/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.mucommander.commons.file.archive.zip.provider; import java.util.Calendar; import java.util.Vector; import java.util.zip.ZipException; /** * Extension that adds better handling of extra fields and provides * access to the internal and external file attributes. * * <p>--------------------------------------------------------------------------------------------------------------<br> * <br> * This class is based off the <code>org.apache.tools.zip</code> package of the <i>Apache Ant</i> project. The Ant * code has been modified under the terms of the Apache License which you can find in the bundled muCommander license * file. It was forked at version 1.7.0 of Ant.</p> * * @author Apache Ant, Maxence Bernard */ public class ZipEntry implements Cloneable { /** Name/path of this entry */ protected String name = null; /** Uncompressed size of the entry data */ protected long size = -1; /** Compressed size of the entry data */ protected long compressedSize = -1; /** CRC-32 checksum of the uncompressed entry data */ protected long crc = -1; /** Data/time of this entry, in the DOS time format */ protected long dosTime = -1; /** Data/time of this entry, in the Java time format */ protected long javaTime = -1; /** Compression method that was used for the entry data */ protected int method = -1; /** An optional comment for this entry */ protected String comment; /** Platform, part of the 'version made by' central directory field */ protected int platform = PLATFORM_FAT; /** Internal attributes (2 bytes) */ protected int internalAttributes = 0; /** External attributes (4 bytes) */ protected long externalAttributes = 0; /** List of extra fields, as ZipEntraField instances */ protected Vector<ZipExtraField> extraFields = null; /** Contains info about how this entry is stored in the zip file */ protected ZipEntryInfo entryInfo; /** An instance of Calendar shared through all instances of this class and used for Java<->DOS time conversion */ protected final static Calendar CALENDAR = Calendar.getInstance(); /** Smallest DOS time (Epoch 1980) */ protected final static long MIN_DOS_TIME = 0x00002100L; /** Value of the bit flag that denotes a Unix directory in the external attributes */ protected final static int UNIX_DIRECTORY_FLAG = 16384; /** Value of the bit flag that denotes a Unix file in the external attributes */ protected final static int UNIX_FILE_FLAG = 32768; /** Value of the bit flag that denotes an MS-DOS directory in the external attributes */ protected final static int MSDOS_DIRECTORY_FLAG = 0x10; /** Value of the bit flag that denotes a read-only MS-DOS file in the external attributes */ protected final static int MSDOS_READ_ONLY_FLAG = 1; /** Value of the user write permission bit */ protected final static int USER_WRITE_PERMISSION_BIT = 128; /** Value of the Unix platform used in the 'version made by' central directory field */ protected static final int PLATFORM_UNIX = 3; /** Value of the MSDOS/OS-2 platform (FAT filesystem) used in the 'version made by' central directory field */ protected static final int PLATFORM_FAT = 0; /** * Creates a new Zip entry with an empty name. */ public ZipEntry() { this(""); } /** * Creates a new Zip entry with the specified name. * * @param name the name of the entry */ public ZipEntry(String name) { this.name = name; } /** * Creates a new Zip entry with fields taken from the specified zip entry. * * @param entry the entry to get fields from * @throws ZipException on error */ public ZipEntry(java.util.zip.ZipEntry entry) throws ZipException { this.name = entry.getName(); this.crc = entry.getCrc(); this.size = entry.getSize(); this.compressedSize = entry.getCompressedSize(); this.method = entry.getMethod(); this.comment = entry.getComment(); setExtra(entry.getExtra()); // ZipEntry.getTime() has to do a DOS time to Java time conversion, and we have to do the opposite. // This is inefficient but there is unfortunately no way to retrieve the DOS time field as it is private. setTime(entry.getTime()); } /** * Retrieves the internal file attributes. * * @return the internal file attributes */ public int getInternalAttributes() { return internalAttributes; } /** * Sets the internal file attributes. * * @param value an <code>int</code> value */ public void setInternalAttributes(int value) { internalAttributes = value; } /** * Retrieves the external file attributes. * * @return the external file attributes */ public long getExternalAttributes() { return externalAttributes; } /** * Sets the external file attributes. * * @param value an <code>long</code> value */ public void setExternalAttributes(long value) { externalAttributes = value; } /** * Sets Unix permissions in a way that is understood by Info-Zip's unzip command. * * @param mode an <code>int</code> value */ public void setUnixMode(int mode) { boolean isDirectory = isDirectory(); setExternalAttributes( // Unix directory flag ((isDirectory ? UNIX_DIRECTORY_FLAG : UNIX_FILE_FLAG) << 16) // Unix file permissions | (mode << 16) // MS-DOS read-only attribute | ((mode & USER_WRITE_PERMISSION_BIT) == 0 ? MSDOS_READ_ONLY_FLAG : 0) // MS-DOS directory flag | (isDirectory ? MSDOS_DIRECTORY_FLAG : 0)); platform = PLATFORM_UNIX; } /** * Unix permission. * * @return the unix permissions */ public int getUnixMode() { return (int) ((getExternalAttributes() >> 16) & 0xFFFF); } /** * Returns <code>true</code> if this ZipEntry has Unix mode/permissions. * If that's not the case, the value returned by {@link #getUnixMode()} has no meaning. * * @return <code>true</code> if this ZipEntry has Unix mode/permissions */ public boolean hasUnixMode() { return getPlatform()==PLATFORM_UNIX; } /** * Returns the platform specification to put into the 'version made by' part of the central file header. * * @return {@link #PLATFORM_FAT} unless {@link #setUnixMode setUnixMode} has been called, * in which case {@link #PLATFORM_UNIX} will be returned. */ public int getPlatform() { return platform; } /** * Sets the platform: {@link #PLATFORM_FAT} or {@link #PLATFORM_UNIX}. * * @param platform {@link #PLATFORM_FAT} or {@link #PLATFORM_UNIX} */ protected void setPlatform(int platform) { this.platform = platform; } /** * Replaces all current extra fields with the specified ones. * * @param fields an array of extra fields */ public void setExtraFields(ZipExtraField[] fields) { extraFields = new Vector<ZipExtraField>(); for (ZipExtraField field : fields) extraFields.addElement(field); } /** * Returns the extra fields of this entry. * * @return the extra fields of this entry */ public ZipExtraField[] getExtraFields() { if (extraFields == null) return new ZipExtraField[0]; ZipExtraField[] result = new ZipExtraField[extraFields.size()]; extraFields.copyInto(result); return result; } /** * Adds an extra fields, replacing any extra field of the same type previously added. * * @param ze the extra field to add */ public void addExtraField(ZipExtraField ze) { if (extraFields == null) extraFields = new Vector<ZipExtraField>(); ZipShort type = ze.getHeaderId(); for (int i=0, nbFields=extraFields.size(); i<nbFields; i++) { if (extraFields.elementAt(i).getHeaderId().equals(type)) { extraFields.setElementAt(ze, i); return; } } extraFields.addElement(ze); } /** * Removes the first extra field corresponding to the given type. * * @param type the type of extra field to remove * @return <code>true</code> if an extra field corresponding to given type was removed, <code>false</code> if no * matching field was found */ public boolean removeExtraField(ZipShort type) { if (extraFields == null) return false; for (int i=0, nbFields=extraFields.size(); i<nbFields; i++) { if (extraFields.elementAt(i).getHeaderId().equals(type)) { extraFields.removeElementAt(i); return true; } } return false; } /** * Returns the data of the local file extra fields. The returned byte array may be empty but never * <code>null</code>. * * @return the data of the local file extra fields */ public byte[] getLocalFileDataExtra() { return ExtraFieldUtils.mergeLocalExtraFields(getExtraFields()); } /** * Returns the data of the central directory extra fields. The returned byte array may be empty but never * <code>null</code>. * * @return the data of the central directory extra fields */ public byte[] getCentralDirectoryExtra() { return ExtraFieldUtils.mergeCentralExtraFields(getExtraFields()); } /** * Returns the name of this entry. * * @return the name of this entry */ public String getName() { return name; } /** * Sets the name of this entry. * * @param name the new name for this entry */ protected void setName(String name) { this.name = name; } /** * Returns <code>true</code> if the entry is a directory. Directory entries are characterized by their name * ending with a '/' character. * * @return <code>true</code> if the entry is a directory */ public boolean isDirectory() { return getName().endsWith("/"); } /** * Returns the {@link ZipEntryInfo} instance that contains info about how this entry is stored in the zip file. * * @return the {@link ZipEntryInfo} instance that contains info about how this entry is stored in the zip file */ protected ZipEntryInfo getEntryInfo() { return entryInfo; } /** * Sets the {@link ZipEntryInfo} instance that contains info about how this entry is stored in the zip file. * * @param entryInfo the {@link ZipEntryInfo} instance that contains info about how this entry is stored in the zip file */ protected void setEntryInfo(ZipEntryInfo entryInfo) { this.entryInfo = entryInfo; } /** * Returns the uncompressed size of the entry data, or <code>-1</code> if not known. * * @return the uncompressed size of the entry data, or <code>-1</code> if not known */ public long getSize() { return size; } /** * Sets the uncompressed size of the entry data. * * @param size the uncompressed size in bytes * @throws IllegalArgumentException if the specified size is less than 0 or greater than 0xFFFFFFFF bytes */ public void setSize(long size) { if(!isValidUnsignedInt(size)) throw new IllegalArgumentException("Invalid entry size"); this.size = size; } /** * Returns the size of the compressed entry data, or <code>-1</code> if not known. In the case of a stored entry, * the compressed size will be the same as the uncompressed size of the entry. * * @return the size of the compressed entry data, or <code>-1</code> if not known */ public long getCompressedSize() { return compressedSize; } /** * Sets the size of the compressed entry data. * * @param csize the compressed size to set to */ public void setCompressedSize(long csize) { if(!isValidUnsignedInt(csize)) throw new IllegalArgumentException("Invalid entry size"); this.compressedSize = csize; } /** * Returns the CRC-32 checksum of the uncompressed entry data, or <code>-1</code> if not known. * * @return the CRC-32 checksum of the uncompressed entry data, or <code>-1</code> if not known */ public long getCrc() { return crc; } /** * Sets the CRC-32 checksum of the uncompressed entry data. * * @param crc the new CRC-32 value * @throws IllegalArgumentException if the specified CRC-32 value is less than 0 or greater than 0xFFFFFFFF */ public void setCrc(long crc) { if(!isValidUnsignedInt(crc)) throw new IllegalArgumentException("invalid entry crc-32"); this.crc = crc; } /** * Returns this entry's date/time expressed in the Java time format, i.e. as a number of milliseconds since * the Epoch. * * @return this entry's date/time expressed in the Java time format */ public long getTime() { return javaTime; } /** * Sets this entry's date/time to the specified one. The time must be expressed in the Java time format, * i.e. as a number of milliseconds since the Epoch. * * @param javaTime the new time of this entry, expressed in the Java time format */ public void setTime(long javaTime) { this.javaTime = javaTime; this.dosTime = javaTime==-1?-1:javaToDosTime(javaTime); } /** * Returns this entry's date/time expressed in the DOS time format. * * @return this entry's date/time expressed in the DOS time format */ protected long getDosTime() { return dosTime; } /** * Sets this entry's date/time to the specified one. The time must be expressed in the DOS time format. * * @param dosTime the new time of this entry, expressed in the DOS time format */ protected void setDosTime(long dosTime) { this.dosTime = dosTime; this.javaTime = dosTime==-1?-1:dosToJavaTime(dosTime); } /** * Returns the compression method of the entry, or <code>-1</code> if not specified. * * @return the compression method of the entry, or <code>-1</code> if not specified */ public int getMethod() { return method; } /** * Sets the compression method for the entry. * * @param method the compression method, either {@link ZipConstants#STORED} or {@link ZipConstants#DEFLATED} * @throws IllegalArgumentException if the specified compression method is invalid */ public void setMethod(int method) { if (method != ZipConstants.STORED && method != ZipConstants.DEFLATED) throw new IllegalArgumentException("Invalid compression method"); this.method = method; } /** * Returns the comment string for the entry, or <code>null</code> if there is none. * * @return the comment string for the entry, or <code>null</code> if there is none */ public String getComment() { return comment; } /** * Sets the optional comment string for the entry. * * @param comment the comment string * @throws IllegalArgumentException if the length of the specified comment string is greater than 0xFFFF bytes */ public void setComment(String comment) { if (comment != null && comment.length() > 0xffff/3 && getUTF8Length(comment) > 0xffff) throw new IllegalArgumentException("invalid entry comment length"); this.comment = comment; } /** * Throws an <code>IllegalArgumentException</code> if byte array cannot be parsed into extra fields. * * @param extra an array of bytes to be parsed into extra fields * @throws IllegalArgumentException if the byte array cannot be parsed into extra fields */ public void setExtra(byte[] extra) throws IllegalArgumentException { if(extra==null || extra.length==0) { extraFields = null; } else { try { setExtraFields(ExtraFieldUtils.parse(extra)); } catch (Exception e) { throw new IllegalArgumentException(e.getMessage()); } } } //////////////////// // Helper methods // //////////////////// /* * Converts DOS time (Epoch=1980) to Java time (Epoch=1970). * * @param dosTime time expressed in the convoluted DOS time format * @return time expressed as the number of milliseconds since the epoch */ protected static long dosToJavaTime(long dosTime) { synchronized(CALENDAR) { CALENDAR.set(Calendar.YEAR, (int) ((dosTime >> 25) & 0x7f) + 1980); CALENDAR.set(Calendar.MONTH, (int) ((dosTime >> 21) & 0x0f) - 1); CALENDAR.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f); CALENDAR.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f); CALENDAR.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f); CALENDAR.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e); return CALENDAR.getTimeInMillis(); } } /** * Converts Java time (Epoch=1970) to DOS time (Epoch=1980). * * @param javaTime number of milliseconds since the epoch * @return time expressed in the convoluted DOS time format */ protected static long javaToDosTime(long javaTime) { synchronized(CALENDAR) { CALENDAR.setTimeInMillis(javaTime); int year = CALENDAR.get(Calendar.YEAR); if (year < 1980) { return MIN_DOS_TIME; } return ((year - 1980) << 25) | ((CALENDAR.get(Calendar.MONTH)+1) << 21) | (CALENDAR.get(Calendar.DAY_OF_MONTH) << 16) | (CALENDAR.get(Calendar.HOUR_OF_DAY) << 11) | (CALENDAR.get(Calendar.MINUTE) << 5) | (CALENDAR.get(Calendar.SECOND) >> 1); } } /* * Returns the length of the given String's <code>UTF-8</code> representation. */ protected static int getUTF8Length(String s) { // This method is a dup from java.util.ZipOutputStream int count = 0; for (int i = 0; i < s.length(); i++) { char ch = s.charAt(i); if (ch <= 0x7f) count++; else if (ch <= 0x7ff) count += 2; else count += 3; } return count; } /** * Returns <code>true</code> if the given long is a valid unsigned int value, i.e. comprised between 0 and 2^32-1. * * @param l the long value to test * @return <code>true</code> if the given long is a valid unsigned int value, i.e. comprised between 0 and 2^32-1 */ protected boolean isValidUnsignedInt(long l) { return l>=0 && l<=0xFFFFFFFFL; } //////////////////////// // Overridden methods // //////////////////////// /** * Returns a cloned instance of this entry. * * @return a cloned instance of this entry * @throws CloneNotSupportedException should never happen */ @Override public Object clone() throws CloneNotSupportedException { ZipEntry ze = (ZipEntry)super.clone(); if(extraFields!=null) ze.extraFields = (Vector<ZipExtraField>)extraFields.clone(); return ze; } /** * Returns a hash of this entry's name. * * @return a hash of this entry's name */ public int hashCode() { return getName().hashCode(); } /** * Returns <code>true</code> if the given object is a <code>ZipEntry</code> that has the same name as this one. * * @param o the object to test for equality * @return <code>true</code> if the given object is a <code>ZipEntry</code> that has the same name as this one */ public boolean equals(Object o) { return (o instanceof ZipEntry) && ((ZipEntry) o).getName().equals(getName()); } }