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 * {@link ZipExtraField} related methods.
028 *
029 * @NotThreadSafe because the HashMap is not synchronized.
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.getConstructor().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        } catch (ReflectiveOperationException e) {
420            throw new IllegalArgumentException(c + ": " + e); //NOSONAR
421        }
422    }
423}