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(" 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;