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