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