Home | History | Annotate | Download | only in util
      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 java.io.IOException;
     20 import java.io.RandomAccessFile;
     21 import java.nio.ByteBuffer;
     22 import java.nio.ByteOrder;
     23 import java.nio.channels.FileChannel.MapMode;
     24 import java.nio.charset.StandardCharsets;
     25 import java.util.ArrayList;
     26 import java.util.Arrays;
     27 import java.util.List;
     28 import java.util.TimeZone;
     29 import libcore.io.BufferIterator;
     30 import libcore.io.ErrnoException;
     31 import libcore.io.IoUtils;
     32 import libcore.io.MemoryMappedFile;
     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 public final class ZoneInfoDB {
     43   private static final TzData DATA =
     44       new TzData(System.getenv("ANDROID_DATA") + "/misc/zoneinfo/tzdata",
     45                  System.getenv("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata");
     46 
     47   public static class TzData {
     48     /**
     49      * Rather than open, read, and close the big data file each time we look up a time zone,
     50      * we map the big data file during startup, and then just use the MemoryMappedFile.
     51      *
     52      * At the moment, this "big" data file is about 500 KiB. At some point, that will be small
     53      * enough that we could just keep the byte[] in memory, but using mmap(2) like this has the
     54      * nice property that even if someone replaces the file under us (because multiple gservices
     55      * updates have gone out, say), we still get a consistent (if outdated) view of the world.
     56      */
     57     private MemoryMappedFile mappedFile;
     58 
     59     private String version;
     60     private String zoneTab;
     61 
     62     /**
     63      * The 'ids' array contains time zone ids sorted alphabetically, for binary searching.
     64      * The other two arrays are in the same order. 'byteOffsets' gives the byte offset
     65      * of each time zone, and 'rawUtcOffsets' gives the time zone's raw UTC offset.
     66      */
     67     private String[] ids;
     68     private int[] byteOffsets;
     69     private int[] rawUtcOffsets;
     70 
     71     public TzData(String... paths) {
     72       for (String path : paths) {
     73         if (loadData(path)) {
     74           return;
     75         }
     76       }
     77 
     78       // We didn't find any usable tzdata on disk, so let's just hard-code knowledge of "GMT".
     79       // This is actually implemented in TimeZone itself, so if this is the only time zone
     80       // we report, we won't be asked any more questions.
     81       System.logE("Couldn't find any tzdata!");
     82       version = "missing";
     83       zoneTab = "# Emergency fallback data.\n";
     84       ids = new String[] { "GMT" };
     85       byteOffsets = rawUtcOffsets = new int[1];
     86     }
     87 
     88     private boolean loadData(String path) {
     89       try {
     90         mappedFile = MemoryMappedFile.mmapRO(path);
     91       } catch (ErrnoException errnoException) {
     92         return false;
     93       }
     94       try {
     95         readHeader();
     96         return true;
     97       } catch (Exception ex) {
     98         // Something's wrong with the file.
     99         // Log the problem and return false so we try the next choice.
    100         System.logE("tzdata file \"" + path + "\" was present but invalid!", ex);
    101         return false;
    102       }
    103     }
    104 
    105     private void readHeader() {
    106       // byte[12] tzdata_version  -- "tzdata2012f\0"
    107       // int index_offset
    108       // int data_offset
    109       // int zonetab_offset
    110       BufferIterator it = mappedFile.bigEndianIterator();
    111 
    112       byte[] tzdata_version = new byte[12];
    113       it.readByteArray(tzdata_version, 0, tzdata_version.length);
    114       String magic = new String(tzdata_version, 0, 6, StandardCharsets.US_ASCII);
    115       if (!magic.equals("tzdata") || tzdata_version[11] != 0) {
    116         throw new RuntimeException("bad tzdata magic: " + Arrays.toString(tzdata_version));
    117       }
    118       version = new String(tzdata_version, 6, 5, StandardCharsets.US_ASCII);
    119 
    120       int index_offset = it.readInt();
    121       int data_offset = it.readInt();
    122       int zonetab_offset = it.readInt();
    123 
    124       readIndex(it, index_offset, data_offset);
    125       readZoneTab(it, zonetab_offset, (int) mappedFile.size() - zonetab_offset);
    126     }
    127 
    128     private void readZoneTab(BufferIterator it, int zoneTabOffset, int zoneTabSize) {
    129       byte[] bytes = new byte[zoneTabSize];
    130       it.seek(zoneTabOffset);
    131       it.readByteArray(bytes, 0, bytes.length);
    132       zoneTab = new String(bytes, 0, bytes.length, StandardCharsets.US_ASCII);
    133     }
    134 
    135     private void readIndex(BufferIterator it, int indexOffset, int dataOffset) {
    136       it.seek(indexOffset);
    137 
    138       // The database reserves 40 bytes for each id.
    139       final int SIZEOF_TZNAME = 40;
    140       // The database uses 32-bit (4 byte) integers.
    141       final int SIZEOF_TZINT = 4;
    142 
    143       byte[] idBytes = new byte[SIZEOF_TZNAME];
    144       int indexSize = (dataOffset - indexOffset);
    145       int entryCount = indexSize / (SIZEOF_TZNAME + 3*SIZEOF_TZINT);
    146 
    147       char[] idChars = new char[entryCount * SIZEOF_TZNAME];
    148       int[] idEnd = new int[entryCount];
    149       int idOffset = 0;
    150 
    151       byteOffsets = new int[entryCount];
    152       rawUtcOffsets = new int[entryCount];
    153 
    154       for (int i = 0; i < entryCount; i++) {
    155         it.readByteArray(idBytes, 0, idBytes.length);
    156 
    157         byteOffsets[i] = it.readInt();
    158         byteOffsets[i] += dataOffset; // TODO: change the file format so this is included.
    159 
    160         int length = it.readInt();
    161         if (length < 44) {
    162           throw new AssertionError("length in index file < sizeof(tzhead)");
    163         }
    164         rawUtcOffsets[i] = it.readInt();
    165 
    166         // Don't include null chars in the String
    167         int len = idBytes.length;
    168         for (int j = 0; j < len; j++) {
    169           if (idBytes[j] == 0) {
    170             break;
    171           }
    172           idChars[idOffset++] = (char) (idBytes[j] & 0xFF);
    173         }
    174 
    175         idEnd[i] = idOffset;
    176       }
    177 
    178       // We create one string containing all the ids, and then break that into substrings.
    179       // This way, all ids share a single char[] on the heap.
    180       String allIds = new String(idChars, 0, idOffset);
    181       ids = new String[entryCount];
    182       for (int i = 0; i < entryCount; i++) {
    183         ids[i] = allIds.substring(i == 0 ? 0 : idEnd[i - 1], idEnd[i]);
    184       }
    185     }
    186 
    187     public String[] getAvailableIDs() {
    188       return ids.clone();
    189     }
    190 
    191     public String[] getAvailableIDs(int rawOffset) {
    192       List<String> matches = new ArrayList<String>();
    193       for (int i = 0, end = rawUtcOffsets.length; i < end; ++i) {
    194         if (rawUtcOffsets[i] == rawOffset) {
    195           matches.add(ids[i]);
    196         }
    197       }
    198       return matches.toArray(new String[matches.size()]);
    199     }
    200 
    201     public String getVersion() {
    202       return version;
    203     }
    204 
    205     public String getZoneTab() {
    206       return zoneTab;
    207     }
    208 
    209     public TimeZone makeTimeZone(String id) throws IOException {
    210       // Work out where in the big data file this time zone is.
    211       int index = Arrays.binarySearch(ids, id);
    212       if (index < 0) {
    213         return null;
    214       }
    215 
    216       BufferIterator it = mappedFile.bigEndianIterator();
    217       it.skip(byteOffsets[index]);
    218 
    219       return ZoneInfo.makeTimeZone(id, it);
    220     }
    221   }
    222 
    223   private ZoneInfoDB() {
    224   }
    225 
    226   public static TzData getInstance() {
    227     return DATA;
    228   }
    229 }
    230