Skip to content

Commit 1713d65

Browse files
authored
Replace readdir_r call in TimeZoneInfo.Unix (#116119)
* Replace readdir_r call with FileSystemEnumerable in TimeZoneInfo.Unix * Bring back using needed for WASM * Address test feedback * Use Diretory.EnumerateFiles instead of FSE
1 parent a950953 commit 1713d65

File tree

3 files changed

+34
-104
lines changed

3 files changed

+34
-104
lines changed

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

Lines changed: 5 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Buffers;
54
using System.Collections.Generic;
65
using System.Diagnostics;
76
using System.Diagnostics.CodeAnalysis;
87
using System.IO;
8+
using System.IO.Enumeration;
99
using System.Runtime.InteropServices;
1010
using System.Security;
1111
using System.Text;
@@ -271,102 +271,6 @@ private static List<string> ParseTimeZoneIds(StreamReader reader)
271271
return Path.Join(currentPath.AsSpan(), direntName);
272272
}
273273

274-
/// <summary>
275-
/// Enumerate files
276-
/// </summary>
277-
private static unsafe void EnumerateFilesRecursively(string path, Predicate<string> condition)
278-
{
279-
List<string>? toExplore = null; // List used as a stack
280-
281-
int bufferSize = Interop.Sys.GetReadDirRBufferSize();
282-
byte[] dirBuffer = ArrayPool<byte>.Shared.Rent(bufferSize);
283-
try
284-
{
285-
string currentPath = path;
286-
287-
fixed (byte* dirBufferPtr = dirBuffer)
288-
{
289-
while (true)
290-
{
291-
IntPtr dirHandle = Interop.Sys.OpenDir(currentPath);
292-
if (dirHandle == IntPtr.Zero)
293-
{
294-
throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), currentPath, isDirError: true);
295-
}
296-
297-
try
298-
{
299-
// Read each entry from the enumerator
300-
Interop.Sys.DirectoryEntry dirent;
301-
while (Interop.Sys.ReadDirR(dirHandle, dirBufferPtr, bufferSize, &dirent) == 0)
302-
{
303-
string? fullPath = GetDirectoryEntryFullPath(ref dirent, currentPath);
304-
if (fullPath == null)
305-
continue;
306-
307-
// Get from the dir entry whether the entry is a file or directory.
308-
// We classify everything as a file unless we know it to be a directory.
309-
bool isDir;
310-
if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR)
311-
{
312-
// We know it's a directory.
313-
isDir = true;
314-
}
315-
else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.InodeType == Interop.Sys.NodeType.DT_UNKNOWN)
316-
{
317-
// It's a symlink or unknown: stat to it to see if we can resolve it to a directory.
318-
// If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file.
319-
320-
Interop.Sys.FileStatus fileinfo;
321-
if (Interop.Sys.Stat(fullPath, out fileinfo) >= 0)
322-
{
323-
isDir = (fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR;
324-
}
325-
else
326-
{
327-
isDir = false;
328-
}
329-
}
330-
else
331-
{
332-
// Otherwise, treat it as a file. This includes regular files, FIFOs, etc.
333-
isDir = false;
334-
}
335-
336-
// Yield the result if the user has asked for it. In the case of directories,
337-
// always explore it by pushing it onto the stack, regardless of whether
338-
// we're returning directories.
339-
if (isDir)
340-
{
341-
toExplore ??= new List<string>();
342-
toExplore.Add(fullPath);
343-
}
344-
else if (condition(fullPath))
345-
{
346-
return;
347-
}
348-
}
349-
}
350-
finally
351-
{
352-
if (dirHandle != IntPtr.Zero)
353-
Interop.Sys.CloseDir(dirHandle);
354-
}
355-
356-
if (toExplore == null || toExplore.Count == 0)
357-
break;
358-
359-
currentPath = toExplore[toExplore.Count - 1];
360-
toExplore.RemoveAt(toExplore.Count - 1);
361-
}
362-
}
363-
}
364-
finally
365-
{
366-
ArrayPool<byte>.Shared.Return(dirBuffer);
367-
}
368-
}
369-
370274
private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData)
371275
{
372276
try
@@ -424,7 +328,7 @@ private static string FindTimeZoneId(byte[] rawData)
424328

425329
try
426330
{
427-
EnumerateFilesRecursively(timeZoneDirectory, (string filePath) =>
331+
foreach (string filePath in Directory.EnumerateFiles(timeZoneDirectory, "*", SearchOption.AllDirectories))
428332
{
429333
// skip the localtime and posixrules file, since they won't give us the correct id
430334
if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase)
@@ -440,11 +344,11 @@ private static string FindTimeZoneId(byte[] rawData)
440344
{
441345
id = id.Substring(timeZoneDirectory.Length);
442346
}
443-
return true;
347+
348+
break;
444349
}
445350
}
446-
return false;
447-
});
351+
}
448352
}
449353
catch (IOException) { }
450354
catch (SecurityException) { }

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ public static partial class TimeZoneInfoTests
4242
private static string s_strFiji = s_isWindows ? "Fiji Standard Time" : "Pacific/Fiji";
4343

4444
private static TimeZoneInfo s_myUtc = TimeZoneInfo.Utc;
45-
private static TimeZoneInfo s_myLocal = TimeZoneInfo.Local;
4645

4746
[Fact]
4847
public static void Kind()

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

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2159,7 +2159,7 @@ public static void GetSystemTimeZones_AllTimeZonesHaveOffsetInValidRange()
21592159
}
21602160
}
21612161

2162-
private static byte[] timeZoneFileContents = new byte[]
2162+
private static byte[] s_timeZoneFileContents = new byte[]
21632163
{
21642164
//
21652165
// Start of v1 Header
@@ -2274,7 +2274,7 @@ public static void NJulianRuleTest(string posixRule, int dayNumber, int monthNum
22742274
string zoneFilePath = Path.GetTempPath() + Path.GetRandomFileName();
22752275
using (FileStream fs = new FileStream(zoneFilePath, FileMode.Create))
22762276
{
2277-
fs.Write(timeZoneFileContents.AsSpan());
2277+
fs.Write(s_timeZoneFileContents.AsSpan());
22782278

22792279
// Append the POSIX rule
22802280
fs.WriteByte(0x0A);
@@ -2316,6 +2316,33 @@ public static void NJulianRuleTest(string posixRule, int dayNumber, int monthNum
23162316
}
23172317
}
23182318

2319+
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
2320+
[PlatformSpecific(TestPlatforms.AnyUnix)]
2321+
public static void ArbitraryTZ_UsedAsLocal()
2322+
{
2323+
const string tzId = "America/Monterrey";
2324+
const string tzPath = "/usr/share/zoneinfo/" + tzId;
2325+
2326+
if (!File.Exists(tzPath))
2327+
{
2328+
throw new SkipTestException($"The file {tzPath} does not exist.");
2329+
}
2330+
2331+
string tmp = Path.GetTempPath() + Path.GetRandomFileName();
2332+
File.WriteAllBytes(tmp, File.ReadAllBytes(tzPath));
2333+
2334+
ProcessStartInfo psi = new ProcessStartInfo() { UseShellExecute = false };
2335+
psi.Environment.Add("TZ", tzPath);
2336+
2337+
RemoteExecutor.Invoke(() =>
2338+
{
2339+
TimeZoneInfo tzi = TimeZoneInfo.GetSystemTimeZones().FirstOrDefault(t => t.Id == tzId);
2340+
2341+
Assert.NotNull(tzi);
2342+
Assert.Equal(tzi.Id, TimeZoneInfo.Local.Id);
2343+
}, new RemoteInvokeOptions { StartInfo = psi }).Dispose();
2344+
}
2345+
23192346
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
23202347
public static void TimeZoneInfo_LocalZoneWithInvariantMode()
23212348
{

0 commit comments

Comments
 (0)