blob: d06a66c759f5dc7b1636efff4a1166f0ea21c56a [file] [log] [blame]
Neil Fuller0480f462019-04-09 19:23:35 +01001/*
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
17import com.google.common.base.Joiner;
18
19import java.io.File;
20import java.io.FileOutputStream;
21import java.io.IOException;
22import java.io.OutputStreamWriter;
23import java.io.Writer;
24import java.nio.MappedByteBuffer;
25import java.nio.charset.StandardCharsets;
26import java.time.Duration;
27import java.time.Instant;
28import java.util.ArrayList;
29import java.util.List;
30
31/**
Neil Fullerbfcc9852019-04-18 16:35:47 +010032 * Dumps out the contents of a tzfile in a CSV form.
Neil Fuller0480f462019-04-09 19:23:35 +010033 *
Neil Fullerbfcc9852019-04-18 16:35:47 +010034 * <p>This class contains a near copy of logic found in Android's ZoneInfo class.
Neil Fuller0480f462019-04-09 19:23:35 +010035 */
36public class TzFileDumper {
37
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 {
Neil Fuller0480f462019-04-09 19:23:35 +010063 new TzFileDumper(input, output).execute();
64 }
65 }
66
67 private final File inputFile;
68 private final File outputFile;
69
70 private TzFileDumper(File inputFile, File outputFile) {
71 this.inputFile = inputFile;
72 this.outputFile = outputFile;
73 }
74
75 private void execute() throws IOException {
76 System.out.println("Dumping " + inputFile + " to " + outputFile);
77 MappedByteBuffer mappedTzFile = ZoneSplitter.createMappedByteBuffer(inputFile);
78
Neil Fullerbfcc9852019-04-18 16:35:47 +010079 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
112 private Header readHeader(MappedByteBuffer mappedTzFile) throws IOException {
Neil Fuller0480f462019-04-09 19:23:35 +0100113 // 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 }
Neil Fullerbfcc9852019-04-18 16:35:47 +0100119
120 byte tzh_version = mappedTzFile.get();
121
Neil Fuller0480f462019-04-09 19:23:35 +0100122 // Skip the uninteresting part of the header.
Neil Fullerbfcc9852019-04-18 16:35:47 +0100123 mappedTzFile.position(mappedTzFile.position() + 15);
124 int tzh_ttisgmtcnt = mappedTzFile.getInt();
125 int tzh_ttisstdcnt = mappedTzFile.getInt();
126 int tzh_leapcnt = mappedTzFile.getInt();
Neil Fuller0480f462019-04-09 19:23:35 +0100127
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
Neil Fullerbfcc9852019-04-18 16:35:47 +0100148 int tzh_charcnt = mappedTzFile.getInt();
Neil Fuller0480f462019-04-09 19:23:35 +0100149
Neil Fullerbfcc9852019-04-18 16:35:47 +0100150 return new Header(
151 tzh_version, tzh_ttisgmtcnt, tzh_ttisstdcnt, tzh_leapcnt, tzh_timecnt, tzh_typecnt,
152 tzh_charcnt);
Neil Fuller0480f462019-04-09 19:23:35 +0100153 }
154
Neil Fullerbfcc9852019-04-18 16:35:47 +0100155 private List<Transition> read32BitTransitions(MappedByteBuffer mappedTzFile, Header header)
156 throws IOException {
Neil Fuller0480f462019-04-09 19:23:35 +0100157
158 // Read the data.
Neil Fullerbfcc9852019-04-18 16:35:47 +0100159 int[] transitionTimes = new int[header.tzh_timecnt];
Neil Fuller0480f462019-04-09 19:23:35 +0100160 fillIntArray(mappedTzFile, transitionTimes);
Neil Fullerbfcc9852019-04-18 16:35:47 +0100161
162 byte[] typeIndexes = new byte[header.tzh_timecnt];
Neil Fuller0480f462019-04-09 19:23:35 +0100163 mappedTzFile.get(typeIndexes);
164
Neil Fullerbfcc9852019-04-18 16:35:47 +0100165 // 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
174 private List<Transition> createTransitions(Header header,
175 long[] transitionTimes, byte[] typeIndexes) throws IOException {
Neil Fuller0480f462019-04-09 19:23:35 +0100176 List<Transition> transitions = new ArrayList<>();
Neil Fullerbfcc9852019-04-18 16:35:47 +0100177 for (int i = 0; i < header.tzh_timecnt; ++i) {
Neil Fuller0480f462019-04-09 19:23:35 +0100178 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;
Neil Fullerbfcc9852019-04-18 16:35:47 +0100185 if (typeIndex >= header.tzh_typecnt) {
186 throw new IOException(inputFile + " type at " + i + " is not < "
187 + header.tzh_typecnt + ", is " + typeIndex);
Neil Fuller0480f462019-04-09 19:23:35 +0100188 }
189
190 Transition transition = new Transition(transitionTimes[i], typeIndex);
191 transitions.add(transition);
192 }
Neil Fuller0480f462019-04-09 19:23:35 +0100193 return transitions;
194 }
195
Neil Fullerbfcc9852019-04-18 16:35:47 +0100196 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
Neil Fuller0480f462019-04-09 19:23:35 +0100207 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
Neil Fullerbfcc9852019-04-18 16:35:47 +0100228 private List<Type> readTypes(MappedByteBuffer mappedTzFile, Header header) throws IOException {
Neil Fuller0480f462019-04-09 19:23:35 +0100229 List<Type> types = new ArrayList<>();
Neil Fullerbfcc9852019-04-18 16:35:47 +0100230 for (int i = 0; i < header.tzh_typecnt; ++i) {
Neil Fuller0480f462019-04-09 19:23:35 +0100231 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 }
Neil Fuller0480f462019-04-09 19:23:35 +0100242 return types;
243 }
244
Neil Fullerbfcc9852019-04-18 16:35:47 +0100245 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 }
Neil Fuller0480f462019-04-09 19:23:35 +0100250
Neil Fullerbfcc9852019-04-18 16:35:47 +0100251
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 */
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
282 private void writeTypes(List<Type> types, Writer fileWriter) throws IOException {
Neil Fuller0480f462019-04-09 19:23:35 +0100283 List<Object[]> rows = new ArrayList<>();
284 for (Type type : types) {
285 Object[] row = new Object[] {
286 type.gmtOffsetSeconds,
287 type.isDst,
Neil Fullerbfcc9852019-04-18 16:35:47 +0100288 type.ttisgmt,
289 type.ttisstd,
Neil Fuller0480f462019-04-09 19:23:35 +0100290 formatDurationSeconds(type.gmtOffsetSeconds),
291 formatIsDst(type.isDst),
292 };
293 rows.add(row);
294 }
295
296 writeCsvRow(fileWriter, "Types");
297 writeTuplesCsv(
Neil Fullerbfcc9852019-04-18 16:35:47 +0100298 fileWriter, rows, "gmtOffset (seconds)", "isDst", "ttisgmt", "ttisstd",
299 "[gmtOffset ISO]", "[DST?]");
Neil Fuller0480f462019-04-09 19:23:35 +0100300 }
301
302 private static void fillIntArray(MappedByteBuffer mappedByteBuffer, int[] toFill) {
303 for (int i = 0; i < toFill.length; i++) {
304 toFill[i] = mappedByteBuffer.getInt();
305 }
306 }
307
Neil Fullerbfcc9852019-04-18 16:35:47 +0100308 private static void fillLongArray(MappedByteBuffer mappedByteBuffer, long[] toFill) {
309 for (int i = 0; i < toFill.length; i++) {
310 toFill[i] = mappedByteBuffer.getLong();
311 }
312 }
313
Neil Fuller0480f462019-04-09 19:23:35 +0100314 private static String formatTimeSeconds(long timeInSeconds) {
315 long timeInMillis = timeInSeconds * 1000L;
316 return Instant.ofEpochMilli(timeInMillis).toString();
317 }
318
319 private static String formatDurationSeconds(int duration) {
320 return Duration.ofSeconds(duration).toString();
321 }
322
323 private String formatIsDst(byte isDst) {
324 return isDst == 0 ? "STD" : "DST";
325 }
326
327 private static void writeCsvRow(Writer writer, Object... values) throws IOException {
328 writer.append(Joiner.on(',').join(values));
329 writer.append('\n');
330 }
331
332 private static void writeTuplesCsv(Writer writer, List<Object[]> lines, String... headings)
333 throws IOException {
334
335 writeCsvRow(writer, (Object[]) headings);
336 for (Object[] line : lines) {
337 writeCsvRow(writer, line);
338 }
339 }
340
Neil Fullerbfcc9852019-04-18 16:35:47 +0100341 private static class Header {
342
343 /** The version. Known values are 0 (ASCII NUL), 50 (ASCII '2'), 51 (ASCII '3'). */
344 final byte tzh_version;
345 final int tzh_timecnt;
346 final int tzh_typecnt;
347 final int tzh_charcnt;
348 final int tzh_leapcnt;
349 final int tzh_ttisstdcnt;
350 final int tzh_ttisgmtcnt;
351
352 Header(byte tzh_version, int tzh_ttisgmtcnt, int tzh_ttisstdcnt, int tzh_leapcnt,
353 int tzh_timecnt, int tzh_typecnt, int tzh_charcnt) {
354 this.tzh_version = tzh_version;
355 this.tzh_timecnt = tzh_timecnt;
356 this.tzh_typecnt = tzh_typecnt;
357 this.tzh_charcnt = tzh_charcnt;
358 this.tzh_leapcnt = tzh_leapcnt;
359 this.tzh_ttisstdcnt = tzh_ttisstdcnt;
360 this.tzh_ttisgmtcnt = tzh_ttisgmtcnt;
361 }
362 }
363
Neil Fuller0480f462019-04-09 19:23:35 +0100364 private static class Type {
365
366 final int gmtOffsetSeconds;
367 final byte isDst;
Neil Fullerbfcc9852019-04-18 16:35:47 +0100368 final Byte ttisstd;
369 final Byte ttisgmt;
Neil Fuller0480f462019-04-09 19:23:35 +0100370
371 Type(int gmtOffsetSeconds, byte isDst) {
Neil Fullerbfcc9852019-04-18 16:35:47 +0100372 this(gmtOffsetSeconds, isDst, null, null);
373 }
374
375 Type(int gmtOffsetSeconds, byte isDst, Byte ttisstd, Byte ttisgmt) {
Neil Fuller0480f462019-04-09 19:23:35 +0100376 this.gmtOffsetSeconds = gmtOffsetSeconds;
377 this.isDst = isDst;
Neil Fullerbfcc9852019-04-18 16:35:47 +0100378 this.ttisstd = ttisstd;
379 this.ttisgmt = ttisgmt;
Neil Fuller0480f462019-04-09 19:23:35 +0100380 }
381 }
382
383 private static class Transition {
384
385 final long transitionTimeSeconds;
386 final int typeIndex;
387
388 Transition(long transitionTimeSeconds, int typeIndex) {
389 this.transitionTimeSeconds = transitionTimeSeconds;
390 this.typeIndex = typeIndex;
391 }
392 }
393}