1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package libcore.util;
18 
19 import android.system.ErrnoException;
20 
21 import java.io.File;
22 import java.io.FileInputStream;
23 import java.io.IOException;
24 import java.nio.charset.StandardCharsets;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.List;
28 import libcore.io.BufferIterator;
29 import libcore.io.MemoryMappedFile;
30 
31 /**
32  * A class used to initialize the time zone database. This implementation uses the
33  * Olson tzdata as the source of time zone information. However, to conserve
34  * disk space (inodes) and reduce I/O, all the data is concatenated into a single file,
35  * with an index to indicate the starting position of each time zone record.
36  *
37  * @hide - used to implement TimeZone
38  */
39 public final class ZoneInfoDB {
40 
41   // VisibleForTesting
42   public static final String TZDATA_FILE = "tzdata";
43 
44   private static final TzData DATA =
45           TzData.loadTzDataWithFallback(TimeZoneDataFiles.getTimeZoneFilePaths(TZDATA_FILE));
46 
47   public static class TzData {
48 
49     // The database reserves 40 bytes for each id.
50     private static final int SIZEOF_TZNAME = 40;
51 
52     // The database uses 32-bit (4 byte) integers.
53     private static final int SIZEOF_TZINT = 4;
54 
55     // Each index entry takes up this number of bytes.
56     public static final int SIZEOF_INDEX_ENTRY = SIZEOF_TZNAME + 3 * SIZEOF_TZINT;
57 
58     /**
59      * {@code true} if {@link #close()} has been called meaning the instance cannot provide any
60      * data.
61      */
62     private boolean closed;
63 
64     /**
65      * Rather than open, read, and close the big data file each time we look up a time zone,
66      * we map the big data file during startup, and then just use the MemoryMappedFile.
67      *
68      * At the moment, this "big" data file is about 500 KiB. At some point, that will be small
69      * enough that we could just keep the byte[] in memory, but using mmap(2) like this has the
70      * nice property that even if someone replaces the file under us (because multiple gservices
71      * updates have gone out, say), we still get a consistent (if outdated) view of the world.
72      */
73     private MemoryMappedFile mappedFile;
74 
75     private String version;
76     private String zoneTab;
77 
78     /**
79      * The 'ids' array contains time zone ids sorted alphabetically, for binary searching.
80      * The other two arrays are in the same order. 'byteOffsets' gives the byte offset
81      * of each time zone, and 'rawUtcOffsetsCache' gives the time zone's raw UTC offset.
82      */
83     private String[] ids;
84     private int[] byteOffsets;
85     private int[] rawUtcOffsetsCache; // Access this via getRawUtcOffsets instead.
86 
87     /**
88      * ZoneInfo objects are worth caching because they are expensive to create.
89      * See http://b/8270865 for context.
90      */
91     private final static int CACHE_SIZE = 1;
92     private final BasicLruCache<String, ZoneInfo> cache =
93         new BasicLruCache<String, ZoneInfo>(CACHE_SIZE) {
94       @Override
95       protected ZoneInfo create(String id) {
96         try {
97           return makeTimeZoneUncached(id);
98         } catch (IOException e) {
99           throw new IllegalStateException("Unable to load timezone for ID=" + id, e);
100         }
101       }
102     };
103 
104     /**
105      * Loads the data at the specified paths in order, returning the first valid one as a
106      * {@link TzData} object. If there is no valid one found a basic fallback instance is created
107      * containing just GMT.
108      */
loadTzDataWithFallback(String... paths)109     public static TzData loadTzDataWithFallback(String... paths) {
110       for (String path : paths) {
111         TzData tzData = new TzData();
112         if (tzData.loadData(path)) {
113           return tzData;
114         }
115       }
116 
117       // We didn't find any usable tzdata on disk, so let's just hard-code knowledge of "GMT".
118       // This is actually implemented in TimeZone itself, so if this is the only time zone
119       // we report, we won't be asked any more questions.
120       System.logE("Couldn't find any " + TZDATA_FILE + " file!");
121       return TzData.createFallback();
122     }
123 
124     /**
125      * Loads the data at the specified path and returns the {@link TzData} object if it is valid,
126      * otherwise {@code null}.
127      */
loadTzData(String path)128     public static TzData loadTzData(String path) {
129       TzData tzData = new TzData();
130       if (tzData.loadData(path)) {
131         return tzData;
132       }
133       return null;
134     }
135 
createFallback()136     private static TzData createFallback() {
137       TzData tzData = new TzData();
138       tzData.populateFallback();
139       return tzData;
140     }
141 
TzData()142     private TzData() {
143     }
144 
145     /**
146      * Visible for testing.
147      */
getBufferIterator(String id)148     public BufferIterator getBufferIterator(String id) {
149       checkNotClosed();
150 
151       // Work out where in the big data file this time zone is.
152       int index = Arrays.binarySearch(ids, id);
153       if (index < 0) {
154         return null;
155       }
156 
157       int byteOffset = byteOffsets[index];
158       BufferIterator it = mappedFile.bigEndianIterator();
159       it.skip(byteOffset);
160       return it;
161     }
162 
populateFallback()163     private void populateFallback() {
164       version = "missing";
165       zoneTab = "# Emergency fallback data.\n";
166       ids = new String[] { "GMT" };
167       byteOffsets = rawUtcOffsetsCache = new int[1];
168     }
169 
170     /**
171      * Loads the data file at the specified path. If the data is valid {@code true} will be
172      * returned and the {@link TzData} instance can be used. If {@code false} is returned then the
173      * TzData instance is left in a closed state and must be discarded.
174      */
loadData(String path)175     private boolean loadData(String path) {
176       try {
177         mappedFile = MemoryMappedFile.mmapRO(path);
178       } catch (ErrnoException errnoException) {
179         return false;
180       }
181       try {
182         readHeader();
183         return true;
184       } catch (Exception ex) {
185         close();
186 
187         // Something's wrong with the file.
188         // Log the problem and return false so we try the next choice.
189         System.logE(TZDATA_FILE + " file \"" + path + "\" was present but invalid!", ex);
190         return false;
191       }
192     }
193 
readHeader()194     private void readHeader() throws IOException {
195       // byte[12] tzdata_version  -- "tzdata2012f\0"
196       // int index_offset
197       // int data_offset
198       // int zonetab_offset
199       BufferIterator it = mappedFile.bigEndianIterator();
200 
201       try {
202         byte[] tzdata_version = new byte[12];
203         it.readByteArray(tzdata_version, 0, tzdata_version.length);
204         String magic = new String(tzdata_version, 0, 6, StandardCharsets.US_ASCII);
205         if (!magic.equals("tzdata") || tzdata_version[11] != 0) {
206           throw new IOException("bad tzdata magic: " + Arrays.toString(tzdata_version));
207         }
208         version = new String(tzdata_version, 6, 5, StandardCharsets.US_ASCII);
209 
210         final int fileSize = mappedFile.size();
211         int index_offset = it.readInt();
212         validateOffset(index_offset, fileSize);
213         int data_offset = it.readInt();
214         validateOffset(data_offset, fileSize);
215         int zonetab_offset = it.readInt();
216         validateOffset(zonetab_offset, fileSize);
217 
218         if (index_offset >= data_offset || data_offset >= zonetab_offset) {
219           throw new IOException("Invalid offset: index_offset=" + index_offset
220                   + ", data_offset=" + data_offset + ", zonetab_offset=" + zonetab_offset
221                   + ", fileSize=" + fileSize);
222         }
223 
224         readIndex(it, index_offset, data_offset);
225         readZoneTab(it, zonetab_offset, fileSize - zonetab_offset);
226       } catch (IndexOutOfBoundsException e) {
227         throw new IOException("Invalid read from data file", e);
228       }
229     }
230 
validateOffset(int offset, int size)231     private static void validateOffset(int offset, int size) throws IOException {
232       if (offset < 0 || offset >= size) {
233         throw new IOException("Invalid offset=" + offset + ", size=" + size);
234       }
235     }
236 
readZoneTab(BufferIterator it, int zoneTabOffset, int zoneTabSize)237     private void readZoneTab(BufferIterator it, int zoneTabOffset, int zoneTabSize) {
238       byte[] bytes = new byte[zoneTabSize];
239       it.seek(zoneTabOffset);
240       it.readByteArray(bytes, 0, bytes.length);
241       zoneTab = new String(bytes, 0, bytes.length, StandardCharsets.US_ASCII);
242     }
243 
readIndex(BufferIterator it, int indexOffset, int dataOffset)244     private void readIndex(BufferIterator it, int indexOffset, int dataOffset) throws IOException {
245       it.seek(indexOffset);
246 
247       byte[] idBytes = new byte[SIZEOF_TZNAME];
248       int indexSize = (dataOffset - indexOffset);
249       if (indexSize % SIZEOF_INDEX_ENTRY != 0) {
250         throw new IOException("Index size is not divisible by " + SIZEOF_INDEX_ENTRY
251                 + ", indexSize=" + indexSize);
252       }
253       int entryCount = indexSize / SIZEOF_INDEX_ENTRY;
254 
255       byteOffsets = new int[entryCount];
256       ids = new String[entryCount];
257 
258       for (int i = 0; i < entryCount; i++) {
259         // Read the fixed length timezone ID.
260         it.readByteArray(idBytes, 0, idBytes.length);
261 
262         // Read the offset into the file where the data for ID can be found.
263         byteOffsets[i] = it.readInt();
264         byteOffsets[i] += dataOffset;
265 
266         int length = it.readInt();
267         if (length < 44) {
268           throw new IOException("length in index file < sizeof(tzhead)");
269         }
270         it.skip(4); // Skip the unused 4 bytes that used to be the raw offset.
271 
272         // Calculate the true length of the ID.
273         int len = 0;
274         while (idBytes[len] != 0 && len < idBytes.length) {
275           len++;
276         }
277         if (len == 0) {
278           throw new IOException("Invalid ID at index=" + i);
279         }
280         ids[i] = new String(idBytes, 0, len, StandardCharsets.US_ASCII);
281         if (i > 0) {
282           if (ids[i].compareTo(ids[i - 1]) <= 0) {
283             throw new IOException("Index not sorted or contains multiple entries with the same ID"
284                     + ", index=" + i + ", ids[i]=" + ids[i] + ", ids[i - 1]=" + ids[i - 1]);
285           }
286         }
287       }
288     }
289 
validate()290     public void validate() throws IOException {
291       checkNotClosed();
292       // Validate the data in the tzdata file by loading each and every zone.
293       for (String id : getAvailableIDs()) {
294         ZoneInfo zoneInfo = makeTimeZoneUncached(id);
295         if (zoneInfo == null) {
296           throw new IOException("Unable to find data for ID=" + id);
297         }
298       }
299     }
300 
makeTimeZoneUncached(String id)301     ZoneInfo makeTimeZoneUncached(String id) throws IOException {
302       BufferIterator it = getBufferIterator(id);
303       if (it == null) {
304         return null;
305       }
306 
307       return ZoneInfo.readTimeZone(id, it, System.currentTimeMillis());
308     }
309 
getAvailableIDs()310     public String[] getAvailableIDs() {
311       checkNotClosed();
312       return ids.clone();
313     }
314 
getAvailableIDs(int rawUtcOffset)315     public String[] getAvailableIDs(int rawUtcOffset) {
316       checkNotClosed();
317       List<String> matches = new ArrayList<String>();
318       int[] rawUtcOffsets = getRawUtcOffsets();
319       for (int i = 0; i < rawUtcOffsets.length; ++i) {
320         if (rawUtcOffsets[i] == rawUtcOffset) {
321           matches.add(ids[i]);
322         }
323       }
324       return matches.toArray(new String[matches.size()]);
325     }
326 
getRawUtcOffsets()327     private synchronized int[] getRawUtcOffsets() {
328       if (rawUtcOffsetsCache != null) {
329         return rawUtcOffsetsCache;
330       }
331       rawUtcOffsetsCache = new int[ids.length];
332       for (int i = 0; i < ids.length; ++i) {
333         // This creates a TimeZone, which is quite expensive. Hence the cache.
334         // Note that icu4c does the same (without the cache), so if you're
335         // switching this code over to icu4j you should check its performance.
336         // Telephony shouldn't care, but someone converting a bunch of calendar
337         // events might.
338         rawUtcOffsetsCache[i] = cache.get(ids[i]).getRawOffset();
339       }
340       return rawUtcOffsetsCache;
341     }
342 
getVersion()343     public String getVersion() {
344       checkNotClosed();
345       return version;
346     }
347 
getZoneTab()348     public String getZoneTab() {
349       checkNotClosed();
350       return zoneTab;
351     }
352 
makeTimeZone(String id)353     public ZoneInfo makeTimeZone(String id) throws IOException {
354       checkNotClosed();
355       ZoneInfo zoneInfo = cache.get(id);
356       // The object from the cache is cloned because TimeZone / ZoneInfo are mutable.
357       return zoneInfo == null ? null : (ZoneInfo) zoneInfo.clone();
358     }
359 
hasTimeZone(String id)360     public boolean hasTimeZone(String id) throws IOException {
361       checkNotClosed();
362       return cache.get(id) != null;
363     }
364 
close()365     public void close() {
366       if (!closed) {
367         closed = true;
368 
369         // Clear state that takes up appreciable heap.
370         ids = null;
371         byteOffsets = null;
372         rawUtcOffsetsCache = null;
373         mappedFile = null;
374         cache.evictAll();
375 
376         // Remove the mapped file (if needed).
377         if (mappedFile != null) {
378           try {
379             mappedFile.close();
380           } catch (ErrnoException ignored) {
381           }
382         }
383       }
384     }
385 
checkNotClosed()386     private void checkNotClosed() throws IllegalStateException {
387       if (closed) {
388         throw new IllegalStateException("TzData is closed");
389       }
390     }
391 
finalize()392     @Override protected void finalize() throws Throwable {
393       try {
394         close();
395       } finally {
396         super.finalize();
397       }
398     }
399 
400     /**
401      * Returns the String describing the IANA version of the rules contained in the specified TzData
402      * file. This method just reads the header of the file, and so is less expensive than mapping
403      * the whole file into memory (and provides no guarantees about validity).
404      */
getRulesVersion(File tzDataFile)405     public static String getRulesVersion(File tzDataFile) throws IOException {
406       try (FileInputStream is = new FileInputStream(tzDataFile)) {
407 
408         final int bytesToRead = 12;
409         byte[] tzdataVersion = new byte[bytesToRead];
410         int bytesRead = is.read(tzdataVersion, 0, bytesToRead);
411         if (bytesRead != bytesToRead) {
412           throw new IOException("File too short: only able to read " + bytesRead + " bytes.");
413         }
414 
415         String magic = new String(tzdataVersion, 0, 6, StandardCharsets.US_ASCII);
416         if (!magic.equals("tzdata") || tzdataVersion[11] != 0) {
417           throw new IOException("bad tzdata magic: " + Arrays.toString(tzdataVersion));
418         }
419         return new String(tzdataVersion, 6, 5, StandardCharsets.US_ASCII);
420       }
421     }
422   }
423 
ZoneInfoDB()424   private ZoneInfoDB() {
425   }
426 
getInstance()427   public static TzData getInstance() {
428     return DATA;
429   }
430 }
431