/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.i18n.timezone; import android.system.ErrnoException; import com.android.i18n.timezone.internal.BasicLruCache; import com.android.i18n.timezone.internal.BufferIterator; import com.android.i18n.timezone.internal.MemoryMappedFile; import dalvik.annotation.optimization.ReachabilitySensitive; import libcore.util.NonNull; import libcore.util.Nullable; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * A class used to initialize the time zone database. This implementation uses the * Olson tzdata as the source of time zone information. However, to conserve * disk space (inodes) and reduce I/O, all the data is concatenated into a single file, * with an index to indicate the starting position of each time zone record. * * @hide - used to implement TimeZone */ @libcore.api.CorePlatformApi @libcore.api.IntraCoreApi public final class ZoneInfoDb { // VisibleForTesting public static final String TZDATA_FILE_NAME = "tzdata"; private static final ZoneInfoDb DATA = ZoneInfoDb.loadTzDataWithFallback( TimeZoneDataFiles.getTimeZoneFilePaths(TZDATA_FILE_NAME)); // The database reserves 40 bytes for each id. private static final int SIZEOF_TZNAME = 40; // The database uses 32-bit (4 byte) integers. private static final int SIZEOF_TZINT = 4; // Each index entry takes up this number of bytes. public static final int SIZEOF_INDEX_ENTRY = SIZEOF_TZNAME + 3 * SIZEOF_TZINT; /** * {@code true} if {@link #close()} has been called meaning the instance cannot provide any * data. */ private boolean closed; /** * Rather than open, read, and close the big data file each time we look up a time zone, * we map the big data file during startup, and then just use the MemoryMappedFile. * * At the moment, this "big" data file is about 500 KiB. At some point, that will be small * enough that we could just keep the byte[] in memory, but using mmap(2) like this has the * nice property that even if someone replaces the file under us (because multiple gservices * updates have gone out, say), we still get a consistent (if outdated) view of the world. */ // Android-added: @ReachabilitySensitive @ReachabilitySensitive private MemoryMappedFile mappedFile; private String version; /** * The 'ids' array contains time zone ids sorted alphabetically, for binary searching. * The other two arrays are in the same order. 'byteOffsets' gives the byte offset * of each time zone, and 'rawUtcOffsetsCache' gives the time zone's raw UTC offset. */ private String[] ids; private int[] byteOffsets; private int[] rawUtcOffsetsCache; // Access this via getRawUtcOffsets instead. /** * ZoneInfo objects are worth caching because they are expensive to create. * See http://b/8270865 for context. */ private final static int CACHE_SIZE = 1; private final BasicLruCache cache = new BasicLruCache(CACHE_SIZE) { @Override protected ZoneInfoData create(String id) { try { return makeZoneInfoDataUncached(id); } catch (IOException e) { throw new IllegalStateException("Unable to load timezone for ID=" + id, e); } } }; /** * Obtains the singleton instance. * * @hide */ @libcore.api.CorePlatformApi @libcore.api.IntraCoreApi public static @NonNull ZoneInfoDb getInstance() { return DATA; } /** * Loads the data at the specified paths in order, returning the first valid one as a * {@link ZoneInfoDb} object. If there is no valid one found a basic fallback instance is created * containing just GMT. */ public static ZoneInfoDb loadTzDataWithFallback(String... paths) { for (String path : paths) { ZoneInfoDb tzData = new ZoneInfoDb(); if (tzData.loadData(path)) { return tzData; } } // We didn't find any usable tzdata on disk, so let's just hard-code knowledge of "GMT". // This is actually implemented in TimeZone itself, so if this is the only time zone // we report, we won't be asked any more questions. // !! System.logE("Couldn't find any " + TZDATA_FILE_NAME + " file!"); return ZoneInfoDb.createFallback(); } /** * Loads the data at the specified path and returns the {@link ZoneInfoDb} object if it is valid, * otherwise {@code null}. */ // VisibleForTesting public static ZoneInfoDb loadTzData(String path) { ZoneInfoDb tzData = new ZoneInfoDb(); if (tzData.loadData(path)) { return tzData; } return null; } private static ZoneInfoDb createFallback() { ZoneInfoDb tzData = new ZoneInfoDb(); tzData.populateFallback(); return tzData; } private ZoneInfoDb() { } /** * Visible for testing. */ public BufferIterator getBufferIterator(String id) { checkNotClosed(); // Work out where in the big data file this time zone is. int index = Arrays.binarySearch(ids, id); if (index < 0) { return null; } int byteOffset = byteOffsets[index]; BufferIterator it = mappedFile.bigEndianIterator(); it.skip(byteOffset); return it; } private void populateFallback() { version = "missing"; ids = new String[] { "GMT" }; byteOffsets = rawUtcOffsetsCache = new int[1]; } /** * Loads the data file at the specified path. If the data is valid {@code true} will be * returned and the {@link ZoneInfoDb} instance can be used. If {@code false} is returned then the * ZoneInfoDB instance is left in a closed state and must be discarded. */ private boolean loadData(String path) { try { mappedFile = MemoryMappedFile.mmapRO(path); } catch (ErrnoException errnoException) { return false; } try { readHeader(); return true; } catch (Exception ex) { close(); // Something's wrong with the file. // Log the problem and return false so we try the next choice. // !! System.logE(TZDATA_FILE_NAME + " file \"" + path + "\" was present but invalid!", ex); return false; } } private void readHeader() throws IOException { // byte[12] tzdata_version -- "tzdata2012f\0" // int index_offset // int data_offset // int final_offset BufferIterator it = mappedFile.bigEndianIterator(); try { byte[] tzdata_version = new byte[12]; it.readByteArray(tzdata_version, 0, tzdata_version.length); String magic = new String(tzdata_version, 0, 6, StandardCharsets.US_ASCII); if (!magic.equals("tzdata") || tzdata_version[11] != 0) { throw new IOException("bad tzdata magic: " + Arrays.toString(tzdata_version)); } version = new String(tzdata_version, 6, 5, StandardCharsets.US_ASCII); final int fileSize = mappedFile.size(); int index_offset = it.readInt(); int data_offset = it.readInt(); int final_offset = it.readInt(); if (index_offset >= data_offset || data_offset >= final_offset || final_offset > fileSize) { throw new IOException("Invalid offset: index_offset=" + index_offset + ", data_offset=" + data_offset + ", final_offset=" + final_offset + ", fileSize=" + fileSize); } readIndex(it, index_offset, data_offset); } catch (IndexOutOfBoundsException e) { throw new IOException("Invalid read from data file", e); } } private void readIndex(BufferIterator it, int indexOffset, int dataOffset) throws IOException { it.seek(indexOffset); byte[] idBytes = new byte[SIZEOF_TZNAME]; int indexSize = (dataOffset - indexOffset); if (indexSize % SIZEOF_INDEX_ENTRY != 0) { throw new IOException("Index size is not divisible by " + SIZEOF_INDEX_ENTRY + ", indexSize=" + indexSize); } int entryCount = indexSize / SIZEOF_INDEX_ENTRY; byteOffsets = new int[entryCount]; ids = new String[entryCount]; for (int i = 0; i < entryCount; i++) { // Read the fixed length timezone ID. it.readByteArray(idBytes, 0, idBytes.length); // Read the offset into the file where the data for ID can be found. byteOffsets[i] = it.readInt(); byteOffsets[i] += dataOffset; int length = it.readInt(); if (length < 44) { throw new IOException("length in index file < sizeof(tzhead)"); } it.skip(4); // Skip the unused 4 bytes that used to be the raw offset. // Calculate the true length of the ID. int len = 0; while (len < idBytes.length && idBytes[len] != 0) { len++; } if (len == 0) { throw new IOException("Invalid ID at index=" + i); } String zoneId = new String(idBytes, 0, len, StandardCharsets.US_ASCII); // intern() zone Ids because they are a fixed set of well-known strings that are used in // other low-level library calls. ids[i] = zoneId.intern(); if (i > 0) { if (ids[i].compareTo(ids[i - 1]) <= 0) { throw new IOException("Index not sorted or contains multiple entries with the same ID" + ", index=" + i + ", ids[i]=" + ids[i] + ", ids[i - 1]=" + ids[i - 1]); } } } } /** * Validate the data at the specified path. Throws {@link IOException} if it's not valid. */ @libcore.api.CorePlatformApi public static void validateTzData(@NonNull String path) throws IOException { ZoneInfoDb tzData = ZoneInfoDb.loadTzData(path); if (tzData == null) { throw new IOException("failed to read tzData at " + path); } try { tzData.validate(); } finally { tzData.close(); } } private void validate() throws IOException { checkNotClosed(); // Validate the data in the tzdata file by loading each and every zone. for (String id : getAvailableIDs()) { ZoneInfoData zoneInfoData = makeZoneInfoDataUncached(id); if (zoneInfoData == null) { throw new IOException("Unable to find data for ID=" + id); } } } ZoneInfoData makeZoneInfoDataUncached(String id) throws IOException { BufferIterator it = getBufferIterator(id); if (it == null) { return null; } return ZoneInfoData.readTimeZone(id, it); } /** * Returns an array containing all time zone ids sorted in lexicographical order for * binary searching. * * @hide */ @libcore.api.IntraCoreApi public @NonNull String @NonNull[] getAvailableIDs() { checkNotClosed(); return ids.clone(); } /** * Returns ids of all time zones with the given raw UTC offset. * * @hide */ @libcore.api.IntraCoreApi public @NonNull String @NonNull[] getAvailableIDs(int rawUtcOffset) { checkNotClosed(); List matches = new ArrayList(); int[] rawUtcOffsets = getRawUtcOffsets(); for (int i = 0; i < rawUtcOffsets.length; ++i) { if (rawUtcOffsets[i] == rawUtcOffset) { matches.add(ids[i]); } } return matches.toArray(new String[matches.size()]); } private synchronized int[] getRawUtcOffsets() { if (rawUtcOffsetsCache != null) { return rawUtcOffsetsCache; } rawUtcOffsetsCache = new int[ids.length]; for (int i = 0; i < ids.length; ++i) { // This creates a TimeZone, which is quite expensive. Hence the cache. // Note that icu4c does the same (without the cache), so if you're // switching this code over to icu4j you should check its performance. // Telephony shouldn't care, but someone converting a bunch of calendar // events might. rawUtcOffsetsCache[i] = cache.get(ids[i]).getRawOffset(); } return rawUtcOffsetsCache; } /** * Returns the tzdb version in use. */ @libcore.api.CorePlatformApi public @NonNull String getVersion() { checkNotClosed(); return version; } /** * Creates {@link ZoneInfoData} object from the time zone {@code id}. Returns null if the id * is not found. * * @hide */ @libcore.api.CorePlatformApi @libcore.api.IntraCoreApi public @Nullable ZoneInfoData makeZoneInfoData(@NonNull String id) { checkNotClosed(); ZoneInfoData zoneInfoData = cache.get(id); // The object from the cache is not cloned because ZoneInfoData is immutable. // Note that zoneInfoData can be null here. return zoneInfoData; } @libcore.api.CorePlatformApi public boolean hasTimeZone(@NonNull String id) { checkNotClosed(); return Arrays.binarySearch(ids, id) >= 0; } // VisibleForTesting public void close() { if (!closed) { closed = true; // Clear state that takes up appreciable heap. ids = null; byteOffsets = null; rawUtcOffsetsCache = null; cache.evictAll(); // Remove the mapped file (if needed). if (mappedFile != null) { try { mappedFile.close(); } catch (ErrnoException ignored) { } mappedFile = null; } } } private void checkNotClosed() throws IllegalStateException { if (closed) { throw new IllegalStateException("ZoneInfoDB instance is closed"); } } @Override protected void finalize() throws Throwable { try { close(); } finally { super.finalize(); } } }