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