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, "32-bit Header");
88             writeCsvRow(fileWriter, "tzh_version", "tzh_timecnt", "tzh_typecnt", "tzh_charcnt",
89                     "tzh_leapcnt", "tzh_ttisstdcnt", "tzh_ttisgmtcnt");
90             writeCsvRow(fileWriter, header32Bit.tzh_version, header32Bit.tzh_timecnt,
91                     header32Bit.tzh_typecnt, header32Bit.tzh_charcnt, header32Bit.tzh_leapcnt,
92                     header32Bit.tzh_ttisstdcnt, header32Bit.tzh_ttisgmtcnt);
93             writeCsvRow(fileWriter);
94 
95             writeCsvRow(fileWriter, "Format version: " + (char) header32Bit.tzh_version);
96             writeCsvRow(fileWriter);
97 
98             writeCsvRow(fileWriter, "32-bit data");
99             writeCsvRow(fileWriter);
100             writeTypes(types32Bit, fileWriter);
101             writeCsvRow(fileWriter);
102             writeTransitions(transitions32Bit, types32Bit, fileWriter);
103             writeCsvRow(fileWriter);
104 
105             if (header32Bit.tzh_version >= '2') {
106                 Header header64Bit = readHeader(mappedTzFile);
107                 List<Transition> transitions64Bit = read64BitTransitions(mappedTzFile, header64Bit);
108                 List<Type> types64Bit = readTypes(mappedTzFile, header64Bit);
109                 skipUninteresting64BitData(mappedTzFile, header64Bit);
110                 types64Bit = mergeTodInfo(mappedTzFile, header64Bit, types64Bit);
111 
112                 writeCsvRow(fileWriter, "64-bit Header");
113                 writeCsvRow(fileWriter, "tzh_version", "tzh_timecnt", "tzh_typecnt", "tzh_charcnt",
114                         "tzh_leapcnt", "tzh_ttisstdcnt", "tzh_ttisgmtcnt");
115                 writeCsvRow(fileWriter, header64Bit.tzh_version, header64Bit.tzh_timecnt,
116                         header64Bit.tzh_typecnt, header64Bit.tzh_charcnt, header64Bit.tzh_leapcnt,
117                         header64Bit.tzh_ttisstdcnt, header64Bit.tzh_ttisgmtcnt);
118                 writeCsvRow(fileWriter);
119 
120                 writeCsvRow(fileWriter, "Format version: " + (char) header64Bit.tzh_version);
121                 writeCsvRow(fileWriter);
122 
123                 writeCsvRow(fileWriter, "64-bit data");
124                 writeCsvRow(fileWriter);
125                 writeTypes(types64Bit, fileWriter);
126                 writeCsvRow(fileWriter);
127                 writeTransitions(transitions64Bit, types64Bit, fileWriter);
128             }
129         }
130     }
131 
readHeader(MappedByteBuffer mappedTzFile)132     private Header readHeader(MappedByteBuffer mappedTzFile) throws IOException {
133         // Variable names beginning tzh_ correspond to those in "tzfile.h".
134         // Check tzh_magic.
135         int tzh_magic = mappedTzFile.getInt();
136         if (tzh_magic != 0x545a6966) { // "TZif"
137             throw new IOException("File=" + inputFile + " has an invalid header=" + tzh_magic);
138         }
139 
140         byte tzh_version = mappedTzFile.get();
141 
142         // Skip the uninteresting part of the header.
143         mappedTzFile.position(mappedTzFile.position() + 15);
144         int tzh_ttisgmtcnt = mappedTzFile.getInt();
145         int tzh_ttisstdcnt = mappedTzFile.getInt();
146         int tzh_leapcnt = mappedTzFile.getInt();
147 
148         // Read the sizes of the arrays we're about to read.
149         int tzh_timecnt = mappedTzFile.getInt();
150         // Arbitrary ceiling to prevent allocating memory for corrupt data.
151         // 2 per year with 2^32 seconds would give ~272 transitions.
152         final int MAX_TRANSITIONS = 2000;
153         if (tzh_timecnt < 0 || tzh_timecnt > MAX_TRANSITIONS) {
154             throw new IOException(
155                     "File=" + inputFile + " has an invalid number of transitions=" + tzh_timecnt);
156         }
157 
158         int tzh_typecnt = mappedTzFile.getInt();
159         final int MAX_TYPES = 256;
160         if (tzh_typecnt < 1) {
161             throw new IOException("ZoneInfo requires at least one type to be provided for each"
162                     + " timezone but could not find one for '" + inputFile + "'");
163         } else if (tzh_typecnt > MAX_TYPES) {
164             throw new IOException(
165                     "File=" + inputFile + " has too many types=" + tzh_typecnt);
166         }
167 
168         int tzh_charcnt = mappedTzFile.getInt();
169 
170         return new Header(
171                 tzh_version, tzh_ttisgmtcnt, tzh_ttisstdcnt, tzh_leapcnt, tzh_timecnt, tzh_typecnt,
172                 tzh_charcnt);
173     }
174 
read32BitTransitions(MappedByteBuffer mappedTzFile, Header header)175     private List<Transition> read32BitTransitions(MappedByteBuffer mappedTzFile, Header header)
176             throws IOException {
177 
178         // Read the data.
179         int[] transitionTimes = new int[header.tzh_timecnt];
180         fillIntArray(mappedTzFile, transitionTimes);
181 
182         byte[] typeIndexes = new byte[header.tzh_timecnt];
183         mappedTzFile.get(typeIndexes);
184 
185         // Convert int times to longs
186         long[] transitionTimesLong = new long[header.tzh_timecnt];
187         for (int i = 0; i < header.tzh_timecnt; ++i) {
188             transitionTimesLong[i] = transitionTimes[i];
189         }
190 
191         return createTransitions(header, transitionTimesLong, typeIndexes);
192     }
193 
createTransitions(Header header, long[] transitionTimes, byte[] typeIndexes)194     private List<Transition> createTransitions(Header header,
195             long[] transitionTimes, byte[] typeIndexes) throws IOException {
196         List<Transition> transitions = new ArrayList<>();
197         for (int i = 0; i < header.tzh_timecnt; ++i) {
198             if (i > 0 && transitionTimes[i] <= transitionTimes[i - 1]) {
199                 throw new IOException(
200                         inputFile + " transition at " + i + " is not sorted correctly, is "
201                                 + transitionTimes[i] + ", previous is " + transitionTimes[i - 1]);
202             }
203 
204             int typeIndex = typeIndexes[i] & 0xff;
205             if (typeIndex >= header.tzh_typecnt) {
206                 throw new IOException(inputFile + " type at " + i + " is not < "
207                         + header.tzh_typecnt + ", is " + typeIndex);
208             }
209 
210             Transition transition = new Transition(transitionTimes[i], typeIndex);
211             transitions.add(transition);
212         }
213         return transitions;
214     }
215 
read64BitTransitions(MappedByteBuffer mappedTzFile, Header header)216     private List<Transition> read64BitTransitions(MappedByteBuffer mappedTzFile, Header header)
217             throws IOException {
218         long[] transitionTimes = new long[header.tzh_timecnt];
219         fillLongArray(mappedTzFile, transitionTimes);
220 
221         byte[] typeIndexes = new byte[header.tzh_timecnt];
222         mappedTzFile.get(typeIndexes);
223 
224         return createTransitions(header, transitionTimes, typeIndexes);
225     }
226 
writeTransitions(List<Transition> transitions, List<Type> types, Writer fileWriter)227     private void writeTransitions(List<Transition> transitions, List<Type> types, Writer fileWriter)
228             throws IOException {
229 
230         List<Object[]> rows = new ArrayList<>();
231         for (Transition transition : transitions) {
232             Type type = types.get(transition.typeIndex);
233             Object[] row = new Object[] {
234                     transition.transitionTimeSeconds,
235                     transition.typeIndex,
236                     formatTimeSeconds(transition.transitionTimeSeconds),
237                     formatDurationSeconds(type.gmtOffsetSeconds),
238                     formatIsDst(type.isDst),
239             };
240             rows.add(row);
241         }
242 
243         writeCsvRow(fileWriter, "Transitions");
244         writeTuplesCsv(fileWriter, rows, "transition", "type", "[UTC time]", "[Type offset]",
245                 "[Type isDST]");
246     }
247 
readTypes(MappedByteBuffer mappedTzFile, Header header)248     private List<Type> readTypes(MappedByteBuffer mappedTzFile, Header header) throws IOException {
249         List<Type> types = new ArrayList<>();
250         for (int i = 0; i < header.tzh_typecnt; ++i) {
251             int gmtOffsetSeconds = mappedTzFile.getInt();
252             byte isDst = mappedTzFile.get();
253             if (isDst != 0 && isDst != 1) {
254                 throw new IOException(inputFile + " dst at " + i + " is not 0 or 1, is " + isDst);
255             }
256 
257             // We skip the abbreviation index.
258             mappedTzFile.get();
259 
260             types.add(new Type(gmtOffsetSeconds, isDst));
261         }
262         return types;
263     }
264 
skipUninteresting32BitData(MappedByteBuffer mappedTzFile, Header header)265     private static void skipUninteresting32BitData(MappedByteBuffer mappedTzFile, Header header) {
266         mappedTzFile.get(new byte[header.tzh_charcnt]);
267         int leapInfoSize = 4 + 4;
268         mappedTzFile.get(new byte[header.tzh_leapcnt * leapInfoSize]);
269     }
270 
271 
skipUninteresting64BitData(MappedByteBuffer mappedTzFile, Header header)272     private void skipUninteresting64BitData(MappedByteBuffer mappedTzFile, Header header) {
273         mappedTzFile.get(new byte[header.tzh_charcnt]);
274         int leapInfoSize = 8 + 4;
275         mappedTzFile.get(new byte[header.tzh_leapcnt * leapInfoSize]);
276     }
277 
278     /**
279      * Populate ttisstd and ttisgmt information by copying {@code types} and populating those fields
280      * in the copies.
281      */
mergeTodInfo( MappedByteBuffer mappedTzFile, Header header, List<Type> types)282     private static List<Type> mergeTodInfo(
283             MappedByteBuffer mappedTzFile, Header header, List<Type> types) {
284 
285         byte[] ttisstds = new byte[header.tzh_ttisstdcnt];
286         mappedTzFile.get(ttisstds);
287         byte[] ttisgmts = new byte[header.tzh_ttisgmtcnt];
288         mappedTzFile.get(ttisgmts);
289 
290         List<Type> outputTypes = new ArrayList<>();
291         for (int i = 0; i < types.size(); i++) {
292             Type inputType = types.get(i);
293             Byte ttisstd = ttisstds.length == 0 ? null : ttisstds[i];
294             Byte ttisgmt = ttisgmts.length == 0 ? null : ttisgmts[i];
295             Type outputType =
296                     new Type(inputType.gmtOffsetSeconds, inputType.isDst, ttisstd, ttisgmt);
297             outputTypes.add(outputType);
298         }
299         return outputTypes;
300     }
301 
writeTypes(List<Type> types, Writer fileWriter)302     private void writeTypes(List<Type> types, Writer fileWriter) throws IOException {
303         List<Object[]> rows = new ArrayList<>();
304         for (Type type : types) {
305             Object[] row = new Object[] {
306                     type.gmtOffsetSeconds,
307                     type.isDst,
308                     nullToEmptyString(type.ttisgmt),
309                     nullToEmptyString(type.ttisstd),
310                     formatDurationSeconds(type.gmtOffsetSeconds),
311                     formatIsDst(type.isDst),
312             };
313             rows.add(row);
314         }
315 
316         writeCsvRow(fileWriter, "Types");
317         writeTuplesCsv(
318                 fileWriter, rows, "gmtOffset (seconds)", "isDst", "ttisgmt", "ttisstd",
319                 "[gmtOffset ISO]", "[DST?]");
320     }
321 
nullToEmptyString(Object object)322     private static Object nullToEmptyString(Object object) {
323         return object == null ? "" : object;
324     }
325 
fillIntArray(MappedByteBuffer mappedByteBuffer, int[] toFill)326     private static void fillIntArray(MappedByteBuffer mappedByteBuffer, int[] toFill) {
327         for (int i = 0; i < toFill.length; i++) {
328             toFill[i] = mappedByteBuffer.getInt();
329         }
330     }
331 
fillLongArray(MappedByteBuffer mappedByteBuffer, long[] toFill)332     private static void fillLongArray(MappedByteBuffer mappedByteBuffer, long[] toFill) {
333         for (int i = 0; i < toFill.length; i++) {
334             toFill[i] = mappedByteBuffer.getLong();
335         }
336     }
337 
formatTimeSeconds(long timeInSeconds)338     private static String formatTimeSeconds(long timeInSeconds) {
339         long timeInMillis = timeInSeconds * 1000L;
340         return Instant.ofEpochMilli(timeInMillis).toString();
341     }
342 
formatDurationSeconds(int duration)343     private static String formatDurationSeconds(int duration) {
344         return Duration.ofSeconds(duration).toString();
345     }
346 
formatIsDst(byte isDst)347     private String formatIsDst(byte isDst) {
348         return isDst == 0 ? "STD" : "DST";
349     }
350 
writeCsvRow(Writer writer, Object... values)351     private static void writeCsvRow(Writer writer, Object... values) throws IOException {
352         writer.append(Joiner.on(',').join(values));
353         writer.append('\n');
354     }
355 
writeTuplesCsv(Writer writer, List<Object[]> lines, String... headings)356     private static void writeTuplesCsv(Writer writer, List<Object[]> lines, String... headings)
357             throws IOException {
358 
359         writeCsvRow(writer, (Object[]) headings);
360         for (Object[] line : lines) {
361             writeCsvRow(writer, line);
362         }
363     }
364 
365     private static class Header {
366 
367         /** The version. Known values are 0 (ASCII NUL), 50 (ASCII '2'), 51 (ASCII '3'). */
368         final byte tzh_version;
369         final int tzh_timecnt;
370         final int tzh_typecnt;
371         final int tzh_charcnt;
372         final int tzh_leapcnt;
373         final int tzh_ttisstdcnt;
374         final int tzh_ttisgmtcnt;
375 
Header(byte tzh_version, int tzh_ttisgmtcnt, int tzh_ttisstdcnt, int tzh_leapcnt, int tzh_timecnt, int tzh_typecnt, int tzh_charcnt)376         Header(byte tzh_version, int tzh_ttisgmtcnt, int tzh_ttisstdcnt, int tzh_leapcnt,
377                 int tzh_timecnt, int tzh_typecnt, int tzh_charcnt) {
378             this.tzh_version = tzh_version;
379             this.tzh_timecnt = tzh_timecnt;
380             this.tzh_typecnt = tzh_typecnt;
381             this.tzh_charcnt = tzh_charcnt;
382             this.tzh_leapcnt = tzh_leapcnt;
383             this.tzh_ttisstdcnt = tzh_ttisstdcnt;
384             this.tzh_ttisgmtcnt = tzh_ttisgmtcnt;
385         }
386     }
387 
388     private static class Type {
389 
390         final int gmtOffsetSeconds;
391         final byte isDst;
392         final Byte ttisstd;
393         final Byte ttisgmt;
394 
Type(int gmtOffsetSeconds, byte isDst)395         Type(int gmtOffsetSeconds, byte isDst) {
396             this(gmtOffsetSeconds, isDst, null, null);
397         }
398 
Type(int gmtOffsetSeconds, byte isDst, Byte ttisstd, Byte ttisgmt)399         Type(int gmtOffsetSeconds, byte isDst, Byte ttisstd, Byte ttisgmt) {
400             this.gmtOffsetSeconds = gmtOffsetSeconds;
401             this.isDst = isDst;
402             this.ttisstd = ttisstd;
403             this.ttisgmt = ttisgmt;
404         }
405     }
406 
407     private static class Transition {
408 
409         final long transitionTimeSeconds;
410         final int typeIndex;
411 
Transition(long transitionTimeSeconds, int typeIndex)412         Transition(long transitionTimeSeconds, int typeIndex) {
413             this.transitionTimeSeconds = transitionTimeSeconds;
414             this.typeIndex = typeIndex;
415         }
416     }
417 }
418