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