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