Skip to content

Replace readdir_r call in TimeZoneInfo.Unix #116119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Enumeration;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
Expand Down Expand Up @@ -271,102 +271,6 @@ private static List<string> ParseTimeZoneIds(StreamReader reader)
return Path.Join(currentPath.AsSpan(), direntName);
}

/// <summary>
/// Enumerate files
/// </summary>
private static unsafe void EnumerateFilesRecursively(string path, Predicate<string> condition)
{
List<string>? toExplore = null; // List used as a stack

int bufferSize = Interop.Sys.GetReadDirRBufferSize();
byte[] dirBuffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
string currentPath = path;

fixed (byte* dirBufferPtr = dirBuffer)
{
while (true)
{
IntPtr dirHandle = Interop.Sys.OpenDir(currentPath);
if (dirHandle == IntPtr.Zero)
{
throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), currentPath, isDirError: true);
}

try
{
// Read each entry from the enumerator
Interop.Sys.DirectoryEntry dirent;
while (Interop.Sys.ReadDirR(dirHandle, dirBufferPtr, bufferSize, &dirent) == 0)
{
string? fullPath = GetDirectoryEntryFullPath(ref dirent, currentPath);
if (fullPath == null)
continue;

// Get from the dir entry whether the entry is a file or directory.
// We classify everything as a file unless we know it to be a directory.
bool isDir;
if (dirent.InodeType == Interop.Sys.NodeType.DT_DIR)
{
// We know it's a directory.
isDir = true;
}
else if (dirent.InodeType == Interop.Sys.NodeType.DT_LNK || dirent.InodeType == Interop.Sys.NodeType.DT_UNKNOWN)
{
// It's a symlink or unknown: stat to it to see if we can resolve it to a directory.
// If we can't (e.g. symlink to a file, broken symlink, etc.), we'll just treat it as a file.

Interop.Sys.FileStatus fileinfo;
if (Interop.Sys.Stat(fullPath, out fileinfo) >= 0)
{
isDir = (fileinfo.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFDIR;
}
else
{
isDir = false;
}
}
else
{
// Otherwise, treat it as a file. This includes regular files, FIFOs, etc.
isDir = false;
}

// Yield the result if the user has asked for it. In the case of directories,
// always explore it by pushing it onto the stack, regardless of whether
// we're returning directories.
if (isDir)
{
toExplore ??= new List<string>();
toExplore.Add(fullPath);
}
else if (condition(fullPath))
{
return;
}
}
}
finally
{
if (dirHandle != IntPtr.Zero)
Interop.Sys.CloseDir(dirHandle);
}

if (toExplore == null || toExplore.Count == 0)
break;

currentPath = toExplore[toExplore.Count - 1];
toExplore.RemoveAt(toExplore.Count - 1);
}
}
}
finally
{
ArrayPool<byte>.Shared.Return(dirBuffer);
}
}

private static bool CompareTimeZoneFile(string filePath, byte[] buffer, byte[] rawData)
{
try
Expand Down Expand Up @@ -424,7 +328,7 @@ private static string FindTimeZoneId(byte[] rawData)

try
{
EnumerateFilesRecursively(timeZoneDirectory, (string filePath) =>
foreach (string filePath in Directory.EnumerateFiles(timeZoneDirectory, "*", SearchOption.AllDirectories))
{
// skip the localtime and posixrules file, since they won't give us the correct id
if (!string.Equals(filePath, localtimeFilePath, StringComparison.OrdinalIgnoreCase)
Expand All @@ -440,11 +344,11 @@ private static string FindTimeZoneId(byte[] rawData)
{
id = id.Substring(timeZoneDirectory.Length);
}
return true;

break;
}
}
return false;
});
}
}
catch (IOException) { }
catch (SecurityException) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ public static partial class TimeZoneInfoTests
private static string s_strFiji = s_isWindows ? "Fiji Standard Time" : "Pacific/Fiji";

private static TimeZoneInfo s_myUtc = TimeZoneInfo.Utc;
private static TimeZoneInfo s_myLocal = TimeZoneInfo.Local;

[Fact]
public static void Kind()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2159,7 +2159,7 @@ public static void GetSystemTimeZones_AllTimeZonesHaveOffsetInValidRange()
}
}

private static byte[] timeZoneFileContents = new byte[]
private static byte[] s_timeZoneFileContents = new byte[]
{
//
// Start of v1 Header
Expand Down Expand Up @@ -2274,7 +2274,7 @@ public static void NJulianRuleTest(string posixRule, int dayNumber, int monthNum
string zoneFilePath = Path.GetTempPath() + Path.GetRandomFileName();
using (FileStream fs = new FileStream(zoneFilePath, FileMode.Create))
{
fs.Write(timeZoneFileContents.AsSpan());
fs.Write(s_timeZoneFileContents.AsSpan());

// Append the POSIX rule
fs.WriteByte(0x0A);
Expand Down Expand Up @@ -2316,6 +2316,33 @@ public static void NJulianRuleTest(string posixRule, int dayNumber, int monthNum
}
}

[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[PlatformSpecific(TestPlatforms.AnyUnix)]
public static void ArbitraryTZ_UsedAsLocal()
{
const string tzId = "America/Monterrey";
const string tzPath = "/usr/share/zoneinfo/" + tzId;

if (!File.Exists(tzPath))
{
throw new SkipTestException($"The file {tzPath} does not exist.");
}

string tmp = Path.GetTempPath() + Path.GetRandomFileName();
File.WriteAllBytes(tmp, File.ReadAllBytes(tzPath));

ProcessStartInfo psi = new ProcessStartInfo() { UseShellExecute = false };
psi.Environment.Add("TZ", tzPath);

RemoteExecutor.Invoke(() =>
{
TimeZoneInfo tzi = TimeZoneInfo.GetSystemTimeZones().FirstOrDefault(t => t.Id == tzId);

Assert.NotNull(tzi);
Assert.Equal(tzi.Id, TimeZoneInfo.Local.Id);
}, new RemoteInvokeOptions { StartInfo = psi }).Dispose();
}

[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public static void TimeZoneInfo_LocalZoneWithInvariantMode()
{
Expand Down
Loading