View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.archivers.zip;
20  
21  import java.io.IOException;
22  import java.math.BigInteger;
23  import java.time.Instant;
24  import java.time.LocalDateTime;
25  import java.time.ZoneId;
26  import java.util.Arrays;
27  import java.util.Calendar;
28  import java.util.Date;
29  import java.util.zip.CRC32;
30  import java.util.zip.ZipEntry;
31  import java.util.zip.ZipException;
32  
33  /**
34   * Utility class for handling DOS and Java time conversions.
35   *
36   * @Immutable
37   */
38  public abstract class ZipUtil {
39  
40      /**
41       * DOS time constant for representing timestamps before 1980. Smallest date/time ZIP can handle.
42       * <p>
43       * MS-DOS records file dates and times as packed 16-bit values. An MS-DOS date has the following format.
44       * </p>
45       * <p>
46       * Bits Contents
47       * </p>
48       * <ul>
49       * <li>0-4: Day of the month (1-31).</li>
50       * <li>5-8: Month (1 = January, 2 = February, and so on).</li>
51       * <li>9-15: Year offset from 1980 (add 1980 to get the actual year).</li>
52       * </ul>
53       *
54       * An MS-DOS time has the following format.
55       * <p>
56       * Bits Contents
57       * </p>
58       * <ul>
59       * <li>0-4: Second divided by 2.</li>
60       * <li>5-10: Minute (0-59).</li>
61       * <li>11-15: Hour (0-23 on a 24-hour clock).</li>
62       * </ul>
63       *
64       * This constant expresses the minimum DOS date of January 1st 1980 at 00:00:00 or, bit-by-bit:
65       * <ul>
66       * <li>Year: 0000000</li>
67       * <li>Month: 0001</li>
68       * <li>Day: 00001</li>
69       * <li>Hour: 00000</li>
70       * <li>Minute: 000000</li>
71       * <li>Seconds: 00000</li>
72       * </ul>
73       *
74       * <p>
75       * This was copied from {@link ZipEntry}.
76       * </p>
77       *
78       * @since 1.23
79       */
80      private static final long DOSTIME_BEFORE_1980 = 1 << 21 | 1 << 16; // 0x210000
81  
82      /** Java time representation of the smallest date/time ZIP can handle */
83      private static final long DOSTIME_BEFORE_1980_AS_JAVA_TIME = dosToJavaTime(DOSTIME_BEFORE_1980);
84  
85      /**
86       * Approximately 128 years, in milliseconds (ignoring leap years, etc.).
87       *
88       * <p>
89       * This establish an approximate high-bound value for DOS times in milliseconds since epoch, used to enable an efficient but sufficient bounds check to
90       * avoid generating extended last modified time entries.
91       * </p>
92       * <p>
93       * Calculating the exact number is locale dependent, would require loading TimeZone data eagerly, and would make little practical sense. Since DOS times
94       * theoretically go to 2107 - with compatibility not guaranteed after 2099 - setting this to a time that is before but near 2099 should be sufficient.
95       * </p>
96       *
97       * <p>
98       * This was copied from {@link ZipEntry}.
99       * </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 }