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, "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 readHeader(MappedByteBuffer mappedTzFile)112 private Header readHeader(MappedByteBuffer mappedTzFile) throws IOException { 113 // 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 } 119 120 byte tzh_version = mappedTzFile.get(); 121 122 // Skip the uninteresting part of the header. 123 mappedTzFile.position(mappedTzFile.position() + 15); 124 int tzh_ttisgmtcnt = mappedTzFile.getInt(); 125 int tzh_ttisstdcnt = mappedTzFile.getInt(); 126 int tzh_leapcnt = mappedTzFile.getInt(); 127 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 148 int tzh_charcnt = mappedTzFile.getInt(); 149 150 return new Header( 151 tzh_version, tzh_ttisgmtcnt, tzh_ttisstdcnt, tzh_leapcnt, tzh_timecnt, tzh_typecnt, 152 tzh_charcnt); 153 } 154 read32BitTransitions(MappedByteBuffer mappedTzFile, Header header)155 private List<Transition> read32BitTransitions(MappedByteBuffer mappedTzFile, Header header) 156 throws IOException { 157 158 // Read the data. 159 int[] transitionTimes = new int[header.tzh_timecnt]; 160 fillIntArray(mappedTzFile, transitionTimes); 161 162 byte[] typeIndexes = new byte[header.tzh_timecnt]; 163 mappedTzFile.get(typeIndexes); 164 165 // 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 createTransitions(Header header, long[] transitionTimes, byte[] typeIndexes)174 private List<Transition> createTransitions(Header header, 175 long[] transitionTimes, byte[] typeIndexes) throws IOException { 176 List<Transition> transitions = new ArrayList<>(); 177 for (int i = 0; i < header.tzh_timecnt; ++i) { 178 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; 185 if (typeIndex >= header.tzh_typecnt) { 186 throw new IOException(inputFile + " type at " + i + " is not < " 187 + header.tzh_typecnt + ", is " + typeIndex); 188 } 189 190 Transition transition = new Transition(transitionTimes[i], typeIndex); 191 transitions.add(transition); 192 } 193 return transitions; 194 } 195 read64BitTransitions(MappedByteBuffer mappedTzFile, Header header)196 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 writeTransitions(List<Transition> transitions, List<Type> types, Writer fileWriter)207 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 readTypes(MappedByteBuffer mappedTzFile, Header header)228 private List<Type> readTypes(MappedByteBuffer mappedTzFile, Header header) throws IOException { 229 List<Type> types = new ArrayList<>(); 230 for (int i = 0; i < header.tzh_typecnt; ++i) { 231 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 } 242 return types; 243 } 244 skipUninteresting32BitData(MappedByteBuffer mappedTzFile, Header header)245 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 } 250 251 skipUninteresting64BitData(MappedByteBuffer mappedTzFile, Header header)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 */ mergeTodInfo( MappedByteBuffer mappedTzFile, Header header, List<Type> types)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 writeTypes(List<Type> types, Writer fileWriter)282 private void writeTypes(List<Type> types, Writer fileWriter) throws IOException { 283 List<Object[]> rows = new ArrayList<>(); 284 for (Type type : types) { 285 Object[] row = new Object[] { 286 type.gmtOffsetSeconds, 287 type.isDst, 288 nullToEmptyString(type.ttisgmt), 289 nullToEmptyString(type.ttisstd), 290 formatDurationSeconds(type.gmtOffsetSeconds), 291 formatIsDst(type.isDst), 292 }; 293 rows.add(row); 294 } 295 296 writeCsvRow(fileWriter, "Types"); 297 writeTuplesCsv( 298 fileWriter, rows, "gmtOffset (seconds)", "isDst", "ttisgmt", "ttisstd", 299 "[gmtOffset ISO]", "[DST?]"); 300 } 301 nullToEmptyString(Object object)302 private static Object nullToEmptyString(Object object) { 303 return object == null ? "" : object; 304 } 305 fillIntArray(MappedByteBuffer mappedByteBuffer, int[] toFill)306 private static void fillIntArray(MappedByteBuffer mappedByteBuffer, int[] toFill) { 307 for (int i = 0; i < toFill.length; i++) { 308 toFill[i] = mappedByteBuffer.getInt(); 309 } 310 } 311 fillLongArray(MappedByteBuffer mappedByteBuffer, long[] toFill)312 private static void fillLongArray(MappedByteBuffer mappedByteBuffer, long[] toFill) { 313 for (int i = 0; i < toFill.length; i++) { 314 toFill[i] = mappedByteBuffer.getLong(); 315 } 316 } 317 formatTimeSeconds(long timeInSeconds)318 private static String formatTimeSeconds(long timeInSeconds) { 319 long timeInMillis = timeInSeconds * 1000L; 320 return Instant.ofEpochMilli(timeInMillis).toString(); 321 } 322 formatDurationSeconds(int duration)323 private static String formatDurationSeconds(int duration) { 324 return Duration.ofSeconds(duration).toString(); 325 } 326 formatIsDst(byte isDst)327 private String formatIsDst(byte isDst) { 328 return isDst == 0 ? "STD" : "DST"; 329 } 330 writeCsvRow(Writer writer, Object... values)331 private static void writeCsvRow(Writer writer, Object... values) throws IOException { 332 writer.append(Joiner.on(',').join(values)); 333 writer.append('\n'); 334 } 335 writeTuplesCsv(Writer writer, List<Object[]> lines, String... headings)336 private static void writeTuplesCsv(Writer writer, List<Object[]> lines, String... headings) 337 throws IOException { 338 339 writeCsvRow(writer, (Object[]) headings); 340 for (Object[] line : lines) { 341 writeCsvRow(writer, line); 342 } 343 } 344 345 private static class Header { 346 347 /** The version. Known values are 0 (ASCII NUL), 50 (ASCII '2'), 51 (ASCII '3'). */ 348 final byte tzh_version; 349 final int tzh_timecnt; 350 final int tzh_typecnt; 351 final int tzh_charcnt; 352 final int tzh_leapcnt; 353 final int tzh_ttisstdcnt; 354 final int tzh_ttisgmtcnt; 355 Header(byte tzh_version, int tzh_ttisgmtcnt, int tzh_ttisstdcnt, int tzh_leapcnt, int tzh_timecnt, int tzh_typecnt, int tzh_charcnt)356 Header(byte tzh_version, int tzh_ttisgmtcnt, int tzh_ttisstdcnt, int tzh_leapcnt, 357 int tzh_timecnt, int tzh_typecnt, int tzh_charcnt) { 358 this.tzh_version = tzh_version; 359 this.tzh_timecnt = tzh_timecnt; 360 this.tzh_typecnt = tzh_typecnt; 361 this.tzh_charcnt = tzh_charcnt; 362 this.tzh_leapcnt = tzh_leapcnt; 363 this.tzh_ttisstdcnt = tzh_ttisstdcnt; 364 this.tzh_ttisgmtcnt = tzh_ttisgmtcnt; 365 } 366 } 367 368 private static class Type { 369 370 final int gmtOffsetSeconds; 371 final byte isDst; 372 final Byte ttisstd; 373 final Byte ttisgmt; 374 Type(int gmtOffsetSeconds, byte isDst)375 Type(int gmtOffsetSeconds, byte isDst) { 376 this(gmtOffsetSeconds, isDst, null, null); 377 } 378 Type(int gmtOffsetSeconds, byte isDst, Byte ttisstd, Byte ttisgmt)379 Type(int gmtOffsetSeconds, byte isDst, Byte ttisstd, Byte ttisgmt) { 380 this.gmtOffsetSeconds = gmtOffsetSeconds; 381 this.isDst = isDst; 382 this.ttisstd = ttisstd; 383 this.ttisgmt = ttisgmt; 384 } 385 } 386 387 private static class Transition { 388 389 final long transitionTimeSeconds; 390 final int typeIndex; 391 Transition(long transitionTimeSeconds, int typeIndex)392 Transition(long transitionTimeSeconds, int typeIndex) { 393 this.transitionTimeSeconds = transitionTimeSeconds; 394 this.typeIndex = typeIndex; 395 } 396 } 397 } 398