Skip to content

Commit

Permalink
[monodroid] Add support for the Android TimeZone file format.
Browse files Browse the repository at this point in the history
Fixes #657609.

Android uses "ye standard" timezone file format, but instead of using
a directory + file structure as libc uses, they throw everything into
two files, a "zoneinfo.dat" and a "zoneinfo.idx", where "zoneinfo.dat"
is the concatenation of all the TZIF files and "zoneinfo.idx" contains
the timezone names and offsets into "zoneinfo.dat".  From the
ZoneInfoDB documentation:

	However, to conserve disk space the data for all time zones
	are concatenated into a single file, and a second file is
	used to indicate the starting position of each time zone
	record.  A third file indicates the version of the zoneinfo
	databse used to generate the data.

TimeZoneInfo.Android.cs is a C# port of the corresponding Android
ZoneInfoDB type so that Mono can use Android's timezone DB.
  • Loading branch information
jonpryor committed Dec 16, 2010
1 parent 8c686c3 commit 8a26398
Show file tree
Hide file tree
Showing 4 changed files with 320 additions and 5 deletions.
7 changes: 7 additions & 0 deletions LICENSE
Expand Up @@ -98,6 +98,13 @@ For comments, corrections and updates, please contact mono@novell.com
ICSharpCode.SharpZipLib, GPL with exceptions.
See: mcs/class/ICSharpCode.SharpZipLib/README

** mcs/class/System.Core/System/TimeZoneInfo.Android.cs

This is a port of Apache 2.0-licensed Android code, and thus is
licensed under the Apache 2.0 license:

http://www.apache.org/licenses/LICENSE-2.0


** mcs/tools

Expand Down
299 changes: 299 additions & 0 deletions mcs/class/System.Core/System/TimeZoneInfo.Android.cs
@@ -0,0 +1,299 @@
/*
* System.TimeZoneInfo Android Support
*
* Author(s)
* Jonathan Pryor <jpryor@novell.com>
* The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#if MONODROID

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace System {

partial class TimeZoneInfo {

/*
* Android Timezone support infrastructure.
*
* This is a C# port of org.apache.harmony.luni.internal.util.ZoneInfoDB:
*
* 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
*
* From the ZoneInfoDB source:
*
* However, to conserve disk space the data for all time zones are
* concatenated into a single file, and a second file is used to indicate
* the starting position of each time zone record. A third file indicates
* the version of the zoneinfo databse used to generate the data.
*
* 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 {
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";

static readonly object _lock = new object ();

static readonly string version;
static readonly string[] names;
static readonly int[] starts;
static readonly int[] lengths;
static readonly int[] offsets;

static ZoneInfoDB ()
{
try {
version = ReadVersion ();
} catch {
version = DefaultVersion;
}

try {
ReadDatabase (out names, out starts, out lengths, out offsets);
} catch {
names = new string [0];
starts = new int [0];
lengths = new int [0];
offsets = new int [0];
}
}

static string ReadVersion ()
{
using (var file = new StreamReader (VersionFileName, Encoding.GetEncoding ("iso-8859-1"))) {
return file.ReadToEnd ().Trim ();
}
}

static void ReadDatabase (out string[] names, out int[] starts, out int[] lengths, out int[] offsets)
{
using (var file = File.OpenRead (IndexFileName)) {
var nbuf = new byte [TimeZoneNameLength];

int numEntries = (int) (file.Length / (TimeZoneNameLength + 3*TimeZoneIntSize));

char[] namebuf = new char [TimeZoneNameLength];

names = new string [numEntries];
starts = new int [numEntries];
lengths = new int [numEntries];
offsets = new int [numEntries];

for (int i = 0; i < numEntries; ++i) {
Fill (file, nbuf, nbuf.Length);
int namelen;
for (namelen = 0; namelen < nbuf.Length; ++namelen) {
if (nbuf [namelen] == '\0')
break;
namebuf [namelen] = (char) (nbuf [namelen] & 0xFF);
}

names [i] = new string (namebuf, 0, namelen);
starts [i] = ReadInt32 (file, nbuf);
lengths [i] = ReadInt32 (file, nbuf);
offsets [i] = ReadInt32 (file, nbuf);
}
}
}

static void Fill (Stream stream, byte[] nbuf, int required)
{
int read, offset = 0;
while (offset < required && (read = stream.Read (nbuf, offset, required - offset)) > 0)
offset += read;
if (read != required)
throw new EndOfStreamException ("Needed to read " + required + " bytes; read " + read + " bytes");
}

// From java.io.RandomAccessFioe.readInt(), as we need to use the same
// byte ordering as Java uses.
static int ReadInt32 (Stream stream, byte[] nbuf)
{
Fill (stream, nbuf, 4);
return ((nbuf [0] & 0xff) << 24) + ((nbuf [1] & 0xff) << 16) +
((nbuf [2] & 0xff) << 8) + (nbuf [3] & 0xff);
}

internal static string Version {
get {return version;}
}

internal static IEnumerable<string> GetAvailableIds ()
{
return GetAvailableIds (0, false);
}

internal static IEnumerable<string> GetAvailableIds (int rawOffset)
{
return GetAvailableIds (rawOffset, true);
}

static IEnumerable<string> GetAvailableIds (int rawOffset, bool checkOffset)
{
for (int i = 0; i < offsets.Length; ++i) {
if (!checkOffset || offsets [i] == rawOffset)
yield return names [i];
}
}

static TimeZoneInfo _GetTimeZone (string name)
{
int start, length;
using (var stream = GetTimeZoneData (name, 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);
}
}

static FileStream GetTimeZoneData (string name, out int start, out int length)
{
var f = new FileInfo (Path.Combine (ZoneDirectoryName, name));
if (f.Exists) {
start = 0;
length = (int) f.Length;
return f.OpenRead ();
}

start = length = 0;

int i = Array.BinarySearch (names, name);
if (i < 0)
return null;

start = starts [i];
length = lengths [i];

var stream = File.OpenRead (ZoneFileName);
stream.Seek (start, SeekOrigin.Begin);

return stream;
}

internal static TimeZoneInfo GetTimeZone (string id)
{
if (id != null) {
if (id == "GMT" || id == "UTC")
return new TimeZoneInfo (id, TimeSpan.FromSeconds (0), id, id, id, null, true);
if (id.StartsWith ("GMT"))
return new TimeZoneInfo (id,
TimeSpan.FromSeconds (ParseNumericZone (id)),
id, id, id, null, true);
}

try {
return _GetTimeZone (id);
} catch (Exception e) {
return null;
}
}

static int ParseNumericZone (string name)
{
if (name == null || !name.StartsWith ("GMT") || name.Length <= 3)
return 0;

int sign;
if (name [3] == '+')
sign = 1;
else if (name [3] == '-')
sign = -1;
else
return 0;

int where;
int hour = 0;
bool colon = false;
for (where = 4; where < name.Length; where++) {
char c = name [where];

if (c == ':') {
where++;
colon = true;
break;
}

if (c >= '0' && c <= '9')
hour = hour * 10 + c - '0';
else
return 0;
}

int min = 0;
for (; where < name.Length; where++) {
char c = name [where];

if (c >= '0' && c <= '9')
min = min * 10 + c - '0';
else
return 0;
}

if (colon)
return sign * (hour * 60 + min) * 60;
else if (hour >= 100)
return sign * ((hour / 100) * 60 + (hour % 100)) * 60;
else
return sign * (hour * 60) * 60;
}

static TimeZoneInfo defaultZone;
internal static TimeZoneInfo Default {
get {
lock (_lock) {
if (defaultZone != null)
return defaultZone;
return defaultZone = GetTimeZone (GetDefaultTimeZoneName ());
}
}
}

// <sys/system_properties.h>
[DllImport ("/system/lib/libc.so")]
static extern int __system_property_get (string name, StringBuilder value);

const int MaxPropertyNameLength = 32; // <sys/system_properties.h>
const int MaxPropertyValueLength = 92; // <sys/system_properties.h>

static string GetDefaultTimeZoneName ()
{
var buf = new StringBuilder (MaxPropertyValueLength + 1);
int n = __system_property_get ("persist.sys.timezone", buf);
if (n > 0)
return buf.ToString ();
return null;
}
}
}
}

#endif // MONODROID

18 changes: 13 additions & 5 deletions mcs/class/System.Core/System/TimeZoneInfo.cs
Expand Up @@ -35,10 +35,11 @@

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;

#if LIBC
#if LIBC || MONODROID
using System.IO;
using Mono;
#endif
Expand Down Expand Up @@ -83,7 +84,9 @@ public sealed partial class TimeZoneInfo : IEquatable<TimeZoneInfo>, ISerializab
public static TimeZoneInfo Local {
get {
if (local == null) {
#if LIBC
#if MONODROID
local = ZoneInfoDB.Default;
#elif LIBC
try {
local = FindSystemTimeZoneByFileName ("Local", "/etc/localtime");
} catch {
Expand Down Expand Up @@ -320,7 +323,9 @@ public static TimeZoneInfo FindSystemTimeZoneById (string id)
return FromRegistryKey(id, key);
}
#endif
#if LIBC
#if MONODROID
return ZoneInfoDB.GetTimeZone (id);
#elif LIBC
string filepath = Path.Combine (TimeZoneDirectory, id);
return FindSystemTimeZoneByFileName (id, filepath);
#else
Expand Down Expand Up @@ -522,7 +527,10 @@ public static ReadOnlyCollection<TimeZoneInfo> GetSystemTimeZones ()
return new ReadOnlyCollection<TimeZoneInfo> (systemTimeZones);
}
#endif
#if LIBC
#if MONODROID
systemTimeZones.AddRange (ZoneInfoDB.GetAvailableIds ()
.Select (id => ZoneInfoDB.GetTimeZone (id)));
#elif LIBC
string[] continents = new string [] {"Africa", "America", "Antarctica", "Arctic", "Asia", "Atlantic", "Brazil", "Canada", "Chile", "Europe", "Indian", "Mexico", "Mideast", "Pacific", "US"};
foreach (string continent in continents) {
try {
Expand Down Expand Up @@ -782,7 +790,7 @@ static List<AdjustmentRule> ValidateRules (List<AdjustmentRule> adjustmentRules)
return adjustmentRules;
}

#if LIBC
#if LIBC || MONODROID
private static bool ValidTZFile (byte [] buffer, int length)
{
StringBuilder magic = new StringBuilder ();
Expand Down
1 change: 1 addition & 0 deletions mcs/class/System.Core/monodroid_System.Core.dll.sources
@@ -1 +1,2 @@
#include monodroid_bootstrap_System.Core.dll.sources
System/TimeZoneInfo.Android.cs

0 comments on commit 8a26398

Please sign in to comment.