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