diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs
index 53baf4eb65111f..23c1044e1983cd 100644
--- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs
@@ -193,6 +193,7 @@ private sealed class AndroidTzData
private string[] _ids;
private int[] _byteOffsets;
private int[] _lengths;
+ private bool[] _isBackwards;
private string _tzFileDir;
private string _tzFilePath;
@@ -230,7 +231,7 @@ public AndroidTzData()
foreach (var tzFileDir in tzFileDirList)
{
string tzFilePath = Path.Combine(tzFileDir, TimeZoneFileName);
- if (LoadData(tzFilePath))
+ if (LoadData(tzFileDir, tzFilePath))
{
_tzFileDir = tzFileDir;
_tzFilePath = tzFilePath;
@@ -241,10 +242,62 @@ public AndroidTzData()
throw new TimeZoneNotFoundException(SR.TimeZoneNotFound_ValidTimeZoneFileMissing);
}
+ // On some versions of Android, the tzdata file may still contain backward timezone ids.
+ // We attempt to use tzlookup.xml, which is available on some versions of Android to help
+ // validate non-backward timezone ids
+ // tzlookup.xml is an autogenerated file that contains timezone ids in this form:
+ //
+ //
+ //
+ //
+ // Australia/Sydney
+ // ...
+ // ...
+ // Australia/Eucla
+ //
+ //
+ // ...
+ // ...
+ // ...
+ //
+ //
+ //
+ //
+ // Once the timezone cache is populated with the IDs, we reference tzlookup id tags
+ // to determine if an id is backwards and label it as such if they are.
+ private void FilterBackwardIDs(string tzFileDir, out HashSet tzLookupIDs)
+ {
+ tzLookupIDs = new HashSet();
+ try
+ {
+ using (StreamReader sr = File.OpenText(Path.Combine(tzFileDir, "tzlookup.xml")))
+ {
+ string? tzLookupLine;
+ while (sr.Peek() >= 0)
+ {
+ if (!(tzLookupLine = sr.ReadLine())!.AsSpan().TrimStart().StartsWith("') + 1;
+ int idLength = tzLookupLine.LastIndexOf("", StringComparison.Ordinal) - idStart;
+ if (idStart <= 0 || idLength < 0)
+ {
+ // Either the start tag or the end tag are not found
+ continue;
+ }
+ string id = tzLookupLine.Substring(idStart, idLength);
+ tzLookupIDs.Add(id);
+ }
+ }
+ }
+ catch {}
+ }
+
[MemberNotNullWhen(true, nameof(_ids))]
[MemberNotNullWhen(true, nameof(_byteOffsets))]
[MemberNotNullWhen(true, nameof(_lengths))]
- private bool LoadData(string path)
+ [MemberNotNullWhen(true, nameof(_isBackwards))]
+ private bool LoadData(string tzFileDir, string path)
{
if (!File.Exists(path))
{
@@ -254,7 +307,7 @@ private bool LoadData(string path)
{
using (FileStream fs = File.OpenRead(path))
{
- LoadTzFile(fs);
+ LoadTzFile(tzFileDir, fs);
}
return true;
}
@@ -266,7 +319,8 @@ private bool LoadData(string path)
[MemberNotNull(nameof(_ids))]
[MemberNotNull(nameof(_byteOffsets))]
[MemberNotNull(nameof(_lengths))]
- private void LoadTzFile(Stream fs)
+ [MemberNotNull(nameof(_isBackwards))]
+ private void LoadTzFile(string tzFileDir, Stream fs)
{
const int HeaderSize = 24;
Span buffer = stackalloc byte[HeaderSize];
@@ -274,7 +328,7 @@ private void LoadTzFile(Stream fs)
ReadTzDataIntoBuffer(fs, 0, buffer);
LoadHeader(buffer, out int indexOffset, out int dataOffset);
- ReadIndex(fs, indexOffset, dataOffset);
+ ReadIndex(tzFileDir, fs, indexOffset, dataOffset);
}
private void LoadHeader(Span buffer, out int indexOffset, out int dataOffset)
@@ -303,16 +357,17 @@ private void LoadHeader(Span buffer, out int indexOffset, out int dataOffs
[MemberNotNull(nameof(_ids))]
[MemberNotNull(nameof(_byteOffsets))]
[MemberNotNull(nameof(_lengths))]
- private void ReadIndex(Stream fs, int indexOffset, int dataOffset)
+ [MemberNotNull(nameof(_isBackwards))]
+ private void ReadIndex(string tzFileDir, Stream fs, int indexOffset, int dataOffset)
{
int indexSize = dataOffset - indexOffset;
const int entrySize = 52; // Data entry size
int entryCount = indexSize / entrySize;
-
_byteOffsets = new int[entryCount];
_ids = new string[entryCount];
_lengths = new int[entryCount];
-
+ _isBackwards = new bool[entryCount];
+ FilterBackwardIDs(tzFileDir, out HashSet tzLookupIDs);
for (int i = 0; i < entryCount; ++i)
{
LoadEntryAt(fs, indexOffset + (entrySize*i), out string id, out int byteOffset, out int length);
@@ -320,6 +375,7 @@ private void ReadIndex(Stream fs, int indexOffset, int dataOffset)
_byteOffsets[i] = byteOffset + dataOffset;
_ids[i] = id;
_lengths[i] = length;
+ _isBackwards[i] = !tzLookupIDs.Contains(id);
if (length < 24) // Header Size
{
@@ -372,7 +428,25 @@ private void LoadEntryAt(Stream fs, long position, out string id, out int byteOf
public string[] GetTimeZoneIds()
{
- return _ids;
+ int numTimeZoneIDs = 0;
+ for (int i = 0; i < _ids.Length; i++)
+ {
+ if (!_isBackwards[i])
+ {
+ numTimeZoneIDs++;
+ }
+ }
+ string[] nonBackwardsTZIDs = new string[numTimeZoneIDs];
+ var index = 0;
+ for (int i = 0; i < _ids.Length; i++)
+ {
+ if (!_isBackwards[i])
+ {
+ nonBackwardsTZIDs[index] = _ids[i];
+ index++;
+ }
+ }
+ return nonBackwardsTZIDs;
}
public string GetTimeZoneDirectory()
diff --git a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs
index b352a529d6b009..c9e3bdbaa9ea50 100644
--- a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs
+++ b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs
@@ -2921,6 +2921,20 @@ public static void AdjustmentRuleBaseUtcOffsetDeltaTest()
Assert.Equal(new TimeSpan(2, 0, 0), customTimeZone.GetUtcOffset(new DateTime(2021, 3, 10, 2, 0, 0)));
}
+ [Fact]
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/64111", TestPlatforms.Linux)]
+ public static void NoBackwardTimeZones()
+ {
+ ReadOnlyCollection tzCollection = TimeZoneInfo.GetSystemTimeZones();
+ HashSet tzDisplayNames = new HashSet();
+
+ foreach (TimeZoneInfo timezone in tzCollection)
+ {
+ tzDisplayNames.Add(timezone.DisplayName);
+ }
+ Assert.Equal(tzCollection.Count, tzDisplayNames.Count);
+ }
+
private static bool IsEnglishUILanguage => CultureInfo.CurrentUICulture.Name.Length == 0 || CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == "en";
private static bool IsEnglishUILanguageAndRemoteExecutorSupported => IsEnglishUILanguage && RemoteExecutor.IsSupported;