TimeZoneInfo.Android.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
  1. /*
  2. * System.TimeZoneInfo Android Support
  3. *
  4. * Author(s)
  5. * Jonathan Pryor <[email protected]>
  6. * The Android Open Source Project
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. #if (INSIDE_CORLIB && MONODROID)
  21. using System;
  22. using System.Collections.Generic;
  23. using System.IO;
  24. using System.Runtime.CompilerServices;
  25. using System.Runtime.InteropServices;
  26. using System.Text;
  27. namespace System {
  28. interface IAndroidTimeZoneDB {
  29. IEnumerable<string> GetAvailableIds ();
  30. byte[] GetTimeZoneData (string id);
  31. }
  32. [StructLayout (LayoutKind.Sequential, Pack=1)]
  33. unsafe struct AndroidTzDataHeader {
  34. public fixed byte signature [12];
  35. public int indexOffset;
  36. public int dataOffset;
  37. public int zoneTabOffset;
  38. }
  39. [StructLayout (LayoutKind.Sequential, Pack=1)]
  40. unsafe struct AndroidTzDataEntry {
  41. public fixed byte id [40];
  42. public int byteOffset;
  43. public int length;
  44. public int rawUtcOffset;
  45. }
  46. /*
  47. * Android v4.3 Timezone support infrastructure.
  48. *
  49. * This is a C# port of libcore.util.ZoneInfoDB:
  50. *
  51. * https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/util/ZoneInfoDB.java
  52. *
  53. * This is needed in order to read Android v4.3 tzdata files.
  54. */
  55. sealed class AndroidTzData : IAndroidTimeZoneDB {
  56. internal static readonly string[] Paths = new string[]{
  57. Environment.GetEnvironmentVariable ("ANDROID_DATA") + "/misc/zoneinfo/tzdata",
  58. Environment.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata",
  59. };
  60. string tzdataPath;
  61. Stream data;
  62. string version;
  63. string zoneTab;
  64. string[] ids;
  65. int[] byteOffsets;
  66. int[] lengths;
  67. public AndroidTzData (params string[] paths)
  68. {
  69. foreach (var path in paths)
  70. if (LoadData (path)) {
  71. tzdataPath = path;
  72. return;
  73. }
  74. Console.Error.WriteLine ("Couldn't find any tzdata!");
  75. tzdataPath = "/";
  76. version = "missing";
  77. zoneTab = "# Emergency fallback data.\n";
  78. ids = new[]{ "GMT" };
  79. }
  80. public string Version {
  81. get {return version;}
  82. }
  83. public string ZoneTab {
  84. get {return zoneTab;}
  85. }
  86. bool LoadData (string path)
  87. {
  88. if (!File.Exists (path))
  89. return false;
  90. try {
  91. data = File.OpenRead (path);
  92. } catch (IOException) {
  93. return false;
  94. } catch (UnauthorizedAccessException) {
  95. return false;
  96. }
  97. try {
  98. ReadHeader ();
  99. return true;
  100. } catch (Exception e) {
  101. Console.Error.WriteLine ("tzdata file \"{0}\" was present but invalid: {1}", path, e);
  102. }
  103. return false;
  104. }
  105. unsafe void ReadHeader ()
  106. {
  107. int size = System.Math.Max (Marshal.SizeOf (typeof (AndroidTzDataHeader)), Marshal.SizeOf (typeof (AndroidTzDataEntry)));
  108. var buffer = new byte [size];
  109. var header = ReadAt<AndroidTzDataHeader>(0, buffer);
  110. header.indexOffset = NetworkToHostOrder (header.indexOffset);
  111. header.dataOffset = NetworkToHostOrder (header.dataOffset);
  112. header.zoneTabOffset = NetworkToHostOrder (header.zoneTabOffset);
  113. sbyte* s = (sbyte*) header.signature;
  114. string magic = new string (s, 0, 6, Encoding.ASCII);
  115. if (magic != "tzdata" || header.signature [11] != 0) {
  116. var b = new StringBuilder ();
  117. b.Append ("bad tzdata magic:");
  118. for (int i = 0; i < 12; ++i) {
  119. b.Append (" ").Append (((byte) s [i]).ToString ("x2"));
  120. }
  121. throw new InvalidOperationException ("bad tzdata magic: " + b.ToString ());
  122. }
  123. version = new string (s, 6, 5, Encoding.ASCII);
  124. ReadIndex (header.indexOffset, header.dataOffset, buffer);
  125. ReadZoneTab (header.zoneTabOffset, checked ((int) data.Length) - header.zoneTabOffset);
  126. }
  127. unsafe T ReadAt<T> (long position, byte[] buffer)
  128. where T : struct
  129. {
  130. int size = Marshal.SizeOf (typeof (T));
  131. if (buffer.Length < size)
  132. throw new InvalidOperationException ("Internal error: buffer too small");
  133. data.Position = position;
  134. int r;
  135. if ((r = data.Read (buffer, 0, size)) < size)
  136. throw new InvalidOperationException (
  137. string.Format ("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath, r, size));
  138. fixed (byte* b = buffer)
  139. return (T) Marshal.PtrToStructure ((IntPtr) b, typeof (T));
  140. }
  141. static int NetworkToHostOrder (int value)
  142. {
  143. if (!BitConverter.IsLittleEndian)
  144. return value;
  145. return
  146. (((value >> 24) & 0xFF) |
  147. ((value >> 08) & 0xFF00) |
  148. ((value << 08) & 0xFF0000) |
  149. ((value << 24)));
  150. }
  151. unsafe void ReadIndex (int indexOffset, int dataOffset, byte[] buffer)
  152. {
  153. int indexSize = dataOffset - indexOffset;
  154. int entryCount = indexSize / Marshal.SizeOf (typeof (AndroidTzDataEntry));
  155. int entrySize = Marshal.SizeOf (typeof (AndroidTzDataEntry));
  156. byteOffsets = new int [entryCount];
  157. ids = new string [entryCount];
  158. lengths = new int [entryCount];
  159. for (int i = 0; i < entryCount; ++i) {
  160. var entry = ReadAt<AndroidTzDataEntry>(indexOffset + (entrySize*i), buffer);
  161. var p = (sbyte*) entry.id;
  162. byteOffsets [i] = NetworkToHostOrder (entry.byteOffset) + dataOffset;
  163. ids [i] = new string (p, 0, GetStringLength (p, 40), Encoding.ASCII);
  164. lengths [i] = NetworkToHostOrder (entry.length);
  165. if (lengths [i] < Marshal.SizeOf (typeof (AndroidTzDataHeader)))
  166. throw new InvalidOperationException ("Length in index file < sizeof(tzhead)");
  167. }
  168. }
  169. static unsafe int GetStringLength (sbyte* s, int maxLength)
  170. {
  171. int len;
  172. for (len = 0; len < maxLength; len++, s++) {
  173. if (*s == 0)
  174. break;
  175. }
  176. return len;
  177. }
  178. unsafe void ReadZoneTab (int zoneTabOffset, int zoneTabSize)
  179. {
  180. byte[] zoneTab = new byte [zoneTabSize];
  181. data.Position = zoneTabOffset;
  182. int r;
  183. if ((r = data.Read (zoneTab, 0, zoneTab.Length)) < zoneTab.Length)
  184. throw new InvalidOperationException (
  185. string.Format ("Error reading zonetab: read {0} bytes, expected {1}", r, zoneTabSize));
  186. this.zoneTab = Encoding.ASCII.GetString (zoneTab, 0, zoneTab.Length);
  187. }
  188. public IEnumerable<string> GetAvailableIds ()
  189. {
  190. return ids;
  191. }
  192. public byte[] GetTimeZoneData (string id)
  193. {
  194. int i = Array.BinarySearch (ids, id, StringComparer.Ordinal);
  195. if (i < 0)
  196. return null;
  197. int offset = byteOffsets [i];
  198. int length = lengths [i];
  199. var buffer = new byte [length];
  200. lock (data) {
  201. data.Position = offset;
  202. int r;
  203. if ((r = data.Read (buffer, 0, buffer.Length)) < buffer.Length)
  204. throw new InvalidOperationException (
  205. string.Format ("Unable to fully read from file '{0}' at offset {1} length {2}; read {3} bytes expected {4}.",
  206. tzdataPath, offset, length, r, buffer.Length));
  207. }
  208. return buffer;
  209. }
  210. }
  211. partial class TimeZoneInfo {
  212. /*
  213. * Android < v4.3 Timezone support infrastructure.
  214. *
  215. * This is a C# port of org.apache.harmony.luni.internal.util.ZoneInfoDB:
  216. *
  217. * http://android.git.kernel.org/?p=platform/libcore.git;a=blob;f=luni/src/main/java/org/apache/harmony/luni/internal/util/ZoneInfoDB.java;h=3e7bdc3a952b24da535806d434a3a27690feae26;hb=HEAD
  218. *
  219. * From the ZoneInfoDB source:
  220. *
  221. * However, to conserve disk space the data for all time zones are
  222. * concatenated into a single file, and a second file is used to indicate
  223. * the starting position of each time zone record. A third file indicates
  224. * the version of the zoneinfo databse used to generate the data.
  225. *
  226. * which succinctly describes why we can't just use the LIBC implementation in
  227. * TimeZoneInfo.cs -- the "standard Unixy" directory structure is NOT used.
  228. */
  229. sealed class ZoneInfoDB : IAndroidTimeZoneDB {
  230. const int TimeZoneNameLength = 40;
  231. const int TimeZoneIntSize = 4;
  232. internal static readonly string ZoneDirectoryName = Environment.GetEnvironmentVariable ("ANDROID_ROOT") + "/usr/share/zoneinfo/";
  233. const string ZoneFileName = "zoneinfo.dat";
  234. const string IndexFileName = "zoneinfo.idx";
  235. const string DefaultVersion = "2007h";
  236. const string VersionFileName = "zoneinfo.version";
  237. readonly string zoneRoot;
  238. readonly string version;
  239. readonly string[] names;
  240. readonly int[] starts;
  241. readonly int[] lengths;
  242. readonly int[] offsets;
  243. public ZoneInfoDB (string zoneInfoDB = null)
  244. {
  245. zoneRoot = zoneInfoDB ?? ZoneDirectoryName;
  246. try {
  247. version = ReadVersion (Path.Combine (zoneRoot, VersionFileName));
  248. } catch {
  249. version = DefaultVersion;
  250. }
  251. try {
  252. ReadDatabase (Path.Combine (zoneRoot, IndexFileName), out names, out starts, out lengths, out offsets);
  253. } catch {
  254. names = new string [0];
  255. starts = new int [0];
  256. lengths = new int [0];
  257. offsets = new int [0];
  258. }
  259. }
  260. static string ReadVersion (string path)
  261. {
  262. using (var file = new StreamReader (path, Encoding.GetEncoding ("iso-8859-1"))) {
  263. return file.ReadToEnd ().Trim ();
  264. }
  265. }
  266. void ReadDatabase (string path, out string[] names, out int[] starts, out int[] lengths, out int[] offsets)
  267. {
  268. using (var file = File.OpenRead (path)) {
  269. var nbuf = new byte [TimeZoneNameLength];
  270. int numEntries = (int) (file.Length / (TimeZoneNameLength + 3*TimeZoneIntSize));
  271. char[] namebuf = new char [TimeZoneNameLength];
  272. names = new string [numEntries];
  273. starts = new int [numEntries];
  274. lengths = new int [numEntries];
  275. offsets = new int [numEntries];
  276. for (int i = 0; i < numEntries; ++i) {
  277. Fill (file, nbuf, nbuf.Length);
  278. int namelen;
  279. for (namelen = 0; namelen < nbuf.Length; ++namelen) {
  280. if (nbuf [namelen] == '\0')
  281. break;
  282. namebuf [namelen] = (char) (nbuf [namelen] & 0xFF);
  283. }
  284. names [i] = new string (namebuf, 0, namelen);
  285. starts [i] = ReadInt32 (file, nbuf);
  286. lengths [i] = ReadInt32 (file, nbuf);
  287. offsets [i] = ReadInt32 (file, nbuf);
  288. }
  289. }
  290. }
  291. static void Fill (Stream stream, byte[] nbuf, int required)
  292. {
  293. int read = 0, offset = 0;
  294. while (offset < required && (read = stream.Read (nbuf, offset, required - offset)) > 0)
  295. offset += read;
  296. if (read != required)
  297. throw new EndOfStreamException ("Needed to read " + required + " bytes; read " + read + " bytes");
  298. }
  299. // From java.io.RandomAccessFioe.readInt(), as we need to use the same
  300. // byte ordering as Java uses.
  301. static int ReadInt32 (Stream stream, byte[] nbuf)
  302. {
  303. Fill (stream, nbuf, 4);
  304. return ((nbuf [0] & 0xff) << 24) + ((nbuf [1] & 0xff) << 16) +
  305. ((nbuf [2] & 0xff) << 8) + (nbuf [3] & 0xff);
  306. }
  307. internal string Version {
  308. get {return version;}
  309. }
  310. public IEnumerable<string> GetAvailableIds ()
  311. {
  312. return GetAvailableIds (0, false);
  313. }
  314. IEnumerable<string> GetAvailableIds (int rawOffset)
  315. {
  316. return GetAvailableIds (rawOffset, true);
  317. }
  318. IEnumerable<string> GetAvailableIds (int rawOffset, bool checkOffset)
  319. {
  320. for (int i = 0; i < offsets.Length; ++i) {
  321. if (!checkOffset || offsets [i] == rawOffset)
  322. yield return names [i];
  323. }
  324. }
  325. public byte[] GetTimeZoneData (string id)
  326. {
  327. int start, length;
  328. using (var stream = GetTimeZoneData (id, out start, out length)) {
  329. if (stream == null)
  330. return null;
  331. byte[] buf = new byte [length];
  332. Fill (stream, buf, buf.Length);
  333. return buf;
  334. }
  335. }
  336. FileStream GetTimeZoneData (string name, out int start, out int length)
  337. {
  338. if (name == null) { // Just in case, to avoid NREX as in xambug #4902
  339. start = 0;
  340. length = 0;
  341. return null;
  342. }
  343. var f = new FileInfo (Path.Combine (zoneRoot, name));
  344. if (f.Exists) {
  345. start = 0;
  346. length = (int) f.Length;
  347. return f.OpenRead ();
  348. }
  349. start = length = 0;
  350. int i = Array.BinarySearch (names, name, StringComparer.Ordinal);
  351. if (i < 0)
  352. return null;
  353. start = starts [i];
  354. length = lengths [i];
  355. var stream = File.OpenRead (Path.Combine (zoneRoot, ZoneFileName));
  356. stream.Seek (start, SeekOrigin.Begin);
  357. return stream;
  358. }
  359. }
  360. static class AndroidTimeZones {
  361. static IAndroidTimeZoneDB db;
  362. static AndroidTimeZones ()
  363. {
  364. db = GetDefaultTimeZoneDB ();
  365. }
  366. static IAndroidTimeZoneDB GetDefaultTimeZoneDB ()
  367. {
  368. foreach (var p in AndroidTzData.Paths)
  369. if (File.Exists (p))
  370. return new AndroidTzData (AndroidTzData.Paths);
  371. if (Directory.Exists (ZoneInfoDB.ZoneDirectoryName))
  372. return new ZoneInfoDB ();
  373. return null;
  374. }
  375. internal static IEnumerable<string> GetAvailableIds ()
  376. {
  377. return db == null
  378. ? new string [0]
  379. : db.GetAvailableIds ();
  380. }
  381. static TimeZoneInfo _GetTimeZone (string name)
  382. {
  383. if (db == null)
  384. return null;
  385. byte[] buffer = db.GetTimeZoneData (name);
  386. if (buffer == null)
  387. return null;
  388. return TimeZoneInfo.ParseTZBuffer (name, buffer, buffer.Length);
  389. }
  390. internal static TimeZoneInfo GetTimeZone (string id)
  391. {
  392. if (id != null) {
  393. if (id == "GMT" || id == "UTC")
  394. return new TimeZoneInfo (id, TimeSpan.FromSeconds (0), id, id, id, null, true);
  395. if (id.StartsWith ("GMT"))
  396. return new TimeZoneInfo (id,
  397. TimeSpan.FromSeconds (ParseNumericZone (id)),
  398. id, id, id, null, true);
  399. }
  400. try {
  401. return _GetTimeZone (id);
  402. } catch (Exception) {
  403. return null;
  404. }
  405. }
  406. static int ParseNumericZone (string name)
  407. {
  408. if (name == null || !name.StartsWith ("GMT") || name.Length <= 3)
  409. return 0;
  410. int sign;
  411. if (name [3] == '+')
  412. sign = 1;
  413. else if (name [3] == '-')
  414. sign = -1;
  415. else
  416. return 0;
  417. int where;
  418. int hour = 0;
  419. bool colon = false;
  420. for (where = 4; where < name.Length; where++) {
  421. char c = name [where];
  422. if (c == ':') {
  423. where++;
  424. colon = true;
  425. break;
  426. }
  427. if (c >= '0' && c <= '9')
  428. hour = hour * 10 + c - '0';
  429. else
  430. return 0;
  431. }
  432. int min = 0;
  433. for (; where < name.Length; where++) {
  434. char c = name [where];
  435. if (c >= '0' && c <= '9')
  436. min = min * 10 + c - '0';
  437. else
  438. return 0;
  439. }
  440. if (colon)
  441. return sign * (hour * 60 + min) * 60;
  442. else if (hour >= 100)
  443. return sign * ((hour / 100) * 60 + (hour % 100)) * 60;
  444. else
  445. return sign * (hour * 60) * 60;
  446. }
  447. static readonly object _lock = new object ();
  448. static TimeZoneInfo defaultZone;
  449. internal static TimeZoneInfo Default {
  450. get {
  451. lock (_lock) {
  452. if (defaultZone != null)
  453. return defaultZone;
  454. return defaultZone = GetTimeZone (GetDefaultTimeZoneName ());
  455. }
  456. }
  457. }
  458. [DllImport ("__Internal")]
  459. static extern int monodroid_get_system_property (string name, ref IntPtr value);
  460. [DllImport ("__Internal")]
  461. static extern void monodroid_free (IntPtr ptr);
  462. static string GetDefaultTimeZoneName ()
  463. {
  464. IntPtr value = IntPtr.Zero;
  465. int n = 0;
  466. string defaultTimeZone;
  467. // Used by the tests
  468. if (Environment.GetEnvironmentVariable ("__XA_USE_JAVA_DEFAULT_TIMEZONE_ID__") == null)
  469. n = monodroid_get_system_property ("persist.sys.timezone", ref value);
  470. if (n > 0 && value != IntPtr.Zero) {
  471. defaultTimeZone = (Marshal.PtrToStringAnsi (value) ?? String.Empty).Trim ();
  472. monodroid_free (value);
  473. if (!String.IsNullOrEmpty (defaultTimeZone))
  474. return defaultTimeZone;
  475. }
  476. defaultTimeZone = (AndroidPlatform.GetDefaultTimeZone () ?? String.Empty).Trim ();
  477. if (!String.IsNullOrEmpty (defaultTimeZone))
  478. return defaultTimeZone;
  479. return null;
  480. }
  481. #if SELF_TEST
  482. /*
  483. * Compile:
  484. * 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
  485. * Prep:
  486. * mkdir -p usr/share/zoneinfo
  487. * android_root=`adb shell echo '$ANDROID_ROOT' | tr -d "\r"`
  488. * adb pull $android_root/usr/share/zoneinfo usr/share/zoneinfo
  489. * Run:
  490. * ANDROID_ROOT=`pwd` mono tzi.exe
  491. */
  492. static void Main (string[] args)
  493. {
  494. Func<IAndroidTimeZoneDB> c = () => GetDefaultTimeZoneDB ();
  495. Mono.Options.OptionSet p = null;
  496. p = new Mono.Options.OptionSet () {
  497. { "T=", "Create AndroidTzData from {PATH}.", v => {
  498. c = () => new AndroidTzData (v);
  499. } },
  500. { "Z=", "Create ZoneInfoDB from {DIR}.", v => {
  501. c = () => new ZoneInfoDB (v);
  502. } },
  503. { "help", "Show this message and exit", v => {
  504. p.WriteOptionDescriptions (Console.Out);
  505. Environment.Exit (0);
  506. } },
  507. };
  508. p.Parse (args);
  509. AndroidTimeZones.db = c ();
  510. Console.WriteLine ("DB type: {0}", AndroidTimeZones.db.GetType ().FullName);
  511. foreach (var id in GetAvailableIds ()) {
  512. Console.Write ("name={0,-40}", id);
  513. try {
  514. TimeZoneInfo zone = _GetTimeZone (id);
  515. if (zone != null)
  516. Console.Write (" {0}", zone);
  517. else {
  518. Console.Write (" ERROR:null");
  519. }
  520. } catch (Exception e) {
  521. Console.WriteLine ();
  522. Console.Write ("ERROR: {0}", e);
  523. }
  524. Console.WriteLine ();
  525. }
  526. }
  527. #endif
  528. }
  529. }
  530. }
  531. #endif // MONODROID