• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2025 Huawei Device Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 package com.oh.tzdata.tools;
17 
18 import java.io.InputStream;
19 import java.io.BufferedReader;
20 import java.io.RandomAccessFile;
21 import java.io.ByteArrayOutputStream;
22 import java.io.File;
23 import java.io.FileInputStream;
24 import java.io.FileReader;
25 import java.io.OutputStream;
26 import java.util.Collections;
27 import java.util.Map;
28 import java.util.HashMap;
29 import java.util.Set;
30 import java.util.LinkedHashSet;
31 import java.util.ArrayList;
32 
33 /**
34  * usage: java ZoneCompactor <setup file> <data directory> <output directory> <tzdata version>
35  *
36  * Compile a set of tzfile-formatted files into a single file containing an index.
37  *
38  * The compilation is controlled by a setup file, which is provided as a
39  * command-line argument.  The setup file has the form:
40  *
41  * Link <toName> <fromName>
42  *
43  * <zone filename>
44  *
45  *
46  * Note that the links must be declared prior to the zone names.
47  * A zone name is a filename relative to the source directory such as
48  * 'GMT', 'Africa/Dakar', or 'America/Argentina/Jujuy'.
49  *
50  * Use the 'zic' command-line tool to convert from flat files
51  * (such as 'africa' or 'northamerica') to a directory
52  * hierarchy suitable for this tool (containing files such as 'data/Africa/Abidjan').
53  *
54  * @since 2025/02/08
55  */
56 public class ZoneCompactor {
57     // Maximum number of characters in a zone name, including '\0' terminator.
58     private static final int MAXNAME = 40;
59 
60     private static final String READ_WRITE_MODE = "rw";
61 
62     // Zone name synonyms.
63     private Map<String, String> links = new HashMap<>();
64 
65     // File offsets by zone name.
66     private Map<String, Integer> offsets = new HashMap<>();
67 
68     // File lengths by zone name.
69     private Map<String, Integer> lengths = new HashMap<>();
70 
71     /**
72      * Main method to compact timezone data file
73      *
74      * @param setupFile timezone list file
75      * @param dataDirectory timezone binary file directory
76      * @param outputDirectory generate tzdata file directory
77      * @param version tzdata version eg:"tzdata2025a"
78      * @throws Exception input param invalid
79      */
ZoneCompactor(String setupFile, String dataDirectory, String outputDirectory, String version)80     public ZoneCompactor(String setupFile, String dataDirectory, String outputDirectory,
81         String version) throws Exception {
82         // Read the setup file and concatenate all the data.
83         Set<String> zoneIds = new LinkedHashSet<>();
84         try (BufferedReader reader = new BufferedReader(new FileReader(setupFile))) {
85             String line;
86             while ((line = reader.readLine()) != null) {
87                 line = line.trim();
88                 zoneIds.add(line);
89             }
90         }
91 
92         ByteArrayOutputStream allData = new ByteArrayOutputStream();
93         int offset = 0;
94         for (String zoneId : zoneIds) {
95             File sourceFile = new File(dataDirectory, zoneId);
96             long length = sourceFile.length();
97             offsets.put(zoneId, offset);
98             lengths.put(zoneId, (int) length);
99             offset += length;
100             copyFile(sourceFile, allData);
101         }
102         writeFile(outputDirectory, version, allData);
103     }
104 
105     /**
106      * Write tzdata file
107      *
108      * @param outputDirectory generate tzdata file directory
109      * @param version tzdata version eg:"tzdata2025a"
110      * @param allData temp file
111      * @throws Exception input param invalid
112      */
writeFile(String outputDirectory, String version, ByteArrayOutputStream allData)113     private void writeFile(String outputDirectory, String version,
114                            ByteArrayOutputStream allData) throws Exception {
115         // Create/truncate the destination file.
116         try (RandomAccessFile f = new RandomAccessFile(new File(outputDirectory, "tzdata"), READ_WRITE_MODE)) {
117             f.setLength(0);
118             // tzdata_version
119             f.write(toAscii(new byte[12], version));
120             // Write placeholder values for the offsets, and remember where we need to seek back to later
121             // when we have the real values.
122             int indexOffsetOffset = (int) f.getFilePointer();
123             f.writeInt(0);
124             int dataOffsetOffset = (int) f.getFilePointer();
125             f.writeInt(0);
126             // The final offset serves as a placeholder for sections that might be added in future and
127             // ensures we know the size of the final "real" section. Relying on the last section ending at
128             // EOF would make it harder to append sections to the end of the file in a backward compatible
129             // way.
130             int finalOffsetOffset = (int) f.getFilePointer();
131             f.writeInt(0);
132             int indexOffset = (int) f.getFilePointer();
133             // Write the index.
134             ArrayList<String> sortedOlsonIds = new ArrayList<String>();
135             sortedOlsonIds.addAll(offsets.keySet());
136             Collections.sort(sortedOlsonIds);
137             for (String zoneName : sortedOlsonIds) {
138                 if (zoneName.length() >= MAXNAME) {
139                     throw new IllegalArgumentException("zone filename too long: " + zoneName.length());
140                 }
141                 f.write(toAscii(new byte[MAXNAME], zoneName));
142                 f.writeInt(offsets.get(zoneName));
143                 f.writeInt(lengths.get(zoneName));
144             }
145             int dataOffset = (int) f.getFilePointer();
146             // Write the data.
147             f.write(allData.toByteArray());
148             int finalOffset = (int) f.getFilePointer();
149             // Go back and fix up the offsets in the header.
150             f.seek(indexOffsetOffset);
151             f.writeInt(indexOffset);
152             f.seek(dataOffsetOffset);
153             f.writeInt(dataOffset);
154             f.seek(finalOffsetOffset);
155             f.writeInt(finalOffset);
156         }
157     }
158 
159     // Concatenate the contents of 'inFile' onto 'out'.
copyFile(File inFile, OutputStream out)160     private static void copyFile(File inFile, OutputStream out) throws Exception {
161         try (InputStream in = new FileInputStream(inFile)) {
162             byte[] buf = new byte[8192];
163             while (true) {
164                 int nbytes = in.read(buf);
165                 if (nbytes == -1) {
166                     break;
167                 }
168                 out.write(buf, 0, nbytes);
169             }
170             out.flush();
171         }
172     }
173 
toAscii(byte[] dst, String src)174     private static byte[] toAscii(byte[] dst, String src) {
175         for (int i = 0; i < src.length(); ++i) {
176             if (src.charAt(i) > '~') {
177                 throw new IllegalArgumentException("non-ASCII string: " + src);
178             }
179             dst[i] = (byte) src.charAt(i);
180         }
181         return dst;
182     }
183 
main(String[] args)184     public static void main(String[] args) throws Exception {
185         String setupFilePath = args[0];
186         String dataDir = args[1];
187         String outputDir = args[2];
188         String tzdataVersion = args[3];
189         new ZoneCompactor(setupFilePath, dataDir, outputDir, tzdataVersion);
190     }
191 }