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 java.io.IOException;
022import java.math.BigInteger;
023import java.time.Instant;
024import java.time.LocalDateTime;
025import java.time.ZoneId;
026import java.util.Arrays;
027import java.util.Calendar;
028import java.util.Date;
029import java.util.zip.CRC32;
030import java.util.zip.ZipEntry;
031import java.util.zip.ZipException;
032
033/**
034 * Utility class for handling DOS and Java time conversions.
035 *
036 * @Immutable
037 */
038public abstract class ZipUtil {
039
040    /**
041     * DOS time constant for representing timestamps before 1980. Smallest date/time ZIP can handle.
042     * <p>
043     * MS-DOS records file dates and times as packed 16-bit values. An MS-DOS date has the following format.
044     * </p>
045     * <p>
046     * Bits Contents
047     * </p>
048     * <ul>
049     * <li>0-4: Day of the month (1-31).</li>
050     * <li>5-8: Month (1 = January, 2 = February, and so on).</li>
051     * <li>9-15: Year offset from 1980 (add 1980 to get the actual year).</li>
052     * </ul>
053     *
054     * An MS-DOS time has the following format.
055     * <p>
056     * Bits Contents
057     * </p>
058     * <ul>
059     * <li>0-4: Second divided by 2.</li>
060     * <li>5-10: Minute (0-59).</li>
061     * <li>11-15: Hour (0-23 on a 24-hour clock).</li>
062     * </ul>
063     *
064     * This constant expresses the minimum DOS date of January 1st 1980 at 00:00:00 or, bit-by-bit:
065     * <ul>
066     * <li>Year: 0000000</li>
067     * <li>Month: 0001</li>
068     * <li>Day: 00001</li>
069     * <li>Hour: 00000</li>
070     * <li>Minute: 000000</li>
071     * <li>Seconds: 00000</li>
072     * </ul>
073     *
074     * <p>
075     * This was copied from {@link ZipEntry}.
076     * </p>
077     *
078     * @since 1.23
079     */
080    private static final long DOSTIME_BEFORE_1980 = 1 << 21 | 1 << 16; // 0x210000
081
082    /** Java time representation of the smallest date/time ZIP can handle */
083    private static final long DOSTIME_BEFORE_1980_AS_JAVA_TIME = dosToJavaTime(DOSTIME_BEFORE_1980);
084
085    /**
086     * Approximately 128 years, in milliseconds (ignoring leap years, etc.).
087     *
088     * <p>
089     * This establish an approximate high-bound value for DOS times in milliseconds since epoch, used to enable an efficient but sufficient bounds check to
090     * avoid generating extended last modified time entries.
091     * </p>
092     * <p>
093     * Calculating the exact number is locale dependent, would require loading TimeZone data eagerly, and would make little practical sense. Since DOS times
094     * theoretically go to 2107 - with compatibility not guaranteed after 2099 - setting this to a time that is before but near 2099 should be sufficient.
095     * </p>
096     *
097     * <p>
098     * This was copied from {@link ZipEntry}.
099     * </p>
100     *
101     * @since 1.23
102     */
103    private static final long UPPER_DOSTIME_BOUND = 128L * 365 * 24 * 60 * 60 * 1000;
104
105    /**
106     * Assumes a negative integer really is a positive integer that has wrapped around and re-creates the original value.
107     *
108     * @param i the value to treat as unsigned int.
109     * @return the unsigned int as a long.
110     */
111    public static long adjustToLong(final int i) {
112        if (i < 0) {
113            return 2 * (long) Integer.MAX_VALUE + 2 + i;
114        }
115        return i;
116    }
117
118    /**
119     * Tests if this library is able to read or write the given entry.
120     */
121    static boolean canHandleEntryData(final ZipArchiveEntry entry) {
122        return supportsEncryptionOf(entry) && supportsMethodOf(entry);
123    }
124
125    /**
126     * Checks whether the entry requires features not (yet) supported by the library and throws an exception if it does.
127     */
128    static void checkRequestedFeatures(final ZipArchiveEntry ze) throws UnsupportedZipFeatureException {
129        if (!supportsEncryptionOf(ze)) {
130            throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException.Feature.ENCRYPTION, ze);
131        }
132        if (!supportsMethodOf(ze)) {
133            final ZipMethod m = ZipMethod.getMethodByCode(ze.getMethod());
134            if (m == null) {
135                throw new UnsupportedZipFeatureException(UnsupportedZipFeatureException.Feature.METHOD, ze);
136            }
137            throw new UnsupportedZipFeatureException(m, ze);
138        }
139    }
140
141    /**
142     * Creates a copy of the given array - or return null if the argument is null.
143     */
144    static byte[] copy(final byte[] from) {
145        if (from != null) {
146            return Arrays.copyOf(from, from.length);
147        }
148        return null;
149    }
150
151    static void copy(final byte[] from, final byte[] to, final int offset) {
152        if (from != null) {
153            System.arraycopy(from, 0, to, offset, from.length);
154        }
155    }
156
157    private static Date dosToJavaDate(final long dosTime) {
158        final Calendar cal = Calendar.getInstance();
159        // CheckStyle:MagicNumberCheck OFF - no point
160        cal.set(Calendar.YEAR, (int) (dosTime >> 25 & 0x7f) + 1980);
161        cal.set(Calendar.MONTH, (int) (dosTime >> 21 & 0x0f) - 1);
162        cal.set(Calendar.DATE, (int) (dosTime >> 16) & 0x1f);
163        cal.set(Calendar.HOUR_OF_DAY, (int) (dosTime >> 11) & 0x1f);
164        cal.set(Calendar.MINUTE, (int) (dosTime >> 5) & 0x3f);
165        cal.set(Calendar.SECOND, (int) (dosTime << 1) & 0x3e);
166        cal.set(Calendar.MILLISECOND, 0);
167        // CheckStyle:MagicNumberCheck ON
168        return cal.getTime();
169    }
170
171    /**
172     * Converts DOS time to Java time (number of milliseconds since epoch).
173     *
174     * @param dosTime time to convert
175     * @return converted time
176     */
177    public static long dosToJavaTime(final long dosTime) {
178        return dosToJavaDate(dosTime).getTime();
179    }
180
181    /**
182     * Converts a DOS date/time field to a Date object.
183     *
184     * @param zipDosTime contains the stored DOS time.
185     * @return a Date instance corresponding to the given time.
186     */
187    public static Date fromDosTime(final ZipLong zipDosTime) {
188        final long dosTime = zipDosTime.getValue();
189        return dosToJavaDate(dosTime);
190    }
191
192    /**
193     * If the stored CRC matches the one of the given name, return the Unicode name of the given field.
194     *
195     * <p>
196     * If the field is null or the CRCs don't match, return null instead.
197     * </p>
198     */
199    private static String getUnicodeStringIfOriginalMatches(final AbstractUnicodeExtraField field, final byte[] originalNameBytes) {
200        if (field != null) {
201            final CRC32 crc32 = new CRC32();
202            crc32.update(originalNameBytes);
203            final long origCRC32 = crc32.getValue();
204
205            if (origCRC32 == field.getNameCRC32()) {
206                try {
207                    return ZipEncodingHelper.ZIP_ENCODING_UTF_8.decode(field.getUnicodeName());
208                } catch (final IOException ignored) {
209                    // UTF-8 unsupported? should be impossible the
210                    // Unicode*ExtraField must contain some bad bytes
211                }
212            }
213        }
214        // TODO log this anywhere?
215        return null;
216    }
217
218    /**
219     * Tests whether a given time (in milliseconds since Epoch) can be safely represented as DOS time
220     *
221     * @param time time in milliseconds since epoch
222     * @return true if the time can be safely represented as DOS time, false otherwise
223     * @since 1.23
224     */
225    public static boolean isDosTime(final long time) {
226        return time <= UPPER_DOSTIME_BOUND &&
227                (time == DOSTIME_BEFORE_1980_AS_JAVA_TIME || javaToDosTime(time) != DOSTIME_BEFORE_1980);
228    }
229
230    private static LocalDateTime javaEpochToLocalDateTime(final long time) {
231        final Instant instant = Instant.ofEpochMilli(time);
232        return LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
233    }
234
235    // version with integer overflow fixed - see https://bugs.openjdk.org/browse/JDK-8130914
236    private static long javaToDosTime(final long t) {
237        final LocalDateTime ldt = javaEpochToLocalDateTime(t);
238        if (ldt.getYear() < 1980) {
239            return DOSTIME_BEFORE_1980;
240        }
241        return (ldt.getYear() - 1980 << 25 | ldt.getMonthValue() << 21 | ldt.getDayOfMonth() << 16 | ldt.getHour() << 11 | ldt.getMinute() << 5
242                | ldt.getSecond() >> 1) & 0xffffffffL;
243    }
244
245    /**
246     * <p>
247     * Converts a long into a BigInteger. Negative numbers between -1 and -2^31 are treated as unsigned 32 bit (for example, positive) integers. Negative
248     * numbers below -2^31 cause an IllegalArgumentException to be thrown.
249     * </p>
250     *
251     * @param l long to convert to BigInteger.
252     * @return BigInteger representation of the provided long.
253     */
254    static BigInteger longToBig(long l) {
255        if (l < Integer.MIN_VALUE) {
256            throw new IllegalArgumentException("Negative longs < -2^31 not permitted: [" + l + "]");
257        }
258        if (l < 0 && l >= Integer.MIN_VALUE) {
259            // If someone passes in a -2, they probably mean 4294967294
260            // (For example, Unix UID/GID's are 32 bit unsigned.)
261            l = adjustToLong((int) l);
262        }
263        return BigInteger.valueOf(l);
264    }
265
266    /**
267     * Constructs a new ZipException.
268     *
269     * @param message the detail message.
270     * @param cause   throwable The cause of this Throwable.
271     * @return a new ZipException.
272     */
273    static ZipException newZipException(final String message, final Throwable cause) {
274        return (ZipException) new ZipException(message).initCause(cause);
275    }
276
277    /**
278     * Reverses a byte[] array. Reverses in-place (thus provided array is mutated), but also returns same for convenience.
279     *
280     * @param array to reverse (mutated in-place, but also returned for convenience).
281     * @return the reversed array (mutated in-place, but also returned for convenience).
282     * @since 1.5
283     */
284    public static byte[] reverse(final byte[] array) {
285        final int z = array.length - 1; // position of last element
286        for (int i = 0; i < array.length / 2; i++) {
287            final byte x = array[i];
288            array[i] = array[z - i];
289            array[z - i] = x;
290        }
291        return array;
292    }
293
294    /**
295     * If the entry has Unicode*ExtraFields and the CRCs of the names/comments match those of the extra fields, transfer the known Unicode values from the extra
296     * field.
297     */
298    static void setNameAndCommentFromExtraFields(final ZipArchiveEntry ze, final byte[] originalNameBytes, final byte[] commentBytes) {
299        final ZipExtraField nameCandidate = ze.getExtraField(UnicodePathExtraField.UPATH_ID);
300        final UnicodePathExtraField name = nameCandidate instanceof UnicodePathExtraField ? (UnicodePathExtraField) nameCandidate : null;
301        final String newName = getUnicodeStringIfOriginalMatches(name, originalNameBytes);
302        if (newName != null) {
303            ze.setName(newName);
304            ze.setNameSource(ZipArchiveEntry.NameSource.UNICODE_EXTRA_FIELD);
305        }
306
307        if (commentBytes != null && commentBytes.length > 0) {
308            final ZipExtraField cmtCandidate = ze.getExtraField(UnicodeCommentExtraField.UCOM_ID);
309            final UnicodeCommentExtraField cmt = cmtCandidate instanceof UnicodeCommentExtraField ? (UnicodeCommentExtraField) cmtCandidate : null;
310            final String newComment = getUnicodeStringIfOriginalMatches(cmt, commentBytes);
311            if (newComment != null) {
312                ze.setComment(newComment);
313                ze.setCommentSource(ZipArchiveEntry.CommentSource.UNICODE_EXTRA_FIELD);
314            }
315        }
316    }
317
318    /**
319     * Converts a signed byte into an unsigned integer representation (for example, -1 becomes 255).
320     *
321     * @param b byte to convert to int
322     * @return int representation of the provided byte
323     * @since 1.5
324     * @deprecated Use {@link Byte#toUnsignedInt(byte)}.
325     */
326    @Deprecated
327    public static int signedByteToUnsignedInt(final byte b) {
328        return Byte.toUnsignedInt(b);
329    }
330
331    /**
332     * Tests if this library supports the encryption used by the given entry.
333     *
334     * @return true if the entry isn't encrypted at all
335     */
336    private static boolean supportsEncryptionOf(final ZipArchiveEntry entry) {
337        return !entry.getGeneralPurposeBit().usesEncryption();
338    }
339
340    /**
341     * Tests if this library supports the compression method used by the given entry.
342     *
343     * @return true if the compression method is supported
344     */
345    private static boolean supportsMethodOf(final ZipArchiveEntry entry) {
346        final int method = entry.getMethod();
347        return method == ZipEntry.STORED || method == ZipMethod.UNSHRINKING.getCode()
348                || method == ZipMethod.IMPLODING.getCode() || method == ZipEntry.DEFLATED
349                || method == ZipMethod.ENHANCED_DEFLATED.getCode() || method == ZipMethod.BZIP2.getCode()
350                || ZipMethod.isZstd(method)
351                || method == ZipMethod.XZ.getCode();
352    }
353
354    /**
355     * Converts a Date object to a DOS date/time field.
356     *
357     * @param time the {@code Date} to convert
358     * @return the date as a {@code ZipLong}
359     */
360    public static ZipLong toDosTime(final Date time) {
361        return new ZipLong(toDosTime(time.getTime()));
362    }
363
364    /**
365     * Converts a Date object to a DOS date/time field.
366     *
367     * <p>
368     * Stolen from InfoZip's {@code fileio.c}
369     * </p>
370     *
371     * @param t number of milliseconds since the epoch
372     * @return the date as a byte array
373     */
374    public static byte[] toDosTime(final long t) {
375        final byte[] result = new byte[4];
376        toDosTime(t, result, 0);
377        return result;
378    }
379
380    /**
381     * Converts a Date object to a DOS date/time field.
382     *
383     * <p>
384     * Stolen from InfoZip's {@code fileio.c}
385     * </p>
386     *
387     * @param t      number of milliseconds since the epoch
388     * @param buf    the output buffer
389     * @param offset The offset within the output buffer of the first byte to be written. must be non-negative and no larger than {@code buf.length-4}
390     */
391    public static void toDosTime(final long t, final byte[] buf, final int offset) {
392        ZipLong.putLong(javaToDosTime(t), buf, offset);
393    }
394
395    /**
396     * Converts a BigInteger to a long, and throws a NumberFormatException if the BigInteger is too big.
397     *
398     * @param big BigInteger to convert.
399     * @return {@code BigInteger} converted to a {@code long}.
400     */
401    static long toLong(final BigInteger big) {
402        try {
403            return big.longValueExact();
404        } catch (final ArithmeticException e) {
405            throw new NumberFormatException("The BigInteger cannot fit inside a 64 bit java long: [" + big + "]");
406        }
407    }
408
409    /**
410     * Converts an unsigned integer to a signed byte (for example, 255 becomes -1).
411     *
412     * @param i integer to convert to byte
413     * @return byte representation of the provided int
414     * @throws IllegalArgumentException if the provided integer is not inside the range [0,255].
415     * @since 1.5
416     */
417    public static byte unsignedIntToSignedByte(final int i) {
418        if (i > 255 || i < 0) {
419            throw new IllegalArgumentException("Can only convert non-negative integers between [0,255] to byte: [" + i + "]");
420        }
421        if (i < 128) {
422            return (byte) i;
423        }
424        return (byte) (i - 256);
425    }
426}