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