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..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,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, 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..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 @@ -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,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); 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..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 @@ -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 && GetFileType() == System.IO.FileType.RegularFile; internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; } @@ -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() 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..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 @@ -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,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.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index c744f85d3e05ce..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 @@ -514,6 +514,7 @@ + @@ -1953,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.Private.CoreLib/src/System/IO/FileType.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs new file mode 100644 index 00000000000000..3f4290c2286388 --- /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. + /// + [System.Runtime.Versioning.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) { } 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..35069ebfaf28b8 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs @@ -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"); + } + } + } +} 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..0b45b1d547dcc1 --- /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()); + } + + [Fact] + 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 new file mode 100644 index 00000000000000..9cf46dfdeb9cc0 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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_NullDevice() + { + using SafeFileHandle handle = File.OpenNullHandle(); + Assert.Equal(FileType.CharacterDevice, handle.GetFileType()); + } + + [Fact] + public void GetFileType_AnonymousPipe() + { + using AnonymousPipeServerStream server = new(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()); + } + + [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()); + } + + [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); + } + } +} 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..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 @@ -93,12 +93,20 @@ + + + + + @@ -112,6 +120,7 @@ + @@ -241,6 +250,7 @@ +