/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import com.google.common.base.Joiner; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.MappedByteBuffer; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; import java.util.List; /** * Dumps out the contents of a tzfile in a CSV form. * *

This class contains a near copy of logic found in Android's ZoneInfo class. */ public class TzFileDumper { public static void main(String[] args) throws Exception { if (args.length != 2) { System.err.println("usage: java TzFileDumper "); System.exit(0); } File input = new File(args[0]); File output = new File(args[1]); if (input.isDirectory()) { if (!output.isDirectory()) { System.err.println("If first args is a directory, second arg must be a directory"); System.exit(1); } for (File inputFile : input.listFiles()) { if (inputFile.isFile()) { File outputFile = new File(output, inputFile.getName() + ".csv"); try { new TzFileDumper(inputFile, outputFile).execute(); } catch (IOException e) { System.err.println("Error processing:" + inputFile); } } } } else { new TzFileDumper(input, output).execute(); } } private final File inputFile; private final File outputFile; private TzFileDumper(File inputFile, File outputFile) { this.inputFile = inputFile; this.outputFile = outputFile; } private void execute() throws IOException { System.out.println("Dumping " + inputFile + " to " + outputFile); MappedByteBuffer mappedTzFile = ZoneSplitter.createMappedByteBuffer(inputFile); try (Writer fileWriter = new OutputStreamWriter( new FileOutputStream(outputFile), StandardCharsets.UTF_8)) { Header header32Bit = readHeader(mappedTzFile); List transitions32Bit = read32BitTransitions(mappedTzFile, header32Bit); List types32Bit = readTypes(mappedTzFile, header32Bit); skipUninteresting32BitData(mappedTzFile, header32Bit); types32Bit = mergeTodInfo(mappedTzFile, header32Bit, types32Bit); writeCsvRow(fileWriter, "32-bit Header"); writeCsvRow(fileWriter, "tzh_version", "tzh_timecnt", "tzh_typecnt", "tzh_charcnt", "tzh_leapcnt", "tzh_ttisstdcnt", "tzh_ttisgmtcnt"); writeCsvRow(fileWriter, header32Bit.tzh_version, header32Bit.tzh_timecnt, header32Bit.tzh_typecnt, header32Bit.tzh_charcnt, header32Bit.tzh_leapcnt, header32Bit.tzh_ttisstdcnt, header32Bit.tzh_ttisgmtcnt); writeCsvRow(fileWriter); writeCsvRow(fileWriter, "Format version: " + (char) header32Bit.tzh_version); writeCsvRow(fileWriter); writeCsvRow(fileWriter, "32-bit data"); writeCsvRow(fileWriter); writeTypes(types32Bit, fileWriter); writeCsvRow(fileWriter); writeTransitions(transitions32Bit, types32Bit, fileWriter); writeCsvRow(fileWriter); if (header32Bit.tzh_version >= '2') { Header header64Bit = readHeader(mappedTzFile); List transitions64Bit = read64BitTransitions(mappedTzFile, header64Bit); List types64Bit = readTypes(mappedTzFile, header64Bit); skipUninteresting64BitData(mappedTzFile, header64Bit); types64Bit = mergeTodInfo(mappedTzFile, header64Bit, types64Bit); writeCsvRow(fileWriter, "64-bit Header"); writeCsvRow(fileWriter, "tzh_version", "tzh_timecnt", "tzh_typecnt", "tzh_charcnt", "tzh_leapcnt", "tzh_ttisstdcnt", "tzh_ttisgmtcnt"); writeCsvRow(fileWriter, header64Bit.tzh_version, header64Bit.tzh_timecnt, header64Bit.tzh_typecnt, header64Bit.tzh_charcnt, header64Bit.tzh_leapcnt, header64Bit.tzh_ttisstdcnt, header64Bit.tzh_ttisgmtcnt); writeCsvRow(fileWriter); writeCsvRow(fileWriter, "Format version: " + (char) header64Bit.tzh_version); writeCsvRow(fileWriter); writeCsvRow(fileWriter, "64-bit data"); writeCsvRow(fileWriter); writeTypes(types64Bit, fileWriter); writeCsvRow(fileWriter); writeTransitions(transitions64Bit, types64Bit, fileWriter); } } } private Header readHeader(MappedByteBuffer mappedTzFile) throws IOException { // Variable names beginning tzh_ correspond to those in "tzfile.h". // Check tzh_magic. int tzh_magic = mappedTzFile.getInt(); if (tzh_magic != 0x545a6966) { // "TZif" throw new IOException("File=" + inputFile + " has an invalid header=" + tzh_magic); } byte tzh_version = mappedTzFile.get(); // Skip the uninteresting part of the header. mappedTzFile.position(mappedTzFile.position() + 15); int tzh_ttisgmtcnt = mappedTzFile.getInt(); int tzh_ttisstdcnt = mappedTzFile.getInt(); int tzh_leapcnt = mappedTzFile.getInt(); // Read the sizes of the arrays we're about to read. int tzh_timecnt = mappedTzFile.getInt(); // Arbitrary ceiling to prevent allocating memory for corrupt data. // 2 per year with 2^32 seconds would give ~272 transitions. final int MAX_TRANSITIONS = 2000; if (tzh_timecnt < 0 || tzh_timecnt > MAX_TRANSITIONS) { throw new IOException( "File=" + inputFile + " has an invalid number of transitions=" + tzh_timecnt); } int tzh_typecnt = mappedTzFile.getInt(); final int MAX_TYPES = 256; if (tzh_typecnt < 1) { throw new IOException("ZoneInfo requires at least one type to be provided for each" + " timezone but could not find one for '" + inputFile + "'"); } else if (tzh_typecnt > MAX_TYPES) { throw new IOException( "File=" + inputFile + " has too many types=" + tzh_typecnt); } int tzh_charcnt = mappedTzFile.getInt(); return new Header( tzh_version, tzh_ttisgmtcnt, tzh_ttisstdcnt, tzh_leapcnt, tzh_timecnt, tzh_typecnt, tzh_charcnt); } private List read32BitTransitions(MappedByteBuffer mappedTzFile, Header header) throws IOException { // Read the data. int[] transitionTimes = new int[header.tzh_timecnt]; fillIntArray(mappedTzFile, transitionTimes); byte[] typeIndexes = new byte[header.tzh_timecnt]; mappedTzFile.get(typeIndexes); // Convert int times to longs long[] transitionTimesLong = new long[header.tzh_timecnt]; for (int i = 0; i < header.tzh_timecnt; ++i) { transitionTimesLong[i] = transitionTimes[i]; } return createTransitions(header, transitionTimesLong, typeIndexes); } private List createTransitions(Header header, long[] transitionTimes, byte[] typeIndexes) throws IOException { List transitions = new ArrayList<>(); for (int i = 0; i < header.tzh_timecnt; ++i) { if (i > 0 && transitionTimes[i] <= transitionTimes[i - 1]) { throw new IOException( inputFile + " transition at " + i + " is not sorted correctly, is " + transitionTimes[i] + ", previous is " + transitionTimes[i - 1]); } int typeIndex = typeIndexes[i] & 0xff; if (typeIndex >= header.tzh_typecnt) { throw new IOException(inputFile + " type at " + i + " is not < " + header.tzh_typecnt + ", is " + typeIndex); } Transition transition = new Transition(transitionTimes[i], typeIndex); transitions.add(transition); } return transitions; } private List read64BitTransitions(MappedByteBuffer mappedTzFile, Header header) throws IOException { long[] transitionTimes = new long[header.tzh_timecnt]; fillLongArray(mappedTzFile, transitionTimes); byte[] typeIndexes = new byte[header.tzh_timecnt]; mappedTzFile.get(typeIndexes); return createTransitions(header, transitionTimes, typeIndexes); } private void writeTransitions(List transitions, List types, Writer fileWriter) throws IOException { List rows = new ArrayList<>(); for (Transition transition : transitions) { Type type = types.get(transition.typeIndex); Object[] row = new Object[] { transition.transitionTimeSeconds, transition.typeIndex, formatTimeSeconds(transition.transitionTimeSeconds), formatDurationSeconds(type.gmtOffsetSeconds), formatIsDst(type.isDst), }; rows.add(row); } writeCsvRow(fileWriter, "Transitions"); writeTuplesCsv(fileWriter, rows, "transition", "type", "[UTC time]", "[Type offset]", "[Type isDST]"); } private List readTypes(MappedByteBuffer mappedTzFile, Header header) throws IOException { List types = new ArrayList<>(); for (int i = 0; i < header.tzh_typecnt; ++i) { int gmtOffsetSeconds = mappedTzFile.getInt(); byte isDst = mappedTzFile.get(); if (isDst != 0 && isDst != 1) { throw new IOException(inputFile + " dst at " + i + " is not 0 or 1, is " + isDst); } // We skip the abbreviation index. mappedTzFile.get(); types.add(new Type(gmtOffsetSeconds, isDst)); } return types; } private static void skipUninteresting32BitData(MappedByteBuffer mappedTzFile, Header header) { mappedTzFile.get(new byte[header.tzh_charcnt]); int leapInfoSize = 4 + 4; mappedTzFile.get(new byte[header.tzh_leapcnt * leapInfoSize]); } private void skipUninteresting64BitData(MappedByteBuffer mappedTzFile, Header header) { mappedTzFile.get(new byte[header.tzh_charcnt]); int leapInfoSize = 8 + 4; mappedTzFile.get(new byte[header.tzh_leapcnt * leapInfoSize]); } /** * Populate ttisstd and ttisgmt information by copying {@code types} and populating those fields * in the copies. */ private static List mergeTodInfo( MappedByteBuffer mappedTzFile, Header header, List types) { byte[] ttisstds = new byte[header.tzh_ttisstdcnt]; mappedTzFile.get(ttisstds); byte[] ttisgmts = new byte[header.tzh_ttisgmtcnt]; mappedTzFile.get(ttisgmts); List outputTypes = new ArrayList<>(); for (int i = 0; i < types.size(); i++) { Type inputType = types.get(i); Byte ttisstd = ttisstds.length == 0 ? null : ttisstds[i]; Byte ttisgmt = ttisgmts.length == 0 ? null : ttisgmts[i]; Type outputType = new Type(inputType.gmtOffsetSeconds, inputType.isDst, ttisstd, ttisgmt); outputTypes.add(outputType); } return outputTypes; } private void writeTypes(List types, Writer fileWriter) throws IOException { List rows = new ArrayList<>(); for (Type type : types) { Object[] row = new Object[] { type.gmtOffsetSeconds, type.isDst, nullToEmptyString(type.ttisgmt), nullToEmptyString(type.ttisstd), formatDurationSeconds(type.gmtOffsetSeconds), formatIsDst(type.isDst), }; rows.add(row); } writeCsvRow(fileWriter, "Types"); writeTuplesCsv( fileWriter, rows, "gmtOffset (seconds)", "isDst", "ttisgmt", "ttisstd", "[gmtOffset ISO]", "[DST?]"); } private static Object nullToEmptyString(Object object) { return object == null ? "" : object; } private static void fillIntArray(MappedByteBuffer mappedByteBuffer, int[] toFill) { for (int i = 0; i < toFill.length; i++) { toFill[i] = mappedByteBuffer.getInt(); } } private static void fillLongArray(MappedByteBuffer mappedByteBuffer, long[] toFill) { for (int i = 0; i < toFill.length; i++) { toFill[i] = mappedByteBuffer.getLong(); } } private static String formatTimeSeconds(long timeInSeconds) { long timeInMillis = timeInSeconds * 1000L; return Instant.ofEpochMilli(timeInMillis).toString(); } private static String formatDurationSeconds(int duration) { return Duration.ofSeconds(duration).toString(); } private String formatIsDst(byte isDst) { return isDst == 0 ? "STD" : "DST"; } private static void writeCsvRow(Writer writer, Object... values) throws IOException { writer.append(Joiner.on(',').join(values)); writer.append('\n'); } private static void writeTuplesCsv(Writer writer, List lines, String... headings) throws IOException { writeCsvRow(writer, (Object[]) headings); for (Object[] line : lines) { writeCsvRow(writer, line); } } private static class Header { /** The version. Known values are 0 (ASCII NUL), 50 (ASCII '2'), 51 (ASCII '3'). */ final byte tzh_version; final int tzh_timecnt; final int tzh_typecnt; final int tzh_charcnt; final int tzh_leapcnt; final int tzh_ttisstdcnt; final int tzh_ttisgmtcnt; Header(byte tzh_version, int tzh_ttisgmtcnt, int tzh_ttisstdcnt, int tzh_leapcnt, int tzh_timecnt, int tzh_typecnt, int tzh_charcnt) { this.tzh_version = tzh_version; this.tzh_timecnt = tzh_timecnt; this.tzh_typecnt = tzh_typecnt; this.tzh_charcnt = tzh_charcnt; this.tzh_leapcnt = tzh_leapcnt; this.tzh_ttisstdcnt = tzh_ttisstdcnt; this.tzh_ttisgmtcnt = tzh_ttisgmtcnt; } } private static class Type { final int gmtOffsetSeconds; final byte isDst; final Byte ttisstd; final Byte ttisgmt; Type(int gmtOffsetSeconds, byte isDst) { this(gmtOffsetSeconds, isDst, null, null); } Type(int gmtOffsetSeconds, byte isDst, Byte ttisstd, Byte ttisgmt) { this.gmtOffsetSeconds = gmtOffsetSeconds; this.isDst = isDst; this.ttisstd = ttisstd; this.ttisgmt = ttisgmt; } } private static class Transition { final long transitionTimeSeconds; final int typeIndex; Transition(long transitionTimeSeconds, int typeIndex) { this.transitionTimeSeconds = transitionTimeSeconds; this.typeIndex = typeIndex; } } }