/* 
 * 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 java.util.zip;

// BEGIN android-added
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
// END android-added
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

/**
 * An instance of {@code ZipEntry} represents an entry within a <i>ZIP-archive</i>.
 * An entry has attributes such as name (= path) or the size of its data. While
 * an entry identifies data stored in an archive, it does not hold the data
 * itself. For example when reading a <i>ZIP-file</i> you will first retrieve
 * all its entries in a collection and then read the data for a specific entry
 * through an input stream.
 * 
 * @see ZipFile
 * @see ZipOutputStream
 * @since Android 1.0
 */
public class ZipEntry implements ZipConstants, Cloneable {
    String name, comment;

    long compressedSize = -1, crc = -1, size = -1;
    // BEGIN android-removed
    // long dataOffset = -1;
    // END android-removed

    int compressionMethod = -1, time = -1, modDate = -1;

    byte[] extra;

    // BEGIN android-added
    /*
     * Fields, present in the Central Directory Entry and Local File Entry.
     *
     * Not all of these are part of the interface, but we need them if we
     * want to be able to copy entries from one archive to another without
     * losing any meta-data.
     *
     * We use over-sized fields so we can indicate whether a field has been
     * initialized or not.
     */
    private int mVersionMadeBy;             // CDE
    private int mVersionToExtract;          // CDE, LFE
    private int mGPBitFlag;                 // CDE, LFE
    // private int mCompressionMethod;         // CDE, LFE   = compressionMethod
    // private int mLastModFileTime;           // CDE, LFE   = time
    // private int mLastModFileDate;           // CDE, LFE   = modDate
    // private long mCRC32;                    // CDE, LFE   = crc
    // private long mCompressedSize;           // CDE, LFE   = compressedSize
    // private long mUncompressedSize;         // CDE, LFE   = size
    int nameLen, extraLen, commentLen;
    //private int mFileNameLength;            // CDE, LFE
    //private int mExtraFieldLength;          // CDE, LFE
    //private int mFileCommentLength;         // CDE
    private int mDiskNumberStart;           // CDE
    private int mInternalAttrs;             // CDE
    private long mExternalAttrs;            // CDE
    long mLocalHeaderRelOffset;     // CDE  ? dataOffset
    // private String mFileName;               // CDE, LFE   = name
    // private byte[] mExtraField;             // CDE, LFE   = extra
    // private String mFileComment;            // CDE   = comment


    // GPBitFlag 3: uses a Data Descriptor block (need for deflated data)
    /*package*/ static final int USES_DATA_DESCR = 0x0008;

    // private static Calendar mCalendar = Calendar.getInstance();
    // END android-added

    /**
     * Zip entry state: Deflated.
     * 
     * @since Android 1.0
     */
    public static final int DEFLATED = 8;

    /**
     * Zip entry state: Stored.
     * 
     * @since Android 1.0
     */
    public static final int STORED = 0;

    /**
     * Constructs a new {@code ZipEntry} with the specified name.
     * 
     * @param name
     *            the name of the ZIP entry.
     * @throws IllegalArgumentException
     *             if the name length is outside the range (> 0xFFFF).
     * @since Android 1.0
     */
    public ZipEntry(String name) {
        if (name == null) {
            throw new NullPointerException();
        }
        if (name.length() > 0xFFFF) {
            throw new IllegalArgumentException();
        }
        this.name = name;

        // BEGIN android-added
        mVersionMadeBy = 0x0317;        // 03=UNIX, 17=spec v2.3
        mVersionToExtract = 20;         // need deflate, not much else
        mGPBitFlag = 0;
        compressionMethod = -1;
        time = -1;
        modDate = -1;
        crc = -1L;
        compressedSize = -1L;
        size = -1L;
        extraLen = -1;
        nameLen = -1;
        mDiskNumberStart = 0;
        mInternalAttrs = 0;
        mExternalAttrs = 0x81b60020L;       // matches WinZip
        mLocalHeaderRelOffset = -1;
        extra = null;
        comment = null;
        // END android-added
    }

    /**
     * Gets the comment for this {@code ZipEntry}.
     * 
     * @return the comment for this {@code ZipEntry}, or {@code null} if there
     *         is no comment. If we're reading an archive with
     *         {@code ZipInputStream} the comment is not available.
     * @since Android 1.0
     */
    public String getComment() {
        return comment;
    }

    /**
     * Gets the compressed size of this {@code ZipEntry}.
     * 
     * @return the compressed size, or -1 if the compressed size has not been
     *         set.
     * @since Android 1.0
     */
    public long getCompressedSize() {
        return compressedSize;
    }

    /**
     * Gets the checksum for this {@code ZipEntry}.
     * 
     * @return the checksum, or -1 if the checksum has not been set.
     * @since Android 1.0
     */
    public long getCrc() {
        return crc;
    }

    /**
     * Gets the extra information for this {@code ZipEntry}.
     * 
     * @return a byte array containing the extra information, or {@code null} if
     *         there is none.
     * @since Android 1.0
     */
    public byte[] getExtra() {
        return extra;
    }

    /**
     * Gets the compression method for this {@code ZipEntry}.
     * 
     * @return the compression method, either {@code DEFLATED}, {@code STORED}
     *         or -1 if the compression method has not been set.
     * @since Android 1.0
     */
    public int getMethod() {
        return compressionMethod;
    }

    /**
     * Gets the name of this {@code ZipEntry}.
     * 
     * @return the entry name.
     * @since Android 1.0
     */
    public String getName() {
        return name;
    }

    /**
     * Gets the uncompressed size of this {@code ZipEntry}.
     * 
     * @return the uncompressed size, or {@code -1} if the size has not been
     *         set.
     * @since Android 1.0
     */
    public long getSize() {
        return size;
    }

    /**
     * Gets the last modification time of this {@code ZipEntry}.
     * 
     * @return the last modification time as the number of milliseconds since
     *         Jan. 1, 1970.
     * @since Android 1.0
     */
    public long getTime() {
        if (time != -1) {
            GregorianCalendar cal = new GregorianCalendar();
            cal.set(Calendar.MILLISECOND, 0);
            cal.set(1980 + ((modDate >> 9) & 0x7f), ((modDate >> 5) & 0xf) - 1,
                    modDate & 0x1f, (time >> 11) & 0x1f, (time >> 5) & 0x3f,
                    (time & 0x1f) << 1);
            return cal.getTime().getTime();
        }
        return -1;
    }

    /**
     * Determine whether or not this {@code ZipEntry} is a directory.
     * 
     * @return {@code true} when this {@code ZipEntry} is a directory, {@code
     *         false} otherwise.
     * @since Android 1.0
     */
    public boolean isDirectory() {
        return name.charAt(name.length() - 1) == '/';
    }

    /**
     * Sets the comment for this {@code ZipEntry}.
     * 
     * @param string
     *            the comment for this entry.
     * @since Android 1.0
     */
    public void setComment(String string) {
        if (string == null || string.length() <= 0xFFFF) {
            comment = string;
        } else {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Sets the compressed size for this {@code ZipEntry}.
     * 
     * @param value
     *            the compressed size (in bytes).
     * @since Android 1.0
     */
    public void setCompressedSize(long value) {
        compressedSize = value;
    }

    /**
     * Sets the checksum for this {@code ZipEntry}.
     * 
     * @param value
     *            the checksum for this entry.
     * @throws IllegalArgumentException
     *             if {@code value} is < 0 or > 0xFFFFFFFFL.
     * @since Android 1.0
     */
    public void setCrc(long value) {
        if (value >= 0 && value <= 0xFFFFFFFFL) {
            crc = value;
        } else {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Sets the extra information for this {@code ZipEntry}.
     * 
     * @param data
     *            a byte array containing the extra information.
     * @throws IllegalArgumentException
     *             when the length of data is greater than 0xFFFF bytes.
     * @since Android 1.0
     */
    public void setExtra(byte[] data) {
        if (data == null || data.length <= 0xFFFF) {
            extra = data;
        } else {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Sets the compression method for this {@code ZipEntry}.
     * 
     * @param value
     *            the compression method, either {@code DEFLATED} or {@code
     *            STORED}.
     * @throws IllegalArgumentException
     *             when value is not {@code DEFLATED} or {@code STORED}.
     * @since Android 1.0
     */
    public void setMethod(int value) {
        if (value != STORED && value != DEFLATED) {
            throw new IllegalArgumentException();
        }
        compressionMethod = value;
    }

    /**
     * Sets the uncompressed size of this {@code ZipEntry}.
     * 
     * @param value
     *            the uncompressed size for this entry.
     * @throws IllegalArgumentException
     *             if {@code value} < 0 or {@code value} > 0xFFFFFFFFL.
     * @since Android 1.0
     */
    public void setSize(long value) {
        if (value >= 0 && value <= 0xFFFFFFFFL) {
            size = value;
        } else {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Sets the modification time of this {@code ZipEntry}.
     * 
     * @param value
     *            the modification time as the number of milliseconds since Jan.
     *            1, 1970.
     * @since Android 1.0
     */
    public void setTime(long value) {
        GregorianCalendar cal = new GregorianCalendar();
        cal.setTime(new Date(value));
        int year = cal.get(Calendar.YEAR);
        if (year < 1980) {
            modDate = 0x21;
            time = 0;
        } else {
            modDate = cal.get(Calendar.DATE);
            modDate = (cal.get(Calendar.MONTH) + 1 << 5) | modDate;
            modDate = ((cal.get(Calendar.YEAR) - 1980) << 9) | modDate;
            time = cal.get(Calendar.SECOND) >> 1;
            time = (cal.get(Calendar.MINUTE) << 5) | time;
            time = (cal.get(Calendar.HOUR_OF_DAY) << 11) | time;
        }
    }

    /**
     * Returns the string representation of this {@code ZipEntry}.
     * 
     * @return the string representation of this {@code ZipEntry}.
     * @since Android 1.0
     */
    @Override
    public String toString() {
        return name;
    }

    // BEGIN android-removed
    // ZipEntry(String name, String comment, byte[] extra, long modTime,
    //         long size, long compressedSize, long crc, int compressionMethod,
    //         long modDate, long offset) {
    //     this.name = name;
    //     this.comment = comment;
    //     this.extra = extra;
    //     time = (int) modTime;
    //     this.size = size;
    //     this.compressedSize = compressedSize;
    //     this.crc = crc;
    //     this.compressionMethod = compressionMethod;
    //     this.modDate = (int) modDate;
    //     dataOffset = offset;
    // }
    // END android-removed

    /**
     * Constructs a new {@code ZipEntry} using the values obtained from {@code
     * ze}.
     * 
     * @param ze
     *            the {@code ZipEntry} from which to obtain values.
     * @since Android 1.0
     */
    public ZipEntry(ZipEntry ze) {
        name = ze.name;
        comment = ze.comment;
        time = ze.time;
        size = ze.size;
        compressedSize = ze.compressedSize;
        crc = ze.crc;
        compressionMethod = ze.compressionMethod;
        modDate = ze.modDate;
        extra = ze.extra;
        // BEGIN android-removed
        // dataOffset = ze.dataOffset;
        // END android-removed
        // BEGIN android-added
        mVersionMadeBy = ze.mVersionMadeBy;
        mVersionToExtract = ze.mVersionToExtract;
        mGPBitFlag = ze.mGPBitFlag;
        extraLen = ze.extraLen;
        nameLen = ze.nameLen;
        mDiskNumberStart = ze.mDiskNumberStart;
        mInternalAttrs = ze.mInternalAttrs;
        mExternalAttrs = ze.mExternalAttrs;
        mLocalHeaderRelOffset = ze.mLocalHeaderRelOffset;
        // END android-added
    }

    /**
     * Returns a shallow copy of this entry.
     * 
     * @return a copy of this entry.
     * @since Android 1.0
     */
    @Override
    public Object clone() {
        return new ZipEntry(this);
    }

    /**
     * Returns the hash code for this {@code ZipEntry}.
     * 
     * @return the hash code of the entry.
     * @since Android 1.0
     */
    @Override
    public int hashCode() {
        return name.hashCode();
    }

    // BEGIN android-added
    // readShortLE is not used.
    // readIntLE is used only once in ZipFile.
    /*
     * Internal constructor.  Creates a new ZipEntry by reading the
     * Central Directory Entry from "in", which must be positioned at
     * the CDE signature.
     *
     * On exit, "in" will be positioned at the start of the next entry.
     */
    /*package*/ ZipEntry(LittleEndianReader ler, InputStream in) throws IOException {

        /*
         * We're seeing performance issues when we call readShortLE and
         * readIntLE, so we're going to read the entire header at once
         * and then parse the results out without using any function calls.
         * Uglier, but should be much faster.
         * 
         * Note that some lines look a bit different, because the corresponding
         * fields or locals are long and so we need to do & 0xffffffffl to avoid
         * problems induced by sign extension.
         */

        byte[] hdrBuf = ler.hdrBuf;
        myReadFully(in, hdrBuf);

        long sig = (hdrBuf[0] & 0xff) | ((hdrBuf[1] & 0xff) << 8) |
            ((hdrBuf[2] & 0xff) << 16) | ((hdrBuf[3] << 24) & 0xffffffffL);
        if (sig != CENSIG)
             throw new ZipException("Central Directory Entry not found");

        mVersionMadeBy = (hdrBuf[4] & 0xff) | ((hdrBuf[5] & 0xff) << 8);
        mVersionToExtract = (hdrBuf[6] & 0xff) | ((hdrBuf[7] & 0xff) << 8);
        mGPBitFlag = (hdrBuf[8] & 0xff) | ((hdrBuf[9] & 0xff) << 8);
        compressionMethod = (hdrBuf[10] & 0xff) | ((hdrBuf[11] & 0xff) << 8);
        time = (hdrBuf[12] & 0xff) | ((hdrBuf[13] & 0xff) << 8);
        modDate = (hdrBuf[14] & 0xff) | ((hdrBuf[15] & 0xff) << 8);
        crc = (hdrBuf[16] & 0xff) | ((hdrBuf[17] & 0xff) << 8)
                | ((hdrBuf[18] & 0xff) << 16)
                | ((hdrBuf[19] << 24) & 0xffffffffL);
        compressedSize = (hdrBuf[20] & 0xff) | ((hdrBuf[21] & 0xff) << 8)
                | ((hdrBuf[22] & 0xff) << 16)
                | ((hdrBuf[23] << 24) & 0xffffffffL);
        size = (hdrBuf[24] & 0xff) | ((hdrBuf[25] & 0xff) << 8)
                | ((hdrBuf[26] & 0xff) << 16)
                | ((hdrBuf[27] << 24) & 0xffffffffL);
        nameLen = (hdrBuf[28] & 0xff) | ((hdrBuf[29] & 0xff) << 8);
        extraLen = (hdrBuf[30] & 0xff) | ((hdrBuf[31] & 0xff) << 8);
        commentLen = (hdrBuf[32] & 0xff) | ((hdrBuf[33] & 0xff) << 8);
        mDiskNumberStart = (hdrBuf[34] & 0xff) | ((hdrBuf[35] & 0xff) << 8);
        mInternalAttrs = (hdrBuf[36] & 0xff) | ((hdrBuf[37] & 0xff) << 8);
        mExternalAttrs = (hdrBuf[38] & 0xff) | ((hdrBuf[39] & 0xff) << 8)
                | ((hdrBuf[40] & 0xff) << 16)
                | ((hdrBuf[41] << 24) & 0xffffffffL);
        mLocalHeaderRelOffset = (hdrBuf[42] & 0xff) | ((hdrBuf[43] & 0xff) << 8)
                | ((hdrBuf[44] & 0xff) << 16)
                | ((hdrBuf[45] << 24) & 0xffffffffL);

        byte[] nameBytes = new byte[nameLen];
        myReadFully(in, nameBytes);

        byte[] commentBytes = null;
        if (commentLen > 0) {
            commentBytes = new byte[commentLen];
            myReadFully(in, commentBytes);
        }

        if (extraLen > 0) {
            extra = new byte[extraLen];
            myReadFully(in, extra);
        }

        try {
            /*
             * The actual character set is "IBM Code Page 437".  As of
             * Sep 2006, the Zip spec (APPNOTE.TXT) supports UTF-8.  When
             * bit 11 of the GP flags field is set, the file name and
             * comment fields are UTF-8.
             *
             * TODO: add correct UTF-8 support.
             */
            name = new String(nameBytes, "ISO-8859-1");
            if (commentBytes != null)
                comment = new String(commentBytes, "ISO-8859-1");
            else
                comment = null;
        }
        catch (UnsupportedEncodingException uee) {
            throw new InternalError(uee.getMessage());
        }
    }
    private void myReadFully(InputStream in, byte[] b) throws IOException {
        int count;
        int len = b.length;
        int off = 0;
    
        while (len > 0) {
            count = in.read(b, off, len);
            if (count <= 0)
                throw new EOFException();
            off += count;
            len -= count;
        }
    }

    /*package*/ void setVersionToExtract(int version) {
        mVersionToExtract = version;
    }

    /*package*/ int getGPBitFlag() {
        return mGPBitFlag;
    }
    /*package*/ void setGPBitFlag(int flags) {
        mGPBitFlag = flags;
    }

    /*package*/ long getLocalHeaderRelOffset() {
        return mLocalHeaderRelOffset;
    }
    /*package*/ void setLocalHeaderRelOffset(long offset) {
        mLocalHeaderRelOffset = offset;
    }

    /*package*/ void setDateTime(int lastModFileDate, int lastModFileTime) {
        time = lastModFileTime;
        modDate = lastModFileDate;
    }

    /*
     * Read a two-byte short in little-endian order.
     *
     * The DataInput interface, which RandomAccessFile implements, provides
     * a readInt() function.  Unfortunately, it's defined to use big-endian.
     */
    /*package*/ static int readShortLE(RandomAccessFile raf) throws IOException
    {
        int b0, b1;

        b0 = raf.read();
        b1 = raf.read();
        if (b1 < 0)
            throw new EOFException("in ZipEntry.readShortLE(RandomAccessFile)");
        return b0 | (b1 << 8);
    }

    /*
     * Read a four-byte int in little-endian order.
     */
    /*package*/ static long readIntLE(RandomAccessFile raf) throws IOException
    {
        int b0, b1, b2, b3;

        b0 = raf.read();
        b1 = raf.read();
        b2 = raf.read();
        b3 = raf.read();
        if (b3 < 0)
            throw new EOFException("in ZipEntry.readIntLE(RandomAccessFile)");
        return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24); // ATTENTION: DOES SIGN EXTENSION: IS THIS WANTED?
    }

    static class LittleEndianReader {
        private byte[] b = new byte[4];
        byte[] hdrBuf = new byte[CENHDR];

        /*
         * Read a two-byte short in little-endian order.
         */
        int readShortLE(InputStream in) throws IOException {
            if (in.read(b, 0, 2) == 2) {
                return (b[0] & 0XFF) | ((b[1] & 0XFF) << 8);
            } else {
                throw new EOFException("in ZipEntry.readShortLE(InputStream)");
            }
        }

        /*
         * Read a four-byte int in little-endian order.
         */
        long readIntLE(InputStream in) throws IOException {
            if (in.read(b, 0, 4) == 4) {
                return (   ((b[0] & 0XFF))
                         | ((b[1] & 0XFF) << 8)
                         | ((b[2] & 0XFF) << 16)
                         | ((b[3] & 0XFF) << 24))
                       & 0XFFFFFFFFL; // Here for sure NO sign extension is wanted.
            } else {
                throw new EOFException("in ZipEntry.readIntLE(InputStream)");
            }
        }
    }

    /*
     * Write a two-byte short in little-endian order.
     */
    /*package*/ static void writeShortLE(OutputStream out, int val)
        throws IOException
    {
        out.write(val & 0xff);
        out.write((val >> 8) & 0xff);
    }

    /*
     * Write a 4-byte int in little-endian order.  This takes a long because
     * all of our 4-byte values are stored locally in longs.
     */
    /*package*/ static void writeIntLE(OutputStream out, long val)
        throws IOException
    {
        if (val < 0)
            throw new InternalError();
        out.write((int) val & 0xff);
        out.write(((int) val >> 8) & 0xff);
        out.write(((int) val >> 16) & 0xff);
        out.write(((int) val >> 24) & 0xff);
    }

    /*
     * Write the Local File Header for this entry to the specified stream.
     *
     * Returns the #of bytes written.
     */
    /*package*/ int writeLFH(OutputStream out) throws IOException {
        if (compressionMethod < 0 ||
            time < 0 ||
            modDate < 0 ||
            crc < 0 ||
            compressedSize < 0 ||
            size < 0)
            throw new InternalError();

        writeIntLE(out, LOCSIG);
        writeShortLE(out, mVersionToExtract);
        writeShortLE(out, mGPBitFlag);
        writeShortLE(out, compressionMethod);
        writeShortLE(out, time);
        writeShortLE(out, modDate);
        writeIntLE(out, crc);
        writeIntLE(out, compressedSize);
        writeIntLE(out, size);

        byte[] nameBytes;
        try {
            nameBytes = name.getBytes("ISO-8859-1");
        }
        catch (UnsupportedEncodingException uee) {
            throw new InternalError(uee.getMessage());
        }

        int extraLen = 0;
        if (extra != null)
            extraLen = extra.length;

        writeShortLE(out, nameBytes.length);
        writeShortLE(out, extraLen);
        out.write(nameBytes);
        if (extra != null)
            out.write(extra);

        return LOCHDR + nameBytes.length + extraLen;
    }

    /*
     * Write the Data Descriptor for this entry to the specified stream.
     *
     * Returns the #of bytes written.
     */
    /*package*/ int writeDD(OutputStream out) throws IOException {
        writeIntLE(out, EXTSIG);
        writeIntLE(out, crc);
        writeIntLE(out, compressedSize);
        writeIntLE(out, size);
        return EXTHDR;
    }

    /*
     * Write the Central Directory Entry for this entry.
     *
     * Returns the #of bytes written.
     */
    /*package*/ int writeCDE(OutputStream out) throws IOException {
        writeIntLE(out, CENSIG);
        writeShortLE(out, mVersionMadeBy);
        writeShortLE(out, mVersionToExtract);
        writeShortLE(out, mGPBitFlag);
        writeShortLE(out, compressionMethod);
        writeShortLE(out, time);
        writeShortLE(out, modDate);
        writeIntLE(out, crc);
        writeIntLE(out, compressedSize);
        writeIntLE(out, size);

        byte[] nameBytes = null, commentBytes = null;
        try {
            nameBytes = name.getBytes("ISO-8859-1");
            if (comment != null)
                commentBytes = comment.getBytes("ISO-8859-1");
        }
        catch (UnsupportedEncodingException uee) {
            throw new InternalError(uee.getMessage());
        }

        int extraLen = 0, commentLen = 0;
        if (extra != null)
            extraLen = extra.length;
        if (commentBytes != null)
            commentLen = commentBytes.length;

        writeShortLE(out, nameBytes.length);
        writeShortLE(out, extraLen);
        writeShortLE(out, commentLen);
        writeShortLE(out, mDiskNumberStart);
        writeShortLE(out, mInternalAttrs);
        writeIntLE(out, mExternalAttrs);
        writeIntLE(out, mLocalHeaderRelOffset);
        out.write(nameBytes);
        if (extra != null)
            out.write(extra);
        if (commentBytes != null)
            out.write(commentBytes);

        return CENHDR + nameBytes.length + extraLen + commentLen;
    }
    // END android-added
}
