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