• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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