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 import java.io.IOException;
21 import java.nio.charset.StandardCharsets;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.List;
25 import libcore.io.BufferIterator;
26 import libcore.io.MemoryMappedFile;
27 
28 /**
29  * A class used to initialize the time zone database. This implementation uses the
30  * Olson tzdata as the source of time zone information. However, to conserve
31  * disk space (inodes) and reduce I/O, all the data is concatenated into a single file,
32  * with an index to indicate the starting position of each time zone record.
33  *
34  * @hide - used to implement TimeZone
35  */
36 public final class ZoneInfoDB {
37   private static final TzData DATA =
38       new TzData(System.getenv("ANDROID_DATA") + "/misc/zoneinfo/current/tzdata",
39           System.getenv("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata");
40 
41   public static class TzData {
42     /**
43      * Rather than open, read, and close the big data file each time we look up a time zone,
44      * we map the big data file during startup, and then just use the MemoryMappedFile.
45      *
46      * At the moment, this "big" data file is about 500 KiB. At some point, that will be small
47      * enough that we could just keep the byte[] in memory, but using mmap(2) like this has the
48      * nice property that even if someone replaces the file under us (because multiple gservices
49      * updates have gone out, say), we still get a consistent (if outdated) view of the world.
50      */
51     private MemoryMappedFile mappedFile;
52 
53     private String version;
54     private String zoneTab;
55 
56     /**
57      * The 'ids' array contains time zone ids sorted alphabetically, for binary searching.
58      * The other two arrays are in the same order. 'byteOffsets' gives the byte offset
59      * of each time zone, and 'rawUtcOffsetsCache' gives the time zone's raw UTC offset.
60      */
61     private String[] ids;
62     private int[] byteOffsets;
63     private int[] rawUtcOffsetsCache; // Access this via getRawUtcOffsets instead.
64 
65     /**
66      * ZoneInfo objects are worth caching because they are expensive to create.
67      * See http://b/8270865 for context.
68      */
69     private final static int CACHE_SIZE = 1;
70     private final BasicLruCache<String, ZoneInfo> cache =
71         new BasicLruCache<String, ZoneInfo>(CACHE_SIZE) {
72       @Override
73       protected ZoneInfo create(String id) {
74         BufferIterator it = getBufferIterator(id);
75         if (it == null) {
76           return null;
77         }
78 
79         return ZoneInfo.makeTimeZone(id, it);
80       }
81     };
82 
TzData(String... paths)83     public TzData(String... paths) {
84       for (String path : paths) {
85         if (loadData(path)) {
86           return;
87         }
88       }
89 
90       // We didn't find any usable tzdata on disk, so let's just hard-code knowledge of "GMT".
91       // This is actually implemented in TimeZone itself, so if this is the only time zone
92       // we report, we won't be asked any more questions.
93       System.logE("Couldn't find any tzdata!");
94       version = "missing";
95       zoneTab = "# Emergency fallback data.\n";
96       ids = new String[] { "GMT" };
97       byteOffsets = rawUtcOffsetsCache = new int[1];
98     }
99 
100     /**
101      * Visible for testing.
102      */
getBufferIterator(String id)103     public BufferIterator getBufferIterator(String id) {
104       // Work out where in the big data file this time zone is.
105       int index = Arrays.binarySearch(ids, id);
106       if (index < 0) {
107         return null;
108       }
109 
110       BufferIterator it = mappedFile.bigEndianIterator();
111       it.skip(byteOffsets[index]);
112       return it;
113     }
114 
loadData(String path)115     private boolean loadData(String path) {
116       try {
117         mappedFile = MemoryMappedFile.mmapRO(path);
118       } catch (ErrnoException errnoException) {
119         return false;
120       }
121       try {
122         readHeader();
123         return true;
124       } catch (Exception ex) {
125         // Something's wrong with the file.
126         // Log the problem and return false so we try the next choice.
127         System.logE("tzdata file \"" + path + "\" was present but invalid!", ex);
128         return false;
129       }
130     }
131 
readHeader()132     private void readHeader() {
133       // byte[12] tzdata_version  -- "tzdata2012f\0"
134       // int index_offset
135       // int data_offset
136       // int zonetab_offset
137       BufferIterator it = mappedFile.bigEndianIterator();
138 
139       byte[] tzdata_version = new byte[12];
140       it.readByteArray(tzdata_version, 0, tzdata_version.length);
141       String magic = new String(tzdata_version, 0, 6, StandardCharsets.US_ASCII);
142       if (!magic.equals("tzdata") || tzdata_version[11] != 0) {
143         throw new RuntimeException("bad tzdata magic: " + Arrays.toString(tzdata_version));
144       }
145       version = new String(tzdata_version, 6, 5, StandardCharsets.US_ASCII);
146 
147       int index_offset = it.readInt();
148       int data_offset = it.readInt();
149       int zonetab_offset = it.readInt();
150 
151       readIndex(it, index_offset, data_offset);
152       readZoneTab(it, zonetab_offset, (int) mappedFile.size() - zonetab_offset);
153     }
154 
readZoneTab(BufferIterator it, int zoneTabOffset, int zoneTabSize)155     private void readZoneTab(BufferIterator it, int zoneTabOffset, int zoneTabSize) {
156       byte[] bytes = new byte[zoneTabSize];
157       it.seek(zoneTabOffset);
158       it.readByteArray(bytes, 0, bytes.length);
159       zoneTab = new String(bytes, 0, bytes.length, StandardCharsets.US_ASCII);
160     }
161 
readIndex(BufferIterator it, int indexOffset, int dataOffset)162     private void readIndex(BufferIterator it, int indexOffset, int dataOffset) {
163       it.seek(indexOffset);
164 
165       // The database reserves 40 bytes for each id.
166       final int SIZEOF_TZNAME = 40;
167       // The database uses 32-bit (4 byte) integers.
168       final int SIZEOF_TZINT = 4;
169 
170       byte[] idBytes = new byte[SIZEOF_TZNAME];
171       int indexSize = (dataOffset - indexOffset);
172       int entryCount = indexSize / (SIZEOF_TZNAME + 3*SIZEOF_TZINT);
173 
174       char[] idChars = new char[entryCount * SIZEOF_TZNAME];
175       int[] idEnd = new int[entryCount];
176       int idOffset = 0;
177 
178       byteOffsets = new int[entryCount];
179 
180       for (int i = 0; i < entryCount; i++) {
181         it.readByteArray(idBytes, 0, idBytes.length);
182 
183         byteOffsets[i] = it.readInt();
184         byteOffsets[i] += dataOffset; // TODO: change the file format so this is included.
185 
186         int length = it.readInt();
187         if (length < 44) {
188           throw new AssertionError("length in index file < sizeof(tzhead)");
189         }
190         it.skip(4); // Skip the unused 4 bytes that used to be the raw offset.
191 
192         // Don't include null chars in the String
193         int len = idBytes.length;
194         for (int j = 0; j < len; j++) {
195           if (idBytes[j] == 0) {
196             break;
197           }
198           idChars[idOffset++] = (char) (idBytes[j] & 0xFF);
199         }
200 
201         idEnd[i] = idOffset;
202       }
203 
204       // We create one string containing all the ids, and then break that into substrings.
205       // This way, all ids share a single char[] on the heap.
206       String allIds = new String(idChars, 0, idOffset);
207       ids = new String[entryCount];
208       for (int i = 0; i < entryCount; i++) {
209         ids[i] = allIds.substring(i == 0 ? 0 : idEnd[i - 1], idEnd[i]);
210       }
211     }
212 
getAvailableIDs()213     public String[] getAvailableIDs() {
214       return ids.clone();
215     }
216 
getAvailableIDs(int rawUtcOffset)217     public String[] getAvailableIDs(int rawUtcOffset) {
218       List<String> matches = new ArrayList<String>();
219       int[] rawUtcOffsets = getRawUtcOffsets();
220       for (int i = 0; i < rawUtcOffsets.length; ++i) {
221         if (rawUtcOffsets[i] == rawUtcOffset) {
222           matches.add(ids[i]);
223         }
224       }
225       return matches.toArray(new String[matches.size()]);
226     }
227 
getRawUtcOffsets()228     private synchronized int[] getRawUtcOffsets() {
229       if (rawUtcOffsetsCache != null) {
230         return rawUtcOffsetsCache;
231       }
232       rawUtcOffsetsCache = new int[ids.length];
233       for (int i = 0; i < ids.length; ++i) {
234         // This creates a TimeZone, which is quite expensive. Hence the cache.
235         // Note that icu4c does the same (without the cache), so if you're
236         // switching this code over to icu4j you should check its performance.
237         // Telephony shouldn't care, but someone converting a bunch of calendar
238         // events might.
239         rawUtcOffsetsCache[i] = cache.get(ids[i]).getRawOffset();
240       }
241       return rawUtcOffsetsCache;
242     }
243 
getVersion()244     public String getVersion() {
245       return version;
246     }
247 
getZoneTab()248     public String getZoneTab() {
249       return zoneTab;
250     }
251 
makeTimeZone(String id)252     public ZoneInfo makeTimeZone(String id) throws IOException {
253       ZoneInfo zoneInfo = cache.get(id);
254       // The object from the cache is cloned because TimeZone / ZoneInfo are mutable.
255       return zoneInfo == null ? null : (ZoneInfo) zoneInfo.clone();
256     }
257 
hasTimeZone(String id)258     public boolean hasTimeZone(String id) throws IOException {
259       return cache.get(id) != null;
260     }
261 
finalize()262     @Override protected void finalize() throws Throwable {
263       if (mappedFile != null) {
264         mappedFile.close();
265       }
266       super.finalize();
267     }
268   }
269 
ZoneInfoDB()270   private ZoneInfoDB() {
271   }
272 
getInstance()273   public static TzData getInstance() {
274     return DATA;
275   }
276 }
277