From 9ceefd61fbcb24137380a7f2e81af7b7bec3760f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:06:21 +0000 Subject: [PATCH 01/10] Initial plan From 6f4ff5afa068292a1d254ebc6f8b453e3dc48b61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:13:34 +0000 Subject: [PATCH 02/10] Add FileType enum and GetFileType method to SafeFileHandle Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Kernel32/Interop.GetNamedPipeInfo.cs | 10 +++ .../Win32/SafeHandles/SafeFileHandle.Unix.cs | 45 +++++++++++++ .../SafeHandles/SafeFileHandle.Windows.cs | 66 +++++++++++++++++++ .../Win32/SafeHandles/SafeFileHandle.cs | 8 +++ .../src/System/IO/FileType.cs | 52 +++++++++++++++ .../System.Runtime/ref/System.Runtime.cs | 13 ++++ 6 files changed, 194 insertions(+) create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs index 5945fa95c28263..f883e4120f70a4 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs @@ -17,5 +17,15 @@ internal static unsafe partial bool GetNamedPipeInfo( uint* lpInBufferSize, uint* lpMaxInstances ); + + [LibraryImport(Libraries.Kernel32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static unsafe partial bool GetNamedPipeInfo( + SafeFileHandle hNamedPipe, + uint* lpFlags, + uint* lpOutBufferSize, + uint* lpInBufferSize, + uint* lpMaxInstances + ); } } diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index 196ec738116e6a..7478e13b0ad986 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -339,6 +339,9 @@ private bool Init(string path, FileMode mode, FileAccess access, FileShare share Debug.Assert(status.Size == 0 || Interop.Sys.LSeek(this, 0, Interop.Sys.SeekWhence.SEEK_CUR) >= 0); } + // Cache the file type from the status + _cachedFileType = (int)MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT); + fileLength = status.Size; filePermissions = ((UnixFileMode)status.Mode) & PermissionMask; } @@ -494,6 +497,48 @@ private bool GetCanSeek() return canSeek == NullableBool.True; } + /// + /// Gets the type of the file that this handle represents. + /// + /// The type of the file. + /// The handle is closed. + public new System.IO.FileType GetFileType() + { + ObjectDisposedException.ThrowIf(IsClosed, this); + + int cachedType = _cachedFileType; + if (cachedType != -1) + { + return (System.IO.FileType)cachedType; + } + + // If we don't have a cached value, call FStat to get it + if (Interop.Sys.FStat(this, out Interop.Sys.FileStatus status) == 0) + { + System.IO.FileType fileType = MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT); + _cachedFileType = (int)fileType; + return fileType; + } + + // If FStat fails, return Unknown + return System.IO.FileType.Unknown; + } + + private static System.IO.FileType MapUnixFileTypeToFileType(int unixFileType) + { + return unixFileType switch + { + Interop.Sys.FileTypes.S_IFREG => System.IO.FileType.RegularFile, + Interop.Sys.FileTypes.S_IFDIR => System.IO.FileType.Directory, + Interop.Sys.FileTypes.S_IFLNK => System.IO.FileType.SymbolicLink, + Interop.Sys.FileTypes.S_IFIFO => System.IO.FileType.Pipe, + Interop.Sys.FileTypes.S_IFSOCK => System.IO.FileType.Socket, + Interop.Sys.FileTypes.S_IFCHR => System.IO.FileType.CharacterDevice, + Interop.Sys.FileTypes.S_IFBLK => System.IO.FileType.BlockDevice, + _ => System.IO.FileType.Unknown + }; + } + internal long GetFileLength() { int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status); diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index 8ee1660b705640..e704f0279c7a2c 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -270,6 +270,72 @@ internal int GetFileType() return fileType; } + /// + /// Gets the type of the file that this handle represents. + /// + /// The type of the file. + /// The handle is closed. + public new unsafe System.IO.FileType GetFileType() + { + ObjectDisposedException.ThrowIf(IsClosed, this); + + int cachedType = _cachedFileType; + if (cachedType != -1) + { + return (System.IO.FileType)cachedType; + } + + int kernelFileType = GetFileType(); + + System.IO.FileType result = kernelFileType switch + { + Interop.Kernel32.FileTypes.FILE_TYPE_CHAR => System.IO.FileType.CharacterDevice, + Interop.Kernel32.FileTypes.FILE_TYPE_PIPE => GetPipeOrSocketType(), + Interop.Kernel32.FileTypes.FILE_TYPE_DISK => GetDiskBasedType(), + _ => System.IO.FileType.Unknown + }; + + _cachedFileType = (int)result; + return result; + } + + private unsafe System.IO.FileType GetPipeOrSocketType() + { + // Try to call GetNamedPipeInfo to determine if it's a pipe or socket + uint flags; + if (Interop.Kernel32.GetNamedPipeInfo(this, &flags, null, null, null)) + { + return System.IO.FileType.Pipe; + } + + // If GetNamedPipeInfo fails, it's likely a socket + return System.IO.FileType.Socket; + } + + private unsafe System.IO.FileType GetDiskBasedType() + { + // First check if it's a directory using GetFileInformationByHandle + if (Interop.Kernel32.GetFileInformationByHandle(this, out Interop.Kernel32.BY_HANDLE_FILE_INFORMATION fileInfo)) + { + if ((fileInfo.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0) + { + return System.IO.FileType.Directory; + } + } + + // Check if it's a reparse point (symbolic link) using GetFileInformationByHandleEx + Interop.Kernel32.FILE_BASIC_INFO basicInfo; + if (Interop.Kernel32.GetFileInformationByHandleEx(this, Interop.Kernel32.FileBasicInfo, &basicInfo, (uint)sizeof(Interop.Kernel32.FILE_BASIC_INFO))) + { + if ((basicInfo.FileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT) != 0) + { + return System.IO.FileType.SymbolicLink; + } + } + + return System.IO.FileType.RegularFile; + } + internal long GetFileLength() { if (!_lengthCanBeCached) diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs index 76dc55b74dba71..c2446bcb8d1b79 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs @@ -8,6 +8,7 @@ namespace Microsoft.Win32.SafeHandles public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid { private string? _path; + private volatile int _cachedFileType = -1; /// /// Creates a around a file handle. @@ -20,5 +21,12 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand } internal string? Path => _path; + + /// + /// Gets the type of the file that this handle represents. + /// + /// The type of the file. + /// The handle is closed. + public System.IO.FileType GetFileType() => throw new PlatformNotSupportedException(); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs new file mode 100644 index 00000000000000..d83bff7ab762df --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO +{ + /// + /// Specifies the type of a file. + /// + public enum FileType + { + /// + /// The file type is unknown. + /// + Unknown, + + /// + /// The file is a regular file. + /// + RegularFile, + + /// + /// The file is a pipe (FIFO). + /// + Pipe, + + /// + /// The file is a socket. + /// + Socket, + + /// + /// The file is a character device. + /// + CharacterDevice, + + /// + /// The file is a directory. + /// + Directory, + + /// + /// The file is a symbolic link. + /// + SymbolicLink, + + /// + /// The file is a block device. + /// + [UnsupportedOSPlatform("windows")] + BlockDevice + } +} diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 65b98d13078b29..14d13061412189 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -22,6 +22,7 @@ public SafeFileHandle() : base (default(bool)) { } public SafeFileHandle(System.IntPtr preexistingHandle, bool ownsHandle) : base (default(bool)) { } public override bool IsInvalid { get { throw null; } } public bool IsAsync { get { throw null; } } + public System.IO.FileType GetFileType() { throw null; } protected override bool ReleaseHandle() { throw null; } } public abstract partial class SafeHandleMinusOneIsInvalid : System.Runtime.InteropServices.SafeHandle @@ -10445,6 +10446,18 @@ public enum FileAttributes IntegrityStream = 32768, NoScrubData = 131072, } + public enum FileType + { + Unknown = 0, + RegularFile = 1, + Pipe = 2, + Socket = 3, + CharacterDevice = 4, + Directory = 5, + SymbolicLink = 6, + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")] + BlockDevice = 7, + } public sealed partial class FileInfo : System.IO.FileSystemInfo { public FileInfo(string fileName) { } From 71e5a815b8f5f3241661422dfae0cc134f547011 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:16:00 +0000 Subject: [PATCH 03/10] Add comprehensive tests for SafeFileHandle.GetFileType Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../SafeFileHandle/GetFileType.cs | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs new file mode 100644 index 00000000000000..d3d4d7507372ce --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs @@ -0,0 +1,254 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.XUnitExtensions; +using Microsoft.Win32.SafeHandles; +using System.IO.Pipes; +using System.Net; +using System.Net.Sockets; +using Xunit; + +namespace System.IO.Tests +{ + public class SafeFileHandle_GetFileType : FileSystemTest + { + [Fact] + public void GetFileType_RegularFile() + { + string path = GetTestFilePath(); + File.WriteAllText(path, "test"); + + using SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); + Assert.Equal(FileType.RegularFile, handle.GetFileType()); + } + + [Fact] + public void GetFileType_Directory() + { + string path = GetTestFilePath(); + Directory.CreateDirectory(path); + + if (OperatingSystem.IsWindows()) + { + IntPtr hFile = Interop.Kernel32.CreateFile( + path, + Interop.Kernel32.GenericOperations.GENERIC_READ, + FileShare.ReadWrite, + null, + FileMode.Open, + Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS, + IntPtr.Zero); + + using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true); + Assert.False(handle.IsInvalid); + Assert.Equal(FileType.Directory, handle.GetFileType()); + } + else + { + IntPtr fd = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0); + using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); + Assert.False(handle.IsInvalid); + Assert.Equal(FileType.Directory, handle.GetFileType()); + } + } + + [Fact] + public void GetFileType_NullDevice() + { + using SafeFileHandle handle = File.OpenHandle( + OperatingSystem.IsWindows() ? "NUL" : "/dev/null", + FileMode.Open, + FileAccess.Write); + + Assert.Equal(FileType.CharacterDevice, handle.GetFileType()); + } + + [Fact] + public void GetFileType_AnonymousPipe() + { + using AnonymousPipeServerStream server = new AnonymousPipeServerStream(PipeDirection.Out); + using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false); + + Assert.Equal(FileType.Pipe, serverHandle.GetFileType()); + + using SafeFileHandle clientHandle = new SafeFileHandle(server.ClientSafePipeHandle.DangerousGetHandle(), ownsHandle: false); + Assert.Equal(FileType.Pipe, clientHandle.GetFileType()); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))] + [PlatformSpecific(TestPlatforms.Windows)] + public void GetFileType_NamedPipe_Windows() + { + string pipeName = Path.GetRandomFileName(); + using NamedPipeServerStream server = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + + Task serverTask = Task.Run(async () => await server.WaitForConnectionAsync()); + + using NamedPipeClientStream client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None); + client.Connect(); + serverTask.Wait(); + + using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false); + Assert.Equal(FileType.Pipe, serverHandle.GetFileType()); + + using SafeFileHandle clientHandle = new SafeFileHandle(client.SafePipeHandle.DangerousGetHandle(), ownsHandle: false); + Assert.Equal(FileType.Pipe, clientHandle.GetFileType()); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public void GetFileType_NamedPipe_Unix() + { + string pipePath = GetTestFilePath(); + Assert.Equal(0, Interop.Sys.MkFifo(pipePath, (int)UnixFileMode.UserRead | (int)UnixFileMode.UserWrite)); + + Task readerTask = Task.Run(() => + { + using SafeFileHandle reader = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Read); + Assert.Equal(FileType.Pipe, reader.GetFileType()); + }); + + using SafeFileHandle writer = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Write); + Assert.Equal(FileType.Pipe, writer.GetFileType()); + + readerTask.Wait(); + } + + [Fact] + public void GetFileType_Socket() + { + using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + listener.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + listener.Listen(1); + + using Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + client.Connect(listener.LocalEndPoint); + + using Socket server = listener.Accept(); + + using SafeFileHandle serverHandle = new SafeFileHandle(server.Handle, ownsHandle: false); + using SafeFileHandle clientHandle = new SafeFileHandle(client.Handle, ownsHandle: false); + + Assert.Equal(FileType.Socket, serverHandle.GetFileType()); + Assert.Equal(FileType.Socket, clientHandle.GetFileType()); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] + public void GetFileType_ConsoleInput() + { + if (!Console.IsInputRedirected) + { + using SafeFileHandle handle = new SafeFileHandle(Console.OpenStandardInput().SafeFileHandle.DangerousGetHandle(), ownsHandle: false); + FileType type = handle.GetFileType(); + + Assert.True(type == FileType.CharacterDevice || type == FileType.Pipe || type == FileType.RegularFile, + $"Expected CharacterDevice, Pipe, or RegularFile but got {type}"); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public void GetFileType_SymbolicLink_Unix() + { + string targetPath = GetTestFilePath(); + string linkPath = GetTestFilePath(); + File.WriteAllText(targetPath, "test"); + File.CreateSymbolicLink(linkPath, targetPath); + + IntPtr fd = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0); + using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); + + if (!handle.IsInvalid) + { + Assert.Equal(FileType.SymbolicLink, handle.GetFileType()); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] + [PlatformSpecific(TestPlatforms.Windows)] + public void GetFileType_SymbolicLink_Windows() + { + string targetPath = GetTestFilePath(); + string linkPath = GetTestFilePath(); + File.WriteAllText(targetPath, "test"); + File.CreateSymbolicLink(linkPath, targetPath); + + IntPtr hFile = Interop.Kernel32.CreateFile( + linkPath, + Interop.Kernel32.GenericOperations.GENERIC_READ, + FileShare.ReadWrite, + null, + FileMode.Open, + Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT | Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS, + IntPtr.Zero); + + using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true); + if (!handle.IsInvalid) + { + Assert.Equal(FileType.SymbolicLink, handle.GetFileType()); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] + [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser & ~TestPlatforms.Wasi)] + public void GetFileType_BlockDevice_Unix() + { + string[] possibleBlockDevices = { "/dev/sda", "/dev/loop0", "/dev/vda", "/dev/nvme0n1" }; + + string blockDevice = null; + foreach (string device in possibleBlockDevices) + { + if (File.Exists(device)) + { + blockDevice = device; + break; + } + } + + if (blockDevice == null) + { + throw new SkipTestException("No accessible block device found for testing"); + } + + try + { + IntPtr fd = Interop.Sys.Open(blockDevice, Interop.Sys.OpenFlags.O_RDONLY, 0); + if (fd == (IntPtr)(-1)) + { + throw new SkipTestException($"Could not open {blockDevice}"); + } + + using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); + Assert.Equal(FileType.BlockDevice, handle.GetFileType()); + } + catch (UnauthorizedAccessException) + { + throw new SkipTestException("Insufficient privileges to open block device"); + } + } + + [Fact] + public void GetFileType_ClosedHandle_ThrowsObjectDisposedException() + { + SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create); + handle.Dispose(); + + Assert.Throws(() => handle.GetFileType()); + } + + [Fact] + public void GetFileType_CachesResult() + { + string path = GetTestFilePath(); + File.WriteAllText(path, "test"); + + using SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read); + + FileType firstCall = handle.GetFileType(); + FileType secondCall = handle.GetFileType(); + + Assert.Equal(firstCall, secondCall); + Assert.Equal(FileType.RegularFile, firstCall); + } + } +} From 603de644380a622e1ae58ff61b9f6be667481752 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:20:58 +0000 Subject: [PATCH 04/10] Fix build errors for FileType enum and GetFileType method Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs | 4 +++- .../Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs | 2 +- .../src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs | 7 ------- .../src/System.Private.CoreLib.Shared.projitems | 1 + .../System.Private.CoreLib/src/System/IO/FileType.cs | 2 +- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index 7478e13b0ad986..f4aa338e77f988 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -502,7 +502,7 @@ private bool GetCanSeek() /// /// The type of the file. /// The handle is closed. - public new System.IO.FileType GetFileType() + public System.IO.FileType GetFileType() { ObjectDisposedException.ThrowIf(IsClosed, this); @@ -526,6 +526,7 @@ private bool GetCanSeek() private static System.IO.FileType MapUnixFileTypeToFileType(int unixFileType) { +#pragma warning disable CA1416 // BlockDevice is only returned on Unix platforms return unixFileType switch { Interop.Sys.FileTypes.S_IFREG => System.IO.FileType.RegularFile, @@ -537,6 +538,7 @@ private static System.IO.FileType MapUnixFileTypeToFileType(int unixFileType) Interop.Sys.FileTypes.S_IFBLK => System.IO.FileType.BlockDevice, _ => System.IO.FileType.Unknown }; +#pragma warning restore CA1416 } internal long GetFileLength() diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index e704f0279c7a2c..074fa175196277 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -275,7 +275,7 @@ internal int GetFileType() /// /// The type of the file. /// The handle is closed. - public new unsafe System.IO.FileType GetFileType() + public unsafe System.IO.FileType GetFileType() { ObjectDisposedException.ThrowIf(IsClosed, this); diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs index c2446bcb8d1b79..45d05a97454e7f 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs @@ -21,12 +21,5 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand } internal string? Path => _path; - - /// - /// Gets the type of the file that this handle represents. - /// - /// The type of the file. - /// The handle is closed. - public System.IO.FileType GetFileType() => throw new PlatformNotSupportedException(); } } diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index c744f85d3e05ce..41890be47fba0c 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -514,6 +514,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs index d83bff7ab762df..3f4290c2286388 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs @@ -46,7 +46,7 @@ public enum FileType /// /// The file is a block device. /// - [UnsupportedOSPlatform("windows")] + [System.Runtime.Versioning.UnsupportedOSPlatform("windows")] BlockDevice } } From 9edcc961c043c099e9e22c865ce4cc74003ce683 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 17:49:18 +0000 Subject: [PATCH 05/10] Address PR feedback: refactor GetFileType, split tests, use SafeHandle Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../Kernel32/Interop.GetNamedPipeInfo.cs | 13 +- .../Win32/SafeHandles/SafeFileHandle.Unix.cs | 21 +-- .../SafeHandles/SafeFileHandle.Windows.cs | 37 ++-- .../Win32/SafeHandles/SafeFileHandle.cs | 11 ++ .../SafeFileHandle/GetFileType.Unix.cs | 98 ++++++++++ .../SafeFileHandle/GetFileType.Windows.cs | 90 +++++++++ .../SafeFileHandle/GetFileType.cs | 172 +----------------- .../System.IO.FileSystem.Tests.csproj | 3 + 8 files changed, 228 insertions(+), 217 deletions(-) create mode 100644 src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs create mode 100644 src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs index f883e4120f70a4..686432415150a4 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices; -using Microsoft.Win32.SafeHandles; internal static partial class Interop { @@ -11,17 +10,7 @@ internal static partial class Kernel32 [LibraryImport(Libraries.Kernel32, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static unsafe partial bool GetNamedPipeInfo( - SafePipeHandle hNamedPipe, - uint* lpFlags, - uint* lpOutBufferSize, - uint* lpInBufferSize, - uint* lpMaxInstances - ); - - [LibraryImport(Libraries.Kernel32, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - internal static unsafe partial bool GetNamedPipeInfo( - SafeFileHandle hNamedPipe, + SafeHandle hNamedPipe, uint* lpFlags, uint* lpOutBufferSize, uint* lpInBufferSize, diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index f4aa338e77f988..261307a4c57365 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -497,15 +497,8 @@ private bool GetCanSeek() return canSeek == NullableBool.True; } - /// - /// Gets the type of the file that this handle represents. - /// - /// The type of the file. - /// The handle is closed. - public System.IO.FileType GetFileType() + internal System.IO.FileType GetFileTypeCore() { - ObjectDisposedException.ThrowIf(IsClosed, this); - int cachedType = _cachedFileType; if (cachedType != -1) { @@ -513,15 +506,15 @@ public System.IO.FileType GetFileType() } // If we don't have a cached value, call FStat to get it - if (Interop.Sys.FStat(this, out Interop.Sys.FileStatus status) == 0) + int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status); + if (result != 0) { - System.IO.FileType fileType = MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT); - _cachedFileType = (int)fileType; - return fileType; + throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo()); } - // If FStat fails, return Unknown - return System.IO.FileType.Unknown; + System.IO.FileType fileType = MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT); + _cachedFileType = (int)fileType; + return fileType; } private static System.IO.FileType MapUnixFileTypeToFileType(int unixFileType) diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index 074fa175196277..694df37937fd8b 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -16,7 +16,6 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid private long _length = -1; // negative means that hasn't been fetched. private bool _lengthCanBeCached; // file has been opened for reading and not shared for writing. private volatile FileOptions _fileOptions = (FileOptions)(-1); - private volatile int _fileType = -1; public SafeFileHandle() : base(true) { @@ -26,7 +25,7 @@ public SafeFileHandle() : base(true) internal bool IsNoBuffering => (GetFileOptions() & NoBuffering) != 0; - internal bool CanSeek => !IsClosed && GetFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; + internal bool CanSeek => !IsClosed && GetKernelFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; } @@ -254,38 +253,34 @@ internal unsafe FileOptions GetFileOptions() return _fileOptions = result; } - internal int GetFileType() + private int GetKernelFileType() { - int fileType = _fileType; - if (fileType == -1) + int cachedType = _cachedFileType; + if (cachedType != -1) { - _fileType = fileType = Interop.Kernel32.GetFileType(this); - - Debug.Assert(fileType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK - || fileType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE - || fileType == Interop.Kernel32.FileTypes.FILE_TYPE_CHAR, - $"Unknown file type: {fileType}"); + // Return the Kernel file type from cached FileType enum + System.IO.FileType fileType = (System.IO.FileType)cachedType; + return fileType switch + { + System.IO.FileType.CharacterDevice => Interop.Kernel32.FileTypes.FILE_TYPE_CHAR, + System.IO.FileType.Pipe or System.IO.FileType.Socket => Interop.Kernel32.FileTypes.FILE_TYPE_PIPE, + System.IO.FileType.RegularFile or System.IO.FileType.Directory or System.IO.FileType.SymbolicLink => Interop.Kernel32.FileTypes.FILE_TYPE_DISK, + _ => Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN + }; } - return fileType; + return Interop.Kernel32.GetFileType(this); } - /// - /// Gets the type of the file that this handle represents. - /// - /// The type of the file. - /// The handle is closed. - public unsafe System.IO.FileType GetFileType() + internal unsafe System.IO.FileType GetFileTypeCore() { - ObjectDisposedException.ThrowIf(IsClosed, this); - int cachedType = _cachedFileType; if (cachedType != -1) { return (System.IO.FileType)cachedType; } - int kernelFileType = GetFileType(); + int kernelFileType = Interop.Kernel32.GetFileType(this); System.IO.FileType result = kernelFileType switch { diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs index 45d05a97454e7f..493720deb4b896 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs @@ -21,5 +21,16 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand } internal string? Path => _path; + + /// + /// Gets the type of the file that this handle represents. + /// + /// The type of the file. + /// The handle is closed. + public System.IO.FileType GetFileType() + { + ObjectDisposedException.ThrowIf(IsClosed, this); + return GetFileTypeCore(); + } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs new file mode 100644 index 00000000000000..385df3f381000d --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.XUnitExtensions; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + [PlatformSpecific(TestPlatforms.AnyUnix)] + public class SafeFileHandle_GetFileType_Unix : FileSystemTest + { + [Fact] + public void GetFileType_Directory() + { + string path = GetTestFilePath(); + Directory.CreateDirectory(path); + + IntPtr fd = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0); + using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); + Assert.False(handle.IsInvalid); + Assert.Equal(FileType.Directory, handle.GetFileType()); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))] + public void GetFileType_NamedPipe() + { + string pipePath = GetTestFilePath(); + Assert.Equal(0, Interop.Sys.MkFifo(pipePath, (int)UnixFileMode.UserRead | (int)UnixFileMode.UserWrite)); + + Task readerTask = Task.Run(() => + { + using SafeFileHandle reader = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Read); + Assert.Equal(FileType.Pipe, reader.GetFileType()); + }); + + using SafeFileHandle writer = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Write); + Assert.Equal(FileType.Pipe, writer.GetFileType()); + + readerTask.Wait(); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] + public void GetFileType_SymbolicLink() + { + string targetPath = GetTestFilePath(); + string linkPath = GetTestFilePath(); + File.WriteAllText(targetPath, "test"); + File.CreateSymbolicLink(linkPath, targetPath); + + IntPtr fd = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0); + using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); + + if (!handle.IsInvalid) + { + Assert.Equal(FileType.SymbolicLink, handle.GetFileType()); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] + [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser & ~TestPlatforms.Wasi)] + public void GetFileType_BlockDevice() + { + string[] possibleBlockDevices = { "/dev/sda", "/dev/loop0", "/dev/vda", "/dev/nvme0n1" }; + + string blockDevice = null; + foreach (string device in possibleBlockDevices) + { + if (File.Exists(device)) + { + blockDevice = device; + break; + } + } + + if (blockDevice == null) + { + throw new SkipTestException("No accessible block device found for testing"); + } + + try + { + IntPtr fd = Interop.Sys.Open(blockDevice, Interop.Sys.OpenFlags.O_RDONLY, 0); + if (fd == (IntPtr)(-1)) + { + throw new SkipTestException($"Could not open {blockDevice}"); + } + + using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); + Assert.Equal(FileType.BlockDevice, handle.GetFileType()); + } + catch (UnauthorizedAccessException) + { + throw new SkipTestException("Insufficient privileges to open block device"); + } + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs new file mode 100644 index 00000000000000..6a161a4fd69fbd --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.XUnitExtensions; +using Microsoft.Win32.SafeHandles; +using System.IO.Pipes; +using Xunit; + +namespace System.IO.Tests +{ + [PlatformSpecific(TestPlatforms.Windows)] + public class SafeFileHandle_GetFileType_Windows : FileSystemTest + { + [Fact] + public void GetFileType_Directory() + { + string path = GetTestFilePath(); + Directory.CreateDirectory(path); + + IntPtr hFile = Interop.Kernel32.CreateFile( + path, + Interop.Kernel32.GenericOperations.GENERIC_READ, + FileShare.ReadWrite, + null, + FileMode.Open, + Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS, + IntPtr.Zero); + + using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true); + Assert.False(handle.IsInvalid); + Assert.Equal(FileType.Directory, handle.GetFileType()); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))] + public void GetFileType_NamedPipe() + { + string pipeName = Path.GetRandomFileName(); + using NamedPipeServerStream server = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + + Task serverTask = Task.Run(async () => await server.WaitForConnectionAsync()); + + using NamedPipeClientStream client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None); + client.Connect(); + serverTask.Wait(); + + using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false); + Assert.Equal(FileType.Pipe, serverHandle.GetFileType()); + + using SafeFileHandle clientHandle = new SafeFileHandle(client.SafePipeHandle.DangerousGetHandle(), ownsHandle: false); + Assert.Equal(FileType.Pipe, clientHandle.GetFileType()); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] + public void GetFileType_ConsoleInput() + { + if (!Console.IsInputRedirected) + { + using SafeFileHandle handle = new SafeFileHandle(Console.OpenStandardInput().SafeFileHandle.DangerousGetHandle(), ownsHandle: false); + FileType type = handle.GetFileType(); + + Assert.True(type == FileType.CharacterDevice || type == FileType.Pipe || type == FileType.RegularFile, + $"Expected CharacterDevice, Pipe, or RegularFile but got {type}"); + } + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] + public void GetFileType_SymbolicLink() + { + string targetPath = GetTestFilePath(); + string linkPath = GetTestFilePath(); + File.WriteAllText(targetPath, "test"); + File.CreateSymbolicLink(linkPath, targetPath); + + IntPtr hFile = Interop.Kernel32.CreateFile( + linkPath, + Interop.Kernel32.GenericOperations.GENERIC_READ, + FileShare.ReadWrite, + null, + FileMode.Open, + Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT | Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS, + IntPtr.Zero); + + using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true); + if (!handle.IsInvalid) + { + Assert.Equal(FileType.SymbolicLink, handle.GetFileType()); + } + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs index d3d4d7507372ce..9cf46dfdeb9cc0 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.DotNet.XUnitExtensions; using Microsoft.Win32.SafeHandles; using System.IO.Pipes; using System.Net; @@ -22,51 +21,17 @@ public void GetFileType_RegularFile() Assert.Equal(FileType.RegularFile, handle.GetFileType()); } - [Fact] - public void GetFileType_Directory() - { - string path = GetTestFilePath(); - Directory.CreateDirectory(path); - - if (OperatingSystem.IsWindows()) - { - IntPtr hFile = Interop.Kernel32.CreateFile( - path, - Interop.Kernel32.GenericOperations.GENERIC_READ, - FileShare.ReadWrite, - null, - FileMode.Open, - Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS, - IntPtr.Zero); - - using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true); - Assert.False(handle.IsInvalid); - Assert.Equal(FileType.Directory, handle.GetFileType()); - } - else - { - IntPtr fd = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0); - using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); - Assert.False(handle.IsInvalid); - Assert.Equal(FileType.Directory, handle.GetFileType()); - } - } - [Fact] public void GetFileType_NullDevice() { - using SafeFileHandle handle = File.OpenHandle( - OperatingSystem.IsWindows() ? "NUL" : "/dev/null", - FileMode.Open, - FileAccess.Write); - + using SafeFileHandle handle = File.OpenNullHandle(); Assert.Equal(FileType.CharacterDevice, handle.GetFileType()); } [Fact] public void GetFileType_AnonymousPipe() { - using AnonymousPipeServerStream server = new AnonymousPipeServerStream(PipeDirection.Out); + using AnonymousPipeServerStream server = new(PipeDirection.Out); using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false); Assert.Equal(FileType.Pipe, serverHandle.GetFileType()); @@ -75,45 +40,6 @@ public void GetFileType_AnonymousPipe() Assert.Equal(FileType.Pipe, clientHandle.GetFileType()); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))] - [PlatformSpecific(TestPlatforms.Windows)] - public void GetFileType_NamedPipe_Windows() - { - string pipeName = Path.GetRandomFileName(); - using NamedPipeServerStream server = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); - - Task serverTask = Task.Run(async () => await server.WaitForConnectionAsync()); - - using NamedPipeClientStream client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None); - client.Connect(); - serverTask.Wait(); - - using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false); - Assert.Equal(FileType.Pipe, serverHandle.GetFileType()); - - using SafeFileHandle clientHandle = new SafeFileHandle(client.SafePipeHandle.DangerousGetHandle(), ownsHandle: false); - Assert.Equal(FileType.Pipe, clientHandle.GetFileType()); - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))] - [PlatformSpecific(TestPlatforms.AnyUnix)] - public void GetFileType_NamedPipe_Unix() - { - string pipePath = GetTestFilePath(); - Assert.Equal(0, Interop.Sys.MkFifo(pipePath, (int)UnixFileMode.UserRead | (int)UnixFileMode.UserWrite)); - - Task readerTask = Task.Run(() => - { - using SafeFileHandle reader = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Read); - Assert.Equal(FileType.Pipe, reader.GetFileType()); - }); - - using SafeFileHandle writer = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Write); - Assert.Equal(FileType.Pipe, writer.GetFileType()); - - readerTask.Wait(); - } - [Fact] public void GetFileType_Socket() { @@ -133,100 +59,6 @@ public void GetFileType_Socket() Assert.Equal(FileType.Socket, clientHandle.GetFileType()); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] - public void GetFileType_ConsoleInput() - { - if (!Console.IsInputRedirected) - { - using SafeFileHandle handle = new SafeFileHandle(Console.OpenStandardInput().SafeFileHandle.DangerousGetHandle(), ownsHandle: false); - FileType type = handle.GetFileType(); - - Assert.True(type == FileType.CharacterDevice || type == FileType.Pipe || type == FileType.RegularFile, - $"Expected CharacterDevice, Pipe, or RegularFile but got {type}"); - } - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))] - [PlatformSpecific(TestPlatforms.AnyUnix)] - public void GetFileType_SymbolicLink_Unix() - { - string targetPath = GetTestFilePath(); - string linkPath = GetTestFilePath(); - File.WriteAllText(targetPath, "test"); - File.CreateSymbolicLink(linkPath, targetPath); - - IntPtr fd = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0); - using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); - - if (!handle.IsInvalid) - { - Assert.Equal(FileType.SymbolicLink, handle.GetFileType()); - } - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] - [PlatformSpecific(TestPlatforms.Windows)] - public void GetFileType_SymbolicLink_Windows() - { - string targetPath = GetTestFilePath(); - string linkPath = GetTestFilePath(); - File.WriteAllText(targetPath, "test"); - File.CreateSymbolicLink(linkPath, targetPath); - - IntPtr hFile = Interop.Kernel32.CreateFile( - linkPath, - Interop.Kernel32.GenericOperations.GENERIC_READ, - FileShare.ReadWrite, - null, - FileMode.Open, - Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT | Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS, - IntPtr.Zero); - - using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true); - if (!handle.IsInvalid) - { - Assert.Equal(FileType.SymbolicLink, handle.GetFileType()); - } - } - - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))] - [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser & ~TestPlatforms.Wasi)] - public void GetFileType_BlockDevice_Unix() - { - string[] possibleBlockDevices = { "/dev/sda", "/dev/loop0", "/dev/vda", "/dev/nvme0n1" }; - - string blockDevice = null; - foreach (string device in possibleBlockDevices) - { - if (File.Exists(device)) - { - blockDevice = device; - break; - } - } - - if (blockDevice == null) - { - throw new SkipTestException("No accessible block device found for testing"); - } - - try - { - IntPtr fd = Interop.Sys.Open(blockDevice, Interop.Sys.OpenFlags.O_RDONLY, 0); - if (fd == (IntPtr)(-1)) - { - throw new SkipTestException($"Could not open {blockDevice}"); - } - - using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); - Assert.Equal(FileType.BlockDevice, handle.GetFileType()); - } - catch (UnauthorizedAccessException) - { - throw new SkipTestException("Insufficient privileges to open block device"); - } - } - [Fact] public void GetFileType_ClosedHandle_ThrowsObjectDisposedException() { diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj index d6ca52e5e34c12..bccdc010b62184 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj @@ -227,6 +227,9 @@ + + + From 1f0655fe83d2848b3f44cb96a0570e83637b2259 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 18:07:06 +0000 Subject: [PATCH 06/10] Address feedback: move test files, delete GetKernelFileType, use new API in CanSeek Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../SafeHandles/SafeFileHandle.Windows.cs | 21 +------------------ .../System.IO.FileSystem.Tests.csproj | 6 +++--- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index 694df37937fd8b..9680ac6fe25d86 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -25,7 +25,7 @@ public SafeFileHandle() : base(true) internal bool IsNoBuffering => (GetFileOptions() & NoBuffering) != 0; - internal bool CanSeek => !IsClosed && GetKernelFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; + internal bool CanSeek => !IsClosed && GetFileType() == System.IO.FileType.RegularFile; internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; } @@ -253,25 +253,6 @@ internal unsafe FileOptions GetFileOptions() return _fileOptions = result; } - private int GetKernelFileType() - { - int cachedType = _cachedFileType; - if (cachedType != -1) - { - // Return the Kernel file type from cached FileType enum - System.IO.FileType fileType = (System.IO.FileType)cachedType; - return fileType switch - { - System.IO.FileType.CharacterDevice => Interop.Kernel32.FileTypes.FILE_TYPE_CHAR, - System.IO.FileType.Pipe or System.IO.FileType.Socket => Interop.Kernel32.FileTypes.FILE_TYPE_PIPE, - System.IO.FileType.RegularFile or System.IO.FileType.Directory or System.IO.FileType.SymbolicLink => Interop.Kernel32.FileTypes.FILE_TYPE_DISK, - _ => Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN - }; - } - - return Interop.Kernel32.GetFileType(this); - } - internal unsafe System.IO.FileType GetFileTypeCore() { int cachedType = _cachedFileType; diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj index bccdc010b62184..ee394f212c3f9c 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj @@ -93,12 +93,14 @@ + + @@ -112,6 +114,7 @@ + @@ -227,9 +230,6 @@ - - - From 89944640f76cbcaa129133df97523f3a128fc6f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 19:22:51 +0000 Subject: [PATCH 07/10] Add GetNamedPipeInfo reference and fix Unix test attribute Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../src/System.Private.CoreLib.Shared.projitems | 3 +++ .../SafeFileHandle/GetFileType.Unix.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 41890be47fba0c..b4f043fd3e3063 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1954,6 +1954,9 @@ Common\Interop\Windows\Kernel32\Interop.GetFileType_SafeHandle.cs + + Common\Interop\Windows\Kernel32\Interop.GetNamedPipeInfo.cs + Common\Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs index 385df3f381000d..197e19c38acf5d 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs @@ -22,7 +22,7 @@ public void GetFileType_Directory() Assert.Equal(FileType.Directory, handle.GetFileType()); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))] + [Fact] public void GetFileType_NamedPipe() { string pipePath = GetTestFilePath(); From 98347983793beb300a3141e01bea76f397d92153 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:00:43 +0000 Subject: [PATCH 08/10] Add Unix and Windows Interop references to fix test build errors Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../System.IO.FileSystem.Tests.csproj | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj index ee394f212c3f9c..f9cbc5f387f21a 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj @@ -101,6 +101,10 @@ + + @@ -244,6 +248,7 @@ + From ef2da05b9263d18f132722bab4092d66af161022 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:24:38 +0000 Subject: [PATCH 09/10] Fix Unix test build errors: use Open correctly, add Task using, add MkFifo Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../SafeFileHandle/GetFileType.Unix.cs | 12 +++++------- .../System.IO.FileSystem.Tests.csproj | 2 ++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs index 197e19c38acf5d..35069ebfaf28b8 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs @@ -3,6 +3,7 @@ using Microsoft.DotNet.XUnitExtensions; using Microsoft.Win32.SafeHandles; +using System.Threading.Tasks; using Xunit; namespace System.IO.Tests @@ -16,8 +17,7 @@ public void GetFileType_Directory() string path = GetTestFilePath(); Directory.CreateDirectory(path); - IntPtr fd = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0); - using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); + using SafeFileHandle handle = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0); Assert.False(handle.IsInvalid); Assert.Equal(FileType.Directory, handle.GetFileType()); } @@ -48,8 +48,7 @@ public void GetFileType_SymbolicLink() File.WriteAllText(targetPath, "test"); File.CreateSymbolicLink(linkPath, targetPath); - IntPtr fd = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0); - using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); + using SafeFileHandle handle = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0); if (!handle.IsInvalid) { @@ -80,13 +79,12 @@ public void GetFileType_BlockDevice() try { - IntPtr fd = Interop.Sys.Open(blockDevice, Interop.Sys.OpenFlags.O_RDONLY, 0); - if (fd == (IntPtr)(-1)) + using SafeFileHandle handle = Interop.Sys.Open(blockDevice, Interop.Sys.OpenFlags.O_RDONLY, 0); + if (handle.IsInvalid) { throw new SkipTestException($"Could not open {blockDevice}"); } - using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true); Assert.Equal(FileType.BlockDevice, handle.GetFileType()); } catch (UnauthorizedAccessException) diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj index f9cbc5f387f21a..19a3719f682d97 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj @@ -105,6 +105,8 @@ Link="Common\Interop\Unix\Interop.Open.cs" /> + From 28ec926560ecf27da13d322d7b699ba7e58d61f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:29:42 +0000 Subject: [PATCH 10/10] Remove invalid ConditionalFact from Windows NamedPipe test Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com> --- .../SafeFileHandle/GetFileType.Windows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs index 6a161a4fd69fbd..0b45b1d547dcc1 100644 --- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs @@ -31,7 +31,7 @@ public void GetFileType_Directory() Assert.Equal(FileType.Directory, handle.GetFileType()); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))] + [Fact] public void GetFileType_NamedPipe() { string pipeName = Path.GetRandomFileName();