1 /*
2 * Copyright (C) 2005-2015 Schlichtherle IT Services.
3 * All rights reserved. Use is subject to license terms.
4 */
5 package net.java.truevfs.comp.zip;
6
7 import java.util.Calendar;
8 import java.util.GregorianCalendar;
9 import java.util.SimpleTimeZone;
10 import java.util.TimeZone;
11 import javax.annotation.concurrent.ThreadSafe;
12
13 /**
14 * Converts Java time values to DOS date/time values and vice versa.
15 * This class has been introduced in order to enhance interoperability
16 * between different flavours of the ZIP file format specification when
17 * converting date/time from the serialized DOS format in a ZIP file to
18 * the local system time, which is represented by a UNIX-like encoding
19 * by the Java API.
20 *
21 * @author Christian Schlichtherle
22 */
23 @ThreadSafe
24 public enum DateTimeConverter {
25
26 /**
27 * This instance applies the schedule for Daylight Saving Time (DST),
28 * i.e. all time conversions will apply DST where appropriate to a
29 * particular date.
30 * <p>
31 * This behaviour provides best interoperability with:
32 * <ul>
33 * <li>Java SE: {@code jar} utility
34 * and {@code java.util.zip} package</li>
35 * <li>Info-ZIP: {@code unzip}</li>
36 * </ul>
37 */
38 JAR {
39 @Override
40 GregorianCalendar getThreadLocalCalendar() {
41 GregorianCalendar cal = calendar.get();
42 if (null == cal) {
43 cal = new GregorianCalendar();
44 calendar.set(cal);
45 }
46 return cal;
47 }
48
49 @Override
50 boolean roundUp(long jTime) {
51 return false;
52 }
53 },
54
55 /**
56 * This instance ignores the schedule for Daylight Saving Time (DST),
57 * i.e. all time conversions will use the same raw offset and current
58 * DST savings, regardless of whether DST savings should be applied to
59 * a particular date or not.
60 * <p>
61 * This behavior provides best interoperability with:
62 * <ul>
63 * <li>Windows Vista Explorer (as of June 30<sup>th</sup>, 2009)</li>
64 * <li>WinZip 12.0</li>
65 * <li>7-Zip 4.65</li>
66 * </ul>
67 */
68 ZIP {
69 @Override
70 GregorianCalendar getThreadLocalCalendar() {
71 TimeZone tz = TimeZone.getDefault();
72 tz = new SimpleTimeZone(
73 // See http://java.net/jira/browse/TRUEZIP-191 .
74 tz.getOffset(System.currentTimeMillis()),
75 tz.getID());
76 assert !tz.useDaylightTime();
77 GregorianCalendar cal = calendar.get();
78 if (null == cal) {
79 cal = new GregorianCalendar(tz);
80 calendar.set(cal);
81 } else {
82 // See http://java.net/jira/browse/TRUEZIP-281 .
83 cal.setTimeZone(tz);
84 }
85 assert cal.isLenient();
86 return cal;
87 }
88
89 @Override
90 boolean roundUp(long jTime) {
91 return true;
92 }
93 };
94
95 final ThreadLocal<GregorianCalendar> calendar = new ThreadLocal<>();
96
97 /**
98 * Smallest supported DOS date/time value in a ZIP file,
99 * which is January 1<sup>st</sup>, 1980 AD 00:00:00 local time.
100 */
101 static final long MIN_DOS_TIME = (1 << 21) | (1 << 16); // 0x210000;
102
103 /**
104 * Largest supported DOS date/time value in a ZIP file,
105 * which is December 31<sup>st</sup>, 2107 AD 23:59:58 local time.
106 */
107 static final long MAX_DOS_TIME =
108 ((long) (2107 - 1980) << 25)
109 | (12 << 21)
110 | (31 << 16)
111 | (23 << 11)
112 | (59 << 5)
113 | (58 >> 1);
114
115 /**
116 * Returns whether the given Java time should be rounded up or down to the
117 * next two second interval when converting it to a DOS date/time.
118 *
119 * @param jTime The number of milliseconds since midnight, January 1st,
120 * 1970 AD UTC (called <i>epoch</i> alias <i>Java time</i>).
121 * @return {@code true} for round-up, {@code false} for round-down.
122 */
123 abstract boolean roundUp(long jTime);
124
125 /**
126 * Returns a thread local lenient gregorian calendar for date/time
127 * conversion which has its timezone set according to the conventions of
128 * the represented archive format.
129 *
130 * @return A thread local lenient gregorian calendar.
131 */
132 abstract GregorianCalendar getThreadLocalCalendar();
133
134 /**
135 * Converts a Java time value to a DOS date/time value.
136 * <p>
137 * If the given Java time value preceeds {@link #MIN_DOS_TIME},
138 * then it's adjusted to this value.
139 * If the given Java time value exceeds {@link #MAX_DOS_TIME},
140 * then it's adjusted to this value.
141 * <p>
142 * The return value is rounded up or down to even seconds,
143 * depending on {@link #roundUp}.
144 *
145 * @param jtime The number of milliseconds since midnight, January 1st,
146 * 1970 AD UTC (called <i>the epoch</i> alias Java time).
147 * @return A DOS date/time value reflecting the local time zone and
148 * rounded down to even seconds
149 * and is in between {@link #MIN_DOS_TIME} and {@link #MAX_DOS_TIME}.
150 * @throws IllegalArgumentException If {@code jTime} is negative.
151 * @see #toJavaTime(long)
152 */
153 final long toDosTime(final long jtime) {
154 if (0 > jtime)
155 throw new IllegalArgumentException("Negative Java time: " + jtime);
156 final GregorianCalendar cal = getThreadLocalCalendar();
157 cal.setTimeInMillis(roundUp(jtime) ? jtime + 1999 : jtime);
158 long dtime = cal.get(Calendar.YEAR) - 1980;
159 if (0 > dtime) return MIN_DOS_TIME;
160 dtime = (dtime << 25)
161 | ((cal.get(Calendar.MONTH) + 1) << 21)
162 | (cal.get(Calendar.DAY_OF_MONTH) << 16)
163 | (cal.get(Calendar.HOUR_OF_DAY) << 11)
164 | (cal.get(Calendar.MINUTE) << 5)
165 | (cal.get(Calendar.SECOND) >> 1);
166 if (MAX_DOS_TIME < dtime) return MAX_DOS_TIME;
167 assert MIN_DOS_TIME <= dtime && dtime <= MAX_DOS_TIME;
168 return dtime;
169 }
170
171 /**
172 * Converts a 32 bit integer encoded DOS date/time value to a Java time
173 * value.
174 * <p>
175 * Note that not all 32 bit integers are valid DOS date/time values.
176 * If an invalid DOS date/time value is provided, it gets adjusted by
177 * overflowing the respective field value as if using a
178 * {@link java.util.Calendar#setLenient lenient calendar}.
179 * If the given DOS date/time value preceeds {@link #MIN_DOS_TIME},
180 * then it's adjusted to this value.
181 * If the given DOS date/time value exceeds {@link #MAX_DOS_TIME},
182 * then it's adjusted to this value.
183 * These features are provided in order to read bogus ZIP archive files
184 * created by third party tools.
185 * <p>
186 * Note that the returned Java time may differ from its intended value at
187 * the time of the creation of the ZIP archive file and when converting
188 * it back again, the resulting DOS date/time value will not be the same as
189 * {@code dTime}.
190 * This is because of the limited resolution of two seconds for DOS
191 * data/time values.
192 *
193 * @param dtime The DOS date/time value.
194 * @return The number of milliseconds since midnight, January 1st,
195 * 1970 AD UTC (called <i>epoch</i> alias <i>Java time</i>)
196 * and is in between {@link #MIN_DOS_TIME} and {@link #MAX_DOS_TIME}.
197 * @see #toDosTime(long)
198 */
199 final long toJavaTime(long dtime) {
200 if (MIN_DOS_TIME > dtime) dtime = MIN_DOS_TIME;
201 else if (MAX_DOS_TIME < dtime) dtime = MAX_DOS_TIME;
202 final int time = (int) dtime;
203 final GregorianCalendar cal = getThreadLocalCalendar();
204 cal.set(Calendar.ERA, GregorianCalendar.AD);
205 cal.set(Calendar.YEAR, 1980 + ((time >> 25) & 0x7f));
206 cal.set(Calendar.MONTH, ((time >> 21) & 0x0f) - 1);
207 cal.set(Calendar.DAY_OF_MONTH, (time >> 16) & 0x1f);
208 cal.set(Calendar.HOUR_OF_DAY, (time >> 11) & 0x1f);
209 cal.set(Calendar.MINUTE, (time >> 5) & 0x3f);
210 cal.set(Calendar.SECOND, (time << 1) & 0x3e);
211 // DOS date/time has only two seconds granularity.
212 // Make calendar return only total seconds in order to make this
213 // work correctly.
214 cal.set(Calendar.MILLISECOND, 0);
215 return cal.getTimeInMillis();
216 }
217 }