All Downloads are FREE. Search and download functionalities are using the official Maven repository.

java.util.zip.ZipEntry Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 14-robolectric-10818077
Show newest version
/*
 * 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;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import libcore.io.BufferIterator;
import libcore.io.HeapBufferIterator;
import libcore.io.Streams;

/**
 * An entry within a zip file.
 * An entry has attributes such as its name (which is actually a path) and the uncompressed size
 * of the corresponding data. An entry does not contain the data itself, but can be used as a key
 * with {@link ZipFile#getInputStream}. The class documentation for {@link ZipInputStream} and
 * {@link ZipOutputStream} shows how {@code ZipEntry} is used in conjunction with those two classes.
 */
public class ZipEntry implements ZipConstants, Cloneable {
    String name;
    String comment;

    long crc = -1; // Needs to be a long to distinguish -1 ("not set") from the 0xffffffff CRC32.

    long compressedSize = -1;
    long size = -1;

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

    byte[] extra;

    int nameLength = -1;
    long localHeaderRelOffset = -1;

    long dataOffset = -1;

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

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

    ZipEntry(String name, String comment, long crc, long compressedSize,
            long size, int compressionMethod, int time, int modDate, byte[] extra,
            int nameLength, long localHeaderRelOffset, long dataOffset) {
        this.name = name;
        this.comment = comment;
        this.crc = crc;
        this.compressedSize = compressedSize;
        this.size = size;
        this.compressionMethod = compressionMethod;
        this.time = time;
        this.modDate = modDate;
        this.extra = extra;
        this.nameLength = nameLength;
        this.localHeaderRelOffset = localHeaderRelOffset;
        this.dataOffset = dataOffset;
    }

    /**
     * Constructs a new {@code ZipEntry} with the specified name. The name is actually a path,
     * and may contain {@code /} characters.
     *
     * @throws IllegalArgumentException
     *             if the name length is outside the range (> 0xFFFF).
     */
    public ZipEntry(String name) {
        if (name == null) {
            throw new NullPointerException("name == null");
        }
        validateStringLength("Name", name);
        this.name = name;
    }

    /**
     * Returns the comment for this {@code ZipEntry}, or {@code null} if there is no comment.
     * If we're reading a zip file using {@code ZipInputStream}, the comment is not available.
     */
    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.
     */
    public long getCompressedSize() {
        return compressedSize;
    }

    /**
     * Gets the checksum for this {@code ZipEntry}.
     *
     * @return the checksum, or -1 if the checksum has not been set.
     */
    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.
     */
    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.
     */
    public int getMethod() {
        return compressionMethod;
    }

    /**
     * Gets the name of this {@code ZipEntry}.
     *
     * @return the entry name.
     */
    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.
     */
    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.
     */
    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.
     */
    public boolean isDirectory() {
        return name.charAt(name.length() - 1) == '/';
    }

    /**
     * Sets the comment for this {@code ZipEntry}.
     * @throws IllegalArgumentException if the comment is >= 64 Ki UTF-8 bytes.
     */
    public void setComment(String comment) {
        if (comment == null) {
            this.comment = null;
            return;
        }
        validateStringLength("Comment", comment);

        this.comment = comment;
    }

    /**
     * Sets the compressed size for this {@code ZipEntry}.
     *
     * @param value
     *            the compressed size (in bytes).
     */
    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.
     */
    public void setCrc(long value) {
        if (value >= 0 && value <= 0xFFFFFFFFL) {
            crc = value;
        } else {
            throw new IllegalArgumentException("Bad CRC32: " + value);
        }
    }

    /**
     * Sets the extra information for this {@code ZipEntry}.
     *
     * @throws IllegalArgumentException if the data length >= 64 KiB.
     */
    public void setExtra(byte[] data) {
        if (data != null && data.length > 0xffff) {
            throw new IllegalArgumentException("Extra data too long: " + data.length);
        }
        extra = data;
    }

    /**
     * Sets the compression method for this entry to either {@code DEFLATED} or {@code STORED}.
     * The default is {@code DEFLATED}, which will cause the size, compressed size, and CRC to be
     * set automatically, and the entry's data to be compressed. If you switch to {@code STORED}
     * note that you'll have to set the size (or compressed size; they must be the same, but it's
     * okay to only set one) and CRC yourself because they must appear before the user data
     * in the resulting zip file. See {@link #setSize} and {@link #setCrc}.
     * @throws IllegalArgumentException
     *             when value is not {@code DEFLATED} or {@code STORED}.
     */
    public void setMethod(int value) {
        if (value != STORED && value != DEFLATED) {
            throw new IllegalArgumentException("Bad method: " + value);
        }
        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.
     */
    public void setSize(long value) {
        if (value >= 0 && value <= 0xFFFFFFFFL) {
            size = value;
        } else {
            throw new IllegalArgumentException("Bad size: " + value);
        }
    }

    /**
     * Sets the modification time of this {@code ZipEntry}.
     *
     * @param value
     *            the modification time as the number of milliseconds since Jan.
     *            1, 1970.
     */
    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;
        }
    }


    /** @hide */
    public void setDataOffset(long value) {
        dataOffset = value;
    }

    /** @hide */
    public long getDataOffset() {
        return dataOffset;
    }

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

    /**
     * Constructs a new {@code ZipEntry} using the values obtained from {@code
     * ze}.
     *
     * @param ze
     *            the {@code ZipEntry} from which to obtain values.
     */
    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;
        nameLength = ze.nameLength;
        localHeaderRelOffset = ze.localHeaderRelOffset;
        dataOffset = ze.dataOffset;
    }

    /**
     * Returns a deep copy of this zip entry.
     */
    @Override public Object clone() {
        try {
            ZipEntry result = (ZipEntry) super.clone();
            result.extra = extra != null ? extra.clone() : null;
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }

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

    /*
     * Internal constructor.  Creates a new ZipEntry by reading the
     * Central Directory Entry (CDE) from "in", which must be positioned
     * at the CDE signature. If the GPBF_UTF8_FLAG is set in the CDE then
     * UTF-8 is used to decode the string information, otherwise the
     * defaultCharset is used.
     *
     * On exit, "in" will be positioned at the start of the next entry
     * in the Central Directory.
     */
    ZipEntry(byte[] cdeHdrBuf, InputStream cdStream, Charset defaultCharset) throws IOException {
        Streams.readFully(cdStream, cdeHdrBuf, 0, cdeHdrBuf.length);

        BufferIterator it = HeapBufferIterator.iterator(cdeHdrBuf, 0, cdeHdrBuf.length,
                ByteOrder.LITTLE_ENDIAN);

        int sig = it.readInt();
        if (sig != CENSIG) {
            ZipFile.throwZipException("Central Directory Entry", sig);
        }

        it.seek(8);
        int gpbf = it.readShort() & 0xffff;

        if ((gpbf & ZipFile.GPBF_UNSUPPORTED_MASK) != 0) {
            throw new ZipException("Invalid General Purpose Bit Flag: " + gpbf);
        }

        // If the GPBF_UTF8_FLAG is set then the character encoding is UTF-8 whatever the default
        // provided.
        Charset charset = defaultCharset;
        if ((gpbf & ZipFile.GPBF_UTF8_FLAG) != 0) {
            charset = StandardCharsets.UTF_8;
        }

        compressionMethod = it.readShort() & 0xffff;
        time = it.readShort() & 0xffff;
        modDate = it.readShort() & 0xffff;

        // These are 32-bit values in the file, but 64-bit fields in this object.
        crc = ((long) it.readInt()) & 0xffffffffL;
        compressedSize = ((long) it.readInt()) & 0xffffffffL;
        size = ((long) it.readInt()) & 0xffffffffL;

        nameLength = it.readShort() & 0xffff;
        int extraLength = it.readShort() & 0xffff;
        int commentByteCount = it.readShort() & 0xffff;

        // This is a 32-bit value in the file, but a 64-bit field in this object.
        it.seek(42);
        localHeaderRelOffset = ((long) it.readInt()) & 0xffffffffL;

        byte[] nameBytes = new byte[nameLength];
        Streams.readFully(cdStream, nameBytes, 0, nameBytes.length);
        if (containsNulByte(nameBytes)) {
            throw new ZipException("Filename contains NUL byte: " + Arrays.toString(nameBytes));
        }
        name = new String(nameBytes, 0, nameBytes.length, charset);

        if (extraLength > 0) {
            extra = new byte[extraLength];
            Streams.readFully(cdStream, extra, 0, extraLength);
        }

        if (commentByteCount > 0) {
            byte[] commentBytes = new byte[commentByteCount];
            Streams.readFully(cdStream, commentBytes, 0, commentByteCount);
            comment = new String(commentBytes, 0, commentBytes.length, charset);
        }
    }

    private static boolean containsNulByte(byte[] bytes) {
        for (byte b : bytes) {
            if (b == 0) {
                return true;
            }
        }
        return false;
    }

    private static void validateStringLength(String argument, String string) {
        // This check is not perfect: the character encoding is determined when the entry is
        // written out. UTF-8 is probably a worst-case: most alternatives should be single byte per
        // character.
        byte[] bytes = string.getBytes(StandardCharsets.UTF_8);
        if (bytes.length > 0xffff) {
            throw new IllegalArgumentException(argument + " too long: " + bytes.length);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy