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