Merge branch 'master' into msbuilddll2
[mono.git] / mcs / class / System.Core / System / TimeZoneInfo.Android.cs
index 9518191c849b440568cd1ab4d086ee0a01623add..aa2ae29974894366959757bf8998765aa8fc8476 100644 (file)
@@ -29,10 +29,231 @@ using System.Text;
 
 namespace System {
 
+       interface IAndroidTimeZoneDB {
+               IEnumerable<string>   GetAvailableIds ();
+               byte[]                GetTimeZoneData (string id);
+       }
+
+       [StructLayout (LayoutKind.Sequential, Pack=1)]
+       unsafe struct AndroidTzDataHeader {
+               public fixed byte signature [12];
+               public int        indexOffset;
+               public int        dataOffset;
+               public int        zoneTabOffset;
+       }
+
+       [StructLayout (LayoutKind.Sequential, Pack=1)]
+       unsafe struct AndroidTzDataEntry {
+               public fixed byte id [40];
+               public int        byteOffset;
+               public int        length;
+               public int        rawUtcOffset;
+       }
+
+       /*
+        * Android v4.3 Timezone support infrastructure.
+        *
+        * This is a C# port of libcore.util.ZoneInfoDB:
+        *
+        *    https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/ZoneInfoDB.java
+        *
+        * This is needed in order to read Android v4.3 tzdata files.
+        */
+       sealed class AndroidTzData : IAndroidTimeZoneDB {
+
+               internal static readonly string[] Paths = new string[]{
+                       Environment.GetEnvironmentVariable ("ANDROID_DATA") + "/misc/zoneinfo/tzdata",
+                       Environment.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata",
+               };
+
+               string    tzdataPath;
+               Stream    data;
+               string    version;
+               string    zoneTab;
+
+               string[]  ids;
+               int[]     byteOffsets;
+               int[]     lengths;
+
+               public AndroidTzData (params string[] paths)
+               {
+                       foreach (var path in paths)
+                               if (LoadData (path)) {
+                                       tzdataPath = path;
+                                       return;
+                               }
+
+                       Console.Error.WriteLine ("Couldn't find any tzdata!");
+                       tzdataPath  = "/";
+                       version     = "missing";
+                       zoneTab     = "# Emergency fallback data.\n";
+                       ids         = new[]{ "GMT" };
+               }
+
+               public string Version {
+                       get {return version;}
+               }
+
+               public string ZoneTab {
+                       get {return zoneTab;}
+               }
+
+               bool LoadData (string path)
+               {
+                       if (!File.Exists (path))
+                               return false;
+                       try {
+                               data = File.OpenRead (path);
+                       } catch (IOException) {
+                               return false;
+                       } catch (UnauthorizedAccessException) {
+                               return false;
+                       }
+
+                       try {
+                               ReadHeader ();
+                               return true;
+                       } catch (Exception e) {
+                               Console.Error.WriteLine ("tzdata file \"{0}\" was present but invalid: {1}", path, e);
+                       }
+                       return false;
+               }
+
+               unsafe void ReadHeader ()
+               {
+                       int size   = System.Math.Max (Marshal.SizeOf (typeof (AndroidTzDataHeader)), Marshal.SizeOf (typeof (AndroidTzDataEntry)));
+                       var buffer = new byte [size];
+                       var header = ReadAt<AndroidTzDataHeader>(0, buffer);
+
+                       header.indexOffset    = NetworkToHostOrder (header.indexOffset);
+                       header.dataOffset     = NetworkToHostOrder (header.dataOffset);
+                       header.zoneTabOffset  = NetworkToHostOrder (header.zoneTabOffset);
+
+                       sbyte* s = (sbyte*) header.signature;
+                       string magic = new string (s, 0, 6, Encoding.ASCII);
+                       if (magic != "tzdata" || header.signature [11] != 0) {
+                               var b = new StringBuilder ();
+                               b.Append ("bad tzdata magic:");
+                               for (int i = 0; i < 12; ++i) {
+                                       b.Append (" ").Append (((byte) s [i]).ToString ("x2"));
+                               }
+                               throw new InvalidOperationException ("bad tzdata magic: " + b.ToString ());
+                       }
+
+                       version = new string (s, 6, 5, Encoding.ASCII);
+
+                       ReadIndex (header.indexOffset, header.dataOffset, buffer);
+                       ReadZoneTab (header.zoneTabOffset, checked ((int) data.Length) - header.zoneTabOffset);
+               }
+
+               unsafe T ReadAt<T> (long position, byte[] buffer)
+                       where T : struct
+               {
+                       int size = Marshal.SizeOf (typeof (T));
+                       if (buffer.Length < size)
+                               throw new InvalidOperationException ("Internal error: buffer too small");
+
+                       data.Position = position;
+                       int r;
+                       if ((r = data.Read (buffer, 0, size)) < size)
+                               throw new InvalidOperationException (
+                                               string.Format ("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath, r, size));
+
+                       fixed (byte* b = buffer)
+                               return (T) Marshal.PtrToStructure ((IntPtr) b, typeof (T));
+               }
+
+               static int NetworkToHostOrder (int value)
+               {
+                       if (!BitConverter.IsLittleEndian)
+                               return value;
+
+                       return
+                               (((value >> 24) & 0xFF) |
+                                ((value >> 08) & 0xFF00) |
+                                ((value << 08) & 0xFF0000) |
+                                ((value << 24)));
+               }
+
+               unsafe void ReadIndex (int indexOffset, int dataOffset, byte[] buffer)
+               {
+                       int indexSize   = dataOffset - indexOffset;
+                       int entryCount  = indexSize / Marshal.SizeOf (typeof (AndroidTzDataEntry));
+                       int entrySize   = Marshal.SizeOf (typeof (AndroidTzDataEntry));
+
+                       byteOffsets   = new int [entryCount];
+                       ids           = new string [entryCount];
+                       lengths       = new int [entryCount];
+
+                       for (int i = 0; i < entryCount; ++i) {
+                               var entry = ReadAt<AndroidTzDataEntry>(indexOffset + (entrySize*i), buffer);
+                               var p     = (sbyte*) entry.id;
+
+                               byteOffsets [i]   = NetworkToHostOrder (entry.byteOffset) + dataOffset;
+                               ids [i]           = new string (p, 0, GetStringLength (p, 40), Encoding.ASCII);
+                               lengths [i]       = NetworkToHostOrder (entry.length);
+
+                               if (lengths [i] < Marshal.SizeOf (typeof (AndroidTzDataHeader)))
+                                       throw new InvalidOperationException ("Length in index file < sizeof(tzhead)");
+                       }
+               }
+
+               static unsafe int GetStringLength (sbyte* s, int maxLength)
+               {
+                       int len;
+                       for (len = 0; len < maxLength; len++, s++) {
+                               if (*s == 0)
+                                       break;
+                       }
+                       return len;
+               }
+
+               unsafe void ReadZoneTab (int zoneTabOffset, int zoneTabSize)
+               {
+                       byte[] zoneTab = new byte [zoneTabSize];
+
+                       data.Position = zoneTabOffset;
+
+                       int r;
+                       if ((r = data.Read (zoneTab, 0, zoneTab.Length)) < zoneTab.Length)
+                               throw new InvalidOperationException (
+                                               string.Format ("Error reading zonetab: read {0} bytes, expected {1}", r, zoneTabSize));
+
+                       this.zoneTab = Encoding.ASCII.GetString (zoneTab, 0, zoneTab.Length);
+               }
+
+               public IEnumerable<string> GetAvailableIds ()
+               {
+                       return ids;
+               }
+
+               public byte[] GetTimeZoneData (string id)
+               {
+                       int i = Array.BinarySearch (ids, id, StringComparer.Ordinal);
+                       if (i < 0)
+                               return null;
+
+                       int offset = byteOffsets [i];
+                       int length = lengths [i];
+                       var buffer = new byte [length];
+
+                       lock (data) {
+                               data.Position = offset;
+                               int r;
+                               if ((r = data.Read (buffer, 0, buffer.Length)) < buffer.Length)
+                                       throw new InvalidOperationException (
+                                                       string.Format ("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.",
+                                                               tzdataPath, offset, length, r, buffer.Length));
+                       }
+
+                       return buffer;
+               }
+       }
+
        partial class TimeZoneInfo {
 
                /*
-                * Android Timezone support infrastructure.
+                * Android < v4.3 Timezone support infrastructure.
                 *
                 * This is a C# port of org.apache.harmony.luni.internal.util.ZoneInfoDB:
                 *
@@ -48,34 +269,35 @@ namespace System {
                 * which succinctly describes why we can't just use the LIBC implementation in
                 * TimeZoneInfo.cs -- the "standard Unixy" directory structure is NOT used.
                 */
-               static class ZoneInfoDB {
+               sealed class ZoneInfoDB : IAndroidTimeZoneDB {
                        const int TimeZoneNameLength  = 40;
                        const int TimeZoneIntSize     = 4;
 
-                       static readonly string ZoneDirectoryName  = Environment.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/";
-                       static readonly string ZoneFileName       = ZoneDirectoryName + "zoneinfo.dat";
-                       static readonly string IndexFileName      = ZoneDirectoryName + "zoneinfo.idx";
-                       const           string DefaultVersion     = "2007h";
-                       static readonly string VersionFileName    = ZoneDirectoryName + "zoneinfo.version";
+                       internal static readonly string ZoneDirectoryName  = Environment.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/";
 
-                       static readonly object _lock = new object ();
+                       const    string ZoneFileName       = "zoneinfo.dat";
+                       const    string IndexFileName      = "zoneinfo.idx";
+                       const    string DefaultVersion     = "2007h";
+                       const    string VersionFileName    = "zoneinfo.version";
 
-                       static readonly string    version;
-                       static readonly string[]  names;
-                       static readonly int[]     starts;
-                       static readonly int[]     lengths;
-                       static readonly int[]     offsets;
+                       readonly string    zoneRoot;
+                       readonly string    version;
+                       readonly string[]  names;
+                       readonly int[]     starts;
+                       readonly int[]     lengths;
+                       readonly int[]     offsets;
 
-                       static ZoneInfoDB ()
+                       public ZoneInfoDB (string zoneInfoDB = null)
                        {
+                               zoneRoot = zoneInfoDB ?? ZoneDirectoryName;
                                try {
-                                       version = ReadVersion ();
+                                       version = ReadVersion (Path.Combine (zoneRoot, VersionFileName));
                                } catch {
                                        version = DefaultVersion;
                                }
 
                                try {
-                                       ReadDatabase (out names, out starts, out lengths, out offsets);
+                                       ReadDatabase (Path.Combine (zoneRoot, IndexFileName), out names, out starts, out lengths, out offsets);
                                } catch {
                                        names   = new string [0];
                                        starts  = new int [0];
@@ -84,16 +306,16 @@ namespace System {
                                }
                        }
 
-                       static string ReadVersion ()
+                       static string ReadVersion (string path)
                        {
-                               using (var file = new StreamReader (VersionFileName, Encoding.GetEncoding ("iso-8859-1"))) {
+                               using (var file = new StreamReader (path, Encoding.GetEncoding ("iso-8859-1"))) {
                                        return file.ReadToEnd ().Trim ();
                                }
                        }
 
-                       static void ReadDatabase (out string[] names, out int[] starts, out int[] lengths, out int[] offsets)
+                       void ReadDatabase (string path, out string[] names, out int[] starts, out int[] lengths, out int[] offsets)
                        {
-                               using (var file = File.OpenRead (IndexFileName)) {
+                               using (var file = File.OpenRead (path)) {
                                        var nbuf = new byte [TimeZoneNameLength];
 
                                        int numEntries = (int) (file.Length / (TimeZoneNameLength + 3*TimeZoneIntSize));
@@ -124,7 +346,7 @@ namespace System {
 
                        static void Fill (Stream stream, byte[] nbuf, int required)
                        {
-                               int read, offset = 0;
+                               int read = 0, offset = 0;
                                while (offset < required && (read = stream.Read (nbuf, offset, required - offset)) > 0)
                                        offset += read;
                                if (read != required)
@@ -140,21 +362,21 @@ namespace System {
                                        ((nbuf [2] & 0xff) << 8) + (nbuf [3] & 0xff);
                        }
 
-                       internal static string Version {
+                       internal string Version {
                                get {return version;}
                        }
 
-                       internal static IEnumerable<string> GetAvailableIds ()
+                       public IEnumerable<string> GetAvailableIds ()
                        {
                                return GetAvailableIds (0, false);
                        }
 
-                       internal static IEnumerable<string> GetAvailableIds (int rawOffset)
+                       IEnumerable<string> GetAvailableIds (int rawOffset)
                        {
                                return GetAvailableIds (rawOffset, true);
                        }
 
-                       static IEnumerable<string> GetAvailableIds (int rawOffset, bool checkOffset)
+                       IEnumerable<string> GetAvailableIds (int rawOffset, bool checkOffset)
                        {
                                for (int i = 0; i < offsets.Length; ++i) {
                                        if (!checkOffset || offsets [i] == rawOffset)
@@ -162,21 +384,21 @@ namespace System {
                                }
                        }
 
-                       static TimeZoneInfo _GetTimeZone (string name)
+                       public byte[] GetTimeZoneData (string id)
                        {
                                int start, length;
-                               using (var stream = GetTimeZoneData (name, out start, out length)) {
+                               using (var stream = GetTimeZoneData (id, out start, out length)) {
                                        if (stream == null)
                                                return null;
                                        byte[] buf = new byte [length];
                                        Fill (stream, buf, buf.Length);
-                                       return TimeZoneInfo.ParseTZBuffer (name, buf, length);
+                                       return buf;
                                }
                        }
 
-                       static FileStream GetTimeZoneData (string name, out int start, out int length)
+                       FileStream GetTimeZoneData (string name, out int start, out int length)
                        {
-                               var f = new FileInfo (Path.Combine (ZoneDirectoryName, name));
+                               var f = new FileInfo (Path.Combine (zoneRoot, name));
                                if (f.Exists) {
                                        start   = 0;
                                        length  = (int) f.Length;
@@ -185,18 +407,55 @@ namespace System {
 
                                start = length = 0;
 
-                               int i = Array.BinarySearch (names, name);
+                               int i = Array.BinarySearch (names, name, StringComparer.Ordinal);
                                if (i < 0)
                                        return null;
 
                                start   = starts [i];
                                length  = lengths [i];
 
-                               var stream = File.OpenRead (ZoneFileName);
+                               var stream = File.OpenRead (Path.Combine (zoneRoot, ZoneFileName));
                                stream.Seek (start, SeekOrigin.Begin);
 
                                return stream;
                        }
+               }
+
+               static class AndroidTimeZones {
+
+                       static IAndroidTimeZoneDB db;
+
+                       static AndroidTimeZones ()
+                       {
+                               db = GetDefaultTimeZoneDB ();
+                       }
+
+                       static IAndroidTimeZoneDB GetDefaultTimeZoneDB ()
+                       {
+                               foreach (var p in AndroidTzData.Paths)
+                                       if (File.Exists (p))
+                                               return new AndroidTzData (AndroidTzData.Paths);
+                               if (Directory.Exists (ZoneInfoDB.ZoneDirectoryName))
+                                       return new ZoneInfoDB ();
+                               return null;
+                       }
+
+                       internal static IEnumerable<string> GetAvailableIds ()
+                       {
+                               return db == null
+                                       ? new string [0]
+                                       : db.GetAvailableIds ();
+                       }
+
+                       static TimeZoneInfo _GetTimeZone (string name)
+                       {
+                               if (db == null)
+                                       return null;
+                               byte[] buffer = db.GetTimeZoneData (name);
+                               if (buffer == null)
+                                       return null;
+                               return TimeZoneInfo.ParseTZBuffer (name, buffer, buffer.Length);
+                       }
 
                        internal static TimeZoneInfo GetTimeZone (string id)
                        {
@@ -211,7 +470,7 @@ namespace System {
 
                                try {
                                        return _GetTimeZone (id);
-                               } catch (Exception e) {
+                               } catch (Exception) {
                                        return null;
                                }
                        }
@@ -265,6 +524,8 @@ namespace System {
                                        return sign * (hour * 60) * 60;
                        }
 
+                       static readonly object _lock = new object ();
+
                        static TimeZoneInfo defaultZone;
                        internal static TimeZoneInfo Default {
                                get {
@@ -291,6 +552,54 @@ namespace System {
                                        return buf.ToString ();
                                return null;
                        }
+
+#if SELF_TEST
+                       /*
+                        * Compile:
+                        *    mcs  /out:tzi.exe /unsafe "/d:INSIDE_CORLIB;MONODROID;NET_4_0;LIBC;SELF_TEST" System/TimeZone*.cs ../../build/common/Consts.cs ../Mono.Options/Mono.Options/Options.cs
+                        * Prep:
+                        *    mkdir -p usr/share/zoneinfo
+                        *    android_root=`adb shell echo '$ANDROID_ROOT' | tr -d "\r"`
+                        *    adb pull $android_root/usr/share/zoneinfo usr/share/zoneinfo
+                        * Run:
+                        *    ANDROID_ROOT=`pwd` mono tzi.exe
+                        */
+                       static void Main (string[] args)
+                       {
+                               Func<IAndroidTimeZoneDB> c = () => GetDefaultTimeZoneDB ();
+                               Mono.Options.OptionSet p = null;
+                               p = new Mono.Options.OptionSet () {
+                                       { "T=", "Create AndroidTzData from {PATH}.", v => {
+                                                       c = () => new AndroidTzData (v);
+                                       } },
+                                       { "Z=", "Create ZoneInfoDB from {DIR}.", v => {
+                                                       c = () => new ZoneInfoDB (v);
+                                       } },
+                                       { "help", "Show this message and exit", v => {
+                                                       p.WriteOptionDescriptions (Console.Out);
+                                                       Environment.Exit (0);
+                                       } },
+                               };
+                               p.Parse (args);
+                               AndroidTimeZones.db = c ();
+                               Console.WriteLine ("DB type: {0}", AndroidTimeZones.db.GetType ().FullName);
+                               foreach (var id in GetAvailableIds ()) {
+                                       Console.Write ("name={0,-40}", id);
+                                       try {
+                                               TimeZoneInfo zone = _GetTimeZone (id);
+                                               if (zone != null)
+                                                       Console.Write (" {0}", zone);
+                                               else {
+                                                       Console.Write (" ERROR:null");
+                                               }
+                                       } catch (Exception e) {
+                                               Console.WriteLine ();
+                                               Console.Write ("ERROR: {0}", e);
+                                       }
+                                       Console.WriteLine ();
+                               }
+                       }
+#endif
                }
        }
 }