Skip to content

Add FileHandleType enum and SafeFileHandle.Type property#124561

Open
Copilot wants to merge 21 commits intomainfrom
copilot/add-filetype-enum-and-getfiletype-method
Open

Add FileHandleType enum and SafeFileHandle.Type property#124561
Copilot wants to merge 21 commits intomainfrom
copilot/add-filetype-enum-and-getfiletype-method

Conversation

Copy link
Contributor

Copilot AI commented Feb 18, 2026

Description

Adds System.IO.FileHandleType enum and SafeFileHandle.Type property for efficient runtime detection of file handle types without additional syscalls.

Changes

API Surface

System.IO.FileHandleType enum:

  • Values: Unknown, RegularFile, Pipe, Socket, CharacterDevice, Directory, SymbolicLink, BlockDevice
  • All values supported cross-platform (BlockDevice returns Unknown on Windows)

SafeFileHandle.Type property:

  • Returns cached FileHandleType
  • Throws ObjectDisposedException when handle is closed
  • Single syscall per handle lifetime (cached in volatile int _cachedFileType)

Implementation

Shared (SafeFileHandle.cs):

  • Type property: validates handle state, manages caching, delegates to GetFileTypeCore()
  • Caching: volatile int _cachedFileType = -1 (uncached sentinel)
  • Pattern: read cached value → if uncached, call platform-specific GetFileTypeCore() → store and return

Windows (SafeFileHandle.Windows.cs):

  • GetFileTypeCore(): maps GetFileType() result via switch expression
  • FILE_TYPE_CHARCharacterDevice
  • FILE_TYPE_PIPE → calls GetNamedPipeInfo; success → Pipe, ERROR_INVALID_HANDLESocket, other errors → throws Win32Exception
  • FILE_TYPE_DISK → checks FILE_ATTRIBUTE_DIRECTORY, then queries reparse tag via GetFileInformationByHandleEx with FileAttributeTagInfo - only IO_REPARSE_TAG_SYMLINK returns SymbolicLink (junctions, mount points, etc. return RegularFile)
  • CanSeek updated to check Type == RegularFile (more precise than previous FILE_TYPE_DISK check - excludes directories and symlinks)

Unix (SafeFileHandle.Unix.cs):

  • GetFileTypeCore(): calls FStat, maps S_IFMT mode bits to enum values
  • Init() opportunistically caches file type when FStat already performed (line 342)
  • Throws exception with Path parameter on FStat failure for better diagnostics

Interop (Interop.GetNamedPipeInfo.cs):

  • Changed parameter from SafePipeHandle to SafeHandle (CoreLib cannot reference SafePipeHandle)
  • Added to System.Private.CoreLib.Shared.projitems for Windows builds

Example Usage

using SafeFileHandle handle = File.OpenHandle("file.txt", FileMode.Open);
FileHandleType type = handle.Type;  // FileHandleType.RegularFile (cached)

using SafeFileHandle console = Console.OpenStandardInputHandle();
console.Type;  // FileHandleType.CharacterDevice

Testing

Three platform-organized test files with comprehensive coverage:

  • GetFileType.cs (OS-agnostic): Regular files, null device (File.OpenNullHandle()), anonymous pipes, sockets, closed handle exception, caching behavior. Browser skips for pipes/sockets.

  • GetFileType.Windows.cs: Directories (via CreateFile + FILE_FLAG_BACKUP_SEMANTICS), named pipes (async coordination), console handles (Console.OpenStandardInputHandle()), symlinks (FILE_FLAG_OPEN_REPARSE_POINT). Uses unsafe for CreateFile calls with null security attributes.

  • GetFileType.Unix.cs: Directories, named pipes (mkfifo + async/await pattern), symlinks (O_NOFOLLOW), block devices (conditional on privileged process, tries common device paths). Fixed nullable annotations (string? blockDevice).

All Interop dependencies properly included in platform-specific ItemGroups in .csproj. All 9,195 tests pass with 0 failures.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 3 commits February 18, 2026 17:13
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Copilot AI changed the title [WIP] Add FileType enum and extend SafeFileHandle with GetFileType Add FileType enum and SafeFileHandle.GetFileType() method Feb 18, 2026
Copilot AI requested a review from adamsitnik February 18, 2026 17:36
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please address my feedback

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 18, 2026

@copilot please address my feedback

All feedback has been addressed in commit 9edcc96.

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address my feedback

…API in CanSeek

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 18, 2026

@copilot address my feedback

All feedback addressed in commit 1f0655f.

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please address my feedback

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 18, 2026

@copilot please address my feedback

All feedback addressed in commit 8994464.

Copilot AI requested a review from adamsitnik March 3, 2026 19:57
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please address my feedback

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 3, 2026

@copilot please address my feedback

All feedback addressed. Commit 760a452.

Copilot AI requested a review from adamsitnik March 3, 2026 21:01
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM assuming the tests are going to pass

@adamsitnik adamsitnik marked this pull request as ready for review March 3, 2026 21:02
Copilot AI review requested due to automatic review settings March 3, 2026 21:02
@adamsitnik
Copy link
Member

@copilot please invoke the code-review skill and post the analysis/results as a comment on this PR

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new System.IO.FileHandleType enum and a SafeFileHandle.Type property to expose (and cache) the OS-level handle kind so callers can distinguish files/pipes/sockets/devices/directories/symlinks without repeated native queries.

Changes:

  • Introduces System.IO.FileHandleType and exposes it via SafeFileHandle.Type (with caching and disposed-handle validation).
  • Implements platform-specific detection (GetFileTypeCore) on Windows (Win32 APIs) and Unix (fstat), with Unix additionally priming the cache during Init().
  • Adds new cross-platform + platform-specific tests for the new API surface.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj Includes new SafeFileHandle Type tests and required interop sources per platform.
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs Adds OS-agnostic tests for regular files, null device, anonymous pipes, sockets, caching, and disposed behavior.
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs Adds Windows-specific tests for directories, named pipes, console input, and symlinks.
src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs Adds Unix-specific tests for directories, FIFOs, symlinks (via O_NOFOLLOW), and block devices.
src/libraries/System.Runtime/ref/System.Runtime.cs Updates public ref surface: adds SafeFileHandle.Type and FileHandleType enum.
src/libraries/System.Private.CoreLib/src/System/IO/FileHandleType.cs Implements the new System.IO.FileHandleType enum in CoreLib.
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems Ensures new enum and GetNamedPipeInfo interop compile into CoreLib.
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs Adds _cachedFileType field and public Type property with centralized caching.
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs Implements Windows GetFileTypeCore, removes old cached _fileType, and updates CanSeek to use Type.
src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs Primes cached file type during Init() and adds GetFileTypeCore + Unix type mapping.
src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs Generalizes interop signature to accept SafeHandle so it can be used from SafeFileHandle.

Comment on lines +277 to +278
// If GetNamedPipeInfo fails, it's likely a socket
return System.IO.FileHandleType.Socket;
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetPipeOrSocketType treats any GetNamedPipeInfo failure as a socket. GetNamedPipeInfo can fail for reasons other than “this is a socket” (e.g., access denied, invalid parameters/handle state), which would misclassify non-socket handles as Socket. Consider checking Marshal.GetLastPInvokeError() and only returning Socket for the expected “not a pipe” error; otherwise return Unknown (or propagate) to avoid false positives.

Suggested change
// If GetNamedPipeInfo fails, it's likely a socket
return System.IO.FileHandleType.Socket;
// GetNamedPipeInfo failed; inspect the error to determine if this is actually a socket
const int ERROR_INVALID_HANDLE = 6;
int error = Marshal.GetLastPInvokeError();
if (error == ERROR_INVALID_HANDLE)
{
return System.IO.FileHandleType.Socket;
}
return System.IO.FileHandleType.Unknown;

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, but we should throw Win32Exception(error) rather than return System.IO.FileHandleType.Unknown in such case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit b025b70:

  • Added error code check in GetPipeOrSocketType()
  • Only treats ERROR_INVALID_HANDLE as Socket
  • Throws Win32Exception(error) for unexpected errors

Also fixed:

  • Reparse point handling now checks tag (only IO_REPARSE_TAG_SYMLINK returns SymbolicLink)
  • Unix exception includes Path for better diagnostics
  • Test nullable and platform check fixes

Comment on lines 292 to 300
// 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.FileHandleType.SymbolicLink;
}
}
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetDiskBasedType currently returns FileHandleType.SymbolicLink for any handle with FILE_ATTRIBUTE_REPARSE_POINT. Not all reparse points are symbolic links (e.g., mount points/junctions, OneDrive placeholders), so this can misreport the handle type. Consider querying the reparse tag (e.g., via GetFileInformationByHandleEx with an attribute/tag info class or FSCTL_GET_REPARSE_POINT) and only mapping IO_REPARSE_TAG_SYMLINK to SymbolicLink; otherwise return Unknown/RegularFile/Directory as appropriate.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit b025b70. Now checks reparse tag using GetFileInformationByHandleEx with FileAttributeTagInfo (35). Only IO_REPARSE_TAG_SYMLINK returns SymbolicLink; other reparse points return RegularFile/Directory.

int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status);
if (result != 0)
{
throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo());
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetFileTypeCore throws using Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo()), which omits the handle’s Path from the exception. Other fstat-based paths in this type include the path for better diagnostics (e.g., FStatCheckIO / FileStreamHelpers.CheckFileCall). Consider using the overload that accepts Path when available so failures from the new public Type property are actionable.

Suggested change
throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo());
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
throw Interop.GetExceptionForIoErrno(error, Path);

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit b025b70. Changed to Interop.GetExceptionForIoErrno(error, Path) to include the path in the exception message.

Comment on lines +63 to +76
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)
{
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nullable annotations: string blockDevice = null; will trigger nullable warnings (and this repo treats warnings as errors in tests). Use string? blockDevice = null; and prefer is null for the null check to avoid build breaks.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit b025b70. Changed to string? blockDevice = null; and is null check.

await readerTask;
}

[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetFileType_SymbolicLink is in an AnyUnix-only test file, but it’s guarded by IsNotWindowsNanoServer, which is Windows-specific and effectively redundant here. Consider removing this condition or replacing it with a Unix-relevant capability check to keep the test intent clear.

Suggested change
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
[Fact]

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in commit b025b70. Removed the redundant IsNotWindowsNanoServer condition from the Unix-only test file.

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Mar 3, 2026

@copilot please invoke the code-review skill and post the analysis/results as a comment on this PR

🤖 Copilot Code Review

Holistic Assessment

Motivation: Adds public API (SafeFileHandle.Type property and FileHandleType enum) to expose file handle type information. PR description states this validates feasibility for issue #122806 (inaccessible/non-existent). Without concrete use case or API approval evidence, motivation is unclear.

Approach: Implementation reuses existing platform syscalls and follows established caching patterns. Platform separation is clean. Interop change (SafeHandle vs SafePipeHandle) is necessary and correct.

Summary: ⚠️ Needs Human Review. Code implementation is technically correct and follows codebase patterns, but requires maintainer judgment on: (1) API approval status — no evidence provided, (2) CanSeek behavior change — now excludes directories/symlinks (was FILE_TYPE_DISK, now RegularFile), (3) caching race — pattern allows redundant syscalls on concurrent access (matches existing GetFileOptions pattern), (4) Windows error handling — silently defaults to RegularFile on API failures vs Unix which throws.


Detailed Findings

❌ API Approval Required

Per dotnet/runtime policy: "We do not accept PRs for unapproved APIs." This PR adds public FileHandleType enum and SafeFileHandle.Type property without evidence of API review approval. Referenced issue #122806 is inaccessible.

Required: Provide api-approved issue link or mark APIs internal until approval.

⚠️ CanSeek Behavior Tightening

Changed from GetFileType() == FILE_TYPE_DISK to Type == RegularFile. Old code included directories/symlinks; new code excludes them. Semantically more correct (directories aren't seekable as file streams) but is a behavior change. Verify this doesn't expose latent bugs in callers.

⚠️ Thread-Safety Pattern

Caching uses double-checked locking without synchronization:

int cachedType = _cachedFileType;
if (cachedType == -1) { cachedType = _cachedFileType = (int)GetFileTypeCore(); }

Allows concurrent threads to both call GetFileTypeCore() (redundant syscalls). Matches existing GetFileOptions() pattern — acceptable for benign races, but note if strict once-only init is required.

⚠️ Windows Silent Fallback

GetDiskBasedType() returns RegularFile if GetFileInformationByHandle(Ex) fails. Unix throws on FStat failure. Asymmetry may be intentional (open handles rarely fail these APIs), but worth confirming.

✅ Implementation Quality

  • Platform separation is clean (shared/Windows/Unix)
  • Unix opportunistically caches during Init() (avoids extra syscall)
  • Interop change (SafePipeHandleSafeHandle) is necessary and safe
  • Test coverage is comprehensive with proper platform organization

✅ Test Quality

Well-organized (agnostic/Windows/Unix split), covers regular files, directories, symlinks, pipes, sockets, devices, error cases, caching. Uses async/await for named pipe coordination. Proper browser skips.


Overall: Code is technically sound. Approve pending: API approval confirmation, CanSeek change validation, and caching/error-handling pattern acceptance. Commit a30ca8e.

Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants