blob: 3840df67bebaf3b8deee11703386ffc29455c02a [file] [log] [blame]
Neil Fuller0480f462019-04-09 19:23:35 +01001/*
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
17import java.io.File;
18import java.io.IOException;
19import java.io.RandomAccessFile;
20import java.nio.MappedByteBuffer;
21import java.nio.channels.FileChannel;
22import java.nio.charset.StandardCharsets;
23import java.nio.file.Files;
24import java.nio.file.StandardOpenOption;
25import 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 Fuller92f87e62020-01-28 13:47:49 +000032 * <p>This class contains a copy of logic found in Android's ZoneInfoDb.
Neil Fuller0480f462019-04-09 19:23:35 +010033 */
34public 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 Fuller4da0af12020-05-19 14:56:15 +010065 // int final_offset
Neil Fuller0480f462019-04-09 19:23:35 +010066 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 Fuller4da0af12020-05-19 14:56:15 +010073 int final_offset = mappedFile.getInt();
Neil Fuller0480f462019-04-09 19:23:35 +010074
Neil Fuller4da0af12020-05-19 14:56:15 +010075 if (index_offset >= data_offset
Neil Fuller61b7d562020-05-21 14:43:47 +010076 || data_offset >= final_offset
Neil Fuller4da0af12020-05-19 14:56:15 +010077 || final_offset > fileSize) {
Neil Fuller0480f462019-04-09 19:23:35 +010078 throw new IOException("Invalid offset: index_offset=" + index_offset
Neil Fuller61b7d562020-05-21 14:43:47 +010079 + ", data_offset=" + data_offset + ", final_offset=" + final_offset
80 + ", fileSize=" + fileSize);
Neil Fuller0480f462019-04-09 19:23:35 +010081 }
82
83 File zicFilesDir = new File(outputDir, "zones");
84 zicFilesDir.mkdir();
85 extractZicFiles(mappedFile, index_offset, data_offset, zicFilesDir);
86
Neil Fuller4da0af12020-05-19 14:56:15 +010087 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 Fuller0480f462019-04-09 19:23:35 +010093 }
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 Fuller4da0af12020-05-19 14:56:15 +0100183 + ", index=" + i + ", ids[i]=" + ids[i]
184 + ", ids[i - 1]=" + ids[i - 1]);
Neil Fuller0480f462019-04-09 19:23:35 +0100185 }
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 Fuller0480f462019-04-09 19:23:35 +0100202 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}