    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 *   https://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 * @since 1.2
    041 * @NotThreadSafe
    042 */
    043public class Zip64ExtendedInformationExtraField implements ZipExtraField {
    044
    045    static final ZipShort HEADER_ID = new ZipShort(0x0001);
    046
    047    private static final String LFH_MUST_HAVE_BOTH_SIZES_MSG = "Zip64 extended information must contain both size values in the local file header.";
    048    private ZipEightByteInteger size;
    049    private ZipEightByteInteger compressedSize;
    050    private ZipEightByteInteger relativeHeaderOffset;
    051    private ZipLong diskStart;
    052
    053    /**
    054     * Stored in {@link #parseFromCentralDirectoryData parseFromCentralDirectoryData} so it can be reused when ZipFile calls {@link #reparseCentralDirectoryData
    055     * reparseCentralDirectoryData}.
    056     *
    057     * <p>
    058     * Not used for anything else
    059     * </p>
    060     *
    061     * @since 1.3
    062     */
    063    private byte[] rawCentralDirectoryData;
    064
    065    /**
    066     * This constructor should only be used by the code that reads archives inside of Commons Compress.
    067     */
    068    public Zip64ExtendedInformationExtraField() {
    069    }
    070
    071    /**
    072     * Creates an extra field based on the original and compressed size.
    073     *
    074     * @param size           the entry's original size
    075     * @param compressedSize the entry's compressed size
    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     * @throws IllegalArgumentException if size or compressedSize is null
    090     */
    091    public Zip64ExtendedInformationExtraField(final ZipEightByteInteger size, final ZipEightByteInteger compressedSize,
    092            final ZipEightByteInteger relativeHeaderOffset, final ZipLong diskStart) {
    093        this.size = size;
    094        this.compressedSize = compressedSize;
    095        this.relativeHeaderOffset = relativeHeaderOffset;
    096        this.diskStart = diskStart;
    097    }
    098
    099    private int addSizes(final byte[] data) {
    100        int off = 0;
    101        if (size != null) {
    102            System.arraycopy(size.getBytes(), 0, data, 0, DWORD);
    103            off += DWORD;
    104        }
    105        if (compressedSize != null) {
    106            System.arraycopy(compressedSize.getBytes(), 0, data, off, DWORD);
    107            off += DWORD;
    108        }
    109        return off;
    110    }
    111
    112    @Override
    113    public byte[] getCentralDirectoryData() {
    114        final byte[] data = new byte[getCentralDirectoryLength().getValue()];
    115        int off = addSizes(data);
    116        if (relativeHeaderOffset != null) {
    117            System.arraycopy(relativeHeaderOffset.getBytes(), 0, data, off, DWORD);
    118            off += DWORD;
    119        }
    120        if (diskStart != null) {
    121            System.arraycopy(diskStart.getBytes(), 0, data, off, WORD);
    122            off += WORD; // NOSONAR - assignment as documentation
    123        }
    124        return data;
    125    }
    126
    127    @Override
    128    public ZipShort getCentralDirectoryLength() {
    129        return new ZipShort((size != null ? DWORD : 0) + (compressedSize != null ? DWORD : 0) + (relativeHeaderOffset != null ? DWORD : 0)
    130                + (diskStart != null ? WORD : 0));
    131    }
    132
    133    /**
    134     * The compressed size stored in this extra field.
    135     *
    136     * @return The compressed size stored in this extra field.
    137     */
    138    public ZipEightByteInteger getCompressedSize() {
    139        return compressedSize;
    140    }
    141
    142    /**
    143     * The disk start number stored in this extra field.
    144     *
    145     * @return The disk start number stored in this extra field.
    146     */
    147    public ZipLong getDiskStartNumber() {
    148        return diskStart;
    149    }
    150
    151    @Override
    152    public ZipShort getHeaderId() {
    153        return HEADER_ID;
    154    }
    155
    156    @Override
    157    public byte[] getLocalFileDataData() {
    158        if (size != null || compressedSize != null) {
    159            if (size == null || compressedSize == null) {
    160                throw new IllegalArgumentException(LFH_MUST_HAVE_BOTH_SIZES_MSG);
    161            }
    162            final byte[] data = new byte[2 * DWORD];
    163            addSizes(data);
    164            return data;
    165        }
    166        return ByteUtils.EMPTY_BYTE_ARRAY;
    167    }
    168
    169    @Override
    170    public ZipShort getLocalFileDataLength() {
    171        return new ZipShort(size != null ? 2 * DWORD : 0);
    172    }
    173
    174    /**
    175     * The relative header offset stored in this extra field.
    176     *
    177     * @return The relative header offset stored in this extra field.
    178     */
    179    public ZipEightByteInteger getRelativeHeaderOffset() {
    180        return relativeHeaderOffset;
    181    }
    182
    183    /**
    184     * The uncompressed size stored in this extra field.
    185     *
    186     * @return The uncompressed size stored in this extra field.
    187     */
    188    public ZipEightByteInteger getSize() {
    189        return size;
    190    }
    191
    192    @Override
    193    public void parseFromCentralDirectoryData(final byte[] buffer, int offset, final int length) throws ZipException {
    194        // store for processing in reparseCentralDirectoryData
    195        rawCentralDirectoryData = new byte[length];
    196        System.arraycopy(buffer, offset, rawCentralDirectoryData, 0, length);
    197
    198        // if there is no size information in here, we are screwed and
    199        // can only hope things will get resolved by LFH data later
    200        // But there are some cases that can be detected
    201        // * all data is there
    202        // * length == 24 -> both sizes and offset
    203        // * length % 8 == 4 -> at least we can identify the diskStart field
    204        if (length >= 3 * DWORD + WORD) {
    205            parseFromLocalFileData(buffer, offset, length);
    206        } else if (length == 3 * DWORD) {
    207            size = new ZipEightByteInteger(buffer, offset);
    208            offset += DWORD;
    209            compressedSize = new ZipEightByteInteger(buffer, offset);
    210            offset += DWORD;
    211            relativeHeaderOffset = new ZipEightByteInteger(buffer, offset);
    212        } else if (length % DWORD == WORD) {
    213            diskStart = new ZipLong(buffer, offset + length - WORD);
    214        }
    215    }
    216
    217    @Override
    218    public void parseFromLocalFileData(final byte[] buffer, int offset, final int length) throws ZipException {
    219        if (length == 0) {
    220            // no local file data at all, may happen if an archive
    221            // only holds a ZIP64 extended information extra field
    222            // inside the central directory but not inside the local
    223            // file header
    224            return;
    225        }
    226        if (length < 2 * DWORD) {
    227            throw new ZipException(LFH_MUST_HAVE_BOTH_SIZES_MSG);
    228        }
    229        size = new ZipEightByteInteger(buffer, offset);
    230        offset += DWORD;
    231        compressedSize = new ZipEightByteInteger(buffer, offset);
    232        offset += DWORD;
    233        int remaining = length - 2 * DWORD;
    234        if (remaining >= DWORD) {
    235            relativeHeaderOffset = new ZipEightByteInteger(buffer, offset);
    236            offset += DWORD;
    237            remaining -= DWORD;
    238        }
    239        if (remaining >= WORD) {
    240            diskStart = new ZipLong(buffer, offset);
    241            offset += WORD; // NOSONAR - assignment as documentation
    242            remaining -= WORD; // NOSONAR - assignment as documentation
    243        }
    244    }
    245
    246    /**
    247     * Parses the raw bytes read from the central directory extra field with knowledge which fields are expected to be there.
    248     *
    249     * <p>
    250     * All four fields inside the zip64 extended information extra field are optional and must only be present if their corresponding entry inside the central
    251     * directory contains the correct magic value.
    252     * </p>
    253     *
    254     * @param hasUncompressedSize     flag to read from central directory
    255     * @param hasCompressedSize       flag to read from central directory
    256     * @param hasRelativeHeaderOffset flag to read from central directory
    257     * @param hasDiskStart            flag to read from central directory
    258     * @throws ZipException on error
    259     */
    260    public void reparseCentralDirectoryData(final boolean hasUncompressedSize, final boolean hasCompressedSize, final boolean hasRelativeHeaderOffset,
    261            final boolean hasDiskStart) throws ZipException {
    262        if (rawCentralDirectoryData != null) {
    263            final int expectedLength = (hasUncompressedSize ? DWORD : 0) + (hasCompressedSize ? DWORD : 0) + (hasRelativeHeaderOffset ? DWORD : 0)
    264                    + (hasDiskStart ? WORD : 0);
    265            if (rawCentralDirectoryData.length < expectedLength) {
    266                throw new ZipException("Central directory zip64 extended information extra field's length doesn't match central directory"
    267                        + " data.  Expected length " + expectedLength + " but is " + rawCentralDirectoryData.length);
    268            }
    269            int offset = 0;
    270            if (hasUncompressedSize) {
    271                size = new ZipEightByteInteger(rawCentralDirectoryData, offset);
    272                offset += DWORD;
    273            }
    274            if (hasCompressedSize) {
    275                compressedSize = new ZipEightByteInteger(rawCentralDirectoryData, offset);
    276                offset += DWORD;
    277            }
    278            if (hasRelativeHeaderOffset) {
    279                relativeHeaderOffset = new ZipEightByteInteger(rawCentralDirectoryData, offset);
    280                offset += DWORD;
    281            }
    282            if (hasDiskStart) {
    283                diskStart = new ZipLong(rawCentralDirectoryData, offset);
    284                offset += WORD; // NOSONAR - assignment as documentation
    285            }
    286        }
    287    }
    288
    289    /**
    290     * The uncompressed size stored in this extra field.
    291     *
    292     * @param compressedSize The uncompressed size stored in this extra field.
    293     */
    294    public void setCompressedSize(final ZipEightByteInteger compressedSize) {
    295        this.compressedSize = compressedSize;
    296    }
    297
    298    /**
    299     * The disk start number stored in this extra field.
    300     *
    301     * @param ds The disk start number stored in this extra field.
    302     */
    303    public void setDiskStartNumber(final ZipLong ds) {
    304        diskStart = ds;
    305    }
    306
    307    /**
    308     * The relative header offset stored in this extra field.
    309     *
    310     * @param rho The relative header offset stored in this extra field.
    311     */
    312    public void setRelativeHeaderOffset(final ZipEightByteInteger rho) {
    313        relativeHeaderOffset = rho;
    314    }
    315
    316    /**
    317     * The uncompressed size stored in this extra field.
    318     *
    319     * @param size The uncompressed size stored in this extra field.
    320     */
    321    public void setSize(final ZipEightByteInteger size) {
    322        this.size = size;
    323    }
    324}