    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one
    003 * or more contributor license agreements.  See the NOTICE file
    004 * distributed with this work for additional information
    005 * regarding copyright ownership.  The ASF licenses this file
    006 * to you under the Apache License, Version 2.0 (the
    007 * "License"); you may not use this file except in compliance
    008 * with the License.  You may obtain a copy of the License at
    009 *
    010 * http://www.apache.org/licenses/LICENSE-2.0
    011 *
    012 * Unless required by applicable law or agreed to in writing,
    013 * software distributed under the License is distributed on an
    014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015 * KIND, either express or implied.  See the License for the
    016 * specific language governing permissions and limitations
    017 * under the License.
    018 */
    019package org.apache.commons.compress.archivers.zip;
    020
    021import static org.apache.commons.compress.archivers.zip.ZipConstants.DWORD;
    022import static org.apache.commons.compress.archivers.zip.ZipConstants.WORD;
    023
    024import java.util.zip.ZipException;
    025
    026import org.apache.commons.compress.utils.ByteUtils;
    027
    028/**
    029 * Holds size and other extended information for entries that use Zip64 features.
    030 *
    031 * <p>
    032 * Currently Commons Compress doesn't support encrypting the central directory so the note in APPNOTE.TXT about masking doesn't apply.
    033 * </p>
    034 *
    035 * <p>
    036 * The implementation relies on data being read from the local file header and assumes that both size values are always present.
    037 * </p>
    038 *
    039 * @see <a href="https://www.pkware.com/documents/casestudies/APPNOTE.TXT">PKWARE APPNOTE.TXT, section 4.5.3</a>
    040 *
    041 * @since 1.2
    042 * @NotThreadSafe
    043 */
    044public class Zip64ExtendedInformationExtraField implements ZipExtraField {
    045
    046    static final ZipShort HEADER_ID = new ZipShort(0x0001);
    047
    048    private static final String LFH_MUST_HAVE_BOTH_SIZES_MSG = "Zip64 extended information must contain" + " both size values in the local file header.";
    049    private ZipEightByteInteger size, compressedSize, relativeHeaderOffset;
    050    private ZipLong diskStart;
    051
    052    /**
    053     * Stored in {@link #parseFromCentralDirectoryData parseFromCentralDirectoryData} so it can be reused when ZipFile calls {@link #reparseCentralDirectoryData
    054     * reparseCentralDirectoryData}.
    055     *
    056     * <p>
    057     * Not used for anything else
    058     * </p>
    059     *
    060     * @since 1.3
    061     */
    062    private byte[] rawCentralDirectoryData;
    063
    064    /**
    065     * This constructor should only be used by the code that reads archives inside of Commons Compress.
    066     */
    067    public Zip64ExtendedInformationExtraField() {
    068    }
    069
    070    /**
    071     * Creates an extra field based on the original and compressed size.
    072     *
    073     * @param size           the entry's original size
    074     * @param compressedSize the entry's compressed size
    075     *
    076     * @throws IllegalArgumentException if size or compressedSize is null
    077     */
    078    public Zip64ExtendedInformationExtraField(final ZipEightByteInteger size, final ZipEightByteInteger compressedSize) {
    079        this(size, compressedSize, null, null);
    080    }
    081
    082    /**
    083     * Creates an extra field based on all four possible values.
    084     *
    085     * @param size                 the entry's original size
    086     * @param compressedSize       the entry's compressed size
    087     * @param relativeHeaderOffset the entry's offset
    088     * @param diskStart            the disk start
    089     *
    090     * @throws IllegalArgumentException if size or compressedSize is null
    091     */
    092    public Zip64ExtendedInformationExtraField(final ZipEightByteInteger size, final ZipEightByteInteger compressedSize,
    093            final ZipEightByteInteger relativeHeaderOffset, final ZipLong diskStart) {
    094        this.size = size;
    095        this.compressedSize = compressedSize;
    096        this.relativeHeaderOffset = relativeHeaderOffset;
    097        this.diskStart = diskStart;
    098    }
    099
    100    private int addSizes(final byte[] data) {
    101        int off = 0;
    102        if (size != null) {
    103            System.arraycopy(size.getBytes(), 0, data, 0, DWORD);
    104            off += DWORD;
    105        }
    106        if (compressedSize != null) {
    107            System.arraycopy(compressedSize.getBytes(), 0, data, off, DWORD);
    108            off += DWORD;
    109        }
    110        return off;
    111    }
    112
    113    @Override
    114    public byte[] getCentralDirectoryData() {
    115        final byte[] data = new byte[getCentralDirectoryLength().getValue()];
    116        int off = addSizes(data);
    117        if (relativeHeaderOffset != null) {
    118            System.arraycopy(relativeHeaderOffset.getBytes(), 0, data, off, DWORD);
    119            off += DWORD;
    120        }
    121        if (diskStart != null) {
    122            System.arraycopy(diskStart.getBytes(), 0, data, off, WORD);
    123            off += WORD; // NOSONAR - assignment as documentation
    124        }
    125        return data;
    126    }
    127
    128    @Override
    129    public ZipShort getCentralDirectoryLength() {
    130        return new ZipShort((size != null ? DWORD : 0) + (compressedSize != null ? DWORD : 0) + (relativeHeaderOffset != null ? DWORD : 0)
    131                + (diskStart != null ? WORD : 0));
    132    }
    133
    134    /**
    135     * The compressed size stored in this extra field.
    136     *
    137     * @return The compressed size stored in this extra field.
    138     */
    139    public ZipEightByteInteger getCompressedSize() {
    140        return compressedSize;
    141    }
    142
    143    /**
    144     * The disk start number stored in this extra field.
    145     *
    146     * @return The disk start number stored in this extra field.
    147     */
    148    public ZipLong getDiskStartNumber() {
    149        return diskStart;
    150    }
    151
    152    @Override
    153    public ZipShort getHeaderId() {
    154        return HEADER_ID;
    155    }
    156
    157    @Override
    158    public byte[] getLocalFileDataData() {
    159        if (size != null || compressedSize != null) {
    160            if (size == null || compressedSize == null) {
    161                throw new IllegalArgumentException(LFH_MUST_HAVE_BOTH_SIZES_MSG);
    162            }
    163            final byte[] data = new byte[2 * DWORD];
    164            addSizes(data);
    165            return data;
    166        }
    167        return ByteUtils.EMPTY_BYTE_ARRAY;
    168    }
    169
    170    @Override
    171    public ZipShort getLocalFileDataLength() {
    172        return new ZipShort(size != null ? 2 * DWORD : 0);
    173    }
    174
    175    /**
    176     * The relative header offset stored in this extra field.
    177     *
    178     * @return The relative header offset stored in this extra field.
    179     */
    180    public ZipEightByteInteger getRelativeHeaderOffset() {
    181        return relativeHeaderOffset;
    182    }
    183
    184    /**
    185     * The uncompressed size stored in this extra field.
    186     *
    187     * @return The uncompressed size stored in this extra field.
    188     */
    189    public ZipEightByteInteger getSize() {
    190        return size;
    191    }
    192
    193    @Override
    194    public void parseFromCentralDirectoryData(final byte[] buffer, int offset, final int length) throws ZipException {
    195        // store for processing in reparseCentralDirectoryData
    196        rawCentralDirectoryData = new byte[length];
    197        System.arraycopy(buffer, offset, rawCentralDirectoryData, 0, length);
    198
    199        // if there is no size information in here, we are screwed and
    200        // can only hope things will get resolved by LFH data later
    201        // But there are some cases that can be detected
    202        // * all data is there
    203        // * length == 24 -> both sizes and offset
    204        // * length % 8 == 4 -> at least we can identify the diskStart field
    205        if (length >= 3 * DWORD + WORD) {
    206            parseFromLocalFileData(buffer, offset, length);
    207        } else if (length == 3 * DWORD) {
    208            size = new ZipEightByteInteger(buffer, offset);
    209            offset += DWORD;
    210            compressedSize = new ZipEightByteInteger(buffer, offset);
    211            offset += DWORD;
    212            relativeHeaderOffset = new ZipEightByteInteger(buffer, offset);
    213        } else if (length % DWORD == WORD) {
    214            diskStart = new ZipLong(buffer, offset + length - WORD);
    215        }
    216    }
    217
    218    @Override
    219    public void parseFromLocalFileData(final byte[] buffer, int offset, final int length) throws ZipException {
    220        if (length == 0) {
    221            // no local file data at all, may happen if an archive
    222            // only holds a ZIP64 extended information extra field
    223            // inside the central directory but not inside the local
    224            // file header
    225            return;
    226        }
    227        if (length < 2 * DWORD) {
    228            throw new ZipException(LFH_MUST_HAVE_BOTH_SIZES_MSG);
    229        }
    230        size = new ZipEightByteInteger(buffer, offset);
    231        offset += DWORD;
    232        compressedSize = new ZipEightByteInteger(buffer, offset);
    233        offset += DWORD;
    234        int remaining = length - 2 * DWORD;
    235        if (remaining >= DWORD) {
    236            relativeHeaderOffset = new ZipEightByteInteger(buffer, offset);
    237            offset += DWORD;
    238            remaining -= DWORD;
    239        }
    240        if (remaining >= WORD) {
    241            diskStart = new ZipLong(buffer, offset);
    242            offset += WORD; // NOSONAR - assignment as documentation
    243            remaining -= WORD; // NOSONAR - assignment as documentation
    244        }
    245    }
    246
    247    /**
    248     * Parses the raw bytes read from the central directory extra field with knowledge which fields are expected to be there.
    249     *
    250     * <p>
    251     * All four fields inside the zip64 extended information extra field are optional and must only be present if their corresponding entry inside the central
    252     * directory contains the correct magic value.
    253     * </p>
    254     *
    255     * @param hasUncompressedSize     flag to read from central directory
    256     * @param hasCompressedSize       flag to read from central directory
    257     * @param hasRelativeHeaderOffset flag to read from central directory
    258     * @param hasDiskStart            flag to read from central directory
    259     * @throws ZipException on error
    260     */
    261    public void reparseCentralDirectoryData(final boolean hasUncompressedSize, final boolean hasCompressedSize, final boolean hasRelativeHeaderOffset,
    262            final boolean hasDiskStart) throws ZipException {
    263        if (rawCentralDirectoryData != null) {
    264            final int expectedLength = (hasUncompressedSize ? DWORD : 0) + (hasCompressedSize ? DWORD : 0) + (hasRelativeHeaderOffset ? DWORD : 0)
    265                    + (hasDiskStart ? WORD : 0);
    266            if (rawCentralDirectoryData.length < expectedLength) {
    267                throw new ZipException("Central directory zip64 extended" + " information extra field's length" + " doesn't match central directory"
    268                        + " data.  Expected length " + expectedLength + " but is " + rawCentralDirectoryData.length);
    269            }
    270            int offset = 0;
    271            if (hasUncompressedSize) {
    272                size = new ZipEightByteInteger(rawCentralDirectoryData, offset);
    273                offset += DWORD;
    274            }
    275            if (hasCompressedSize) {
    276                compressedSize = new ZipEightByteInteger(rawCentralDirectoryData, offset);
    277                offset += DWORD;
    278            }
    279            if (hasRelativeHeaderOffset) {
    280                relativeHeaderOffset = new ZipEightByteInteger(rawCentralDirectoryData, offset);
    281                offset += DWORD;
    282            }
    283            if (hasDiskStart) {
    284                diskStart = new ZipLong(rawCentralDirectoryData, offset);
    285                offset += WORD; // NOSONAR - assignment as documentation
    286            }
    287        }
    288    }
    289
    290    /**
    291     * The uncompressed size stored in this extra field.
    292     *
    293     * @param compressedSize The uncompressed size stored in this extra field.
    294     */
    295    public void setCompressedSize(final ZipEightByteInteger compressedSize) {
    296        this.compressedSize = compressedSize;
    297    }
    298
    299    /**
    300     * The disk start number stored in this extra field.
    301     *
    302     * @param ds The disk start number stored in this extra field.
    303     */
    304    public void setDiskStartNumber(final ZipLong ds) {
    305        diskStart = ds;
    306    }
    307
    308    /**
    309     * The relative header offset stored in this extra field.
    310     *
    311     * @param rho The relative header offset stored in this extra field.
    312     */
    313    public void setRelativeHeaderOffset(final ZipEightByteInteger rho) {
    314        relativeHeaderOffset = rho;
    315    }
    316
    317    /**
    318     * The uncompressed size stored in this extra field.
    319     *
    320     * @param size The uncompressed size stored in this extra field.
    321     */
    322    public void setSize(final ZipEightByteInteger size) {
    323        this.size = size;
    324    }
    325}