    001/*
    002 *  Licensed to the Apache Software Foundation (ASF) under one or more
    003 *  contributor license agreements.  See the NOTICE file distributed with
    004 *  this work for additional information regarding copyright ownership.
    005 *  The ASF licenses this file to You under the Apache License, Version 2.0
    006 *  (the "License"); you may not use this file except in compliance with
    007 *  the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 *  Unless required by applicable law or agreed to in writing, software
    012 *  distributed under the License is distributed on an "AS IS" BASIS,
    013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 *  See the License for the specific language governing permissions and
    015 *  limitations under the License.
    016 */
    017package org.apache.commons.compress.archivers.zip;
    018
    019import java.util.ArrayList;
    020import java.util.List;
    021import java.util.Map;
    022import java.util.Objects;
    023import java.util.concurrent.ConcurrentHashMap;
    024import java.util.zip.ZipException;
    025
    026/**
    027 * ZipExtraField related methods
    028 * @NotThreadSafe because the HashMap is not synchronized.
    029 */
    030// CheckStyle:HideUtilityClassConstructorCheck OFF (bc)
    031public class ExtraFieldUtils {
    032
    033    /**
    034     * "enum" for the possible actions to take if the extra field
    035     * cannot be parsed.
    036     *
    037     * <p>This class has been created long before Java 5 and would
    038     * have been a real enum ever since.</p>
    039     *
    040     * @since 1.1
    041     */
    042    public static final class UnparseableExtraField implements UnparseableExtraFieldBehavior {
    043        /**
    044         * Key for "throw an exception" action.
    045         */
    046        public static final int THROW_KEY = 0;
    047        /**
    048         * Key for "skip" action.
    049         */
    050        public static final int SKIP_KEY = 1;
    051        /**
    052         * Key for "read" action.
    053         */
    054        public static final int READ_KEY = 2;
    055
    056        /**
    057         * Throw an exception if field cannot be parsed.
    058         */
    059        public static final UnparseableExtraField THROW
    060            = new UnparseableExtraField(THROW_KEY);
    061
    062        /**
    063         * Skip the extra field entirely and don't make its data
    064         * available - effectively removing the extra field data.
    065         */
    066        public static final UnparseableExtraField SKIP
    067            = new UnparseableExtraField(SKIP_KEY);
    068
    069        /**
    070         * Read the extra field data into an instance of {@link
    071         * UnparseableExtraFieldData UnparseableExtraFieldData}.
    072         */
    073        public static final UnparseableExtraField READ
    074            = new UnparseableExtraField(READ_KEY);
    075
    076        private final int key;
    077
    078        private UnparseableExtraField(final int k) {
    079            key = k;
    080        }
    081
    082        /**
    083         * Key of the action to take.
    084         * @return the key
    085         */
    086        public int getKey() { return key; }
    087
    088        @Override
    089        public ZipExtraField onUnparseableExtraField(final byte[] data, final int off, final int len, final boolean local,
    090            final int claimedLength) throws ZipException {
    091            switch(key) {
    092            case THROW_KEY:
    093                throw new ZipException("Bad extra field starting at "
    094                                       + off + ".  Block length of "
    095                                       + claimedLength + " bytes exceeds remaining"
    096                                       + " data of "
    097                                       + (len - WORD)
    098                                       + " bytes.");
    099            case READ_KEY:
    100                final UnparseableExtraFieldData field = new UnparseableExtraFieldData();
    101                if (local) {
    102                    field.parseFromLocalFileData(data, off, len);
    103                } else {
    104                    field.parseFromCentralDirectoryData(data, off, len);
    105                }
    106                return field;
    107            case SKIP_KEY:
    108                return null;
    109            default:
    110                throw new ZipException("Unknown UnparseableExtraField key: " + key);
    111            }
    112        }
    113
    114    }
    115
    116    private static final int WORD = 4;
    117
    118    /**
    119     * Static registry of known extra fields.
    120     */
    121    private static final Map<ZipShort, Class<?>> IMPLEMENTATIONS;
    122
    123    static {
    124        IMPLEMENTATIONS = new ConcurrentHashMap<>();
    125        register(AsiExtraField.class);
    126        register(X5455_ExtendedTimestamp.class);
    127        register(X7875_NewUnix.class);
    128        register(JarMarker.class);
    129        register(UnicodePathExtraField.class);
    130        register(UnicodeCommentExtraField.class);
    131        register(Zip64ExtendedInformationExtraField.class);
    132        register(X000A_NTFS.class);
    133        register(X0014_X509Certificates.class);
    134        register(X0015_CertificateIdForFile.class);
    135        register(X0016_CertificateIdForCentralDirectory.class);
    136        register(X0017_StrongEncryptionHeader.class);
    137        register(X0019_EncryptionRecipientCertificateList.class);
    138        register(ResourceAlignmentExtraField.class);
    139    }
    140
    141    static final ZipExtraField[] EMPTY_ZIP_EXTRA_FIELD_ARRAY = {};
    142
    143    /**
    144     * Create an instance of the appropriate ExtraField, falls back to
    145     * {@link UnrecognizedExtraField UnrecognizedExtraField}.
    146     * @param headerId the header identifier
    147     * @return an instance of the appropriate ExtraField
    148     * @throws InstantiationException if unable to instantiate the class
    149     * @throws IllegalAccessException if not allowed to instantiate the class
    150     */
    151    public static ZipExtraField createExtraField(final ZipShort headerId)
    152        throws InstantiationException, IllegalAccessException {
    153        final ZipExtraField field = createExtraFieldNoDefault(headerId);
    154        if (field != null) {
    155            return field;
    156        }
    157        final UnrecognizedExtraField u = new UnrecognizedExtraField();
    158        u.setHeaderId(headerId);
    159        return u;
    160    }
    161
    162    /**
    163     * Create an instance of the appropriate ExtraField.
    164     * @param headerId the header identifier
    165     * @return an instance of the appropriate ExtraField or null if
    166     * the id is not supported
    167     * @throws InstantiationException if unable to instantiate the class
    168     * @throws IllegalAccessException if not allowed to instantiate the class
    169     * @since 1.19
    170     */
    171    public static ZipExtraField createExtraFieldNoDefault(final ZipShort headerId)
    172        throws InstantiationException, IllegalAccessException {
    173        final Class<?> c = IMPLEMENTATIONS.get(headerId);
    174        if (c != null) {
    175            return (ZipExtraField) c.newInstance();
    176        }
    177        return null;
    178    }
    179
    180    /**
    181     * Fills in the extra field data into the given instance.
    182     *
    183     * <p>Calls {@link ZipExtraField#parseFromCentralDirectoryData} or {@link ZipExtraField#parseFromLocalFileData} internally and wraps any {@link ArrayIndexOutOfBoundsException} thrown into a {@link ZipException}.</p>
    184     *
    185     * @param ze the extra field instance to fill
    186     * @param data the array of extra field data
    187     * @param off offset into data where this field's data starts
    188     * @param len the length of this field's data
    189     * @param local whether the extra field data stems from the local
    190     * file header. If this is false then the data is part if the
    191     * central directory header extra data.
    192     * @return the filled field, will never be {@code null}
    193     * @throws ZipException if an error occurs
    194     *
    195     * @since 1.19
    196     */
    197    public static ZipExtraField fillExtraField(final ZipExtraField ze, final byte[] data, final int off,
    198        final int len, final boolean local) throws ZipException {
    199        try {
    200            if (local) {
    201                ze.parseFromLocalFileData(data, off, len);
    202            } else {
    203                ze.parseFromCentralDirectoryData(data, off, len);
    204            }
    205            return ze;
    206        } catch (final ArrayIndexOutOfBoundsException aiobe) {
    207            throw (ZipException) new ZipException("Failed to parse corrupt ZIP extra field of type "
    208                + Integer.toHexString(ze.getHeaderId().getValue())).initCause(aiobe);
    209        }
    210    }
    211
    212    /**
    213     * Merges the central directory fields of the given ZipExtraFields.
    214     * @param data an array of ExtraFields
    215     * @return an array of bytes
    216     */
    217    public static byte[] mergeCentralDirectoryData(final ZipExtraField[] data) {
    218        final int dataLength = data.length;
    219        final boolean lastIsUnparseableHolder = dataLength > 0
    220            && data[dataLength - 1] instanceof UnparseableExtraFieldData;
    221        final int regularExtraFieldCount =
    222            lastIsUnparseableHolder ? dataLength - 1 : dataLength;
    223
    224        int sum = WORD * regularExtraFieldCount;
    225        for (final ZipExtraField element : data) {
    226            sum += element.getCentralDirectoryLength().getValue();
    227        }
    228        final byte[] result = new byte[sum];
    229        int start = 0;
    230        for (int i = 0; i < regularExtraFieldCount; i++) {
    231            System.arraycopy(data[i].getHeaderId().getBytes(),
    232                             0, result, start, 2);
    233            System.arraycopy(data[i].getCentralDirectoryLength().getBytes(),
    234                             0, result, start + 2, 2);
    235            start += WORD;
    236            final byte[] central = data[i].getCentralDirectoryData();
    237            if (central != null) {
    238                System.arraycopy(central, 0, result, start, central.length);
    239                start += central.length;
    240            }
    241        }
    242        if (lastIsUnparseableHolder) {
    243            final byte[] central = data[dataLength - 1].getCentralDirectoryData();
    244            if (central != null) {
    245                System.arraycopy(central, 0, result, start, central.length);
    246            }
    247        }
    248        return result;
    249    }
    250
    251    /**
    252     * Merges the local file data fields of the given ZipExtraFields.
    253     * @param data an array of ExtraFiles
    254     * @return an array of bytes
    255     */
    256    public static byte[] mergeLocalFileDataData(final ZipExtraField[] data) {
    257        final int dataLength = data.length;
    258        final boolean lastIsUnparseableHolder = dataLength > 0
    259            && data[dataLength - 1] instanceof UnparseableExtraFieldData;
    260        final int regularExtraFieldCount =
    261            lastIsUnparseableHolder ? dataLength - 1 : dataLength;
    262
    263        int sum = WORD * regularExtraFieldCount;
    264        for (final ZipExtraField element : data) {
    265            sum += element.getLocalFileDataLength().getValue();
    266        }
    267
    268        final byte[] result = new byte[sum];
    269        int start = 0;
    270        for (int i = 0; i < regularExtraFieldCount; i++) {
    271            System.arraycopy(data[i].getHeaderId().getBytes(),
    272                             0, result, start, 2);
    273            System.arraycopy(data[i].getLocalFileDataLength().getBytes(),
    274                             0, result, start + 2, 2);
    275            start += WORD;
    276            final byte[] local = data[i].getLocalFileDataData();
    277            if (local != null) {
    278                System.arraycopy(local, 0, result, start, local.length);
    279                start += local.length;
    280            }
    281        }
    282        if (lastIsUnparseableHolder) {
    283            final byte[] local = data[dataLength - 1].getLocalFileDataData();
    284            if (local != null) {
    285                System.arraycopy(local, 0, result, start, local.length);
    286            }
    287        }
    288        return result;
    289    }
    290
    291    /**
    292     * Split the array into ExtraFields and populate them with the
    293     * given data as local file data, throwing an exception if the
    294     * data cannot be parsed.
    295     * @param data an array of bytes as it appears in local file data
    296     * @return an array of ExtraFields
    297     * @throws ZipException on error
    298     */
    299    public static ZipExtraField[] parse(final byte[] data) throws ZipException {
    300        return parse(data, true, UnparseableExtraField.THROW);
    301    }
    302
    303    /**
    304     * Split the array into ExtraFields and populate them with the
    305     * given data, throwing an exception if the data cannot be parsed.
    306     * @param data an array of bytes
    307     * @param local whether data originates from the local file data
    308     * or the central directory
    309     * @return an array of ExtraFields
    310     * @throws ZipException on error
    311     */
    312    public static ZipExtraField[] parse(final byte[] data, final boolean local)
    313        throws ZipException {
    314        return parse(data, local, UnparseableExtraField.THROW);
    315    }
    316
    317    /**
    318     * Split the array into ExtraFields and populate them with the
    319     * given data.
    320     * @param data an array of bytes
    321     * @param parsingBehavior controls parsing of extra fields.
    322     * @param local whether data originates from the local file data
    323     * or the central directory
    324     * @return an array of ExtraFields
    325     * @throws ZipException on error
    326     *
    327     * @since 1.19
    328     */
    329    public static ZipExtraField[] parse(final byte[] data, final boolean local,
    330                                        final ExtraFieldParsingBehavior parsingBehavior)
    331        throws ZipException {
    332        final List<ZipExtraField> v = new ArrayList<>();
    333        int start = 0;
    334        final int dataLength = data.length;
    335        LOOP:
    336        while (start <= dataLength - WORD) {
    337            final ZipShort headerId = new ZipShort(data, start);
    338            final int length = new ZipShort(data, start + 2).getValue();
    339            if (start + WORD + length > dataLength) {
    340                final ZipExtraField field = parsingBehavior.onUnparseableExtraField(data, start, dataLength - start,
    341                    local, length);
    342                if (field != null) {
    343                    v.add(field);
    344                }
    345                // since we cannot parse the data we must assume
    346                // the extra field consumes the whole rest of the
    347                // available data
    348                break LOOP;
    349            }
    350            try {
    351                final ZipExtraField ze = Objects.requireNonNull(parsingBehavior.createExtraField(headerId),
    352                    "createExtraField must not return null");
    353                v.add(Objects.requireNonNull(parsingBehavior.fill(ze, data, start + WORD, length, local),
    354                    "fill must not return null"));
    355                start += length + WORD;
    356            } catch (final InstantiationException | IllegalAccessException ie) {
    357                throw (ZipException) new ZipException(ie.getMessage()).initCause(ie);
    358            }
    359        }
    360
    361        return v.toArray(EMPTY_ZIP_EXTRA_FIELD_ARRAY);
    362    }
    363
    364    /**
    365     * Split the array into ExtraFields and populate them with the
    366     * given data.
    367     * @param data an array of bytes
    368     * @param local whether data originates from the local file data
    369     * or the central directory
    370     * @param onUnparseableData what to do if the extra field data
    371     * cannot be parsed.
    372     * @return an array of ExtraFields
    373     * @throws ZipException on error
    374     *
    375     * @since 1.1
    376     */
    377    public static ZipExtraField[] parse(final byte[] data, final boolean local,
    378                                        final UnparseableExtraField onUnparseableData)
    379        throws ZipException {
    380        return parse(data, local, new ExtraFieldParsingBehavior() {
    381            @Override
    382            public ZipExtraField createExtraField(final ZipShort headerId)
    383                throws ZipException, InstantiationException, IllegalAccessException {
    384                return ExtraFieldUtils.createExtraField(headerId);
    385            }
    386
    387            @Override
    388            public ZipExtraField fill(final ZipExtraField field, final byte[] data, final int off, final int len, final boolean local)
    389                throws ZipException {
    390                return fillExtraField(field, data, off, len, local);
    391            }
    392
    393            @Override
    394            public ZipExtraField onUnparseableExtraField(final byte[] data, final int off, final int len, final boolean local,
    395                final int claimedLength) throws ZipException {
    396                return onUnparseableData.onUnparseableExtraField(data, off, len, local, claimedLength);
    397            }
    398        });
    399    }
    400
    401    /**
    402     * Register a ZipExtraField implementation.
    403     *
    404     * <p>The given class must have a no-arg constructor and implement
    405     * the {@link ZipExtraField ZipExtraField interface}.</p>
    406     * @param c the class to register
    407     */
    408    public static void register(final Class<?> c) {
    409        try {
    410            final ZipExtraField ze = (ZipExtraField) c.newInstance();
    411            IMPLEMENTATIONS.put(ze.getHeaderId(), c);
    412        } catch (final ClassCastException cc) { // NOSONAR
    413            throw new IllegalArgumentException(c + " doesn't implement ZipExtraField"); //NOSONAR
    414        } catch (final InstantiationException ie) { // NOSONAR
    415            throw new IllegalArgumentException(c + " is not a concrete class"); //NOSONAR
    416        } catch (final IllegalAccessException ie) { // NOSONAR
    417            throw new IllegalArgumentException(c + "'s no-arg constructor is not public"); //NOSONAR
    418        }
    419    }
    420}