1 /* 2 * Copyright (C) 2017 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.*; 18 import java.util.*; 19 20 // usage: java ZoneCompactor <setup file> <data directory> <output directory> <tzdata version> 21 // 22 // Compile a set of tzfile-formatted files into a single file containing an index. 23 // 24 // The compilation is controlled by a setup file, which is provided as a 25 // command-line argument. The setup file has the form: 26 // 27 // Link <toName> <fromName> 28 // ... 29 // <zone filename> 30 // ... 31 // 32 // Note that the links must be declared prior to the zone names. 33 // A zone name is a filename relative to the source directory such as 34 // 'GMT', 'Africa/Dakar', or 'America/Argentina/Jujuy'. 35 // 36 // Use the 'zic' command-line tool to convert from flat files 37 // (such as 'africa' or 'northamerica') to a directory 38 // hierarchy suitable for this tool (containing files such as 'data/Africa/Abidjan'). 39 // 40 41 public class ZoneCompactor { 42 // Maximum number of characters in a zone name, including '\0' terminator. 43 private static final int MAXNAME = 40; 44 45 // Zone name synonyms. 46 private Map<String,String> links = new HashMap<>(); 47 48 // File offsets by zone name. 49 private Map<String,Integer> offsets = new HashMap<>(); 50 51 // File lengths by zone name. 52 private Map<String,Integer> lengths = new HashMap<>(); 53 54 // Concatenate the contents of 'inFile' onto 'out'. copyFile(File inFile, OutputStream out)55 private static void copyFile(File inFile, OutputStream out) throws Exception { 56 byte[] ret = new byte[0]; 57 58 InputStream in = new FileInputStream(inFile); 59 byte[] buf = new byte[8192]; 60 while (true) { 61 int nbytes = in.read(buf); 62 if (nbytes == -1) { 63 break; 64 } 65 out.write(buf, 0, nbytes); 66 67 byte[] nret = new byte[ret.length + nbytes]; 68 System.arraycopy(ret, 0, nret, 0, ret.length); 69 System.arraycopy(buf, 0, nret, ret.length, nbytes); 70 ret = nret; 71 } 72 out.flush(); 73 } 74 ZoneCompactor(String setupFile, String dataDirectory, String outputDirectory, String version)75 public ZoneCompactor(String setupFile, String dataDirectory, String outputDirectory, 76 String version) throws Exception { 77 // Read the setup file and concatenate all the data. 78 ByteArrayOutputStream allData = new ByteArrayOutputStream(); 79 BufferedReader reader = new BufferedReader(new FileReader(setupFile)); 80 String s; 81 int offset = 0; 82 while ((s = reader.readLine()) != null) { 83 s = s.trim(); 84 StringTokenizer st = new StringTokenizer(s); 85 String lineType = st.nextToken(); 86 if (lineType.startsWith("Link")) { 87 String to = st.nextToken(); 88 String from = st.nextToken(); 89 links.put(from, to); 90 } else if (lineType.startsWith("Zone")) { 91 String zoneId = st.nextToken(); 92 String link = links.get(zoneId); 93 if (link == null) { 94 File sourceFile = new File(dataDirectory, zoneId); 95 long length = sourceFile.length(); 96 offsets.put(zoneId, offset); 97 lengths.put(zoneId, (int) length); 98 99 offset += length; 100 copyFile(sourceFile, allData); 101 } 102 } 103 } 104 reader.close(); 105 106 // Fill in fields for links. 107 for (String from : links.keySet()) { 108 String to = links.get(from); 109 110 offsets.put(from, offsets.get(to)); 111 lengths.put(from, lengths.get(to)); 112 } 113 114 // Create/truncate the destination file. 115 RandomAccessFile f = new RandomAccessFile(new File(outputDirectory, "tzdata"), "rw"); 116 f.setLength(0); 117 118 // Write the header. 119 120 // byte[12] tzdata_version -- 'tzdata2012f\0' 121 // int index_offset -- so we can slip in extra header fields in a backwards-compatible way 122 // int data_offset 123 // int final_offset 124 125 // tzdata_version 126 f.write(toAscii(new byte[12], version)); 127 128 // Write placeholder values for the offsets, and remember where we need to seek back to later 129 // when we have the real values. 130 int index_offset_offset = (int) f.getFilePointer(); 131 f.writeInt(0); 132 int data_offset_offset = (int) f.getFilePointer(); 133 f.writeInt(0); 134 // The final offset serves as a placeholder for sections that might be added in future and 135 // ensures we know the size of the final "real" section. Relying on the last section ending at 136 // EOF would make it harder to append sections to the end of the file in a backward compatible 137 // way. 138 int final_offset_offset = (int) f.getFilePointer(); 139 f.writeInt(0); 140 141 int index_offset = (int) f.getFilePointer(); 142 143 // Write the index. 144 ArrayList<String> sortedOlsonIds = new ArrayList<String>(); 145 sortedOlsonIds.addAll(offsets.keySet()); 146 Collections.sort(sortedOlsonIds); 147 for (String zoneName : sortedOlsonIds) { 148 if (zoneName.length() >= MAXNAME) { 149 throw new RuntimeException("zone filename too long: " + zoneName.length()); 150 } 151 152 // Follow the chain of links to work out where the real data for this zone lives. 153 String actualZoneName = zoneName; 154 while (links.get(actualZoneName) != null) { 155 actualZoneName = links.get(actualZoneName); 156 } 157 158 f.write(toAscii(new byte[MAXNAME], zoneName)); 159 f.writeInt(offsets.get(actualZoneName)); 160 f.writeInt(lengths.get(actualZoneName)); 161 f.writeInt(0); // Used to be raw GMT offset. No longer used. 162 } 163 164 int data_offset = (int) f.getFilePointer(); 165 166 // Write the data. 167 f.write(allData.toByteArray()); 168 169 int final_offset = (int) f.getFilePointer(); 170 171 // Go back and fix up the offsets in the header. 172 f.seek(index_offset_offset); 173 f.writeInt(index_offset); 174 f.seek(data_offset_offset); 175 f.writeInt(data_offset); 176 f.seek(final_offset_offset); 177 f.writeInt(final_offset); 178 179 f.close(); 180 } 181 toAscii(byte[] dst, String src)182 private static byte[] toAscii(byte[] dst, String src) { 183 for (int i = 0; i < src.length(); ++i) { 184 if (src.charAt(i) > '~') { 185 throw new RuntimeException("non-ASCII string: " + src); 186 } 187 dst[i] = (byte) src.charAt(i); 188 } 189 return dst; 190 } 191 main(String[] args)192 public static void main(String[] args) throws Exception { 193 if (args.length != 4) { 194 System.err.println("usage: java ZoneCompactor <setup file> <data directory>" 195 + " <output directory> <tzdata version>"); 196 System.exit(1); 197 } 198 new ZoneCompactor(args[0], args[1], args[2], args[3]); 199 } 200 } 201