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