| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import java.io.*; |
| import java.util.*; |
| |
| // usage: java ZoneCompactor <setup file> <data directory> <output directory> <tzdata version> |
| // |
| // Compile a set of tzfile-formatted files into a single file containing an index. |
| // |
| // The compilation is controlled by a setup file, which is provided as a |
| // command-line argument. The setup file has the form: |
| // |
| // Link <toName> <fromName> |
| // ... |
| // <zone filename> |
| // ... |
| // |
| // Note that the links must be declared prior to the zone names. |
| // A zone name is a filename relative to the source directory such as |
| // 'GMT', 'Africa/Dakar', or 'America/Argentina/Jujuy'. |
| // |
| // Use the 'zic' command-line tool to convert from flat files |
| // (such as 'africa' or 'northamerica') to a directory |
| // hierarchy suitable for this tool (containing files such as 'data/Africa/Abidjan'). |
| // |
| |
| public class ZoneCompactor { |
| // Maximum number of characters in a zone name, including '\0' terminator. |
| private static final int MAXNAME = 40; |
| |
| // Zone name synonyms. |
| private Map<String,String> links = new HashMap<>(); |
| |
| // File offsets by zone name. |
| private Map<String,Integer> offsets = new HashMap<>(); |
| |
| // File lengths by zone name. |
| private Map<String,Integer> lengths = new HashMap<>(); |
| |
| // Concatenate the contents of 'inFile' onto 'out'. |
| private static void copyFile(File inFile, OutputStream out) throws Exception { |
| byte[] ret = new byte[0]; |
| |
| InputStream in = new FileInputStream(inFile); |
| byte[] buf = new byte[8192]; |
| while (true) { |
| int nbytes = in.read(buf); |
| if (nbytes == -1) { |
| break; |
| } |
| out.write(buf, 0, nbytes); |
| |
| byte[] nret = new byte[ret.length + nbytes]; |
| System.arraycopy(ret, 0, nret, 0, ret.length); |
| System.arraycopy(buf, 0, nret, ret.length, nbytes); |
| ret = nret; |
| } |
| out.flush(); |
| } |
| |
| public ZoneCompactor(String setupFile, String dataDirectory, String outputDirectory, |
| String version) throws Exception { |
| // Read the setup file and concatenate all the data. |
| ByteArrayOutputStream allData = new ByteArrayOutputStream(); |
| BufferedReader reader = new BufferedReader(new FileReader(setupFile)); |
| String s; |
| int offset = 0; |
| while ((s = reader.readLine()) != null) { |
| s = s.trim(); |
| StringTokenizer st = new StringTokenizer(s); |
| String lineType = st.nextToken(); |
| if (lineType.startsWith("Link")) { |
| String to = st.nextToken(); |
| String from = st.nextToken(); |
| links.put(from, to); |
| } else if (lineType.startsWith("Zone")) { |
| String zoneId = st.nextToken(); |
| String link = links.get(zoneId); |
| if (link == null) { |
| File sourceFile = new File(dataDirectory, zoneId); |
| long length = sourceFile.length(); |
| offsets.put(zoneId, offset); |
| lengths.put(zoneId, (int) length); |
| |
| offset += length; |
| copyFile(sourceFile, allData); |
| } |
| } |
| } |
| reader.close(); |
| |
| // Fill in fields for links. |
| for (String from : links.keySet()) { |
| String to = links.get(from); |
| |
| offsets.put(from, offsets.get(to)); |
| lengths.put(from, lengths.get(to)); |
| } |
| |
| // Create/truncate the destination file. |
| RandomAccessFile f = new RandomAccessFile(new File(outputDirectory, "tzdata"), "rw"); |
| f.setLength(0); |
| |
| // Write the header. |
| |
| // byte[12] tzdata_version -- 'tzdata2012f\0' |
| // int index_offset -- so we can slip in extra header fields in a backwards-compatible way |
| // int data_offset |
| // int final_offset |
| |
| // tzdata_version |
| f.write(toAscii(new byte[12], version)); |
| |
| // Write placeholder values for the offsets, and remember where we need to seek back to later |
| // when we have the real values. |
| int index_offset_offset = (int) f.getFilePointer(); |
| f.writeInt(0); |
| int data_offset_offset = (int) f.getFilePointer(); |
| f.writeInt(0); |
| // The final offset serves as a placeholder for sections that might be added in future and |
| // ensures we know the size of the final "real" section. Relying on the last section ending at |
| // EOF would make it harder to append sections to the end of the file in a backward compatible |
| // way. |
| int final_offset_offset = (int) f.getFilePointer(); |
| f.writeInt(0); |
| |
| int index_offset = (int) f.getFilePointer(); |
| |
| // Write the index. |
| ArrayList<String> sortedOlsonIds = new ArrayList<String>(); |
| sortedOlsonIds.addAll(offsets.keySet()); |
| Collections.sort(sortedOlsonIds); |
| for (String zoneName : sortedOlsonIds) { |
| if (zoneName.length() >= MAXNAME) { |
| throw new RuntimeException("zone filename too long: " + zoneName.length()); |
| } |
| |
| // Follow the chain of links to work out where the real data for this zone lives. |
| String actualZoneName = zoneName; |
| while (links.get(actualZoneName) != null) { |
| actualZoneName = links.get(actualZoneName); |
| } |
| |
| f.write(toAscii(new byte[MAXNAME], zoneName)); |
| f.writeInt(offsets.get(actualZoneName)); |
| f.writeInt(lengths.get(actualZoneName)); |
| f.writeInt(0); // Used to be raw GMT offset. No longer used. |
| } |
| |
| int data_offset = (int) f.getFilePointer(); |
| |
| // Write the data. |
| f.write(allData.toByteArray()); |
| |
| int final_offset = (int) f.getFilePointer(); |
| |
| // Go back and fix up the offsets in the header. |
| f.seek(index_offset_offset); |
| f.writeInt(index_offset); |
| f.seek(data_offset_offset); |
| f.writeInt(data_offset); |
| f.seek(final_offset_offset); |
| f.writeInt(final_offset); |
| |
| f.close(); |
| } |
| |
| private static byte[] toAscii(byte[] dst, String src) { |
| for (int i = 0; i < src.length(); ++i) { |
| if (src.charAt(i) > '~') { |
| throw new RuntimeException("non-ASCII string: " + src); |
| } |
| dst[i] = (byte) src.charAt(i); |
| } |
| return dst; |
| } |
| |
| public static void main(String[] args) throws Exception { |
| if (args.length != 4) { |
| System.err.println("usage: java ZoneCompactor <setup file> <data directory>" |
| + " <output directory> <tzdata version>"); |
| System.exit(1); |
| } |
| new ZoneCompactor(args[0], args[1], args[2], args[3]); |
| } |
| } |