Skip to content

Commit 6371525

Browse files
[release/6.0-maui] Android remove backward timezones (#64228)
Fixes #63693 It was discovered that Android produces duplicate TimeZone DisplayNames among all timezone IDs in GetSystemTimeZones. These duplicate DisplayNames occur across TimeZone IDs that are aliases, where all except one are backward timezone IDs. If a name is changed, put its old spelling in the 'backward' file From the Android TimeZone data file tzdata, it isn't obvious which TimeZone IDs are backward (I find it strange that they're included in the first place), however we discovered that on some versions of Android, there is an adjacent file tzlookup.xml that can aid us in determining which TimeZone IDs are "current" (not backward). This PR aims to utilize tzlookup.xml when it exists and post-filter's the Populated TimeZone IDs in the AndroidTzData instance by removing IDs and their associated information (byteoffset and length) from the AndroidTzData instance if it is not found in tzlookup.xml. This is using the assumption that all non-backward TimeZone IDs make it to the tzlookup.xml file. This PR also adds a new TimeZoneInfo Test to check whether or not there are duplicate DisplayNames in GetSystemTimeZones
1 parent 4f567e5 commit 6371525

File tree

2 files changed

+97
-9
lines changed

2 files changed

+97
-9
lines changed

src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.Android.cs

Lines changed: 83 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ private sealed class AndroidTzData
193193
private string[] _ids;
194194
private int[] _byteOffsets;
195195
private int[] _lengths;
196+
private bool[] _isBackwards;
196197
private string _tzFileDir;
197198
private string _tzFilePath;
198199

@@ -230,7 +231,7 @@ public AndroidTzData()
230231
foreach (var tzFileDir in tzFileDirList)
231232
{
232233
string tzFilePath = Path.Combine(tzFileDir, TimeZoneFileName);
233-
if (LoadData(tzFilePath))
234+
if (LoadData(tzFileDir, tzFilePath))
234235
{
235236
_tzFileDir = tzFileDir;
236237
_tzFilePath = tzFilePath;
@@ -241,10 +242,62 @@ public AndroidTzData()
241242
throw new TimeZoneNotFoundException(SR.TimeZoneNotFound_ValidTimeZoneFileMissing);
242243
}
243244

245+
// On some versions of Android, the tzdata file may still contain backward timezone ids.
246+
// We attempt to use tzlookup.xml, which is available on some versions of Android to help
247+
// validate non-backward timezone ids
248+
// tzlookup.xml is an autogenerated file that contains timezone ids in this form:
249+
//
250+
// <timezones ianaversion="2019b">
251+
// <countryzones>
252+
// <country code="au" default="Australia/Sydney" everutc="n">
253+
// <id alts="Australia/ACT,Australia/Canberra,Australia/NSW">Australia/Sydney</id>
254+
// ...
255+
// ...
256+
// <id>Australia/Eucla</id>
257+
// </country>
258+
// <country ...>
259+
// ...
260+
// ...
261+
// ...
262+
// </country>
263+
// </countryzones>
264+
// </timezones>
265+
//
266+
// Once the timezone cache is populated with the IDs, we reference tzlookup id tags
267+
// to determine if an id is backwards and label it as such if they are.
268+
private void FilterBackwardIDs(string tzFileDir, out HashSet<string> tzLookupIDs)
269+
{
270+
tzLookupIDs = new HashSet<string>();
271+
try
272+
{
273+
using (StreamReader sr = File.OpenText(Path.Combine(tzFileDir, "tzlookup.xml")))
274+
{
275+
string? tzLookupLine;
276+
while (sr.Peek() >= 0)
277+
{
278+
if (!(tzLookupLine = sr.ReadLine())!.AsSpan().TrimStart().StartsWith("<id", StringComparison.Ordinal))
279+
continue;
280+
281+
int idStart = tzLookupLine!.IndexOf('>') + 1;
282+
int idLength = tzLookupLine.LastIndexOf("</", StringComparison.Ordinal) - idStart;
283+
if (idStart <= 0 || idLength < 0)
284+
{
285+
// Either the start tag <id ... > or the end tag </id> are not found
286+
continue;
287+
}
288+
string id = tzLookupLine.Substring(idStart, idLength);
289+
tzLookupIDs.Add(id);
290+
}
291+
}
292+
}
293+
catch {}
294+
}
295+
244296
[MemberNotNullWhen(true, nameof(_ids))]
245297
[MemberNotNullWhen(true, nameof(_byteOffsets))]
246298
[MemberNotNullWhen(true, nameof(_lengths))]
247-
private bool LoadData(string path)
299+
[MemberNotNullWhen(true, nameof(_isBackwards))]
300+
private bool LoadData(string tzFileDir, string path)
248301
{
249302
if (!File.Exists(path))
250303
{
@@ -254,7 +307,7 @@ private bool LoadData(string path)
254307
{
255308
using (FileStream fs = File.OpenRead(path))
256309
{
257-
LoadTzFile(fs);
310+
LoadTzFile(tzFileDir, fs);
258311
}
259312
return true;
260313
}
@@ -266,15 +319,16 @@ private bool LoadData(string path)
266319
[MemberNotNull(nameof(_ids))]
267320
[MemberNotNull(nameof(_byteOffsets))]
268321
[MemberNotNull(nameof(_lengths))]
269-
private void LoadTzFile(Stream fs)
322+
[MemberNotNull(nameof(_isBackwards))]
323+
private void LoadTzFile(string tzFileDir, Stream fs)
270324
{
271325
const int HeaderSize = 24;
272326
Span<byte> buffer = stackalloc byte[HeaderSize];
273327

274328
ReadTzDataIntoBuffer(fs, 0, buffer);
275329

276330
LoadHeader(buffer, out int indexOffset, out int dataOffset);
277-
ReadIndex(fs, indexOffset, dataOffset);
331+
ReadIndex(tzFileDir, fs, indexOffset, dataOffset);
278332
}
279333

280334
private void LoadHeader(Span<byte> buffer, out int indexOffset, out int dataOffset)
@@ -303,23 +357,25 @@ private void LoadHeader(Span<byte> buffer, out int indexOffset, out int dataOffs
303357
[MemberNotNull(nameof(_ids))]
304358
[MemberNotNull(nameof(_byteOffsets))]
305359
[MemberNotNull(nameof(_lengths))]
306-
private void ReadIndex(Stream fs, int indexOffset, int dataOffset)
360+
[MemberNotNull(nameof(_isBackwards))]
361+
private void ReadIndex(string tzFileDir, Stream fs, int indexOffset, int dataOffset)
307362
{
308363
int indexSize = dataOffset - indexOffset;
309364
const int entrySize = 52; // Data entry size
310365
int entryCount = indexSize / entrySize;
311-
312366
_byteOffsets = new int[entryCount];
313367
_ids = new string[entryCount];
314368
_lengths = new int[entryCount];
315-
369+
_isBackwards = new bool[entryCount];
370+
FilterBackwardIDs(tzFileDir, out HashSet<string> tzLookupIDs);
316371
for (int i = 0; i < entryCount; ++i)
317372
{
318373
LoadEntryAt(fs, indexOffset + (entrySize*i), out string id, out int byteOffset, out int length);
319374

320375
_byteOffsets[i] = byteOffset + dataOffset;
321376
_ids[i] = id;
322377
_lengths[i] = length;
378+
_isBackwards[i] = !tzLookupIDs.Contains(id);
323379

324380
if (length < 24) // Header Size
325381
{
@@ -372,7 +428,25 @@ private void LoadEntryAt(Stream fs, long position, out string id, out int byteOf
372428

373429
public string[] GetTimeZoneIds()
374430
{
375-
return _ids;
431+
int numTimeZoneIDs = 0;
432+
for (int i = 0; i < _ids.Length; i++)
433+
{
434+
if (!_isBackwards[i])
435+
{
436+
numTimeZoneIDs++;
437+
}
438+
}
439+
string[] nonBackwardsTZIDs = new string[numTimeZoneIDs];
440+
var index = 0;
441+
for (int i = 0; i < _ids.Length; i++)
442+
{
443+
if (!_isBackwards[i])
444+
{
445+
nonBackwardsTZIDs[index] = _ids[i];
446+
index++;
447+
}
448+
}
449+
return nonBackwardsTZIDs;
376450
}
377451

378452
public string GetTimeZoneDirectory()

src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2873,6 +2873,20 @@ public static void AdjustmentRuleBaseUtcOffsetDeltaTest()
28732873
Assert.Equal(new TimeSpan(2, 0, 0), customTimeZone.GetUtcOffset(new DateTime(2021, 3, 10, 2, 0, 0)));
28742874
}
28752875

2876+
[Fact]
2877+
[ActiveIssue("https://github.com/dotnet/runtime/issues/64111", TestPlatforms.Linux)]
2878+
public static void NoBackwardTimeZones()
2879+
{
2880+
ReadOnlyCollection<TimeZoneInfo> tzCollection = TimeZoneInfo.GetSystemTimeZones();
2881+
HashSet<String> tzDisplayNames = new HashSet<String>();
2882+
2883+
foreach (TimeZoneInfo timezone in tzCollection)
2884+
{
2885+
tzDisplayNames.Add(timezone.DisplayName);
2886+
}
2887+
Assert.Equal(tzCollection.Count, tzDisplayNames.Count);
2888+
}
2889+
28762890
private static bool IsEnglishUILanguage => CultureInfo.CurrentUICulture.Name.Length == 0 || CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == "en";
28772891

28782892
private static bool IsEnglishUILanguageAndRemoteExecutorSupported => IsEnglishUILanguage && RemoteExecutor.IsSupported;

0 commit comments

Comments
 (0)