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 InputStream in = new FileInputStream(inFile); 57 byte[] buf = new byte[8192]; 58 while (true) { 59 int nbytes = in.read(buf); 60 if (nbytes == -1) { 61 break; 62 } 63 out.write(buf, 0, nbytes); 64 } 65 out.flush(); 66 } 67 ZoneCompactor(String setupFile, String dataDirectory, String outputDirectory, String version)68 public ZoneCompactor(String setupFile, String dataDirectory, String outputDirectory, 69 String version) throws Exception { 70 // Read the setup file and concatenate all the data. 71 Set<String> zoneIds = new LinkedHashSet<>(); 72 try (BufferedReader reader = new BufferedReader(new FileReader(setupFile))) { 73 String s; 74 while ((s = reader.readLine()) != null) { 75 s = s.trim(); 76 StringTokenizer st = new StringTokenizer(s); 77 String lineType = st.nextToken(); 78 if (lineType.startsWith("Link")) { 79 String to = st.nextToken(); 80 String from = st.nextToken(); 81 links.put(from, to); 82 } else if (lineType.startsWith("Zone")) { 83 String zoneId = st.nextToken(); 84 if (!zoneIds.add(zoneId)) { 85 throw new IllegalStateException(String.format("There are at least two Zone entries " 86 + "for %s in the setup file", zoneId)); 87 } 88 } 89 } 90 } 91 92 ByteArrayOutputStream allData = new ByteArrayOutputStream(); 93 94 int offset = 0; 95 for (String zoneId : zoneIds) { 96 if (!links.containsKey(zoneId)) { 97 File sourceFile = new File(dataDirectory, zoneId); 98 long length = sourceFile.length(); 99 offsets.put(zoneId, offset); 100 lengths.put(zoneId, (int) length); 101 102 offset += length; 103 copyFile(sourceFile, allData); 104 } 105 } 106 107 // Fill in fields for links. 108 for (String from : links.keySet()) { 109 String to = links.get(from); 110 111 offsets.put(from, offsets.get(to)); 112 lengths.put(from, lengths.get(to)); 113 } 114 115 // Create/truncate the destination file. 116 RandomAccessFile f = new RandomAccessFile(new File(outputDirectory, "tzdata"), "rw"); 117 f.setLength(0); 118 119 // Write the header. 120 121 // byte[12] tzdata_version -- 'tzdata2012f\0' 122 // int index_offset -- so we can slip in extra header fields in a backwards-compatible way 123 // int data_offset 124 // int final_offset 125 126 // tzdata_version 127 f.write(toAscii(new byte[12], version)); 128 129 // Write placeholder values for the offsets, and remember where we need to seek back to later 130 // when we have the real values. 131 int index_offset_offset = (int) f.getFilePointer(); 132 f.writeInt(0); 133 int data_offset_offset = (int) f.getFilePointer(); 134 f.writeInt(0); 135 // The final offset serves as a placeholder for sections that might be added in future and 136 // ensures we know the size of the final "real" section. Relying on the last section ending at 137 // EOF would make it harder to append sections to the end of the file in a backward compatible 138 // way. 139 int final_offset_offset = (int) f.getFilePointer(); 140 f.writeInt(0); 141 142 int index_offset = (int) f.getFilePointer(); 143 144 // Write the index. 145 ArrayList<String> sortedOlsonIds = new ArrayList<String>(); 146 sortedOlsonIds.addAll(offsets.keySet()); 147 Collections.sort(sortedOlsonIds); 148 for (String zoneName : sortedOlsonIds) { 149 if (zoneName.length() >= MAXNAME) { 150 throw new RuntimeException("zone filename too long: " + zoneName.length()); 151 } 152 153 // Follow the chain of links to work out where the real data for this zone lives. 154 String actualZoneName = zoneName; 155 while (links.get(actualZoneName) != null) { 156 actualZoneName = links.get(actualZoneName); 157 } 158 159 f.write(toAscii(new byte[MAXNAME], zoneName)); 160 f.writeInt(offsets.get(actualZoneName)); 161 f.writeInt(lengths.get(actualZoneName)); 162 f.writeInt(0); // Used to be raw GMT offset. No longer used. 163 } 164 165 int data_offset = (int) f.getFilePointer(); 166 167 // Write the data. 168 f.write(allData.toByteArray()); 169 170 int final_offset = (int) f.getFilePointer(); 171 172 // Go back and fix up the offsets in the header. 173 f.seek(index_offset_offset); 174 f.writeInt(index_offset); 175 f.seek(data_offset_offset); 176 f.writeInt(data_offset); 177 f.seek(final_offset_offset); 178 f.writeInt(final_offset); 179 180 f.close(); 181 } 182 toAscii(byte[] dst, String src)183 private static byte[] toAscii(byte[] dst, String src) { 184 for (int i = 0; i < src.length(); ++i) { 185 if (src.charAt(i) > '~') { 186 throw new RuntimeException("non-ASCII string: " + src); 187 } 188 dst[i] = (byte) src.charAt(i); 189 } 190 return dst; 191 } 192 main(String[] args)193 public static void main(String[] args) throws Exception { 194 if (args.length != 4) { 195 System.err.println("usage: java ZoneCompactor <setup file> <data directory>" 196 + " <output directory> <tzdata version>"); 197 System.exit(1); 198 } 199 new ZoneCompactor(args[0], args[1], args[2], args[3]); 200 } 201 } 202