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