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  *
32  * <p>This class contains a copy of logic found in Android's ZoneInfoDb.
33  */
34 public class ZoneSplitter {
35 
main(String[] args)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 
ZoneSplitter(String tzData, String outputDir)47     private ZoneSplitter(String tzData, String outputDir) {
48         this.tzData = new File(tzData);
49         this.outputDir = new File(outputDir);
50     }
51 
execute()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
65         // int final_offset
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);
73         int final_offset = mappedFile.getInt();
74 
75         if (index_offset >= data_offset
76                 || data_offset >= final_offset
77                 || final_offset > fileSize) {
78             throw new IOException("Invalid offset: index_offset=" + index_offset
79                     + ", data_offset=" + data_offset + ", final_offset=" + final_offset
80                     + ", fileSize=" + fileSize);
81         }
82 
83         File zicFilesDir = new File(outputDir, "zones");
84         zicFilesDir.mkdir();
85         extractZicFiles(mappedFile, index_offset, data_offset, zicFilesDir);
86 
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         }
93     }
94 
createMappedByteBuffer(File tzData)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 
validateOffset(int offset, int size)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 
writeVersionFile(MappedByteBuffer mappedFile, File targetDir)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 
extractZicFiles(MappedByteBuffer mappedFile, int indexOffset, int dataOffset, File outputDir)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"
183                                     + ", index=" + i + ", ids[i]=" + ids[i]
184                                     + ", ids[i - 1]=" + ids[i - 1]);
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 
writeStringUtf8ToFile(File file, String string)202     private static void writeStringUtf8ToFile(File file, String string) throws IOException {
203         writeBytesToFile(file, string.getBytes(StandardCharsets.UTF_8));
204     }
205 
writeBytesToFile(File file, byte[] bytes)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 }
211