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

java.util.zip.Zip64 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
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed 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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import static java.util.zip.ZipOutputStream.writeIntAsUint16;
import static java.util.zip.ZipOutputStream.writeLongAsUint32;
import static java.util.zip.ZipOutputStream.writeLongAsUint64;

/**
 * @hide
 */
public class Zip64 {

    /* Non instantiable */
    private Zip64() {}

    /**
     * The maximum supported entry / archive size for standard (non zip64) entries and archives.
     *
     * @hide
     */
    public static final long MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE = 0x00000000ffffffffL;

    /**
     * The header ID of the zip64 extended info header. This value is used to identify
     * zip64 data in the "extra" field in the file headers.
     */
    private static final short ZIP64_EXTENDED_INFO_HEADER_ID = 0x0001;


    /*
     * Size (in bytes) of the zip64 end of central directory locator. This will be located
     * immediately before the end of central directory record if a given zipfile is in the
     * zip64 format.
     */
    private static final int ZIP64_LOCATOR_SIZE = 20;

    /**
     * The zip64 end of central directory locator signature (4 bytes wide).
     */
    private static final int ZIP64_LOCATOR_SIGNATURE = 0x07064b50;

    /**
     * The zip64 end of central directory record singature (4 bytes wide).
     */
    private static final int ZIP64_EOCD_RECORD_SIGNATURE = 0x06064b50;

    /**
     * The "effective" size of the zip64 eocd record. This excludes the fields that
     * are proprietary, signature, or fields we aren't interested in. We include the
     * following (contiguous) fields in this calculation :
     * - disk number (4 bytes)
     * - disk with start of central directory (4 bytes)
     * - number of central directory entries on this disk (8 bytes)
     * - total number of central directory entries (8 bytes)
     * - size of the central directory (8 bytes)
     * - offset of the start of the central directory (8 bytes)
     */
    private static final int ZIP64_EOCD_RECORD_EFFECTIVE_SIZE = 40;

    /**
     * Parses the zip64 end of central directory record locator. The locator
     * must be placed immediately before the end of central directory (eocd) record
     * starting at {@code eocdOffset}.
     *
     * The position of the file cursor for {@code raf} after a call to this method
     * is undefined an callers must reposition it after each call to this method.
     */
    public static long parseZip64EocdRecordLocator(RandomAccessFile raf, long eocdOffset)
            throws IOException {
        // The spec stays curiously silent about whether a zip file with an EOCD record,
        // a zip64 locator and a zip64 eocd record is considered "empty". In our implementation,
        // we parse all records and read the counts from them instead of drawing any size or
        // layout based information.
        if (eocdOffset > ZIP64_LOCATOR_SIZE) {
            raf.seek(eocdOffset - ZIP64_LOCATOR_SIZE);
            if (Integer.reverseBytes(raf.readInt()) == ZIP64_LOCATOR_SIGNATURE) {
                byte[] zip64EocdLocator = new byte[ZIP64_LOCATOR_SIZE  - 4];
                raf.readFully(zip64EocdLocator);
                ByteBuffer buf = ByteBuffer.wrap(zip64EocdLocator).order(ByteOrder.LITTLE_ENDIAN);

                final int diskWithCentralDir = buf.getInt();
                final long zip64EocdRecordOffset = buf.getLong();
                final int numDisks = buf.getInt();

                if (numDisks != 1 || diskWithCentralDir != 0) {
                    throw new ZipException("Spanned archives not supported");
                }

                return zip64EocdRecordOffset;
            }
        }

        return -1;
    }

    public static ZipFile.EocdRecord parseZip64EocdRecord(RandomAccessFile raf,
            long eocdRecordOffset, int commentLength) throws IOException {
        raf.seek(eocdRecordOffset);
        final int signature = Integer.reverseBytes(raf.readInt());
        if (signature != ZIP64_EOCD_RECORD_SIGNATURE) {
            throw new ZipException("Invalid zip64 eocd record offset, sig="
                    + Integer.toHexString(signature) + " offset=" + eocdRecordOffset);
        }

        // The zip64 eocd record specifies its own size as an 8 byte integral type. It is variable
        // length because of the "zip64 extensible data sector" but that field is reserved for
        // pkware's proprietary use. We therefore disregard it altogether and treat the end of
        // central directory structure as fixed length.
        //
        // We also skip "version made by" (2 bytes) and "version needed to extract" (2 bytes)
        // fields. We perform additional validation at the ZipEntry level, where applicable.
        //
        // That's a total of 12 bytes to skip
        raf.skipBytes(12);

        byte[] zip64Eocd = new byte[ZIP64_EOCD_RECORD_EFFECTIVE_SIZE];
        raf.readFully(zip64Eocd);

        ByteBuffer buf = ByteBuffer.wrap(zip64Eocd).order(ByteOrder.LITTLE_ENDIAN);
        try {
            int diskNumber = buf.getInt();
            int diskWithCentralDirStart = buf.getInt();
            long numEntries = buf.getLong();
            long totalNumEntries = buf.getLong();
            buf.getLong(); // Ignore the size of the central directory
            long centralDirOffset = buf.getLong();

            if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDirStart != 0) {
                throw new ZipException("Spanned archives not supported :" +
                        " numEntries=" + numEntries + ", totalNumEntries=" + totalNumEntries +
                        ", diskNumber=" + diskNumber + ", diskWithCentralDirStart=" +
                        diskWithCentralDirStart);
            }

            return new ZipFile.EocdRecord(numEntries, centralDirOffset, commentLength);
        } catch (BufferUnderflowException bue) {
            ZipException zipException = new ZipException("Error parsing zip64 eocd record.");
            zipException.initCause(bue);
            throw zipException;
        }
    }

    /**
     * Parse the zip64 extended info record from the extras present in {@code ze}.
     *
     * If {@code fromCentralDirectory} is true, we assume we're parsing a central directory
     * record. We assume a local file header otherwise. The difference between the two is that
     * a central directory entry is required to be complete, whereas a local file header isn't.
     * This is due to the presence of an optional data descriptor after the file content.
     *
     * @return {@code} true iff. a zip64 extended info record was found.
     */
    public static boolean parseZip64ExtendedInfo(ZipEntry ze, boolean fromCentralDirectory)
            throws ZipException {
        int extendedInfoSize = -1;
        int extendedInfoStart = -1;
        // If this file contains a zip64 central directory locator, entries might
        // optionally contain a zip64 extended information extra entry.
        if (ze.extra != null && ze.extra.length > 0) {
            // Extensible data fields are of the form header1+data1 + header2+data2 and so
            // on, where each header consists of a 2 byte header ID followed by a 2 byte size.
            // We need to iterate through the entire list of headers to find the header ID
            // for the zip64 extended information extra field (0x0001).
            final ByteBuffer buf = ByteBuffer.wrap(ze.extra).order(ByteOrder.LITTLE_ENDIAN);
            extendedInfoSize = getZip64ExtendedInfoSize(buf);
            if (extendedInfoSize != -1) {
                extendedInfoStart = buf.position();
                try {
                    // The size & compressed size only make sense in the central directory *or* if
                    // we know them beforehand. If we don't know them beforehand, they're stored in
                    // the data descriptor and should be read from there.
                    //
                    // Note that the spec says that the local file header "MUST" contain the
                    // original and compressed size fields. We don't care too much about that.
                    // The spec claims that the order of fields is fixed anyway.
                    if (fromCentralDirectory || (ze.getMethod() == ZipEntry.STORED)) {
                        if (ze.size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
                            ze.size = buf.getLong();
                        }

                        if (ze.compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
                            ze.compressedSize = buf.getLong();
                        }
                    }

                    // The local header offset is significant only in the central directory. It makes no
                    // sense within the local header itself.
                    if (fromCentralDirectory) {
                        if (ze.localHeaderRelOffset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
                            ze.localHeaderRelOffset = buf.getLong();
                        }
                    }
                } catch (BufferUnderflowException bue) {
                    ZipException zipException = new ZipException("Error parsing extended info");
                    zipException.initCause(bue);
                    throw zipException;
                }
            }
        }

        // This entry doesn't contain a zip64 extended information data entry header.
        // We have to check that the compressedSize / size / localHeaderRelOffset values
        // are valid and don't require the presence of the extended header.
        if (extendedInfoSize == -1) {
            if (ze.compressedSize == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE ||
                    ze.size == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE ||
                    ze.localHeaderRelOffset == MAX_ZIP_ENTRY_AND_ARCHIVE_SIZE) {
                throw new ZipException("File contains no zip64 extended information: "
                        + "name=" + ze.name + "compressedSize=" + ze.compressedSize + ", size="
                        + ze.size + ", localHeader=" + ze.localHeaderRelOffset);
            }

            return false;
        } else {
            // If we're parsed the zip64 extended info header, we remove it from the extras
            // so that applications that set their own extras will see the data they set.

            // This is an unfortunate workaround needed due to a gap in the spec. The spec demands
            // that extras are present in the "extensible" format, which means that each extra field
            // must be prefixed with a header ID and a length. However, earlier versions of the spec
            // made no mention of this, nor did any existing API enforce it. This means users could
            // set "free form" extras without caring very much whether the implementation wanted to
            // extend or add to them.

            // The start of the extended info header.
            final int extendedInfoHeaderStart = extendedInfoStart - 4;
            // The total size of the extended info, including the header.
            final int extendedInfoTotalSize = extendedInfoSize + 4;

            final int extrasLen = ze.extra.length - extendedInfoTotalSize;
            byte[] extrasWithoutZip64 = new byte[extrasLen];

            System.arraycopy(ze.extra, 0, extrasWithoutZip64, 0, extendedInfoHeaderStart);
            System.arraycopy(ze.extra, extendedInfoHeaderStart + extendedInfoTotalSize,
                    extrasWithoutZip64, extendedInfoHeaderStart, (extrasLen - extendedInfoHeaderStart));

            ze.extra = extrasWithoutZip64;
            return true;
        }
    }

    /**
     * Appends a zip64 extended info record to the extras contained in {@code ze}. If {@code ze}
     * contains no extras, a new extras array is created.
     */
    public static void insertZip64ExtendedInfoToExtras(ZipEntry ze) throws ZipException {
        final byte[] output;
        // We always write the size, uncompressed size and local rel header offset in all our
        // Zip64 extended info headers (in both the local file header as well as the central
        // directory). We always omit the disk number because we don't support spanned
        // archives anyway.
        //
        //  2 bytes : Zip64 Extended Info Header ID
        //  2 bytes : Zip64 Extended Info Field Size.
        //  8 bytes : Uncompressed size
        //  8 bytes : Compressed size
        //  8 bytes : Local header rel offset.
        // ----------
        // 28 bytes : total
        final int extendedInfoSize = 28;

        if (ze.extra == null) {
            output = new byte[extendedInfoSize];
        } else {
            // If the existing extras are already too big, we have no choice but to throw
            // an error.
            if (ze.extra.length + extendedInfoSize > 65535) {
                throw new ZipException("No space in extras for zip64 extended entry info");
            }

            // We copy existing extras over and put the zip64 extended info at the beginning. This
            // is to avoid breakages in the presence of "old style" extras which don't contain
            // headers and lengths. The spec is again silent about these inconsistencies.
            //
            // This means that people that for ZipOutputStream users, the value ZipEntry.getExtra
            // after an entry is written will be different from before. This shouldn't be an issue
            // in practice.
            output = new byte[ze.extra.length + extendedInfoSize];
            System.arraycopy(ze.extra, 0, output,  extendedInfoSize, ze.extra.length);
        }

        ByteBuffer bb = ByteBuffer.wrap(output).order(ByteOrder.LITTLE_ENDIAN);
        bb.putShort(ZIP64_EXTENDED_INFO_HEADER_ID);
        // We subtract four because extendedInfoSize includes the ID and field
        // size itself.
        bb.putShort((short) (extendedInfoSize - 4));

        if (ze.getMethod() == ZipEntry.STORED) {
            bb.putLong(ze.size);
            bb.putLong(ze.compressedSize);
        } else {
            // Store these fields in the data descriptor instead.
            bb.putLong(0); // size.
            bb.putLong(0); // compressed size.
        }

        // The offset is only relevant in the central directory entry, but we write it out here
        // anyway, since we know what it is.
        bb.putLong(ze.localHeaderRelOffset);

        ze.extra = output;
    }

    /**
     * Returns the size of the extended info record if {@code extras} contains a zip64 extended info
     * record, {@code -1} otherwise. The buffer will be positioned at the start of the extended info
     * record.
     */
    private static int getZip64ExtendedInfoSize(ByteBuffer extras) {
        try {
            while (extras.hasRemaining()) {
                final int headerId = extras.getShort() & 0xffff;
                final int length = extras.getShort() & 0xffff;
                if (headerId == ZIP64_EXTENDED_INFO_HEADER_ID) {
                    if (extras.remaining() >= length) {
                        return length;
                    } else {
                        return -1;
                    }
                } else {
                    extras.position(extras.position() + length);
                }
            }

            return -1;
        } catch (BufferUnderflowException bue) {
            // We'll underflow if we have an incomplete header in our extras.
            return -1;
        } catch (IllegalArgumentException iae) {
            // ByteBuffer.position() will throw if we have a truncated extra or
            // an invalid length in the header.
            return -1;
        }
    }

    /**
     * Copy the size, compressed size and local header offset fields from {@code ze} to
     * inside {@code ze}'s extended info record. This is additional step is necessary when
     * we could calculate the correct sizes only after writing out the entry. In this case,
     * the local file header would not contain real sizes, and they would be present in the
     * data descriptor and the central directory only.
     *
     * We choose the simplest strategy of always writing out the size, compressedSize and
     * local header offset in all our Zip64 Extended info records.
     */
    public static void refreshZip64ExtendedInfo(ZipEntry ze) {
        if (ze.extra == null) {
            throw new IllegalStateException("Zip64 entry has no available extras: " + ze);
        }

        ByteBuffer buf = ByteBuffer.wrap(ze.extra).order(ByteOrder.LITTLE_ENDIAN);
        final int extendedInfoSize = getZip64ExtendedInfoSize(buf);
        if (extendedInfoSize == -1) {
            throw new IllegalStateException(
                    "Zip64 entry extras has no zip64 extended info record: " + ze);
        }

        try {
            buf.putLong(ze.size);
            buf.putLong(ze.compressedSize);
            buf.putLong(ze.localHeaderRelOffset);
        } catch (BufferOverflowException boe) {
            throw new IllegalStateException("Invalid extended info extra", boe);
        }
    }

    public static void writeZip64EocdRecordAndLocator(ByteArrayOutputStream baos,
            long numEntries, long offset, long cDirSize) throws IOException {
        // Step 1: Write out the zip64 EOCD record.
        writeLongAsUint32(baos, ZIP64_EOCD_RECORD_SIGNATURE);
        // The size of the zip64 eocd record. This is the effective size + the
        // size of the "version made by" (2 bytes) and the "version needed to extract" (2 bytes)
        // fields.
        writeLongAsUint64(baos, ZIP64_EOCD_RECORD_EFFECTIVE_SIZE + 4);
        // TODO: What values should we put here ? The pre-zip64 values we've chosen don't
        // seem to make much sense either.
        writeIntAsUint16(baos, 20);
        writeIntAsUint16(baos, 20);
        writeLongAsUint32(baos, 0L); // number of disk
        writeLongAsUint32(baos, 0L); // number of disk with start of central dir.
        writeLongAsUint64(baos, numEntries); // number of entries in this disk.
        writeLongAsUint64(baos, numEntries); // number of entries in total.
        writeLongAsUint64(baos, cDirSize); // size of the central directory.
        writeLongAsUint64(baos, offset); // offset of the central directory wrt. this file.

        // Step 2: Write out the zip64 EOCD record locator.
        writeLongAsUint32(baos, ZIP64_LOCATOR_SIGNATURE);
        writeLongAsUint32(baos, 0); // number of disk with start of central dir.
        writeLongAsUint64(baos, offset + cDirSize); // offset of the eocd record wrt. this file.
        writeLongAsUint32(baos, 1); // total number of disks.
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy