Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 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 java.io.File; |
| 18 | import java.io.IOException; |
| 19 | import java.io.RandomAccessFile; |
| 20 | import java.nio.MappedByteBuffer; |
| 21 | import java.nio.channels.FileChannel; |
| 22 | import java.nio.charset.StandardCharsets; |
| 23 | import java.nio.file.Files; |
| 24 | import java.nio.file.StandardOpenOption; |
| 25 | import java.util.Arrays; |
| 26 | |
| 27 | /** |
| 28 | * Reverses the ZoneCompactor process to extract information and zic output files from Android's |
| 29 | * tzdata file. This enables easier debugging / inspection of Android's tzdata file with standard |
| 30 | * tools like zdump or Android tools like TzFileDumper. |
| 31 | * |
Neil Fuller | 92f87e6 | 2020-01-28 13:47:49 +0000 | [diff] [blame] | 32 | * <p>This class contains a copy of logic found in Android's ZoneInfoDb. |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 33 | */ |
| 34 | public class ZoneSplitter { |
| 35 | |
| 36 | public static void main(String[] args) throws Exception { |
| 37 | if (args.length != 2) { |
| 38 | System.err.println("usage: java ZoneSplitter <tzdata file> <output directory>"); |
| 39 | System.exit(0); |
| 40 | } |
| 41 | new ZoneSplitter(args[0], args[1]).execute(); |
| 42 | } |
| 43 | |
| 44 | private final File tzData; |
| 45 | private final File outputDir; |
| 46 | |
| 47 | private ZoneSplitter(String tzData, String outputDir) { |
| 48 | this.tzData = new File(tzData); |
| 49 | this.outputDir = new File(outputDir); |
| 50 | } |
| 51 | |
| 52 | private void execute() throws IOException { |
| 53 | if (!(tzData.exists() && tzData.isFile() && tzData.canRead())) { |
| 54 | throw new IOException(tzData + " not found or is not readable"); |
| 55 | } |
| 56 | if (!(outputDir.exists() && outputDir.isDirectory())) { |
| 57 | throw new IOException(outputDir + " not found or is not a directory"); |
| 58 | } |
| 59 | |
| 60 | MappedByteBuffer mappedFile = createMappedByteBuffer(tzData); |
| 61 | |
| 62 | // byte[12] tzdata_version -- "tzdata2012f\0" |
| 63 | // int index_offset |
| 64 | // int data_offset |
Neil Fuller | 4da0af1 | 2020-05-19 14:56:15 +0100 | [diff] [blame] | 65 | // int final_offset |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 66 | writeVersionFile(mappedFile, outputDir); |
| 67 | |
| 68 | final int fileSize = (int) tzData.length(); |
| 69 | int index_offset = mappedFile.getInt(); |
| 70 | validateOffset(index_offset, fileSize); |
| 71 | int data_offset = mappedFile.getInt(); |
| 72 | validateOffset(data_offset, fileSize); |
Neil Fuller | 4da0af1 | 2020-05-19 14:56:15 +0100 | [diff] [blame] | 73 | int final_offset = mappedFile.getInt(); |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 74 | |
Neil Fuller | 4da0af1 | 2020-05-19 14:56:15 +0100 | [diff] [blame] | 75 | if (index_offset >= data_offset |
Neil Fuller | 61b7d56 | 2020-05-21 14:43:47 +0100 | [diff] [blame] | 76 | || data_offset >= final_offset |
Neil Fuller | 4da0af1 | 2020-05-19 14:56:15 +0100 | [diff] [blame] | 77 | || final_offset > fileSize) { |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 78 | throw new IOException("Invalid offset: index_offset=" + index_offset |
Neil Fuller | 61b7d56 | 2020-05-21 14:43:47 +0100 | [diff] [blame] | 79 | + ", data_offset=" + data_offset + ", final_offset=" + final_offset |
| 80 | + ", fileSize=" + fileSize); |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 81 | } |
| 82 | |
| 83 | File zicFilesDir = new File(outputDir, "zones"); |
| 84 | zicFilesDir.mkdir(); |
| 85 | extractZicFiles(mappedFile, index_offset, data_offset, zicFilesDir); |
| 86 | |
Neil Fuller | 4da0af1 | 2020-05-19 14:56:15 +0100 | [diff] [blame] | 87 | if (final_offset != fileSize) { |
| 88 | // This isn't an error, but it's worth noting: it suggests the file may be in a newer |
| 89 | // format than the current branch. |
| 90 | System.out.println( |
| 91 | "final_offset (" + final_offset + ") != fileSize (" + fileSize + ")"); |
| 92 | } |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 93 | } |
| 94 | |
| 95 | static MappedByteBuffer createMappedByteBuffer(File tzData) throws IOException { |
| 96 | MappedByteBuffer mappedFile; |
| 97 | RandomAccessFile file = new RandomAccessFile(tzData, "r"); |
| 98 | try (FileChannel fileChannel = file.getChannel()) { |
| 99 | mappedFile = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, file.length()); |
| 100 | } |
| 101 | mappedFile.load(); |
| 102 | return mappedFile; |
| 103 | } |
| 104 | |
| 105 | private static void validateOffset(int offset, int size) throws IOException { |
| 106 | if (offset < 0 || offset >= size) { |
| 107 | throw new IOException("Invalid offset=" + offset + ", size=" + size); |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | private static void writeVersionFile(MappedByteBuffer mappedFile, File targetDir) |
| 112 | throws IOException { |
| 113 | |
| 114 | byte[] tzdata_version = new byte[12]; |
| 115 | mappedFile.get(tzdata_version); |
| 116 | |
| 117 | String magic = new String(tzdata_version, 0, 6, StandardCharsets.US_ASCII); |
| 118 | if (!magic.startsWith("tzdata") || tzdata_version[11] != 0) { |
| 119 | throw new IOException("bad tzdata magic: " + Arrays.toString(tzdata_version)); |
| 120 | } |
| 121 | writeStringUtf8ToFile(new File(targetDir, "version"), |
| 122 | new String(tzdata_version, 6, 5, StandardCharsets.US_ASCII)); |
| 123 | } |
| 124 | |
| 125 | private static void extractZicFiles(MappedByteBuffer mappedFile, int indexOffset, |
| 126 | int dataOffset, File outputDir) throws IOException { |
| 127 | |
| 128 | mappedFile.position(indexOffset); |
| 129 | |
| 130 | // The index of the tzdata file is made up of entries for each time zone ID which describe |
| 131 | // the location of the associated zic data in the data section of the file. The index |
| 132 | // section has no padding so we can determine the number of entries from the size. |
| 133 | // |
| 134 | // Each index entry consists of: |
| 135 | // byte[MAXNAME] idBytes - the id string, \0 terminated. e.g. "America/New_York\0" |
| 136 | // int32 byteOffset - the offset of the start of the zic data relative to the start of |
| 137 | // the tzdata data section |
| 138 | // int32 length - the length of the of the zic data |
| 139 | // int32 unused - no longer used |
| 140 | final int MAXNAME = 40; |
| 141 | final int SIZEOF_OFFSET = 4; |
| 142 | final int SIZEOF_INDEX_ENTRY = MAXNAME + 3 * SIZEOF_OFFSET; |
| 143 | |
| 144 | int indexSize = (dataOffset - indexOffset); |
| 145 | if (indexSize % SIZEOF_INDEX_ENTRY != 0) { |
| 146 | throw new IOException("Index size is not divisible by " + SIZEOF_INDEX_ENTRY |
| 147 | + ", indexSize=" + indexSize); |
| 148 | } |
| 149 | |
| 150 | byte[] idBytes = new byte[MAXNAME]; |
| 151 | int entryCount = indexSize / SIZEOF_INDEX_ENTRY; |
| 152 | int[] byteOffsets = new int[entryCount]; |
| 153 | int[] lengths = new int[entryCount]; |
| 154 | String[] ids = new String[entryCount]; |
| 155 | |
| 156 | for (int i = 0; i < entryCount; i++) { |
| 157 | // Read the fixed length timezone ID. |
| 158 | mappedFile.get(idBytes, 0, idBytes.length); |
| 159 | |
| 160 | // Read the offset into the file where the data for ID can be found. |
| 161 | byteOffsets[i] = mappedFile.getInt(); |
| 162 | byteOffsets[i] += dataOffset; |
| 163 | |
| 164 | lengths[i] = mappedFile.getInt(); |
| 165 | if (lengths[i] < 44) { |
| 166 | throw new IOException("length in index file < sizeof(tzhead)"); |
| 167 | } |
| 168 | mappedFile.getInt(); // Skip the unused 4 bytes that used to be the raw offset. |
| 169 | |
| 170 | // Calculate the true length of the ID. |
| 171 | int len = 0; |
| 172 | while (len < idBytes.length && idBytes[len] != 0) { |
| 173 | len++; |
| 174 | } |
| 175 | if (len == 0) { |
| 176 | throw new IOException("Invalid ID at index=" + i); |
| 177 | } |
| 178 | ids[i] = new String(idBytes, 0, len, StandardCharsets.US_ASCII); |
| 179 | if (i > 0) { |
| 180 | if (ids[i].compareTo(ids[i - 1]) <= 0) { |
| 181 | throw new IOException( |
| 182 | "Index not sorted or contains multiple entries with the same ID" |
Neil Fuller | 4da0af1 | 2020-05-19 14:56:15 +0100 | [diff] [blame] | 183 | + ", index=" + i + ", ids[i]=" + ids[i] |
| 184 | + ", ids[i - 1]=" + ids[i - 1]); |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 185 | } |
| 186 | } |
| 187 | } |
| 188 | for (int i = 0; i < entryCount; i++) { |
| 189 | String id = ids[i]; |
| 190 | int byteOffset = byteOffsets[i]; |
| 191 | int length = lengths[i]; |
| 192 | |
| 193 | File subFile = new File(outputDir, id.replace('/', '_')); |
| 194 | mappedFile.position(byteOffset); |
| 195 | byte[] bytes = new byte[length]; |
| 196 | mappedFile.get(bytes, 0, length); |
| 197 | |
| 198 | writeBytesToFile(subFile, bytes); |
| 199 | } |
| 200 | } |
| 201 | |
Neil Fuller | 0480f46 | 2019-04-09 19:23:35 +0100 | [diff] [blame] | 202 | private static void writeStringUtf8ToFile(File file, String string) throws IOException { |
| 203 | writeBytesToFile(file, string.getBytes(StandardCharsets.UTF_8)); |
| 204 | } |
| 205 | |
| 206 | private static void writeBytesToFile(File file, byte[] bytes) throws IOException { |
| 207 | System.out.println("Writing: " + file); |
| 208 | Files.write(file.toPath(), bytes, StandardOpenOption.CREATE); |
| 209 | } |
| 210 | } |