import java.io.*; import java.nio.ByteOrder; import java.util.*; import libcore.io.BufferIterator; import libcore.util.ZoneInfo; // usage: java ZoneCompiler // // 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 // ... // // ... // // 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 { public static class ByteArrayBufferIteratorBE extends BufferIterator { private final byte[] bytes; private int offset = 0; public ByteArrayBufferIteratorBE(byte[] bytes) { this.bytes = bytes; this.offset = 0; } public void seek(int offset) { this.offset = offset; } public void skip(int byteCount) { this.offset += byteCount; } public void readByteArray(byte[] dst, int dstOffset, int byteCount) { System.arraycopy(bytes, offset, dst, dstOffset, byteCount); offset += byteCount; } public byte readByte() { return bytes[offset++]; } public int readInt() { return ((readByte() & 0xff) << 24) | ((readByte() & 0xff) << 16) | ((readByte() & 0xff) << 8) | (readByte() & 0xff); } public void readIntArray(int[] dst, int dstOffset, int intCount) { for (int i = 0; i < intCount; ++i) { dst[dstOffset++] = readInt(); } } public short readShort() { throw new UnsupportedOperationException(); } } // Maximum number of characters in a zone name, including '\0' terminator private static final int MAXNAME = 40; // Zone name synonyms private Map links = new HashMap(); // File starting bytes by zone name private Map starts = new HashMap(); // File lengths by zone name private Map lengths = new HashMap(); // Raw GMT offsets by zone name private Map offsets = new HashMap(); private int start = 0; // Concatenate the contents of 'inFile' onto 'out' // and return the contents as a byte array. private static byte[] 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(); return ret; } 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; while ((s = reader.readLine()) != null) { s = s.trim(); if (s.startsWith("Link")) { StringTokenizer st = new StringTokenizer(s); st.nextToken(); String to = st.nextToken(); String from = st.nextToken(); links.put(from, to); } else { String link = links.get(s); if (link == null) { File sourceFile = new File(dataDirectory, s); long length = sourceFile.length(); starts.put(s, start); lengths.put(s, (int) length); start += length; byte[] data = copyFile(sourceFile, allData); BufferIterator it = new ByteArrayBufferIteratorBE(data); TimeZone tz = ZoneInfo.makeTimeZone(s, it); int gmtOffset = tz.getRawOffset(); offsets.put(s, gmtOffset); } } } // Fill in fields for links. Iterator it = links.keySet().iterator(); while (it.hasNext()) { String from = it.next(); String to = links.get(from); starts.put(from, starts.get(to)); lengths.put(from, lengths.get(to)); offsets.put(from, offsets.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 file_format_version -- probably won't need this, but just in case // int index_offset -- likewise // int data_offset // int zonetab_offset // tzdata_version f.write(toAscii(new byte[12], version)); // file_format_version f.writeInt(1); // Write dummy values for the three 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); int zonetab_offset_offset = (int) f.getFilePointer(); f.writeInt(0); int index_offset = (int) f.getFilePointer(); // Write the index. ArrayList sortedOlsonIds = new ArrayList(); sortedOlsonIds.addAll(starts.keySet()); Collections.sort(sortedOlsonIds); it = sortedOlsonIds.iterator(); while (it.hasNext()) { String zoneName = it.next(); if (zoneName.length() >= MAXNAME) { throw new RuntimeException("zone filename too long: " + zoneName.length()); } f.write(toAscii(new byte[MAXNAME], zoneName)); f.writeInt(starts.get(zoneName)); f.writeInt(lengths.get(zoneName)); f.writeInt(offsets.get(zoneName)); } int data_offset = (int) f.getFilePointer(); // Write the data. f.write(allData.toByteArray()); // TODO: append the zonetab. int zonetab_offset = 0; // 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(zonetab_offset_offset); f.writeInt(zonetab_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 "); System.exit(0); } new ZoneCompactor(args[0], args[1], args[2], args[3]); } }