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