You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
398 lines
15 KiB
398 lines
15 KiB
/*
|
|
* 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.
|
|
*
|
|
* <p>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 <tzfile|dir> <output file|output dir>");
|
|
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<Transition> transitions32Bit = read32BitTransitions(mappedTzFile, header32Bit);
|
|
List<Type> types32Bit = readTypes(mappedTzFile, header32Bit);
|
|
skipUninteresting32BitData(mappedTzFile, header32Bit);
|
|
types32Bit = mergeTodInfo(mappedTzFile, header32Bit, types32Bit);
|
|
|
|
writeCsvRow(fileWriter, "File 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<Transition> transitions64Bit = read64BitTransitions(mappedTzFile, header64Bit);
|
|
List<Type> types64Bit = readTypes(mappedTzFile, header64Bit);
|
|
skipUninteresting64BitData(mappedTzFile, header64Bit);
|
|
types64Bit = mergeTodInfo(mappedTzFile, header64Bit, types64Bit);
|
|
|
|
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<Transition> 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<Transition> createTransitions(Header header,
|
|
long[] transitionTimes, byte[] typeIndexes) throws IOException {
|
|
List<Transition> 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<Transition> 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<Transition> transitions, List<Type> types, Writer fileWriter)
|
|
throws IOException {
|
|
|
|
List<Object[]> 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<Type> readTypes(MappedByteBuffer mappedTzFile, Header header) throws IOException {
|
|
List<Type> 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<Type> mergeTodInfo(
|
|
MappedByteBuffer mappedTzFile, Header header, List<Type> types) {
|
|
|
|
byte[] ttisstds = new byte[header.tzh_ttisstdcnt];
|
|
mappedTzFile.get(ttisstds);
|
|
byte[] ttisgmts = new byte[header.tzh_ttisgmtcnt];
|
|
mappedTzFile.get(ttisgmts);
|
|
|
|
List<Type> 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<Type> types, Writer fileWriter) throws IOException {
|
|
List<Object[]> 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<Object[]> 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;
|
|
}
|
|
}
|
|
}
|