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