1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 import com.google.common.base.Joiner;
18 
19 import java.io.File;
20 import java.io.FileOutputStream;
21 import java.io.IOException;
22 import java.io.OutputStreamWriter;
23 import java.io.Writer;
24 import java.nio.MappedByteBuffer;
25 import java.nio.charset.StandardCharsets;
26 import java.time.Duration;
27 import java.time.Instant;
28 import java.util.ArrayList;
29 import java.util.List;
30 
31 /**
32  * Dumps out the contents of a tzfile in a CSV form.
33  *
34  * <p>This class contains a near copy of logic found in Android's ZoneInfo class.
35  */
36 public class TzFileDumper {
37 
main(String[] args)38     public static void main(String[] args) throws Exception {
39         if (args.length != 2) {
40             System.err.println("usage: java TzFileDumper <tzfile|dir> <output file|output dir>");
41             System.exit(0);
42         }
43 
44         File input = new File(args[0]);
45         File output = new File(args[1]);
46         if (input.isDirectory()) {
47             if (!output.isDirectory()) {
48                 System.err.println("If first args is a directory, second arg must be a directory");
49                 System.exit(1);
50             }
51 
52             for (File inputFile : input.listFiles()) {
53                 if (inputFile.isFile()) {
54                     File outputFile = new File(output, inputFile.getName() + ".csv");
55                     try {
56                         new TzFileDumper(inputFile, outputFile).execute();
57                     } catch (IOException e) {
58                         System.err.println("Error processing:" + inputFile);
59                     }
60                 }
61             }
62         } else {
63             new TzFileDumper(input, output).execute();
64         }
65     }
66 
67     private final File inputFile;
68     private final File outputFile;
69 
TzFileDumper(File inputFile, File outputFile)70     private TzFileDumper(File inputFile, File outputFile) {
71         this.inputFile = inputFile;
72         this.outputFile = outputFile;
73     }
74 
execute()75     private void execute() throws IOException {
76         System.out.println("Dumping " + inputFile + " to " + outputFile);
77         MappedByteBuffer mappedTzFile = ZoneSplitter.createMappedByteBuffer(inputFile);
78 
79         try (Writer fileWriter = new OutputStreamWriter(
80                 new FileOutputStream(outputFile), StandardCharsets.UTF_8)) {
81             Header header32Bit = readHeader(mappedTzFile);
82             List<Transition> transitions32Bit = read32BitTransitions(mappedTzFile, header32Bit);
83             List<Type> types32Bit = readTypes(mappedTzFile, header32Bit);
84             skipUninteresting32BitData(mappedTzFile, header32Bit);
85             types32Bit = mergeTodInfo(mappedTzFile, header32Bit, types32Bit);
86 
87             writeCsvRow(fileWriter, "File format version: " + (char) header32Bit.tzh_version);
88             writeCsvRow(fileWriter);
89             writeCsvRow(fileWriter, "32-bit data");
90             writeCsvRow(fileWriter);
91             writeTypes(types32Bit, fileWriter);
92             writeCsvRow(fileWriter);
93             writeTransitions(transitions32Bit, types32Bit, fileWriter);
94             writeCsvRow(fileWriter);
95 
96             if (header32Bit.tzh_version >= '2') {
97                 Header header64Bit = readHeader(mappedTzFile);
98                 List<Transition> transitions64Bit = read64BitTransitions(mappedTzFile, header64Bit);
99                 List<Type> types64Bit = readTypes(mappedTzFile, header64Bit);
100                 skipUninteresting64BitData(mappedTzFile, header64Bit);
101                 types64Bit = mergeTodInfo(mappedTzFile, header64Bit, types64Bit);
102 
103                 writeCsvRow(fileWriter, "64-bit data");
104                 writeCsvRow(fileWriter);
105                 writeTypes(types64Bit, fileWriter);
106                 writeCsvRow(fileWriter);
107                 writeTransitions(transitions64Bit, types64Bit, fileWriter);
108             }
109         }
110     }
111 
readHeader(MappedByteBuffer mappedTzFile)112     private Header readHeader(MappedByteBuffer mappedTzFile) throws IOException {
113         // Variable names beginning tzh_ correspond to those in "tzfile.h".
114         // Check tzh_magic.
115         int tzh_magic = mappedTzFile.getInt();
116         if (tzh_magic != 0x545a6966) { // "TZif"
117             throw new IOException("File=" + inputFile + " has an invalid header=" + tzh_magic);
118         }
119 
120         byte tzh_version = mappedTzFile.get();
121 
122         // Skip the uninteresting part of the header.
123         mappedTzFile.position(mappedTzFile.position() + 15);
124         int tzh_ttisgmtcnt = mappedTzFile.getInt();
125         int tzh_ttisstdcnt = mappedTzFile.getInt();
126         int tzh_leapcnt = mappedTzFile.getInt();
127 
128         // Read the sizes of the arrays we're about to read.
129         int tzh_timecnt = mappedTzFile.getInt();
130         // Arbitrary ceiling to prevent allocating memory for corrupt data.
131         // 2 per year with 2^32 seconds would give ~272 transitions.
132         final int MAX_TRANSITIONS = 2000;
133         if (tzh_timecnt < 0 || tzh_timecnt > MAX_TRANSITIONS) {
134             throw new IOException(
135                     "File=" + inputFile + " has an invalid number of transitions=" + tzh_timecnt);
136         }
137 
138         int tzh_typecnt = mappedTzFile.getInt();
139         final int MAX_TYPES = 256;
140         if (tzh_typecnt < 1) {
141             throw new IOException("ZoneInfo requires at least one type to be provided for each"
142                     + " timezone but could not find one for '" + inputFile + "'");
143         } else if (tzh_typecnt > MAX_TYPES) {
144             throw new IOException(
145                     "File=" + inputFile + " has too many types=" + tzh_typecnt);
146         }
147 
148         int tzh_charcnt = mappedTzFile.getInt();
149 
150         return new Header(
151                 tzh_version, tzh_ttisgmtcnt, tzh_ttisstdcnt, tzh_leapcnt, tzh_timecnt, tzh_typecnt,
152                 tzh_charcnt);
153     }
154 
read32BitTransitions(MappedByteBuffer mappedTzFile, Header header)155     private List<Transition> read32BitTransitions(MappedByteBuffer mappedTzFile, Header header)
156             throws IOException {
157 
158         // Read the data.
159         int[] transitionTimes = new int[header.tzh_timecnt];
160         fillIntArray(mappedTzFile, transitionTimes);
161 
162         byte[] typeIndexes = new byte[header.tzh_timecnt];
163         mappedTzFile.get(typeIndexes);
164 
165         // Convert int times to longs
166         long[] transitionTimesLong = new long[header.tzh_timecnt];
167         for (int i = 0; i < header.tzh_timecnt; ++i) {
168             transitionTimesLong[i] = transitionTimes[i];
169         }
170 
171         return createTransitions(header, transitionTimesLong, typeIndexes);
172     }
173 
createTransitions(Header header, long[] transitionTimes, byte[] typeIndexes)174     private List<Transition> createTransitions(Header header,
175             long[] transitionTimes, byte[] typeIndexes) throws IOException {
176         List<Transition> transitions = new ArrayList<>();
177         for (int i = 0; i < header.tzh_timecnt; ++i) {
178             if (i > 0 && transitionTimes[i] <= transitionTimes[i - 1]) {
179                 throw new IOException(
180                         inputFile + " transition at " + i + " is not sorted correctly, is "
181                                 + transitionTimes[i] + ", previous is " + transitionTimes[i - 1]);
182             }
183 
184             int typeIndex = typeIndexes[i] & 0xff;
185             if (typeIndex >= header.tzh_typecnt) {
186                 throw new IOException(inputFile + " type at " + i + " is not < "
187                         + header.tzh_typecnt + ", is " + typeIndex);
188             }
189 
190             Transition transition = new Transition(transitionTimes[i], typeIndex);
191             transitions.add(transition);
192         }
193         return transitions;
194     }
195 
read64BitTransitions(MappedByteBuffer mappedTzFile, Header header)196     private List<Transition> read64BitTransitions(MappedByteBuffer mappedTzFile, Header header)
197             throws IOException {
198         long[] transitionTimes = new long[header.tzh_timecnt];
199         fillLongArray(mappedTzFile, transitionTimes);
200 
201         byte[] typeIndexes = new byte[header.tzh_timecnt];
202         mappedTzFile.get(typeIndexes);
203 
204         return createTransitions(header, transitionTimes, typeIndexes);
205     }
206 
writeTransitions(List<Transition> transitions, List<Type> types, Writer fileWriter)207     private void writeTransitions(List<Transition> transitions, List<Type> types, Writer fileWriter)
208             throws IOException {
209 
210         List<Object[]> rows = new ArrayList<>();
211         for (Transition transition : transitions) {
212             Type type = types.get(transition.typeIndex);
213             Object[] row = new Object[] {
214                     transition.transitionTimeSeconds,
215                     transition.typeIndex,
216                     formatTimeSeconds(transition.transitionTimeSeconds),
217                     formatDurationSeconds(type.gmtOffsetSeconds),
218                     formatIsDst(type.isDst),
219             };
220             rows.add(row);
221         }
222 
223         writeCsvRow(fileWriter, "Transitions");
224         writeTuplesCsv(fileWriter, rows, "transition", "type", "[UTC time]", "[Type offset]",
225                 "[Type isDST]");
226     }
227 
readTypes(MappedByteBuffer mappedTzFile, Header header)228     private List<Type> readTypes(MappedByteBuffer mappedTzFile, Header header) throws IOException {
229         List<Type> types = new ArrayList<>();
230         for (int i = 0; i < header.tzh_typecnt; ++i) {
231             int gmtOffsetSeconds = mappedTzFile.getInt();
232             byte isDst = mappedTzFile.get();
233             if (isDst != 0 && isDst != 1) {
234                 throw new IOException(inputFile + " dst at " + i + " is not 0 or 1, is " + isDst);
235             }
236 
237             // We skip the abbreviation index.
238             mappedTzFile.get();
239 
240             types.add(new Type(gmtOffsetSeconds, isDst));
241         }
242         return types;
243     }
244 
skipUninteresting32BitData(MappedByteBuffer mappedTzFile, Header header)245     private static void skipUninteresting32BitData(MappedByteBuffer mappedTzFile, Header header) {
246         mappedTzFile.get(new byte[header.tzh_charcnt]);
247         int leapInfoSize = 4 + 4;
248         mappedTzFile.get(new byte[header.tzh_leapcnt * leapInfoSize]);
249     }
250 
251 
skipUninteresting64BitData(MappedByteBuffer mappedTzFile, Header header)252     private void skipUninteresting64BitData(MappedByteBuffer mappedTzFile, Header header) {
253         mappedTzFile.get(new byte[header.tzh_charcnt]);
254         int leapInfoSize = 8 + 4;
255         mappedTzFile.get(new byte[header.tzh_leapcnt * leapInfoSize]);
256     }
257 
258     /**
259      * Populate ttisstd and ttisgmt information by copying {@code types} and populating those fields
260      * in the copies.
261      */
mergeTodInfo( MappedByteBuffer mappedTzFile, Header header, List<Type> types)262     private static List<Type> mergeTodInfo(
263             MappedByteBuffer mappedTzFile, Header header, List<Type> types) {
264 
265         byte[] ttisstds = new byte[header.tzh_ttisstdcnt];
266         mappedTzFile.get(ttisstds);
267         byte[] ttisgmts = new byte[header.tzh_ttisgmtcnt];
268         mappedTzFile.get(ttisgmts);
269 
270         List<Type> outputTypes = new ArrayList<>();
271         for (int i = 0; i < types.size(); i++) {
272             Type inputType = types.get(i);
273             Byte ttisstd = ttisstds.length == 0 ? null : ttisstds[i];
274             Byte ttisgmt = ttisgmts.length == 0 ? null : ttisgmts[i];
275             Type outputType =
276                     new Type(inputType.gmtOffsetSeconds, inputType.isDst, ttisstd, ttisgmt);
277             outputTypes.add(outputType);
278         }
279         return outputTypes;
280     }
281 
writeTypes(List<Type> types, Writer fileWriter)282     private void writeTypes(List<Type> types, Writer fileWriter) throws IOException {
283         List<Object[]> rows = new ArrayList<>();
284         for (Type type : types) {
285             Object[] row = new Object[] {
286                     type.gmtOffsetSeconds,
287                     type.isDst,
288                     nullToEmptyString(type.ttisgmt),
289                     nullToEmptyString(type.ttisstd),
290                     formatDurationSeconds(type.gmtOffsetSeconds),
291                     formatIsDst(type.isDst),
292             };
293             rows.add(row);
294         }
295 
296         writeCsvRow(fileWriter, "Types");
297         writeTuplesCsv(
298                 fileWriter, rows, "gmtOffset (seconds)", "isDst", "ttisgmt", "ttisstd",
299                 "[gmtOffset ISO]", "[DST?]");
300     }
301 
nullToEmptyString(Object object)302     private static Object nullToEmptyString(Object object) {
303         return object == null ? "" : object;
304     }
305 
fillIntArray(MappedByteBuffer mappedByteBuffer, int[] toFill)306     private static void fillIntArray(MappedByteBuffer mappedByteBuffer, int[] toFill) {
307         for (int i = 0; i < toFill.length; i++) {
308             toFill[i] = mappedByteBuffer.getInt();
309         }
310     }
311 
fillLongArray(MappedByteBuffer mappedByteBuffer, long[] toFill)312     private static void fillLongArray(MappedByteBuffer mappedByteBuffer, long[] toFill) {
313         for (int i = 0; i < toFill.length; i++) {
314             toFill[i] = mappedByteBuffer.getLong();
315         }
316     }
317 
formatTimeSeconds(long timeInSeconds)318     private static String formatTimeSeconds(long timeInSeconds) {
319         long timeInMillis = timeInSeconds * 1000L;
320         return Instant.ofEpochMilli(timeInMillis).toString();
321     }
322 
formatDurationSeconds(int duration)323     private static String formatDurationSeconds(int duration) {
324         return Duration.ofSeconds(duration).toString();
325     }
326 
formatIsDst(byte isDst)327     private String formatIsDst(byte isDst) {
328         return isDst == 0 ? "STD" : "DST";
329     }
330 
writeCsvRow(Writer writer, Object... values)331     private static void writeCsvRow(Writer writer, Object... values) throws IOException {
332         writer.append(Joiner.on(',').join(values));
333         writer.append('\n');
334     }
335 
writeTuplesCsv(Writer writer, List<Object[]> lines, String... headings)336     private static void writeTuplesCsv(Writer writer, List<Object[]> lines, String... headings)
337             throws IOException {
338 
339         writeCsvRow(writer, (Object[]) headings);
340         for (Object[] line : lines) {
341             writeCsvRow(writer, line);
342         }
343     }
344 
345     private static class Header {
346 
347         /** The version. Known values are 0 (ASCII NUL), 50 (ASCII '2'), 51 (ASCII '3'). */
348         final byte tzh_version;
349         final int tzh_timecnt;
350         final int tzh_typecnt;
351         final int tzh_charcnt;
352         final int tzh_leapcnt;
353         final int tzh_ttisstdcnt;
354         final int tzh_ttisgmtcnt;
355 
Header(byte tzh_version, int tzh_ttisgmtcnt, int tzh_ttisstdcnt, int tzh_leapcnt, int tzh_timecnt, int tzh_typecnt, int tzh_charcnt)356         Header(byte tzh_version, int tzh_ttisgmtcnt, int tzh_ttisstdcnt, int tzh_leapcnt,
357                 int tzh_timecnt, int tzh_typecnt, int tzh_charcnt) {
358             this.tzh_version = tzh_version;
359             this.tzh_timecnt = tzh_timecnt;
360             this.tzh_typecnt = tzh_typecnt;
361             this.tzh_charcnt = tzh_charcnt;
362             this.tzh_leapcnt = tzh_leapcnt;
363             this.tzh_ttisstdcnt = tzh_ttisstdcnt;
364             this.tzh_ttisgmtcnt = tzh_ttisgmtcnt;
365         }
366     }
367 
368     private static class Type {
369 
370         final int gmtOffsetSeconds;
371         final byte isDst;
372         final Byte ttisstd;
373         final Byte ttisgmt;
374 
Type(int gmtOffsetSeconds, byte isDst)375         Type(int gmtOffsetSeconds, byte isDst) {
376             this(gmtOffsetSeconds, isDst, null, null);
377         }
378 
Type(int gmtOffsetSeconds, byte isDst, Byte ttisstd, Byte ttisgmt)379         Type(int gmtOffsetSeconds, byte isDst, Byte ttisstd, Byte ttisgmt) {
380             this.gmtOffsetSeconds = gmtOffsetSeconds;
381             this.isDst = isDst;
382             this.ttisstd = ttisstd;
383             this.ttisgmt = ttisgmt;
384         }
385     }
386 
387     private static class Transition {
388 
389         final long transitionTimeSeconds;
390         final int typeIndex;
391 
Transition(long transitionTimeSeconds, int typeIndex)392         Transition(long transitionTimeSeconds, int typeIndex) {
393             this.transitionTimeSeconds = transitionTimeSeconds;
394             this.typeIndex = typeIndex;
395         }
396     }
397 }
398