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