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