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 }