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 }