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