View Javadoc
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 }