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