Skip to content
Draft
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
Expand Up @@ -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
{
Expand All @@ -11,7 +10,7 @@ internal static partial class Kernel32
[LibraryImport(Libraries.Kernel32, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static unsafe partial bool GetNamedPipeInfo(
SafePipeHandle hNamedPipe,
SafeHandle hNamedPipe,
uint* lpFlags,
uint* lpOutBufferSize,
uint* lpInBufferSize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -494,6 +497,43 @@ private bool GetCanSeek()
return canSeek == NullableBool.True;
}

internal System.IO.FileType GetFileTypeCore()
{
int cachedType = _cachedFileType;
if (cachedType != -1)
{
return (System.IO.FileType)cachedType;
}

// If we don't have a cached value, call FStat to get it
int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status);
if (result != 0)
{
throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo());
}

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)
{
#pragma warning disable CA1416 // BlockDevice is only returned on Unix platforms
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
};
#pragma warning restore CA1416
}

internal long GetFileLength()
{
int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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 && GetFileType() == System.IO.FileType.RegularFile;

internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; }

Expand Down Expand Up @@ -254,20 +253,63 @@ internal unsafe FileOptions GetFileOptions()
return _fileOptions = result;
}

internal int GetFileType()
internal unsafe System.IO.FileType GetFileTypeCore()
{
int fileType = _fileType;
if (fileType == -1)
int cachedType = _cachedFileType;
if (cachedType != -1)
{
_fileType = fileType = Interop.Kernel32.GetFileType(this);
return (System.IO.FileType)cachedType;
}

int kernelFileType = Interop.Kernel32.GetFileType(this);

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;
}

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}");
// 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 fileType;
return System.IO.FileType.RegularFile;
}

internal long GetFileLength()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.Win32.SafeHandles
public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private string? _path;
private volatile int _cachedFileType = -1;

/// <summary>
/// Creates a <see cref="T:Microsoft.Win32.SafeHandles.SafeFileHandle" /> around a file handle.
Expand All @@ -20,5 +21,16 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand
}

internal string? Path => _path;

/// <summary>
/// Gets the type of the file that this handle represents.
/// </summary>
/// <returns>The type of the file.</returns>
/// <exception cref="ObjectDisposedException">The handle is closed.</exception>
public System.IO.FileType GetFileType()
{
ObjectDisposedException.ThrowIf(IsClosed, this);
return GetFileTypeCore();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileShare.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStream.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileStreamOptions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileType.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileSystem.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\FileSystemInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\HandleInheritability.cs" />
Expand Down Expand Up @@ -1953,6 +1954,9 @@
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetFileType_SafeHandle.cs">
<Link>Common\Interop\Windows\Kernel32\Interop.GetFileType_SafeHandle.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetNamedPipeInfo.cs">
<Link>Common\Interop\Windows\Kernel32\Interop.GetNamedPipeInfo.cs</Link>
</Compile>
<Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs">
<Link>Common\Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs</Link>
</Compile>
Expand Down
52 changes: 52 additions & 0 deletions src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Specifies the type of a file.
/// </summary>
public enum FileType
{
/// <summary>
/// The file type is unknown.
/// </summary>
Unknown,

/// <summary>
/// The file is a regular file.
/// </summary>
RegularFile,

/// <summary>
/// The file is a pipe (FIFO).
/// </summary>
Pipe,

/// <summary>
/// The file is a socket.
/// </summary>
Socket,

/// <summary>
/// The file is a character device.
/// </summary>
CharacterDevice,

/// <summary>
/// The file is a directory.
/// </summary>
Directory,

/// <summary>
/// The file is a symbolic link.
/// </summary>
SymbolicLink,

/// <summary>
/// The file is a block device.
/// </summary>
[System.Runtime.Versioning.UnsupportedOSPlatform("windows")]
BlockDevice
}
}
13 changes: 13 additions & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// 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.Threading.Tasks;
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);

using SafeFileHandle handle = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0);
Assert.False(handle.IsInvalid);
Assert.Equal(FileType.Directory, handle.GetFileType());
}

[Fact]
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);

using SafeFileHandle handle = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0);

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
{
using SafeFileHandle handle = Interop.Sys.Open(blockDevice, Interop.Sys.OpenFlags.O_RDONLY, 0);
if (handle.IsInvalid)
{
throw new SkipTestException($"Could not open {blockDevice}");
}

Assert.Equal(FileType.BlockDevice, handle.GetFileType());
}
catch (UnauthorizedAccessException)
{
throw new SkipTestException("Insufficient privileges to open block device");
}
}
}
}
Loading
Loading