Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
1014cc6
Initial Android implementation
Jun 24, 2021
d161105
Split implementations for Android specific GetTimeZoneIds
Jun 25, 2021
cfa0b33
Differentiate Unix and Android TimeZoneInfo by PopulateAllSystemTimeZ…
Jun 25, 2021
caa39a6
Utilize Android paths for timezone data
Jun 28, 2021
ee51c67
Clean up mono style to match runtime style
Jun 28, 2021
ce91611
Pulled in parsing implementation from mono and broke out GetLocalTime…
Jun 29, 2021
4ed8616
TimeZoneInfo.Android builds properly now. Found PopulateAllSystemTim…
Jun 29, 2021
555d0b5
Found TryGetTimeZoneFromLocalMachine to map to FindSystemTimeZoneById…
Jun 29, 2021
d59fb1a
Fixed typo on type
Jun 29, 2021
84dcbb7
Added cacheddata param
Jun 30, 2021
cb9efd4
Initial implementation of PopulateAllSystemTimeZonesCore and TryGetTi…
Jun 30, 2021
b68e290
Clean up missing variables and wrong function calls
Jun 30, 2021
65615ca
Fix cachedData parameter overload
Jun 30, 2021
514fa44
Add JNI GetDefaultTimeZone
Jun 30, 2021
6c3e567
Remove excess of imports in pal_datetime
Jun 30, 2021
3787086
Clean up PopulateAllSystemTimeZonesCore comments
Jun 30, 2021
912ab4d
Remove ParseTZBuffer middleman
Jun 30, 2021
d67920e
Add id check and runtime style to _GetTimeZone
Jun 30, 2021
b602bf7
cleanup pal_datetime imports
Jun 30, 2021
caa9229
Remove ZoneTab related code
Jun 30, 2021
a7aee33
Remove Version related code
Jun 30, 2021
fb72a04
Remove PopulateAllSystemTimeZonesCore middleman
Jun 30, 2021
c3f1e18
Add return for non Android
Jul 1, 2021
c314363
Avoid using static constructor for paths
Jul 1, 2021
0890a23
Remove redundant using
Jul 1, 2021
80c28e8
Fix GetTimeZoneIds return
Jul 1, 2021
858ef84
Condition TargetsAndroid around function
Jul 1, 2021
e062265
Clean up functions
Jul 1, 2021
3956ff7
Remove GetTimeZoneDirectory Android implementation
Jul 1, 2021
18c4955
Remove interface
Jul 1, 2021
37c0a0c
Cleanup style
Jul 1, 2021
98ee19d
Remove zoneTabOffset
Jul 1, 2021
fc8ce4a
Temporarily add GetTimeZoneDirectory
Jul 2, 2021
c4397b7
Refactoring code
Jul 2, 2021
7188c6b
Address some feedback
Jul 2, 2021
3198101
Move interop to its own file
Jul 2, 2021
1a495cc
Rename variable
Jul 2, 2021
6e40f61
Refactor to modify access from internal to private
Jul 6, 2021
4c82a11
Remove static Paths, pass tzFilePath as parameter instead of holding
Jul 6, 2021
3cc1da1
Add ordinal comparison for performance gain
Jul 6, 2021
4df973a
Add Softcode Local
Jul 6, 2021
b4d5ba6
Softcode tzdata file path
Jul 6, 2021
2bb25ce
Refactor Unix specific code
Jul 6, 2021
efa339e
Lazy allocation of Android TimeZone Data
Jul 6, 2021
54e3218
Fix up imports
Jul 7, 2021
144f4dd
Apply coding style
Jul 7, 2021
0790650
More clean up
Jul 7, 2021
5f4fae7
Rename property
Jul 8, 2021
d1070d1
Update TimeZone data header and entry structs
Jul 8, 2021
e20c6cb
Refactor header correctness check
Jul 8, 2021
4b31684
Fix resource string message
Jul 8, 2021
2616ca0
Revert to original header check
Jul 8, 2021
ecf6f70
Cleanup comments
Jul 8, 2021
3b424dc
Expect Ansi strings
Jul 9, 2021
ed3355e
Fix casing
Jul 9, 2021
2bfb9e4
Stackalloc tzdata buffer
Jul 9, 2021
5edd7ea
Remove redundant exception message
Jul 9, 2021
966aee5
Remove nullable from fields
Jul 9, 2021
290caa2
Add missing namechange
Jul 9, 2021
fc551d2
Pass stream through ReadHeader and only open file once
Jul 9, 2021
6fb3133
Rename files
Jul 9, 2021
6ccad5d
Avoid unnecessary allocation
Jul 9, 2021
0987ee7
Assert buffer size over creating resource exception
Jul 9, 2021
69d5a84
Merge remote-tracking branch 'upstream/main' into port_android_timezo…
Jul 12, 2021
8d88a6d
Use compare exchange atomic method
Jul 13, 2021
4433be6
Reduce redundant calls
Jul 13, 2021
8330c2a
Cleanup output variables
Jul 13, 2021
b8bcce4
Remove comments
Jul 13, 2021
37b80ca
Use sizeof over Marshal
Jul 13, 2021
8891709
Address feedback
Jul 13, 2021
15d4455
Address Feedback
Jul 14, 2021
6501d7e
Address more feedback
Jul 16, 2021
a63f8f9
Separate buffer loading into independent method
Jul 16, 2021
16289d2
Properly set where to write into the buffer
Jul 16, 2021
d5ffcef
Update comments
Jul 16, 2021
311ecaa
Remove AndroidTzDataHeader and fix typos
Jul 16, 2021
a07da96
Add const keyword, move field declaration, use length comparison
Jul 19, 2021
a47ade2
Change data entry id to string
Jul 19, 2021
cc06d96
Parse id account for null terminating character
Jul 19, 2021
00b0abe
Remove AndroidTzDataEntry struct and use out parameters
Jul 19, 2021
5290490
Remove unsafe keywords
Jul 19, 2021
95f30f6
Close file handle in GetTimeZoneData
Jul 19, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Remove static Paths, pass tzFilePath as parameter instead of holding
  • Loading branch information
Mitchell Hwang committed Jul 6, 2021
commit 4c82a11a4754a290ef97414a02547da199d233a0
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,48 @@ private static TimeZoneInfo GetLocalTimeZoneCore()
return Utc;
}

// TODO could check all paths in AndroidTzData.Paths, or we move the functions that call this into `TimeZoneInfo.Unix.cs`
private static string GetApexTimeDataRoot()
{
string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT");
if (!string.IsNullOrEmpty(ret))
{
return ret;
}

return "/apex/com.android.tzdata";
}

private static string GetApexRuntimeRoot()
{
string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT");
if (!string.IsNullOrEmpty(ret))
{
return ret;
}

return "/apex/com.android.runtime";
}

private static string GetTimeZoneDirectory()
{
// Android 10+, TimeData module where the updates land
if (File.Exists(Path.Combine(GetApexTimeDataRoot() + "/etc/tz/", TimeZoneFileName)))
{
return GetApexTimeDataRoot() + "/etc/tz/";
}
// Android 10+, Fallback location if the above isn't found or corrupted
if (File.Exists(Path.Combine(GetApexRuntimeRoot() + "/etc/tz/", TimeZoneFileName)))
{
return GetApexRuntimeRoot() + "/etc/tz/";
}
if (File.Exists(Path.Combine(Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/", TimeZoneFileName)))
{
return Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/";
}

return Environment.GetEnvironmentVariable("ANDROID_ROOT") + DefaultTimeZoneDirectory;
}

//TODO: TryGetTimeZoneFromLocalMachine maps to FindSystemTimeZoneByIdCore in mono/mono implementation
private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachineCore(string id, out TimeZoneInfo? value, out Exception? e)
{
value = null;
Expand Down Expand Up @@ -175,6 +210,7 @@ private unsafe struct AndroidTzDataHeader
public fixed byte signature[12];
public int indexOffset;
public int dataOffset;
// Do we need zoneTabOFfset? Whats the format of tzdata now vs during mono/mono implementation.
}

[StructLayout(LayoutKind.Sequential, Pack=1)]
Expand All @@ -186,92 +222,20 @@ private unsafe struct AndroidTzDataEntry
public int rawUtcOffset;
}

private static readonly string[] Paths = new string[] {
GetApexTimeDataRoot() + "/etc/tz/tzdata", // Android 10+, TimeData module where the updates land
GetApexRuntimeRoot() + "/etc/tz/tzdata", // Android 10+, Fallback location if the above isn't found or corrupted
Environment.GetEnvironmentVariable("ANDROID_DATA") + "/misc/zoneinfo/tzdata",
Environment.GetEnvironmentVariable("ANDROID_ROOT") + "/usr/share/zoneinfo/tzdata",
};

private string tzdataPath;
private Stream? data;

private string[]? ids;
private int[]? byteOffsets;
private int[]? lengths;

public AndroidTzData()
{
foreach (var path in Paths)
{
if (LoadData(path))
{
tzdataPath = path;
return;
}
}

tzdataPath = "/";
ids = new[]{ "GMT" };
}

private static string GetApexTimeDataRoot()
{
string? ret = Environment.GetEnvironmentVariable("ANDROID_TZDATA_ROOT");
if (!string.IsNullOrEmpty(ret)) {
return ret;
}

return "/apex/com.android.tzdata";
}

private static string GetApexRuntimeRoot()
{
string? ret = Environment.GetEnvironmentVariable("ANDROID_RUNTIME_ROOT");
if (!string.IsNullOrEmpty(ret))
{
return ret;
}

return "/apex/com.android.runtime";
}

private 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
{
return false;
}
ReadHeader(GetTimeZoneDirectory() + TimeZoneFileName);
}

private unsafe void ReadHeader()
private unsafe void ReadHeader(string tzFilePath)
{
int size = Math.Max(Marshal.SizeOf(typeof(AndroidTzDataHeader)), Marshal.SizeOf(typeof(AndroidTzDataEntry)));
var buffer = new byte[size];
var header = ReadAt<AndroidTzDataHeader>(0, buffer);
var header = ReadAt<AndroidTzDataHeader>(tzFilePath, 0, buffer);

header.indexOffset = NetworkToHostOrder(header.indexOffset);
header.dataOffset = NetworkToHostOrder(header.dataOffset);
Expand All @@ -290,13 +254,15 @@ private unsafe void ReadHeader()
//TODO: Put strings in resource file
throw new InvalidOperationException("bad tzdata magic: " + b.ToString());
}
// What exactly are we considering bad tzdata? Seems like if it doesnt start with "tzdata" or if the signature is filled.
// How does filling the AndroidTzDataHeader work? Shouldn't signature be filled up, so its always != 0?

ReadIndex(header.indexOffset, header.dataOffset, buffer);
ReadIndex(tzFilePath, header.indexOffset, header.dataOffset, buffer);
}

[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087:UnrecognizedReflectionPattern",
Justification = "Implementation detail of Android TimeZone")]
private unsafe T ReadAt<T>(long position, byte[] buffer)
private unsafe T ReadAt<T>(string tzFilePath, long position, byte[] buffer)
where T : struct
{
int size = Marshal.SizeOf(typeof(T));
Expand All @@ -306,18 +272,21 @@ private unsafe T ReadAt<T>(long position, byte[] buffer)
throw new InvalidOperationException("private error: buffer too small");
}

data!.Position = position;
int r;
if ((r = data!.Read(buffer, 0, size)) < size)
using (FileStream fs = File.OpenRead(tzFilePath))
{
//TODO: Put strings in resource file
throw new InvalidOperationException(
string.Format("Error reading '{0}': read {1} bytes, expected {2}", tzdataPath, r, size));
}
fs.Position = position;
int numBytesRead;
if ((numBytesRead = fs.Read(buffer, 0, size)) < size)
{
//TODO: Put strings in resource file
throw new InvalidOperationException(
string.Format("Error reading '{0}': read {1} bytes, expected {2}", GetTimeZoneDirectory() + TimeZoneFileName, numBytesRead, size));
}

fixed (byte* b = buffer)
{
return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!;
fixed (byte* b = buffer)
{
return (T)Marshal.PtrToStructure((IntPtr)b, typeof(T))!; // Is ! the right way to handle Unboxing a possibly null value. Should there be some check instead?
}
}
}

Expand All @@ -344,7 +313,8 @@ private static unsafe int GetStringLength(sbyte* s, int maxLength)
return len;
}

private unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer)
// What does the TZdata index look like?
private unsafe void ReadIndex(string tzFilePath, int indexOffset, int dataOffset, byte[] buffer)
{
int indexSize = dataOffset - indexOffset;
int entrySize = Marshal.SizeOf(typeof(AndroidTzDataEntry));
Expand All @@ -356,7 +326,7 @@ private unsafe void ReadIndex(int indexOffset, int dataOffset, byte[] buffer)

for (int i = 0; i < entryCount; ++i)
{
var entry = ReadAt<AndroidTzDataEntry>(indexOffset + (entrySize*i), buffer);
var entry = ReadAt<AndroidTzDataEntry>(tzFilePath, indexOffset + (entrySize*i), buffer);
var p = (sbyte*)entry.id;

byteOffsets![i] = NetworkToHostOrder(entry.byteOffset) + dataOffset;
Expand Down Expand Up @@ -388,17 +358,18 @@ public byte[] GetTimeZoneData(string id)
int offset = byteOffsets![i];
int length = lengths![i];
var buffer = new byte[length];

lock (data!)
// Do we need to lock to prevent multithreading issues like the mono/mono implementation?
var tzFilePath = GetTimeZoneDirectory() + TimeZoneFileName;
using (FileStream fs = File.OpenRead(tzFilePath))
{
data!.Position = offset;
int r;
if ((r = data!.Read(buffer, 0, buffer.Length)) < buffer.Length)
fs.Position = offset;
int numBytesRead;
if ((numBytesRead = fs.Read(buffer, 0, buffer.Length)) < buffer.Length)
{
//TODO: Put strings in resource file
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));
tzFilePath, offset, length, numBytesRead, buffer.Length));
}
}

Expand Down