    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}