/* * 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 java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.StandardOpenOption; import java.util.Arrays; /** * Reverses the ZoneCompactor process to extract information and zic output files from Android's * tzdata file. This enables easier debugging / inspection of Android's tzdata file with standard * tools like zdump or Android tools like TzFileDumper. * *

This class contains a copy of logic found in Android's ZoneInfoDb. */ public class ZoneSplitter { public static void main(String[] args) throws Exception { if (args.length != 2) { System.err.println("usage: java ZoneSplitter "); System.exit(0); } new ZoneSplitter(args[0], args[1]).execute(); } private final File tzData; private final File outputDir; private ZoneSplitter(String tzData, String outputDir) { this.tzData = new File(tzData); this.outputDir = new File(outputDir); } private void execute() throws IOException { if (!(tzData.exists() && tzData.isFile() && tzData.canRead())) { throw new IOException(tzData + " not found or is not readable"); } if (!(outputDir.exists() && outputDir.isDirectory())) { throw new IOException(outputDir + " not found or is not a directory"); } MappedByteBuffer mappedFile = createMappedByteBuffer(tzData); // byte[12] tzdata_version -- "tzdata2012f\0" // int index_offset // int data_offset // int final_offset writeVersionFile(mappedFile, outputDir); final int fileSize = (int) tzData.length(); int index_offset = mappedFile.getInt(); validateOffset(index_offset, fileSize); int data_offset = mappedFile.getInt(); validateOffset(data_offset, fileSize); int final_offset = mappedFile.getInt(); if (index_offset >= data_offset || data_offset >= final_offset || final_offset > fileSize) { throw new IOException("Invalid offset: index_offset=" + index_offset + ", data_offset=" + data_offset + ", final_offset=" + final_offset + ", fileSize=" + fileSize); } File zicFilesDir = new File(outputDir, "zones"); zicFilesDir.mkdir(); extractZicFiles(mappedFile, index_offset, data_offset, zicFilesDir); if (final_offset != fileSize) { // This isn't an error, but it's worth noting: it suggests the file may be in a newer // format than the current branch. System.out.println( "final_offset (" + final_offset + ") != fileSize (" + fileSize + ")"); } } static MappedByteBuffer createMappedByteBuffer(File tzData) throws IOException { MappedByteBuffer mappedFile; RandomAccessFile file = new RandomAccessFile(tzData, "r"); try (FileChannel fileChannel = file.getChannel()) { mappedFile = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length()); } mappedFile.load(); return mappedFile; } private static void validateOffset(int offset, int size) throws IOException { if (offset < 0 || offset >= size) { throw new IOException("Invalid offset=" + offset + ", size=" + size); } } private static void writeVersionFile(MappedByteBuffer mappedFile, File targetDir) throws IOException { byte[] tzdata_version = new byte[12]; mappedFile.get(tzdata_version); String magic = new String(tzdata_version, 0, 6, StandardCharsets.US_ASCII); if (!magic.startsWith("tzdata") || tzdata_version[11] != 0) { throw new IOException("bad tzdata magic: " + Arrays.toString(tzdata_version)); } writeStringUtf8ToFile(new File(targetDir, "version"), new String(tzdata_version, 6, 5, StandardCharsets.US_ASCII)); } private static void extractZicFiles(MappedByteBuffer mappedFile, int indexOffset, int dataOffset, File outputDir) throws IOException { mappedFile.position(indexOffset); // The index of the tzdata file is made up of entries for each time zone ID which describe // the location of the associated zic data in the data section of the file. The index // section has no padding so we can determine the number of entries from the size. // // Each index entry consists of: // byte[MAXNAME] idBytes - the id string, \0 terminated. e.g. "America/New_York\0" // int32 byteOffset - the offset of the start of the zic data relative to the start of // the tzdata data section // int32 length - the length of the of the zic data // int32 unused - no longer used final int MAXNAME = 40; final int SIZEOF_OFFSET = 4; final int SIZEOF_INDEX_ENTRY = MAXNAME + 3 * SIZEOF_OFFSET; int indexSize = (dataOffset - indexOffset); if (indexSize % SIZEOF_INDEX_ENTRY != 0) { throw new IOException("Index size is not divisible by " + SIZEOF_INDEX_ENTRY + ", indexSize=" + indexSize); } byte[] idBytes = new byte[MAXNAME]; int entryCount = indexSize / SIZEOF_INDEX_ENTRY; int[] byteOffsets = new int[entryCount]; int[] lengths = new int[entryCount]; String[] ids = new String[entryCount]; for (int i = 0; i < entryCount; i++) { // Read the fixed length timezone ID. mappedFile.get(idBytes, 0, idBytes.length); // Read the offset into the file where the data for ID can be found. byteOffsets[i] = mappedFile.getInt(); byteOffsets[i] += dataOffset; lengths[i] = mappedFile.getInt(); if (lengths[i] < 44) { throw new IOException("length in index file < sizeof(tzhead)"); } mappedFile.getInt(); // Skip the unused 4 bytes that used to be the raw offset. // Calculate the true length of the ID. int len = 0; while (len < idBytes.length && idBytes[len] != 0) { len++; } if (len == 0) { throw new IOException("Invalid ID at index=" + i); } ids[i] = new String(idBytes, 0, len, StandardCharsets.US_ASCII); if (i > 0) { if (ids[i].compareTo(ids[i - 1]) <= 0) { throw new IOException( "Index not sorted or contains multiple entries with the same ID" + ", index=" + i + ", ids[i]=" + ids[i] + ", ids[i - 1]=" + ids[i - 1]); } } } for (int i = 0; i < entryCount; i++) { String id = ids[i]; int byteOffset = byteOffsets[i]; int length = lengths[i]; File subFile = new File(outputDir, id.replace('/', '_')); mappedFile.position(byteOffset); byte[] bytes = new byte[length]; mappedFile.get(bytes, 0, length); writeBytesToFile(subFile, bytes); } } private static void writeStringUtf8ToFile(File file, String string) throws IOException { writeBytesToFile(file, string.getBytes(StandardCharsets.UTF_8)); } private static void writeBytesToFile(File file, byte[] bytes) throws IOException { System.out.println("Writing: " + file); Files.write(file.toPath(), bytes, StandardOpenOption.CREATE); } }