diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IOVector.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IOVector.cs new file mode 100644 index 00000000000000..9cbf1ee2c34785 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.IOVector.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +internal static partial class Interop +{ + internal static partial class Sys + { + internal unsafe struct IOVector + { + public byte* Base; + public UIntPtr Count; + } + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.MessageHeader.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.MessageHeader.cs index 5e327a64793624..4b71e0e3d37246 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.MessageHeader.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.MessageHeader.cs @@ -8,12 +8,6 @@ internal static partial class Interop { internal static partial class Sys { - internal unsafe struct IOVector - { - public byte* Base; - public UIntPtr Count; - } - internal unsafe struct MessageHeader { public byte* SocketAddress; diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PRead.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PRead.cs new file mode 100644 index 00000000000000..664da015febb2e --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PRead.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_PRead", SetLastError = true)] + internal static extern unsafe int PRead(SafeHandle fd, byte* buffer, int bufferSize, long fileOffset); + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PReadV.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PReadV.cs new file mode 100644 index 00000000000000..5d93078161f05d --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PReadV.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_PReadV", SetLastError = true)] + internal static extern unsafe long PReadV(SafeHandle fd, IOVector* vectors, int vectorCount, long fileOffset); + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PWrite.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PWrite.cs new file mode 100644 index 00000000000000..721a1c8706fd72 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PWrite.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_PWrite", SetLastError = true)] + internal static extern unsafe int PWrite(SafeHandle fd, byte* buffer, int bufferSize, long fileOffset); + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PWriteV.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PWriteV.cs new file mode 100644 index 00000000000000..c17e9964d8fe9a --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PWriteV.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Sys + { + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_PWriteV", SetLastError = true)] + internal static extern unsafe long PWriteV(SafeHandle fd, IOVector* vectors, int vectorCount, long fileOffset); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileScatterGather.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileScatterGather.cs new file mode 100644 index 00000000000000..8e61970c3c2424 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileScatterGather.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Threading; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [DllImport(Libraries.Kernel32, SetLastError = true)] + internal static extern unsafe int ReadFileScatter( + SafeHandle hFile, + long* aSegmentArray, + int nNumberOfBytesToRead, + IntPtr lpReserved, + NativeOverlapped* lpOverlapped); + + [DllImport(Libraries.Kernel32, SetLastError = true)] + internal static extern unsafe int WriteFileGather( + SafeHandle hFile, + long* aSegmentArray, + int nNumberOfBytesToWrite, + IntPtr lpReserved, + NativeOverlapped* lpOverlapped); + } +} diff --git a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs index 9ce82c7f4ad453..1a89d911f83bdf 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs @@ -10,10 +10,6 @@ internal static partial class Interop { internal static partial class NtDll { - internal const uint NT_ERROR_STATUS_DISK_FULL = 0xC000007F; - internal const uint NT_ERROR_STATUS_FILE_TOO_LARGE = 0xC0000904; - internal const uint NT_STATUS_INVALID_PARAMETER = 0xC000000D; - // https://msdn.microsoft.com/en-us/library/bb432380.aspx // https://msdn.microsoft.com/en-us/library/windows/hardware/ff566424.aspx [DllImport(Libraries.NtDll, CharSet = CharSet.Unicode, ExactSpelling = true)] @@ -76,7 +72,7 @@ internal static unsafe (uint status, IntPtr handle) CreateFile( } } - internal static unsafe (uint status, IntPtr handle) CreateFile(ReadOnlySpan path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) + internal static unsafe (uint status, IntPtr handle) NtCreateFile(ReadOnlySpan path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) { // For mitigating local elevation of privilege attack through named pipes // make sure we always call NtCreateFile with SECURITY_ANONYMOUS so that the @@ -120,7 +116,7 @@ private static CreateDisposition GetCreateDisposition(FileMode mode) private static DesiredAccess GetDesiredAccess(FileAccess access, FileMode fileMode, FileOptions options) { - DesiredAccess result = 0; + DesiredAccess result = DesiredAccess.FILE_READ_ATTRIBUTES | DesiredAccess.SYNCHRONIZE; // default values used by CreateFileW if ((access & FileAccess.Read) != 0) { @@ -134,13 +130,9 @@ private static DesiredAccess GetDesiredAccess(FileAccess access, FileMode fileMo { result |= DesiredAccess.FILE_APPEND_DATA; } - if ((options & FileOptions.Asynchronous) == 0) - { - result |= DesiredAccess.SYNCHRONIZE; // required by FILE_SYNCHRONOUS_IO_NONALERT - } - if ((options & FileOptions.DeleteOnClose) != 0 || fileMode == FileMode.Create) + if ((options & FileOptions.DeleteOnClose) != 0) { - result |= DesiredAccess.DELETE; // required by FILE_DELETE_ON_CLOSE and FILE_SUPERSEDE (which deletes a file if it exists) + result |= DesiredAccess.DELETE; // required by FILE_DELETE_ON_CLOSE } return result; @@ -190,7 +182,8 @@ private static CreateOptions GetCreateOptions(FileOptions options) } private static ObjectAttributes GetObjectAttributes(FileShare share) - => (share & FileShare.Inheritable) != 0 ? ObjectAttributes.OBJ_INHERIT : 0; + => ObjectAttributes.OBJ_CASE_INSENSITIVE | // default value used by CreateFileW + ((share & FileShare.Inheritable) != 0 ? ObjectAttributes.OBJ_INHERIT : 0); /// /// File creation disposition when calling directly to NT APIs. diff --git a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtQueryInformationFile.cs b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtQueryInformationFile.cs index 644315f2bc2ccf..402443b05e944e 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtQueryInformationFile.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtQueryInformationFile.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Win32.SafeHandles; -using System; using System.Runtime.InteropServices; internal static partial class Interop diff --git a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtStatus.cs b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtStatus.cs index d79653a64105e2..76715c83be916d 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtStatus.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtStatus.cs @@ -17,5 +17,7 @@ internal static class StatusOptions internal const uint STATUS_ACCOUNT_RESTRICTION = 0xC000006E; internal const uint STATUS_NONE_MAPPED = 0xC0000073; internal const uint STATUS_INSUFFICIENT_RESOURCES = 0xC000009A; + internal const uint STATUS_DISK_FULL = 0xC000007F; + internal const uint STATUS_FILE_TOO_LARGE = 0xC0000904; } } diff --git a/src/libraries/Native/Unix/Common/pal_config.h.in b/src/libraries/Native/Unix/Common/pal_config.h.in index b7bd484e629ffa..034d57b1e1c98b 100644 --- a/src/libraries/Native/Unix/Common/pal_config.h.in +++ b/src/libraries/Native/Unix/Common/pal_config.h.in @@ -36,6 +36,8 @@ #cmakedefine01 HAVE_POSIX_ADVISE #cmakedefine01 HAVE_POSIX_FALLOCATE #cmakedefine01 HAVE_POSIX_FALLOCATE64 +#cmakedefine01 HAVE_PREADV +#cmakedefine01 HAVE_PWRITEV #cmakedefine01 PRIORITY_REQUIRES_INT_WHO #cmakedefine01 KEVENT_REQUIRES_INT_PARAMS #cmakedefine01 HAVE_IOCTL diff --git a/src/libraries/Native/Unix/System.Native/entrypoints.c b/src/libraries/Native/Unix/System.Native/entrypoints.c index 0f7b1a12fa5076..8a1438b6c3d18b 100644 --- a/src/libraries/Native/Unix/System.Native/entrypoints.c +++ b/src/libraries/Native/Unix/System.Native/entrypoints.c @@ -242,6 +242,10 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_iOSSupportVersion) DllImportEntry(SystemNative_GetErrNo) DllImportEntry(SystemNative_SetErrNo) + DllImportEntry(SystemNative_PRead) + DllImportEntry(SystemNative_PWrite) + DllImportEntry(SystemNative_PReadV) + DllImportEntry(SystemNative_PWriteV) }; EXTERN_C const void* SystemResolveDllImport(const char* name); diff --git a/src/libraries/Native/Unix/System.Native/pal_io.c b/src/libraries/Native/Unix/System.Native/pal_io.c index c63eb8898cace9..d7eb6c4ab23aca 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.c +++ b/src/libraries/Native/Unix/System.Native/pal_io.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -1454,3 +1455,107 @@ int32_t SystemNative_ReadProcessStatusInfo(pid_t pid, ProcessStatus* processStat return -1; #endif // __sun } + +int32_t SystemNative_PRead(intptr_t fd, void* buffer, int32_t bufferSize, int64_t fileOffset) +{ + assert(buffer != NULL); + assert(bufferSize >= 0); + + ssize_t count; + while ((count = pread(ToFileDescriptor(fd), buffer, (uint32_t)bufferSize, (off_t)fileOffset)) < 0 && errno == EINTR); + + assert(count >= -1 && count <= bufferSize); + return (int32_t)count; +} + +int32_t SystemNative_PWrite(intptr_t fd, void* buffer, int32_t bufferSize, int64_t fileOffset) +{ + assert(buffer != NULL); + assert(bufferSize >= 0); + + ssize_t count; + while ((count = pwrite(ToFileDescriptor(fd), buffer, (uint32_t)bufferSize, (off_t)fileOffset)) < 0 && errno == EINTR); + + assert(count >= -1 && count <= bufferSize); + return (int32_t)count; +} + +int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset) +{ + assert(vectors != NULL); + assert(vectorCount >= 0); + + int64_t count = 0; + int fileDescriptor = ToFileDescriptor(fd); +#if HAVE_PREADV && !defined(TARGET_WASM) // preadv is buggy on WASM + while ((count = preadv(fileDescriptor, (struct iovec*)vectors, (int)vectorCount, (off_t)fileOffset)) < 0 && errno == EINTR); +#else + int64_t current; + for (int i = 0; i < vectorCount; i++) + { + IOVector vector = vectors[i]; + while ((current = pread(fileDescriptor, vector.Base, vector.Count, (off_t)(fileOffset + count))) < 0 && errno == EINTR); + + if (current < 0) + { + // if previous calls were succesfull, we return what we got so far + // otherwise, we return the error code + return count > 0 ? count : current; + } + + count += current; + + // Incomplete pread operation may happen for two reasons: + // a) We have reached EOF. + // b) The operation was interrupted by a signal handler. + // To mimic preadv, we stop on the first incomplete operation. + if (current != (int64_t)vector.Count) + { + return count; + } + } +#endif + + assert(count >= -1); + return count; +} + +int64_t SystemNative_PWriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset) +{ + assert(vectors != NULL); + assert(vectorCount >= 0); + + int64_t count = 0; + int fileDescriptor = ToFileDescriptor(fd); +#if HAVE_PWRITEV && !defined(TARGET_WASM) // pwritev is buggy on WASM + while ((count = pwritev(fileDescriptor, (struct iovec*)vectors, (int)vectorCount, (off_t)fileOffset)) < 0 && errno == EINTR); +#else + int64_t current; + for (int i = 0; i < vectorCount; i++) + { + IOVector vector = vectors[i]; + while ((current = pwrite(fileDescriptor, vector.Base, vector.Count, (off_t)(fileOffset + count))) < 0 && errno == EINTR); + + if (current < 0) + { + // if previous calls were succesfull, we return what we got so far + // otherwise, we return the error code + return count > 0 ? count : current; + } + + count += current; + + // Incomplete pwrite operation may happen for few reasons: + // a) There was not enough space available or the file is too large for given file system. + // b) The operation was interrupted by a signal handler. + // To mimic pwritev, we stop on the first incomplete operation. + if (current != (int64_t)vector.Count) + { + return count; + } + } +#endif + + assert(count >= -1); + return count; +} diff --git a/src/libraries/Native/Unix/System.Native/pal_io.h b/src/libraries/Native/Unix/System.Native/pal_io.h index e3d402d9be5f66..1dc70387ad5eac 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.h +++ b/src/libraries/Native/Unix/System.Native/pal_io.h @@ -41,6 +41,14 @@ typedef struct // add more fields when needed. } ProcessStatus; +// NOTE: the layout of this type is intended to exactly match the layout of a `struct iovec`. There are +// assertions in pal_networking.c that validate this. +typedef struct +{ + uint8_t* Base; + uintptr_t Count; +} IOVector; + /* Provide consistent access to nanosecond fields, if they exist. */ /* Seconds are always available through st_atime, st_mtime, st_ctime. */ @@ -730,3 +738,31 @@ PALEXPORT int32_t SystemNative_LChflagsCanSetHiddenFlag(void); * Returns 1 if the process status was read; otherwise, 0. */ PALEXPORT int32_t SystemNative_ReadProcessStatusInfo(pid_t pid, ProcessStatus* processStatus); + +/** + * Reads the number of bytes specified into the provided buffer from the specified, opened file descriptor at specified offset. + * + * Returns the number of bytes read on success; otherwise, -1 is returned an errno is set. + */ +PALEXPORT int32_t SystemNative_PRead(intptr_t fd, void* buffer, int32_t bufferSize, int64_t fileOffset); + +/** + * Writes the number of bytes specified in the buffer into the specified, opened file descriptor at specified offset. + * + * Returns the number of bytes written on success; otherwise, -1 is returned an errno is set. + */ +PALEXPORT int32_t SystemNative_PWrite(intptr_t fd, void* buffer, int32_t bufferSize, int64_t fileOffset); + +/** + * Reads the number of bytes specified into the provided buffers from the specified, opened file descriptor at specified offset. + * + * Returns the number of bytes read on success; otherwise, -1 is returned an errno is set. + */ +PALEXPORT int64_t SystemNative_PReadV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset); + +/** + * Writes the number of bytes specified in the buffers into the specified, opened file descriptor at specified offset. + * + * Returns the number of bytes written on success; otherwise, -1 is returned an errno is set. + */ +PALEXPORT int64_t SystemNative_PWriteV(intptr_t fd, IOVector* vectors, int32_t vectorCount, int64_t fileOffset); diff --git a/src/libraries/Native/Unix/System.Native/pal_networking.c b/src/libraries/Native/Unix/System.Native/pal_networking.c index 11124f34c29bb3..3236fe26acfeba 100644 --- a/src/libraries/Native/Unix/System.Native/pal_networking.c +++ b/src/libraries/Native/Unix/System.Native/pal_networking.c @@ -3,7 +3,6 @@ #include "pal_config.h" #include "pal_networking.h" -#include "pal_io.h" #include "pal_safecrt.h" #include "pal_utilities.h" #include @@ -3104,11 +3103,11 @@ int32_t SystemNative_Disconnect(intptr_t socket) addr.sa_family = AF_UNSPEC; err = connect(fd, &addr, sizeof(addr)); - if (err != 0) + if (err != 0) { // On some older kernels connect(AF_UNSPEC) may fail. Fall back to shutdown in these cases: err = shutdown(fd, SHUT_RDWR); - } + } #elif HAVE_DISCONNECTX // disconnectx causes a FIN close on OSX. It's the best we can do. err = disconnectx(fd, SAE_ASSOCID_ANY, SAE_CONNID_ANY); diff --git a/src/libraries/Native/Unix/System.Native/pal_networking.h b/src/libraries/Native/Unix/System.Native/pal_networking.h index bbb0bc0785ccec..4bf9de658e26f8 100644 --- a/src/libraries/Native/Unix/System.Native/pal_networking.h +++ b/src/libraries/Native/Unix/System.Native/pal_networking.h @@ -4,6 +4,7 @@ #pragma once #include "pal_compiler.h" +#include "pal_io.h" #include "pal_types.h" #include "pal_errno.h" #include @@ -275,14 +276,6 @@ typedef struct int32_t Seconds; // Number of seconds to linger for } LingerOption; -// NOTE: the layout of this type is intended to exactly match the layout of a `struct iovec`. There are -// assertions in pal_networking.cpp that validate this. -typedef struct -{ - uint8_t* Base; - uintptr_t Count; -} IOVector; - typedef struct { uint8_t* SocketAddress; diff --git a/src/libraries/Native/Unix/configure.cmake b/src/libraries/Native/Unix/configure.cmake index 7c0ed7a2af90fa..9d40db9dbf2122 100644 --- a/src/libraries/Native/Unix/configure.cmake +++ b/src/libraries/Native/Unix/configure.cmake @@ -219,6 +219,16 @@ check_symbol_exists( fcntl.h HAVE_POSIX_FALLOCATE64) +check_symbol_exists( + preadv + sys/uio.h + HAVE_PREADV) + +check_symbol_exists( + pwritev + sys/uio.h + HAVE_PWRITEV) + check_symbol_exists( ioctl sys/ioctl.h diff --git a/src/libraries/System.IO.FileSystem/tests/File/AppendAsync.cs b/src/libraries/System.IO.FileSystem/tests/File/AppendAsync.cs index 04d7ebec7ae4d8..457366266d69cf 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/AppendAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/AppendAsync.cs @@ -86,7 +86,7 @@ public override Task TaskAlreadyCanceledAsync() } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class File_AppendAllLinesAsync_Encoded : File_AppendAllLinesAsync { protected override Task WriteAsync(string path, string[] content) => diff --git a/src/libraries/System.IO.FileSystem/tests/File/Create.cs b/src/libraries/System.IO.FileSystem/tests/File/Create.cs index a4a8de349f3bb3..bf0417dbfe0610 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/Create.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/Create.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; using Xunit; namespace System.IO.Tests @@ -51,6 +52,37 @@ public void ValidCreation() } } + [Fact] + public void CreateAndOverwrite() + { + const byte initialContent = 1; + + DirectoryInfo testDir = Directory.CreateDirectory(GetTestFilePath()); + string testFile = Path.Combine(testDir.FullName, GetTestFileName()); + + // Create + using (FileStream stream = Create(testFile)) + { + Assert.True(File.Exists(testFile)); + + stream.WriteByte(initialContent); + + Assert.Equal(1, stream.Length); + Assert.Equal(1, stream.Position); + } + + Assert.Equal(initialContent, File.ReadAllBytes(testFile).Single()); + + // Overwrite + using (FileStream stream = Create(testFile)) + { + Assert.Equal(0, stream.Length); + Assert.Equal(0, stream.Position); + } + + Assert.Empty(File.ReadAllBytes(testFile)); + } + [ConditionalFact(nameof(UsingNewNormalization))] [PlatformSpecific(TestPlatforms.Windows)] // Valid Windows path extended prefix public void ValidCreation_ExtendedSyntax() @@ -335,7 +367,7 @@ public void NegativeBuffer() Assert.Throws(() => Create(GetTestFilePath(), -100)); } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class File_Create_str_i_fo : File_Create_str_i { public override FileStream Create(string path) diff --git a/src/libraries/System.IO.FileSystem/tests/File/EncryptDecrypt.cs b/src/libraries/System.IO.FileSystem/tests/File/EncryptDecrypt.cs index 624eb2aae7cd8d..984afef972bc41 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/EncryptDecrypt.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/EncryptDecrypt.cs @@ -7,7 +7,7 @@ namespace System.IO.Tests { - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class EncryptDecrypt : FileSystemTest { [Fact] diff --git a/src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs b/src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs new file mode 100644 index 00000000000000..a283b01a0b7e94 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs @@ -0,0 +1,70 @@ +// 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 Xunit; + +namespace System.IO.Tests +{ + // to avoid a lot of code duplication, we reuse FileStream tests + public class File_OpenHandle : FileStream_ctor_options_as + { + protected override string GetExpectedParamName(string paramName) => paramName; + + protected override FileStream CreateFileStream(string path, FileMode mode) + { + FileAccess access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite; + return new FileStream(File.OpenHandle(path, mode, access, preallocationSize: PreallocationSize), access); + } + + protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access) + => new FileStream(File.OpenHandle(path, mode, access, preallocationSize: PreallocationSize), access); + + protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + => new FileStream(File.OpenHandle(path, mode, access, share, options, PreallocationSize), access, bufferSize, (options & FileOptions.Asynchronous) != 0); + + [Fact] + public override void NegativePreallocationSizeThrows() + { + ArgumentOutOfRangeException ex = Assert.Throws( + () => File.OpenHandle("validPath", FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize: -1)); + } + + [ActiveIssue("https://github.com/dotnet/runtime/issues/53432")] + [Theory, MemberData(nameof(StreamSpecifiers))] + public override void FileModeAppendExisting(string streamSpecifier) + { + _ = streamSpecifier; // to keep the xUnit analyser happy + } + + [Theory] + [InlineData(FileOptions.None)] + [InlineData(FileOptions.Asynchronous)] + public void SafeFileHandle_IsAsync_ReturnsCorrectInformation(FileOptions options) + { + using (var handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write, options: options)) + { + Assert.Equal((options & FileOptions.Asynchronous) != 0, handle.IsAsync); + + // the following code exercises the code path where we don't know FileOptions used for opening the handle + // and instead we ask the OS about it + if (OperatingSystem.IsWindows()) // async file handles are a Windows concept + { + SafeFileHandle createdFromIntPtr = new SafeFileHandle(handle.DangerousGetHandle(), ownsHandle: false); + Assert.Equal((options & FileOptions.Asynchronous) != 0, createdFromIntPtr.IsAsync); + } + } + } + + // Unix doesn't directly support DeleteOnClose + // For FileStream created out of path, we mimic it by closing the handle first + // and then unlinking the path + // Since SafeFileHandle does not always have the path and we can't find path for given file descriptor on Unix + // this test runs only on Windows + [PlatformSpecific(TestPlatforms.Windows)] + [Theory] + [InlineData(FileOptions.DeleteOnClose)] + [InlineData(FileOptions.DeleteOnClose | FileOptions.Asynchronous)] + public override void DeleteOnClose_FileDeletedAfterClose(FileOptions options) => base.DeleteOnClose_FileDeletedAfterClose(options); + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs index fdb0ec63776deb..111ecb545adea3 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs @@ -8,7 +8,7 @@ namespace System.IO.Tests { - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class File_ReadWriteAllBytesAsync : FileSystemTest { [Fact] diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs index f5cbd0f3b7379d..76d1541d60c5e7 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs @@ -10,7 +10,7 @@ namespace System.IO.Tests { - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class File_ReadWriteAllLines_EnumerableAsync : FileSystemTest { #region Utilities diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs index fe67d881c2ad19..3481bff97b4f41 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs @@ -9,7 +9,7 @@ namespace System.IO.Tests { - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class File_ReadWriteAllTextAsync : FileSystemTest { #region Utilities @@ -145,7 +145,7 @@ public virtual Task TaskAlreadyCanceledAsync() #endregion } - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class File_ReadWriteAllText_EncodedAsync : File_ReadWriteAllTextAsync { protected override Task WriteAsync(string path, string content) => diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/CopyToAsync.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/CopyToAsync.cs index f059c49ab4ce03..3a48354964b8a5 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/CopyToAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/CopyToAsync.cs @@ -8,7 +8,7 @@ namespace System.IO.Tests { - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class FileStream_CopyToAsync : FileSystemTest { [Theory] diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs index 0fbc7711045693..d1fd4fe764f4d3 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs @@ -205,7 +205,7 @@ public class BufferedSyncFileStreamStandaloneConformanceTests : FileStreamStanda protected override int BufferSize => 10; } - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] [SkipOnPlatform(TestPlatforms.Browser, "lots of operations aren't supported on browser")] // copied from StreamConformanceTests base class due to https://github.com/xunit/xunit/issues/2186 public class UnbufferedAsyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests { @@ -218,7 +218,7 @@ public class UnbufferedAsyncFileStreamStandaloneConformanceTests : FileStreamSta #endif } - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] [SkipOnPlatform(TestPlatforms.Browser, "lots of operations aren't supported on browser")] // copied from StreamConformanceTests base class due to https://github.com/xunit/xunit/issues/2186 public class BufferedAsyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests { diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/IsAsync.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/IsAsync.cs index b0900760c2687f..e187468035fe60 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/IsAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/IsAsync.cs @@ -8,7 +8,7 @@ namespace System.IO.Tests { - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class FileStream_IsAsync : FileSystemTest { [Fact] diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs index ef4458ef7f45be..da429bbad21874 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs @@ -94,14 +94,14 @@ public async Task ReadAsyncCanceledFile() } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class FileStream_ReadAsync_AsyncReads : FileStream_AsyncReads { protected override Task ReadAsync(FileStream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) => stream.ReadAsync(buffer, offset, count, cancellationToken); } - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class FileStream_BeginEndRead_AsyncReads : FileStream_AsyncReads { protected override Task ReadAsync(FileStream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) => diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs index 40064cd42c359c..0740eed02f0d85 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/SafeFileHandle.cs @@ -9,7 +9,7 @@ namespace System.IO.Tests { - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class FileStream_SafeFileHandle : FileSystemTest { [Fact] diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs index 9e4db63e6fbc3a..7f7fd8ad240bab 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/WriteAsync.cs @@ -324,7 +324,7 @@ public async Task WriteAsyncMiniStress() } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class FileStream_WriteAsync_AsyncWrites : FileStream_AsyncWrites { protected override Task WriteAsync(FileStream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) => @@ -371,7 +371,7 @@ public void CancelledTokenFastPath() } } - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class FileStream_BeginEndWrite_AsyncWrites : FileStream_AsyncWrites { protected override Task WriteAsync(FileStream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) => diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.cs index 3190e66bdc836d..f36e55ea16d2de 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.cs @@ -69,7 +69,7 @@ public partial class NoParallelTests { } public partial class FileStream_ctor_options_as : FileStream_ctor_options_as_base { [Fact] - public void NegativePreallocationSizeThrows() + public virtual void NegativePreallocationSizeThrows() { string filePath = GetPathToNonExistingFile(); ArgumentOutOfRangeException ex = Assert.Throws( diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa.cs index 49881c43dbab16..5ad232a6d3a94d 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa.cs @@ -25,7 +25,7 @@ public void InvalidHandle_Throws() [Fact] public void InvalidAccess_Throws() { - using (var handle = new SafeFileHandle(new IntPtr(1), ownsHandle: false)) + using (var handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write)) { AssertExtensions.Throws("access", () => CreateFileStream(handle, ~FileAccess.Read)); } @@ -34,7 +34,7 @@ public void InvalidAccess_Throws() [Fact] public void InvalidAccess_DoesNotCloseHandle() { - using (var handle = new SafeFileHandle(new IntPtr(1), ownsHandle: false)) + using (var handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write)) { Assert.Throws(() => CreateFileStream(handle, ~FileAccess.Read)); GC.Collect(); diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa_buffer.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa_buffer.cs index 745ff57850888a..0c480d4a4fcb60 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa_buffer.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa_buffer.cs @@ -18,21 +18,19 @@ protected virtual FileStream CreateFileStream(SafeFileHandle handle, FileAccess return new FileStream(handle, access, bufferSize); } - [Theory, - InlineData(0), - InlineData(-1)] - public void InvalidBufferSize_Throws(int size) + [Fact] + public void NegativeBufferSize_Throws() { - using (var handle = new SafeFileHandle(new IntPtr(1), ownsHandle: false)) + using (var handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write)) { - AssertExtensions.Throws("bufferSize", () => CreateFileStream(handle, FileAccess.Read, size)); + AssertExtensions.Throws("bufferSize", () => CreateFileStream(handle, FileAccess.Read, -1)); } } [Fact] public void InvalidBufferSize_DoesNotCloseHandle() { - using (var handle = new SafeFileHandle(new IntPtr(1), ownsHandle: false)) + using (var handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write)) { Assert.Throws(() => CreateFileStream(handle, FileAccess.Read, -1)); GC.Collect(); diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa_buffer_async.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa_buffer_async.cs index f05cbce26f5b9d..b2a8ba19a1d1f7 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa_buffer_async.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_sfh_fa_buffer_async.cs @@ -6,7 +6,7 @@ namespace System.IO.Tests { - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class FileStream_ctor_sfh_fa_buffer_async : FileStream_ctor_sfh_fa_buffer { protected sealed override FileStream CreateFileStream(SafeFileHandle handle, FileAccess access, int bufferSize) diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs index f325fc2ed099d1..61efd825f6e48e 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs @@ -5,7 +5,7 @@ namespace System.IO.Tests { - [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class FileStream_ctor_str_fm_fa_fs_buffer_fo : FileStream_ctor_str_fm_fa_fs_buffer { protected sealed override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize) @@ -79,7 +79,7 @@ public void ValidFileOptions_Encrypted(FileOptions option) [Theory] [InlineData(FileOptions.DeleteOnClose)] [InlineData(FileOptions.DeleteOnClose | FileOptions.Asynchronous)] - public void DeleteOnClose_FileDeletedAfterClose(FileOptions options) + public virtual void DeleteOnClose_FileDeletedAfterClose(FileOptions options) { string path = GetTestFilePath(); Assert.False(File.Exists(path)); diff --git a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj index dfabeb02694b11..628bbb4324aeae 100644 --- a/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/Net5CompatTests/System.IO.FileSystem.Net5Compat.Tests.csproj @@ -19,10 +19,12 @@ - - - - + + + + + + diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/Base.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/Base.cs new file mode 100644 index 00000000000000..8d876f30174cd0 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/Base.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Pipes; +using System.Threading; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + public abstract class RandomAccess_Base : FileSystemTest + { + protected abstract T MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset); + + protected virtual bool ShouldThrowForSyncHandle => false; + + protected virtual bool ShouldThrowForAsyncHandle => false; + + protected virtual bool UsesOffsets => true; + + [Fact] + public void ThrowsArgumentNullExceptionForNullHandle() + { + AssertExtensions.Throws("handle", () => MethodUnderTest(null, Array.Empty(), 0)); + } + + [Fact] + public void ThrowsArgumentExceptionForInvalidHandle() + { + SafeFileHandle handle = new SafeFileHandle(new IntPtr(-1), ownsHandle: false); + + AssertExtensions.Throws("handle", () => MethodUnderTest(handle, Array.Empty(), 0)); + } + + [Fact] + public void ThrowsObjectDisposedExceptionForDisposedHandle() + { + SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write); + handle.Dispose(); + + Assert.Throws(() => MethodUnderTest(handle, Array.Empty(), 0)); + } + + [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "System.IO.Pipes aren't supported on browser")] + public void ThrowsNotSupportedExceptionForUnseekableFile() + { + using (var server = new AnonymousPipeServerStream(PipeDirection.Out)) + using (SafeFileHandle handle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), true)) + { + Assert.Throws(() => MethodUnderTest(handle, Array.Empty(), 0)); + } + } + + [Fact] + public void ThrowsArgumentOutOfRangeExceptionForNegativeFileOffset() + { + if (UsesOffsets) + { + FileOptions options = ShouldThrowForAsyncHandle ? FileOptions.None : FileOptions.Asynchronous; + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.Write, options: options)) + { + AssertExtensions.Throws("fileOffset", () => MethodUnderTest(handle, Array.Empty(), -1)); + } + } + } + + [Fact] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")] + public void ThrowsArgumentExceptionForAsyncFileHandle() + { + if (ShouldThrowForAsyncHandle) + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.Write, options: FileOptions.Asynchronous)) + { + AssertExtensions.Throws("handle", () => MethodUnderTest(handle, new byte[100], 0)); + } + } + } + + [Fact] + public void ThrowsArgumentExceptionForSyncFileHandle() + { + if (ShouldThrowForSyncHandle) + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.Write, options: FileOptions.None)) + { + AssertExtensions.Throws("handle", () => MethodUnderTest(handle, new byte[100], 0)); + } + } + } + + protected static CancellationTokenSource GetCancelledTokenSource() + { + CancellationTokenSource source = new CancellationTokenSource(); + source.Cancel(); + return source; + } + + protected SafeFileHandle GetHandleToExistingFile(FileAccess access) + { + string filePath = GetTestFilePath(); + File.WriteAllBytes(filePath, new byte[1]); + + FileOptions options = ShouldThrowForAsyncHandle ? FileOptions.None : FileOptions.Asynchronous; + return File.OpenHandle(filePath, FileMode.Open, access, FileShare.None, options); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/GetLength.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/GetLength.cs new file mode 100644 index 00000000000000..46e2914aed1679 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/GetLength.cs @@ -0,0 +1,38 @@ +// 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 Xunit; + +namespace System.IO.Tests +{ + public class RandomAccess_GetLength : RandomAccess_Base + { + protected override long MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset) + => RandomAccess.GetLength(handle); + + protected override bool UsesOffsets => false; + + [Fact] + public void ReturnsZeroForEmptyFile() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.Write)) + { + Assert.Equal(0, RandomAccess.GetLength(handle)); + } + } + + [Fact] + public void ReturnsExactSizeForNonEmptyFiles() + { + const int fileSize = 123; + string filePath = GetTestFilePath(); + File.WriteAllBytes(filePath, new byte[fileSize]); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open)) + { + Assert.Equal(fileSize, RandomAccess.GetLength(handle)); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/NoBuffering.Windows.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/NoBuffering.Windows.cs new file mode 100644 index 00000000000000..ea9982600945ea --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/NoBuffering.Windows.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Cryptography; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")] + public class RandomAccess_NoBuffering : FileSystemTest + { + private const FileOptions NoBuffering = (FileOptions)0x20000000; + + [Fact] + public async Task ReadAsyncUsingSingleBuffer() + { + const int fileSize = 1_000_000; // 1 MB + string filePath = GetTestFilePath(); + byte[] expected = RandomNumberGenerator.GetBytes(fileSize); + File.WriteAllBytes(filePath, expected); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open, options: FileOptions.Asynchronous | NoBuffering)) + using (SectorAlignedMemory buffer = SectorAlignedMemory.Allocate(Environment.SystemPageSize)) + { + int current = 0; + int total = 0; + + // From https://docs.microsoft.com/en-us/windows/win32/fileio/file-buffering: + // "File access sizes, including the optional file offset in the OVERLAPPED structure, + // if specified, must be for a number of bytes that is an integer multiple of the volume sector size." + // So if buffer and physical sector size is 4096 and the file size is 4097: + // the read from offset=0 reads 4096 bytes + // the read from offset=4096 reads 1 byte + // the read from offset=4097 THROWS (Invalid argument, offset is not a multiple of sector size!) + // That is why we stop at the first incomplete read (the next one would throw). + // It's possible to get 0 if we are lucky and file size is a multiple of physical sector size. + do + { + current = await RandomAccess.ReadAsync(handle, buffer.Memory, fileOffset: total); + + Assert.True(expected.AsSpan(total, current).SequenceEqual(buffer.GetSpan().Slice(0, current))); + + total += current; + } + while (current == buffer.Memory.Length); + + Assert.Equal(fileSize, total); + } + } + + [Fact] + public async Task ReadAsyncUsingMultipleBuffers() + { + const int fileSize = 1_000_000; // 1 MB + string filePath = GetTestFilePath(); + byte[] expected = RandomNumberGenerator.GetBytes(fileSize); + File.WriteAllBytes(filePath, expected); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open, options: FileOptions.Asynchronous | NoBuffering)) + using (SectorAlignedMemory buffer_1 = SectorAlignedMemory.Allocate(Environment.SystemPageSize)) + using (SectorAlignedMemory buffer_2 = SectorAlignedMemory.Allocate(Environment.SystemPageSize)) + { + long current = 0; + long total = 0; + + do + { + current = await RandomAccess.ReadAsync( + handle, + new Memory[] + { + buffer_1.Memory, + buffer_2.Memory, + }, + fileOffset: total); + + int takeFromFirst = Math.Min(buffer_1.Memory.Length, (int)current); + Assert.True(expected.AsSpan((int)total, takeFromFirst).SequenceEqual(buffer_1.GetSpan().Slice(0, takeFromFirst))); + int takeFromSecond = (int)current - takeFromFirst; + Assert.True(expected.AsSpan((int)total + takeFromFirst, takeFromSecond).SequenceEqual(buffer_2.GetSpan().Slice(0, takeFromSecond))); + + total += current; + } while (current == buffer_1.Memory.Length + buffer_2.Memory.Length); + + Assert.Equal(fileSize, total); + } + } + + [Fact] + public async Task WriteAsyncUsingSingleBuffer() + { + string filePath = GetTestFilePath(); + int bufferSize = Environment.SystemPageSize; + int fileSize = bufferSize * 10; + byte[] content = RandomNumberGenerator.GetBytes(fileSize); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.Asynchronous | NoBuffering)) + using (SectorAlignedMemory buffer = SectorAlignedMemory.Allocate(bufferSize)) + { + int total = 0; + + while (total != fileSize) + { + int take = Math.Min(content.Length - total, bufferSize); + content.AsSpan(total, take).CopyTo(buffer.GetSpan()); + + total += await RandomAccess.WriteAsync( + handle, + buffer.Memory, + fileOffset: total); + } + } + + Assert.Equal(content, File.ReadAllBytes(filePath)); + } + + [Fact] + public async Task WriteAsyncUsingMultipleBuffers() + { + string filePath = GetTestFilePath(); + int bufferSize = Environment.SystemPageSize; + int fileSize = bufferSize * 10; + byte[] content = RandomNumberGenerator.GetBytes(fileSize); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.Asynchronous | NoBuffering)) + using (SectorAlignedMemory buffer_1 = SectorAlignedMemory.Allocate(bufferSize)) + using (SectorAlignedMemory buffer_2 = SectorAlignedMemory.Allocate(bufferSize)) + { + long total = 0; + + while (total != fileSize) + { + content.AsSpan((int)total, bufferSize).CopyTo(buffer_1.GetSpan()); + content.AsSpan((int)total + bufferSize, bufferSize).CopyTo(buffer_2.GetSpan()); + + total += await RandomAccess.WriteAsync( + handle, + new ReadOnlyMemory[] + { + buffer_1.Memory, + buffer_2.Memory, + }, + fileOffset: total); + } + } + + Assert.Equal(content, File.ReadAllBytes(filePath)); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/Read.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/Read.cs new file mode 100644 index 00000000000000..81021d04aee161 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/Read.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Security.Cryptography; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + public class RandomAccess_Read : RandomAccess_Base + { + protected override int MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset) + => RandomAccess.Read(handle, bytes, fileOffset); + + protected override bool ShouldThrowForAsyncHandle + => OperatingSystem.IsWindows(); // on Windows we can NOT perform sync IO using async handle + + [Fact] + public void ThrowsOnWriteAccess() + { + using (SafeFileHandle handle = GetHandleToExistingFile(FileAccess.Write)) + { + Assert.Throws(() => RandomAccess.Read(handle, new byte[1], 0)); + } + } + + [Fact] + public void ReadToAnEmptyBufferReturnsZero() + { + string filePath = GetTestFilePath(); + File.WriteAllBytes(filePath, new byte[1]); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open)) + { + Assert.Equal(0, RandomAccess.Read(handle, Array.Empty(), fileOffset: 0)); + } + } + + [Fact] + public void ReadsBytesFromGivenFileAtGivenOffset() + { + const int fileSize = 4_001; + string filePath = GetTestFilePath(); + byte[] expected = RandomNumberGenerator.GetBytes(fileSize); + File.WriteAllBytes(filePath, expected); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open)) + { + byte[] actual = new byte[fileSize + 1]; + int current = 0; + int total = 0; + + do + { + Span buffer = actual.AsSpan(total, Math.Min(actual.Length - total, fileSize / 4)); + + current = RandomAccess.Read(handle, buffer, fileOffset: total); + + Assert.InRange(current, 0, buffer.Length); + + total += current; + } while (current != 0); + + Assert.Equal(fileSize, total); + Assert.Equal(expected, actual.Take(total).ToArray()); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadAsync.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadAsync.cs new file mode 100644 index 00000000000000..c18da9ecd50595 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadAsync.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")] + public class RandomAccess_ReadAsync : RandomAccess_Base> + { + protected override ValueTask MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset) + => RandomAccess.ReadAsync(handle, bytes, fileOffset); + + protected override bool ShouldThrowForSyncHandle + => OperatingSystem.IsWindows(); // on Windows we can NOT perform async IO using sync handle + + [Fact] + public async Task TaskAlreadyCanceledAsync() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.ReadWrite, options: FileOptions.Asynchronous)) + { + CancellationTokenSource cts = GetCancelledTokenSource(); + CancellationToken token = cts.Token; + + Assert.True(RandomAccess.ReadAsync(handle, new byte[1], 0, token).IsCanceled); + + TaskCanceledException ex = await Assert.ThrowsAsync(() => RandomAccess.ReadAsync(handle, new byte[1], 0, token).AsTask()); + Assert.Equal(token, ex.CancellationToken); + } + } + + [Fact] + public async Task ThrowsOnWriteAccess() + { + using (SafeFileHandle handle = GetHandleToExistingFile(FileAccess.Write)) + { + await Assert.ThrowsAsync(async () => await RandomAccess.ReadAsync(handle, new byte[1], 0)); + } + } + + [Fact] + public async Task ReadToAnEmptyBufferReturnsZeroAsync() + { + string filePath = GetTestFilePath(); + File.WriteAllBytes(filePath, new byte[1]); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open, options: FileOptions.Asynchronous)) + { + Assert.Equal(0, await RandomAccess.ReadAsync(handle, Array.Empty(), fileOffset: 0)); + } + } + + [Fact] + public async Task ReadsBytesFromGivenFileAtGivenOffsetAsync() + { + const int fileSize = 4_001; + string filePath = GetTestFilePath(); + byte[] expected = RandomNumberGenerator.GetBytes(fileSize); + File.WriteAllBytes(filePath, expected); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open, options: FileOptions.Asynchronous)) + { + byte[] actual = new byte[fileSize + 1]; + int current = 0; + int total = 0; + + do + { + Memory buffer = actual.AsMemory(total, Math.Min(actual.Length - total, fileSize / 4)); + + current = await RandomAccess.ReadAsync(handle, buffer, fileOffset: total); + + Assert.InRange(current, 0, buffer.Length); + + total += current; + } while (current != 0); + + Assert.Equal(fileSize, total); + Assert.Equal(expected, actual.Take(total).ToArray()); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadScatter.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadScatter.cs new file mode 100644 index 00000000000000..68058b6242fc79 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadScatter.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Security.Cryptography; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + public class RandomAccess_ReadScatter : RandomAccess_Base + { + protected override long MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset) + => RandomAccess.Read(handle, new Memory[] { bytes }, fileOffset); + + protected override bool ShouldThrowForAsyncHandle + => OperatingSystem.IsWindows(); // on Windows we can NOT perform sync IO using async handle + + [Fact] + public void ThrowsArgumentNullExceptionForNullBuffers() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.Write)) + { + AssertExtensions.Throws("buffers", () => RandomAccess.Read(handle, buffers: null, 0)); + } + } + + [Fact] + public void ThrowsOnWriteAccess() + { + using (SafeFileHandle handle = GetHandleToExistingFile(FileAccess.Write)) + { + Assert.Throws(() => RandomAccess.Read(handle, new Memory[] { new byte[1] }, 0)); + } + } + + [Fact] + public void ReadToAnEmptyBufferReturnsZero() + { + string filePath = GetTestFilePath(); + File.WriteAllBytes(filePath, new byte[1]); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open)) + { + Assert.Equal(0, RandomAccess.Read(handle, new Memory[] { Array.Empty() }, fileOffset: 0)); + } + } + + [Fact] + public void ReadsBytesFromGivenFileAtGivenOffset() + { + const int fileSize = 4_001; + string filePath = GetTestFilePath(); + byte[] expected = RandomNumberGenerator.GetBytes(fileSize); + File.WriteAllBytes(filePath, expected); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open)) + { + byte[] actual = new byte[fileSize + 1]; + long current = 0; + long total = 0; + + do + { + int firstBufferLength = (int)Math.Min(actual.Length - total, fileSize / 4); + Memory buffer_1 = actual.AsMemory((int)total, firstBufferLength); + Memory buffer_2 = actual.AsMemory((int)total + firstBufferLength); + + current = RandomAccess.Read( + handle, + new Memory[] + { + buffer_1, + Array.Empty(), + buffer_2 + }, + fileOffset: total); + + Assert.InRange(current, 0, buffer_1.Length + buffer_2.Length); + + total += current; + } while (current != 0); + + Assert.Equal(fileSize, total); + Assert.Equal(expected, actual.Take((int)total).ToArray()); + } + } + + [Fact] + public void ReadToTheSameBufferOverwritesContent() + { + string filePath = GetTestFilePath(); + File.WriteAllBytes(filePath, new byte[3] { 1, 2, 3 }); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open)) + { + byte[] buffer = new byte[1]; + Assert.Equal(buffer.Length + buffer.Length, RandomAccess.Read(handle, Enumerable.Repeat(buffer.AsMemory(), 2).ToList(), fileOffset: 0)); + Assert.Equal(2, buffer[0]); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadScatterAsync.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadScatterAsync.cs new file mode 100644 index 00000000000000..68d631a6dc390a --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadScatterAsync.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")] + public class RandomAccess_ReadScatterAsync : RandomAccess_Base> + { + protected override ValueTask MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset) + => RandomAccess.ReadAsync(handle, new Memory[] { bytes }, fileOffset); + + protected override bool ShouldThrowForSyncHandle + => OperatingSystem.IsWindows(); // on Windows we can NOT perform async IO using sync handle + + [Fact] + public void ThrowsArgumentNullExceptionForNullBuffers() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.Write, options: FileOptions.Asynchronous)) + { + AssertExtensions.Throws("buffers", () => RandomAccess.ReadAsync(handle, buffers: null, 0)); + } + } + + [Fact] + public async Task TaskAlreadyCanceledAsync() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.ReadWrite, options: FileOptions.Asynchronous)) + { + CancellationTokenSource cts = GetCancelledTokenSource(); + CancellationToken token = cts.Token; + + Assert.True(RandomAccess.ReadAsync(handle, new Memory[] { new byte[1] }, 0, token).IsCanceled); + + TaskCanceledException ex = await Assert.ThrowsAsync(() => RandomAccess.ReadAsync(handle, new Memory[] { new byte[1] }, 0, token).AsTask()); + Assert.Equal(token, ex.CancellationToken); + } + } + + [Fact] + public async Task ThrowsOnWriteAccess() + { + using (SafeFileHandle handle = GetHandleToExistingFile(FileAccess.Write)) + { + await Assert.ThrowsAsync(async () => await RandomAccess.ReadAsync(handle, new Memory[] { new byte[1] }, 0)); + } + } + + [Fact] + public async Task ReadToAnEmptyBufferReturnsZeroAsync() + { + string filePath = GetTestFilePath(); + File.WriteAllBytes(filePath, new byte[1]); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open, options: FileOptions.Asynchronous)) + { + Assert.Equal(0, await RandomAccess.ReadAsync(handle, new Memory[] { Array.Empty() }, fileOffset: 0)); + } + } + + [Fact] + public async Task ReadsBytesFromGivenFileAtGivenOffsetAsync() + { + const int fileSize = 4_001; + string filePath = GetTestFilePath(); + byte[] expected = RandomNumberGenerator.GetBytes(fileSize); + File.WriteAllBytes(filePath, expected); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open, options: FileOptions.Asynchronous)) + { + byte[] actual = new byte[fileSize + 1]; + long current = 0; + long total = 0; + + do + { + int firstBufferLength = (int)Math.Min(actual.Length - total, fileSize / 4); + Memory buffer_1 = actual.AsMemory((int)total, firstBufferLength); + Memory buffer_2 = actual.AsMemory((int)total + firstBufferLength); + + current = await RandomAccess.ReadAsync( + handle, + new Memory[] + { + buffer_1, + buffer_2 + }, + fileOffset: total); + + Assert.InRange(current, 0, buffer_1.Length + buffer_2.Length); + + total += current; + } while (current != 0); + + Assert.Equal(fileSize, total); + Assert.Equal(expected, actual.Take((int)total).ToArray()); + } + } + + [Fact] + public async Task ReadToTheSameBufferOverwritesContent() + { + string filePath = GetTestFilePath(); + File.WriteAllBytes(filePath, new byte[3] { 1, 2, 3 }); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open, options: FileOptions.Asynchronous)) + { + byte[] buffer = new byte[1]; + Assert.Equal(buffer.Length + buffer.Length, await RandomAccess.ReadAsync(handle, Enumerable.Repeat(buffer.AsMemory(), 2).ToList(), fileOffset: 0)); + Assert.Equal(2, buffer[0]); + } + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/SectorAlignedMemory.Windows.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/SectorAlignedMemory.Windows.cs new file mode 100644 index 00000000000000..593cc6e50246d3 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/SectorAlignedMemory.Windows.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using static Interop.Kernel32; + +namespace System.IO.Tests +{ + internal sealed class SectorAlignedMemory : MemoryManager + { + private bool _disposed; + private int _refCount; + private IntPtr _memory; + private int _length; + + private unsafe SectorAlignedMemory(void* memory, int length) + { + _memory = (IntPtr)memory; + _length = length; + } + + public static unsafe SectorAlignedMemory Allocate(int length) + { + void* memory = VirtualAlloc( + IntPtr.Zero.ToPointer(), + new UIntPtr((uint)(Marshal.SizeOf() * length)), + MemOptions.MEM_COMMIT | MemOptions.MEM_RESERVE, + PageOptions.PAGE_READWRITE); + + return new SectorAlignedMemory(memory, length); + } + + public bool IsDisposed => _disposed; + + public unsafe override Span GetSpan() => new Span((void*)_memory, _length); + + public override MemoryHandle Pin(int elementIndex = 0) + { + unsafe + { + Retain(); + if ((uint)elementIndex > _length) throw new ArgumentOutOfRangeException(nameof(elementIndex)); + void* pointer = Unsafe.Add((void*)_memory, elementIndex); + return new MemoryHandle(pointer, default, this); + } + } + + private bool Release() + { + int newRefCount = Interlocked.Decrement(ref _refCount); + + if (newRefCount < 0) + { + throw new InvalidOperationException("Unmatched Release/Retain"); + } + + return newRefCount != 0; + } + + private void Retain() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(SectorAlignedMemory)); + } + + Interlocked.Increment(ref _refCount); + } + + protected override unsafe void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + VirtualAlloc( + _memory.ToPointer(), + new UIntPtr((uint)(Marshal.SizeOf() * _length)), + MemOptions.MEM_FREE, + PageOptions.PAGE_READWRITE); + _memory = IntPtr.Zero; + + _disposed = true; + } + + protected override bool TryGetArray(out ArraySegment arraySegment) + { + // cannot expose managed array + arraySegment = default; + return false; + } + + public override void Unpin() + { + Release(); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/Write.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/Write.cs new file mode 100644 index 00000000000000..abcac008dc6dd2 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/Write.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Cryptography; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + public class RandomAccess_Write : RandomAccess_Base + { + protected override int MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset) + => RandomAccess.Write(handle, bytes, fileOffset); + + protected override bool ShouldThrowForAsyncHandle + => OperatingSystem.IsWindows(); // on Windows we can NOT perform sync IO using async handle + + [Fact] + public void ThrowsOnReadAccess() + { + using (SafeFileHandle handle = GetHandleToExistingFile(FileAccess.Read)) + { + Assert.Throws(() => RandomAccess.Write(handle, new byte[1], 0)); + } + } + + [Fact] + public void WriteUsingEmptyBufferReturnsZero() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write)) + { + Assert.Equal(0, RandomAccess.Write(handle, Array.Empty(), fileOffset: 0)); + } + } + + [Fact] + public void WritesBytesFromGivenBufferToGivenFileAtGivenOffset() + { + const int fileSize = 4_001; + string filePath = GetTestFilePath(); + byte[] content = RandomNumberGenerator.GetBytes(fileSize); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + { + int total = 0; + int current = 0; + + while (total != fileSize) + { + Span buffer = content.AsSpan(total, Math.Min(content.Length - total, fileSize / 4)); + + current = RandomAccess.Write(handle, buffer, fileOffset: total); + + Assert.InRange(current, 0, buffer.Length); + + total += current; + } + } + + Assert.Equal(content, File.ReadAllBytes(filePath)); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteAsync.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteAsync.cs new file mode 100644 index 00000000000000..074f5baac59442 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteAsync.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")] + public class RandomAccess_WriteAsync : RandomAccess_Base> + { + protected override ValueTask MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset) + => RandomAccess.WriteAsync(handle, bytes, fileOffset); + + protected override bool ShouldThrowForSyncHandle + => OperatingSystem.IsWindows(); // on Windows we can NOT perform async IO using sync handle + + [Fact] + public async Task TaskAlreadyCanceledAsync() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.ReadWrite, options: FileOptions.Asynchronous)) + { + CancellationTokenSource cts = GetCancelledTokenSource(); + CancellationToken token = cts.Token; + + Assert.True(RandomAccess.WriteAsync(handle, new byte[1], 0, token).IsCanceled); + + TaskCanceledException ex = await Assert.ThrowsAsync(() => RandomAccess.WriteAsync(handle, new byte[1], 0, token).AsTask()); + Assert.Equal(token, ex.CancellationToken); + } + } + + [Fact] + public async Task ThrowsOnReadAccess() + { + using (SafeFileHandle handle = GetHandleToExistingFile(FileAccess.Read)) + { + await Assert.ThrowsAsync(async () => await RandomAccess.WriteAsync(handle, new byte[1], 0)); + } + } + + [Fact] + public async Task WriteUsingEmptyBufferReturnsZeroAsync() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write, options: FileOptions.Asynchronous)) + { + Assert.Equal(0, await RandomAccess.WriteAsync(handle, Array.Empty(), fileOffset: 0)); + } + } + + [Fact] + public async Task WritesBytesFromGivenBufferToGivenFileAtGivenOffsetAsync() + { + const int fileSize = 4_001; + string filePath = GetTestFilePath(); + byte[] content = RandomNumberGenerator.GetBytes(fileSize); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.Asynchronous)) + { + int total = 0; + int current = 0; + + while (total != fileSize) + { + Memory buffer = content.AsMemory(total, Math.Min(content.Length - total, fileSize / 4)); + + current = await RandomAccess.WriteAsync(handle, buffer, fileOffset: total); + + Assert.InRange(current, 0, buffer.Length); + + total += current; + } + } + + Assert.Equal(content, File.ReadAllBytes(filePath)); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteGather.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteGather.cs new file mode 100644 index 00000000000000..a9483d6c0eae21 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteGather.cs @@ -0,0 +1,103 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + public class RandomAccess_WriteGather : RandomAccess_Base + { + protected override long MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset) + => RandomAccess.Write(handle, new ReadOnlyMemory[] { bytes }, fileOffset); + + protected override bool ShouldThrowForAsyncHandle + => OperatingSystem.IsWindows(); // on Windows we can NOT perform sync IO using async handle + + [Fact] + public void ThrowsArgumentNullExceptionForNullBuffers() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.Write)) + { + AssertExtensions.Throws("buffers", () => RandomAccess.Write(handle, buffers: null, 0)); + } + } + + [Fact] + public void ThrowsOnReadAccess() + { + using (SafeFileHandle handle = GetHandleToExistingFile(FileAccess.Read)) + { + Assert.Throws(() => RandomAccess.Write(handle, new ReadOnlyMemory[] { new byte[1] }, 0)); + } + } + + [Fact] + public void WriteUsingEmptyBufferReturnsZero() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write)) + { + Assert.Equal(0, RandomAccess.Write(handle, new ReadOnlyMemory[] { Array.Empty() }, fileOffset: 0)); + } + } + + [Fact] + public void WritesBytesFromGivenBuffersToGivenFileAtGivenOffset() + { + const int fileSize = 4_001; + string filePath = GetTestFilePath(); + byte[] content = RandomNumberGenerator.GetBytes(fileSize); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + { + long total = 0; + long current = 0; + + while (total != fileSize) + { + int firstBufferLength = (int)Math.Min(content.Length - total, fileSize / 4); + Memory buffer_1 = content.AsMemory((int)total, firstBufferLength); + Memory buffer_2 = content.AsMemory((int)total + firstBufferLength); + + current = RandomAccess.Write( + handle, + new ReadOnlyMemory[] + { + buffer_1, + Array.Empty(), + buffer_2 + }, + fileOffset: total); + + Assert.InRange(current, 0, buffer_1.Length + buffer_2.Length); + + total += current; + } + } + + Assert.Equal(content, File.ReadAllBytes(filePath)); + } + + [Fact] + public void DuplicatedBufferDuplicatesContent() + { + const byte value = 1; + const int repeatCount = 2; + string filePath = GetTestFilePath(); + ReadOnlyMemory buffer = new byte[1] { value }; + List> buffers = Enumerable.Repeat(buffer, repeatCount).ToList(); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Create, FileAccess.Write)) + { + Assert.Equal(repeatCount, RandomAccess.Write(handle, buffers, fileOffset: 0)); + } + + byte[] actualContent = File.ReadAllBytes(filePath); + Assert.Equal(repeatCount, actualContent.Length); + Assert.All(actualContent, actual => Assert.Equal(value, actual)); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteGatherAsync.cs b/src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteGatherAsync.cs new file mode 100644 index 00000000000000..f8369eb13c7823 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteGatherAsync.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; +using Xunit; + +namespace System.IO.Tests +{ + [ActiveIssue("https://github.com/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [SkipOnPlatform(TestPlatforms.Browser, "async file IO is not supported on browser")] + public class RandomAccess_WriteGatherAsync : RandomAccess_Base> + { + protected override ValueTask MethodUnderTest(SafeFileHandle handle, byte[] bytes, long fileOffset) + => RandomAccess.WriteAsync(handle, new ReadOnlyMemory[] { bytes }, fileOffset); + + protected override bool ShouldThrowForSyncHandle + => OperatingSystem.IsWindows(); // on Windows we can NOT perform async IO using sync handle + + [Fact] + public void ThrowsArgumentNullExceptionForNullBuffers() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.Asynchronous)) + { + AssertExtensions.Throws("buffers", () => RandomAccess.WriteAsync(handle, buffers: null, 0)); + } + } + + [Fact] + public async Task TaskAlreadyCanceledAsync() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.CreateNew, FileAccess.ReadWrite, options: FileOptions.Asynchronous)) + { + CancellationTokenSource cts = GetCancelledTokenSource(); + CancellationToken token = cts.Token; + + Assert.True(RandomAccess.WriteAsync(handle, new ReadOnlyMemory[] { new byte[1] }, 0, token).IsCanceled); + + TaskCanceledException ex = await Assert.ThrowsAsync(() => RandomAccess.WriteAsync(handle, new ReadOnlyMemory[] { new byte[1] }, 0, token).AsTask()); + Assert.Equal(token, ex.CancellationToken); + } + } + + [Fact] + public async Task ThrowsOnReadAccess() + { + using (SafeFileHandle handle = GetHandleToExistingFile(FileAccess.Read)) + { + await Assert.ThrowsAsync(async () => await RandomAccess.WriteAsync(handle, new ReadOnlyMemory[] { new byte[1] }, 0)); + } + } + + [Fact] + public async Task WriteUsingEmptyBufferReturnsZeroAsync() + { + using (SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write, options: FileOptions.Asynchronous)) + { + Assert.Equal(0, await RandomAccess.WriteAsync(handle, new ReadOnlyMemory[] { Array.Empty() }, fileOffset: 0)); + } + } + + [Fact] + public async Task WritesBytesFromGivenBufferToGivenFileAtGivenOffsetAsync() + { + const int fileSize = 4_001; + string filePath = GetTestFilePath(); + byte[] content = RandomNumberGenerator.GetBytes(fileSize); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.Asynchronous)) + { + long total = 0; + long current = 0; + + while (total != fileSize) + { + int firstBufferLength = (int)Math.Min(content.Length - total, fileSize / 4); + Memory buffer_1 = content.AsMemory((int)total, firstBufferLength); + Memory buffer_2 = content.AsMemory((int)total + firstBufferLength); + + current = await RandomAccess.WriteAsync( + handle, + new ReadOnlyMemory[] + { + buffer_1, + buffer_2 + }, + fileOffset: total); + + Assert.InRange(current, 0, buffer_1.Length + buffer_2.Length); + + total += current; + } + } + + Assert.Equal(content, File.ReadAllBytes(filePath)); + } + + [Fact] + public async Task DuplicatedBufferDuplicatesContentAsync() + { + const byte value = 1; + const int repeatCount = 2; + string filePath = GetTestFilePath(); + ReadOnlyMemory buffer = new byte[1] { value }; + List> buffers = Enumerable.Repeat(buffer, repeatCount).ToList(); + + using (SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Create, FileAccess.Write, options: FileOptions.Asynchronous)) + { + Assert.Equal(repeatCount, await RandomAccess.WriteAsync(handle, buffers, fileOffset: 0)); + } + + byte[] actualContent = File.ReadAllBytes(filePath); + Assert.Equal(repeatCount, actualContent.Length); + Assert.All(actualContent, actual => Assert.Equal(value, actual)); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index 0bc213c3a2ee38..a271109ce2fc65 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -51,6 +51,16 @@ + + + + + + + + + + @@ -61,10 +71,14 @@ - - - - + + + + + + + + @@ -158,6 +172,7 @@ + diff --git a/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj b/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj index 9f56ca5250a529..35d10bb679f163 100644 --- a/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj +++ b/src/libraries/System.Net.Sockets/src/System.Net.Sockets.csproj @@ -236,6 +236,8 @@ Link="Common\Interop\Unix\Interop.Stat.cs" /> + !IsClosed && GetCanSeek(); /// Opens the specified file with the requested flags and mode. /// The path to the file. /// The flags with which to open the file. /// The mode for opening the file. /// A SafeFileHandle for the opened file. - internal static SafeFileHandle Open(string path, Interop.Sys.OpenFlags flags, int mode) + private static SafeFileHandle Open(string path, Interop.Sys.OpenFlags flags, int mode) { Debug.Assert(path != null); SafeFileHandle handle = Interop.Sys.Open(path, flags, mode); @@ -73,6 +79,15 @@ internal static SafeFileHandle Open(string path, Interop.Sys.OpenFlags flags, in throw Interop.GetExceptionForIoErrno(Interop.Error.EACCES.Info(), path, isDirectory: true); } + if ((status.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFREG) + { + // we take advantage of the information provided by the fstat syscall + // and for regular files (most common case) + // avoid one extra sys call for determining whether file can be seeked + handle._canSeek = NullableBool.True; + Debug.Assert(Interop.Sys.LSeek(handle, 0, Interop.Sys.SeekWhence.SEEK_CUR) >= 0); + } + return handle; } @@ -125,5 +140,189 @@ public override bool IsInvalid return h < 0 || h > int.MaxValue; } } + + internal static SafeFileHandle Open(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) + { + // Translate the arguments into arguments for an open call. + Interop.Sys.OpenFlags openFlags = PreOpenConfigurationFromOptions(mode, access, share, options); + + // If the file gets created a new, we'll select the permissions for it. Most Unix utilities by default use 666 (read and + // write for all), so we do the same (even though this doesn't match Windows, where by default it's possible to write out + // a file and then execute it). No matter what we choose, it'll be subject to the umask applied by the system, such that the + // actual permissions will typically be less than what we select here. + const Interop.Sys.Permissions OpenPermissions = + Interop.Sys.Permissions.S_IRUSR | Interop.Sys.Permissions.S_IWUSR | + Interop.Sys.Permissions.S_IRGRP | Interop.Sys.Permissions.S_IWGRP | + Interop.Sys.Permissions.S_IROTH | Interop.Sys.Permissions.S_IWOTH; + + SafeFileHandle safeFileHandle = Open(fullPath, openFlags, (int)OpenPermissions); + try + { + safeFileHandle.Init(fullPath, mode, access, share, options, preallocationSize); + + return safeFileHandle; + } + catch (Exception) + { + safeFileHandle.Dispose(); + + throw; + } + } + + /// Translates the FileMode, FileAccess, and FileOptions values into flags to be passed when opening the file. + /// The FileMode provided to the stream's constructor. + /// The FileAccess provided to the stream's constructor + /// The FileShare provided to the stream's constructor + /// The FileOptions provided to the stream's constructor + /// The flags value to be passed to the open system call. + private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mode, FileAccess access, FileShare share, FileOptions options) + { + // Translate FileMode. Most of the values map cleanly to one or more options for open. + Interop.Sys.OpenFlags flags = default; + switch (mode) + { + default: + case FileMode.Open: // Open maps to the default behavior for open(...). No flags needed. + case FileMode.Truncate: // We truncate the file after getting the lock + break; + + case FileMode.Append: // Append is the same as OpenOrCreate, except that we'll also separately jump to the end later + case FileMode.OpenOrCreate: + case FileMode.Create: // We truncate the file after getting the lock + flags |= Interop.Sys.OpenFlags.O_CREAT; + break; + + case FileMode.CreateNew: + flags |= (Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL); + break; + } + + // Translate FileAccess. All possible values map cleanly to corresponding values for open. + switch (access) + { + case FileAccess.Read: + flags |= Interop.Sys.OpenFlags.O_RDONLY; + break; + + case FileAccess.ReadWrite: + flags |= Interop.Sys.OpenFlags.O_RDWR; + break; + + case FileAccess.Write: + flags |= Interop.Sys.OpenFlags.O_WRONLY; + break; + } + + // Handle Inheritable, other FileShare flags are handled by Init + if ((share & FileShare.Inheritable) == 0) + { + flags |= Interop.Sys.OpenFlags.O_CLOEXEC; + } + + // Translate some FileOptions; some just aren't supported, and others will be handled after calling open. + // - Asynchronous: Handled in ctor, setting _useAsync and SafeFileHandle.IsAsync to true + // - DeleteOnClose: Doesn't have a Unix equivalent, but we approximate it in Dispose + // - Encrypted: No equivalent on Unix and is ignored + // - RandomAccess: Implemented after open if posix_fadvise is available + // - SequentialScan: Implemented after open if posix_fadvise is available + // - WriteThrough: Handled here + if ((options & FileOptions.WriteThrough) != 0) + { + flags |= Interop.Sys.OpenFlags.O_SYNC; + } + + return flags; + } + + private void Init(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) + { + IsAsync = (options & FileOptions.Asynchronous) != 0; + + // Lock the file if requested via FileShare. This is only advisory locking. FileShare.None implies an exclusive + // lock on the file and all other modes use a shared lock. While this is not as granular as Windows, not mandatory, + // and not atomic with file opening, it's better than nothing. + Interop.Sys.LockOperations lockOperation = (share == FileShare.None) ? Interop.Sys.LockOperations.LOCK_EX : Interop.Sys.LockOperations.LOCK_SH; + if (Interop.Sys.FLock(this, lockOperation | Interop.Sys.LockOperations.LOCK_NB) < 0) + { + // The only error we care about is EWOULDBLOCK, which indicates that the file is currently locked by someone + // else and we would block trying to access it. Other errors, such as ENOTSUP (locking isn't supported) or + // EACCES (the file system doesn't allow us to lock), will only hamper FileStream's usage without providing value, + // given again that this is only advisory / best-effort. + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (errorInfo.Error == Interop.Error.EWOULDBLOCK) + { + throw Interop.GetExceptionForIoErrno(errorInfo, path, isDirectory: false); + } + } + + // These provide hints around how the file will be accessed. Specifying both RandomAccess + // and Sequential together doesn't make sense as they are two competing options on the same spectrum, + // so if both are specified, we prefer RandomAccess (behavior on Windows is unspecified if both are provided). + Interop.Sys.FileAdvice fadv = + (options & FileOptions.RandomAccess) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_RANDOM : + (options & FileOptions.SequentialScan) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_SEQUENTIAL : + 0; + if (fadv != 0) + { + FileStreamHelpers.CheckFileCall(Interop.Sys.PosixFAdvise(this, 0, 0, fadv), path, + ignoreNotSupported: true); // just a hint. + } + + if (mode == FileMode.Create || mode == FileMode.Truncate) + { + // Truncate the file now if the file mode requires it. This ensures that the file only will be truncated + // if opened successfully. + if (Interop.Sys.FTruncate(this, 0) < 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (errorInfo.Error != Interop.Error.EBADF && errorInfo.Error != Interop.Error.EINVAL) + { + // We know the file descriptor is valid and we know the size argument to FTruncate is correct, + // so if EBADF or EINVAL is returned, it means we're dealing with a special file that can't be + // truncated. Ignore the error in such cases; in all others, throw. + throw Interop.GetExceptionForIoErrno(errorInfo, path, isDirectory: false); + } + } + } + + // If preallocationSize has been provided for a creatable and writeable file + if (FileStreamHelpers.ShouldPreallocate(preallocationSize, access, mode)) + { + int fallocateResult = Interop.Sys.PosixFAllocate(this, 0, preallocationSize); + if (fallocateResult != 0) + { + Dispose(); + Interop.Sys.Unlink(path!); // remove the file to mimic Windows behaviour (atomic operation) + + Debug.Assert(fallocateResult == -1 || fallocateResult == -2); + throw new IOException(SR.Format( + fallocateResult == -1 ? SR.IO_DiskFull_Path_AllocationSize : SR.IO_FileTooLarge_Path_AllocationSize, + path, + preallocationSize)); + } + } + } + + private bool GetCanSeek() + { + Debug.Assert(!IsClosed); + Debug.Assert(!IsInvalid); + + NullableBool canSeek = _canSeek; + if (canSeek == NullableBool.Undefined) + { + _canSeek = canSeek = Interop.Sys.LSeek(this, 0, Interop.Sys.SeekWhence.SEEK_CUR) >= 0 ? NullableBool.True : NullableBool.False; + } + + return canSeek == NullableBool.True; + } + + private enum NullableBool + { + Undefined = 0, + False = -1, + True = 1 + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.ValueTaskSource.Windows.cs similarity index 77% rename from src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs rename to src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.ValueTaskSource.Windows.cs index f1153a5cc041ce..69371a45aff9b3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.ValueTaskSource.Windows.cs @@ -1,22 +1,49 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Buffers; using System.Diagnostics; +using System.IO; using System.Threading; using System.Threading.Tasks.Sources; -namespace System.IO.Strategies +namespace Microsoft.Win32.SafeHandles { - internal sealed partial class AsyncWindowsFileStreamStrategy : WindowsFileStreamStrategy + public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid { + private ValueTaskSource? _reusableValueTaskSource; // reusable ValueTaskSource that is currently NOT being used + + // Rent the reusable ValueTaskSource, or create a new one to use if we couldn't get one (which + // should only happen on first use or if the FileStream is being used concurrently). + internal ValueTaskSource GetValueTaskSource() => Interlocked.Exchange(ref _reusableValueTaskSource, null) ?? new ValueTaskSource(this); + + protected override bool ReleaseHandle() + { + bool result = Interop.Kernel32.CloseHandle(handle); + + Interlocked.Exchange(ref _reusableValueTaskSource, null)?.Dispose(); + + return result; + } + + private void TryToReuse(ValueTaskSource source) + { + source._source.Reset(); + + if (Interlocked.CompareExchange(ref _reusableValueTaskSource, source, null) is not null) + { + source._preallocatedOverlapped.Dispose(); + } + } + /// Reusable IValueTaskSource for FileStream ValueTask-returning async operations. - private sealed unsafe class ValueTaskSource : IValueTaskSource, IValueTaskSource + internal sealed unsafe class ValueTaskSource : IValueTaskSource, IValueTaskSource { internal static readonly IOCompletionCallback s_ioCallback = IOCallback; internal readonly PreAllocatedOverlapped _preallocatedOverlapped; - private readonly AsyncWindowsFileStreamStrategy _strategy; + private readonly SafeFileHandle _fileHandle; internal MemoryHandle _memoryHandle; internal ManualResetValueTaskSourceCore _source; // mutable struct; do not make this readonly private NativeOverlapped* _overlapped; @@ -28,9 +55,9 @@ private sealed unsafe class ValueTaskSource : IValueTaskSource, IValueTaskS /// internal ulong _result; - internal ValueTaskSource(AsyncWindowsFileStreamStrategy strategy) + internal ValueTaskSource(SafeFileHandle fileHandle) { - _strategy = strategy; + _fileHandle = fileHandle; _source.RunContinuationsAsynchronously = true; _preallocatedOverlapped = PreAllocatedOverlapped.UnsafeCreate(s_ioCallback, this, null); } @@ -41,11 +68,18 @@ internal void Dispose() _preallocatedOverlapped.Dispose(); } - internal NativeOverlapped* PrepareForOperation(ReadOnlyMemory memory) + internal static Exception GetIOError(int errorCode, string? path) + => errorCode == Interop.Errors.ERROR_HANDLE_EOF + ? ThrowHelper.CreateEndOfFileException() + : Win32Marshal.GetExceptionForWin32Error(errorCode, path); + + internal NativeOverlapped* PrepareForOperation(ReadOnlyMemory memory, long fileOffset) { _result = 0; _memoryHandle = memory.Pin(); - _overlapped = _strategy._fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(_preallocatedOverlapped); + _overlapped = _fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(_preallocatedOverlapped); + _overlapped->OffsetLow = (int)fileOffset; + _overlapped->OffsetHigh = (int)(fileOffset >> 32); return _overlapped; } @@ -63,7 +97,7 @@ private int GetResultAndRelease(short token) finally { // The instance is ready to be reused - _strategy.TryToReuse(this); + _fileHandle.TryToReuse(this); } } @@ -79,11 +113,11 @@ internal void RegisterForCancellation(CancellationToken cancellationToken) _cancellationRegistration = cancellationToken.UnsafeRegister(static (s, token) => { ValueTaskSource vts = (ValueTaskSource)s!; - if (!vts._strategy._fileHandle.IsInvalid) + if (!vts._fileHandle.IsInvalid) { try { - Interop.Kernel32.CancelIoEx(vts._strategy._fileHandle, vts._overlapped); + Interop.Kernel32.CancelIoEx(vts._fileHandle, vts._overlapped); // Ignore all failures: no matter whether it succeeds or fails, completion is handled via the IOCallback. } catch (ObjectDisposedException) { } // in case the SafeHandle is (erroneously) closed concurrently @@ -112,7 +146,7 @@ internal void ReleaseResources() // Free the overlapped. if (_overlapped != null) { - _strategy._fileHandle.ThreadPoolBinding!.FreeNativeOverlapped(_overlapped); + _fileHandle.ThreadPoolBinding!.FreeNativeOverlapped(_overlapped); _overlapped = null; } } @@ -161,6 +195,7 @@ internal void Complete(uint errorCode, uint numBytes) case 0: case Interop.Errors.ERROR_BROKEN_PIPE: case Interop.Errors.ERROR_NO_DATA: + case Interop.Errors.ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file) // Success _source.SetResult((int)numBytes); break; 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 dfcce3cfc65ac8..36710dcdbde5dd 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 @@ -2,35 +2,198 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; +using System.IO; +using System.Text; using System.Threading; namespace Microsoft.Win32.SafeHandles { - public sealed class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid + public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid { - private bool? _isAsync; + internal const FileOptions NoBuffering = (FileOptions)0x20000000; + private volatile FileOptions _fileOptions = (FileOptions)(-1); + private volatile int _fileType = -1; public SafeFileHandle() : base(true) { - _isAsync = null; } public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHandle) { SetHandle(preexistingHandle); - - _isAsync = null; } - internal bool? IsAsync + private SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle, FileOptions fileOptions) : base(ownsHandle) { - get => _isAsync; - set => _isAsync = value; + SetHandle(preexistingHandle); + + _fileOptions = fileOptions; } + public bool IsAsync => (GetFileOptions() & FileOptions.Asynchronous) != 0; + + internal bool CanSeek => !IsClosed && GetFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; + + internal bool IsPipe => GetFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE; + internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; } - protected override bool ReleaseHandle() => - Interop.Kernel32.CloseHandle(handle); + internal static unsafe SafeFileHandle Open(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) + { + using (DisableMediaInsertionPrompt.Create()) + { + SafeFileHandle fileHandle = new SafeFileHandle( + NtCreateFile(fullPath, mode, access, share, options, preallocationSize), + ownsHandle: true, + options); + + fileHandle.InitThreadPoolBindingIfNeeded(); + + return fileHandle; + } + } + + private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) + { + uint ntStatus; + IntPtr fileHandle; + + const string MandatoryNtPrefix = @"\??\"; + if (fullPath.StartsWith(MandatoryNtPrefix, StringComparison.Ordinal)) + { + (ntStatus, fileHandle) = Interop.NtDll.NtCreateFile(fullPath, mode, access, share, options, preallocationSize); + } + else + { + var vsb = new ValueStringBuilder(stackalloc char[256]); + vsb.Append(MandatoryNtPrefix); + + if (fullPath.StartsWith(@"\\?\", StringComparison.Ordinal)) // NtCreateFile does not support "\\?\" prefix, only "\??\" + { + vsb.Append(fullPath.AsSpan(4)); + } + else + { + vsb.Append(fullPath); + } + + (ntStatus, fileHandle) = Interop.NtDll.NtCreateFile(vsb.AsSpan(), mode, access, share, options, preallocationSize); + vsb.Dispose(); + } + + switch (ntStatus) + { + case Interop.StatusOptions.STATUS_SUCCESS: + return fileHandle; + case Interop.StatusOptions.STATUS_DISK_FULL: + throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, fullPath, preallocationSize)); + // NtCreateFile has a bug and it reports STATUS_INVALID_PARAMETER for files + // that are too big for the current file system. Example: creating a 4GB+1 file on a FAT32 drive. + case Interop.StatusOptions.STATUS_INVALID_PARAMETER when preallocationSize > 0: + case Interop.StatusOptions.STATUS_FILE_TOO_LARGE: + throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, fullPath, preallocationSize)); + default: + int error = (int)Interop.NtDll.RtlNtStatusToDosError((int)ntStatus); + throw Win32Marshal.GetExceptionForWin32Error(error, fullPath); + } + } + + internal void InitThreadPoolBindingIfNeeded() + { + if (IsAsync == true && ThreadPoolBinding == null) + { + // This is necessary for async IO using IO Completion ports via our + // managed Threadpool API's. This (theoretically) calls the OS's + // BindIoCompletionCallback method, and passes in a stub for the + // LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped + // struct for this request and gets a delegate to a managed callback + // from there, which it then calls on a threadpool thread. (We allocate + // our native OVERLAPPED structs 2 pointers too large and store EE state + // & GC handles there, one to an IAsyncResult, the other to a delegate.) + try + { + ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(this); + } + catch (ArgumentException ex) + { + if (OwnsHandle) + { + // We should close the handle so that the handle is not open until SafeFileHandle GC + Dispose(); + } + + throw new IOException(SR.IO_BindHandleFailed, ex); + } + } + } + + internal unsafe FileOptions GetFileOptions() + { + FileOptions fileOptions = _fileOptions; + if (fileOptions != (FileOptions)(-1)) + { + return fileOptions; + } + + Interop.NtDll.CreateOptions options; + int ntStatus = Interop.NtDll.NtQueryInformationFile( + FileHandle: this, + IoStatusBlock: out _, + FileInformation: &options, + Length: sizeof(uint), + FileInformationClass: Interop.NtDll.FileModeInformation); + + if (ntStatus != Interop.StatusOptions.STATUS_SUCCESS) + { + int error = (int)Interop.NtDll.RtlNtStatusToDosError(ntStatus); + throw Win32Marshal.GetExceptionForWin32Error(error); + } + + FileOptions result = FileOptions.None; + + if ((options & (Interop.NtDll.CreateOptions.FILE_SYNCHRONOUS_IO_ALERT | Interop.NtDll.CreateOptions.FILE_SYNCHRONOUS_IO_NONALERT)) == 0) + { + result |= FileOptions.Asynchronous; + } + if ((options & Interop.NtDll.CreateOptions.FILE_WRITE_THROUGH) != 0) + { + result |= FileOptions.WriteThrough; + } + if ((options & Interop.NtDll.CreateOptions.FILE_RANDOM_ACCESS) != 0) + { + result |= FileOptions.RandomAccess; + } + if ((options & Interop.NtDll.CreateOptions.FILE_SEQUENTIAL_ONLY) != 0) + { + result |= FileOptions.SequentialScan; + } + if ((options & Interop.NtDll.CreateOptions.FILE_DELETE_ON_CLOSE) != 0) + { + result |= FileOptions.DeleteOnClose; + } + if ((options & Interop.NtDll.CreateOptions.FILE_NO_INTERMEDIATE_BUFFERING) != 0) + { + result |= NoBuffering; + } + + return _fileOptions = result; + } + + internal int GetFileType() + { + int fileType = _fileType; + if (fileType == -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 fileType; + } } } 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 c37a7824f6367c..a189be9ac1517a 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 @@ -431,6 +431,7 @@ + @@ -1595,6 +1596,9 @@ Common\Interop\Windows\Kernel32\Interop.ReadFile_SafeHandle_NativeOverlapped.cs + + Common\Interop\Windows\Kernel32\Interop.FileScatterGather.cs + Common\Interop\Windows\Kernel32\Interop.RemoveDirectory.cs @@ -1613,9 +1617,6 @@ Common\Interop\Windows\Interop.OBJECT_ATTRIBUTES.cs - - Common\Interop\Windows\Kernel32\Interop.SecurityOptions.cs - Common\Interop\Windows\Kernel32\Interop.SetCurrentDirectory.cs @@ -1754,6 +1755,7 @@ + @@ -1780,11 +1782,11 @@ + - @@ -1924,6 +1926,9 @@ Common\Interop\Unix\System.Native\Interop.GetUnixRelease.cs + + Common\Interop\Unix\System.Native\Interop.IOVector.cs + Common\Interop\Unix\System.Native\Interop.LChflags.cs @@ -1972,6 +1977,18 @@ Common\Interop\Unix\System.Native\Interop.PosixFAllocate.cs + + Common\Interop\Unix\System.Native\Interop.PRead.cs + + + Common\Interop\Unix\System.Native\Interop.PReadV.cs + + + Common\Interop\Unix\System.Native\Interop.PWrite.cs + + + Common\Interop\Unix\System.Native\Interop.PWriteV.cs + Common\Interop\Unix\System.Native\Interop.Read.cs @@ -2039,6 +2056,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index c387630b722e85..626dc761e807b7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -1,12 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Runtime.ExceptionServices; using System.Runtime.Versioning; using System.Text; @@ -14,6 +12,9 @@ using System.Threading.Tasks; #if MS_IO_REDIST +using System; +using System.IO; + namespace Microsoft.IO #else namespace System.IO @@ -363,52 +364,6 @@ public static byte[] ReadAllBytes(string path) } } -#if !MS_IO_REDIST - private static byte[] ReadAllBytesUnknownLength(FileStream fs) - { - byte[]? rentedArray = null; - Span buffer = stackalloc byte[512]; - try - { - int bytesRead = 0; - while (true) - { - if (bytesRead == buffer.Length) - { - uint newLength = (uint)buffer.Length * 2; - if (newLength > MaxByteArrayLength) - { - newLength = (uint)Math.Max(MaxByteArrayLength, buffer.Length + 1); - } - - byte[] tmp = ArrayPool.Shared.Rent((int)newLength); - buffer.CopyTo(tmp); - if (rentedArray != null) - { - ArrayPool.Shared.Return(rentedArray); - } - buffer = rentedArray = tmp; - } - - Debug.Assert(bytesRead < buffer.Length); - int n = fs.Read(buffer.Slice(bytesRead)); - if (n == 0) - { - return buffer.Slice(0, bytesRead).ToArray(); - } - bytesRead += n; - } - } - finally - { - if (rentedArray != null) - { - ArrayPool.Shared.Return(rentedArray); - } - } - } -#endif - public static void WriteAllBytes(string path, byte[] bytes) { if (path == null) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs index 1996833ae40c39..b5502ff9926cb8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.netcoreapp.cs @@ -1,14 +1,101 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; +using System.Diagnostics; +using Microsoft.Win32.SafeHandles; + namespace System.IO { public static partial class File { /// - /// Initializes a new instance of the class with the specified path, creation mode, read/write and sharing permission, the access other FileStreams can have to the same file, the buffer size, additional file options and the allocation size. + /// Initializes a new instance of the class with the specified path, creation mode, read/write and sharing permission, the access other FileStreams can have to the same file, the buffer size, additional file options and the allocation size. /// - /// for information about exceptions. + /// for information about exceptions. public static FileStream Open(string path, FileStreamOptions options) => new FileStream(path, options); + + /// + /// Initializes a new instance of the class with the specified path, creation mode, read/write and sharing permission, the access other SafeFileHandles can have to the same file, additional file options and the allocation size. + /// + /// A relative or absolute path for the file that the current instance will encapsulate. + /// One of the enumeration values that determines how to open or create the file. The default value is + /// A bitwise combination of the enumeration values that determines how the file can be accessed. The default value is + /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. The default value is . + /// The initial allocation size in bytes for the file. A positive value is effective only when a regular file is being created, overwritten, or replaced. + /// Negative values are not allowed. In other cases (including the default 0 value), it's ignored. + /// An object that describes optional parameters to use. + /// is . + /// is an empty string (""), contains only white space, or contains one or more invalid characters. + /// -or- + /// refers to a non-file device, such as CON:, COM1:, LPT1:, etc. in an NTFS environment. + /// refers to a non-file device, such as CON:, COM1:, LPT1:, etc. in a non-NTFS environment. + /// is negative. + /// -or- + /// , , or contain an invalid value. + /// The file cannot be found, such as when is or , and the file specified by does not exist. The file must already exist in these modes. + /// An I/O error, such as specifying when the file specified by already exists, occurred. + /// -or- + /// The disk was full (when was provided and was pointing to a regular file). + /// -or- + /// The file was too large (when was provided and was pointing to a regular file). + /// The caller does not have the required permission. + /// The specified path is invalid, such as being on an unmapped drive. + /// The requested is not permitted by the operating system for the specified , such as when is or and the file or directory is set for read-only access. + /// -or- + /// is specified for , but file encryption is not supported on the current platform. + /// The specified path, file name, or both exceed the system-defined maximum length. + public static SafeFileHandle OpenHandle(string path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read, + FileShare share = FileShare.Read, FileOptions options = FileOptions.None, long preallocationSize = 0) + { + Strategies.FileStreamHelpers.ValidateArguments(path, mode, access, share, bufferSize: 0, options, preallocationSize); + + return SafeFileHandle.Open(Path.GetFullPath(path), mode, access, share, options, preallocationSize); + } + + private static byte[] ReadAllBytesUnknownLength(FileStream fs) + { + byte[]? rentedArray = null; + Span buffer = stackalloc byte[512]; + try + { + int bytesRead = 0; + while (true) + { + if (bytesRead == buffer.Length) + { + uint newLength = (uint)buffer.Length * 2; + if (newLength > Array.MaxLength) + { + newLength = (uint)Math.Max(Array.MaxLength, buffer.Length + 1); + } + + byte[] tmp = ArrayPool.Shared.Rent((int)newLength); + buffer.CopyTo(tmp); + byte[]? oldRentedArray = rentedArray; + buffer = rentedArray = tmp; + if (oldRentedArray != null) + { + ArrayPool.Shared.Return(oldRentedArray); + } + } + + Debug.Assert(bytesRead < buffer.Length); + int n = fs.Read(buffer.Slice(bytesRead)); + if (n == 0) + { + return buffer.Slice(0, bytesRead).ToArray(); + } + bytesRead += n; + } + } + finally + { + if (rentedArray != null) + { + ArrayPool.Shared.Return(rentedArray); + } + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs index f892c572a0ec5a..9653bd057bac3a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -3,7 +3,6 @@ using System.ComponentModel; using System.IO.Strategies; -using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -17,29 +16,26 @@ public class FileStream : Stream internal const FileShare DefaultShare = FileShare.Read; private const bool DefaultIsAsync = false; - /// Caches whether Serialization Guard has been disabled for file writes - private static int s_cachedSerializationSwitch; - private readonly FileStreamStrategy _strategy; [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This constructor has been deprecated. Please use new FileStream(SafeFileHandle handle, FileAccess access) instead. https://go.microsoft.com/fwlink/?linkid=14202")] public FileStream(IntPtr handle, FileAccess access) - : this(handle, access, true, DefaultBufferSize, false) + : this(handle, access, true, DefaultBufferSize, DefaultIsAsync) { } [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This constructor has been deprecated. Please use new FileStream(SafeFileHandle handle, FileAccess access) instead, and optionally make a new SafeFileHandle with ownsHandle=false if needed. https://go.microsoft.com/fwlink/?linkid=14202")] public FileStream(IntPtr handle, FileAccess access, bool ownsHandle) - : this(handle, access, ownsHandle, DefaultBufferSize, false) + : this(handle, access, ownsHandle, DefaultBufferSize, DefaultIsAsync) { } [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("This constructor has been deprecated. Please use new FileStream(SafeFileHandle handle, FileAccess access, int bufferSize) instead, and optionally make a new SafeFileHandle with ownsHandle=false if needed. https://go.microsoft.com/fwlink/?linkid=14202")] public FileStream(IntPtr handle, FileAccess access, bool ownsHandle, int bufferSize) - : this(handle, access, ownsHandle, bufferSize, false) + : this(handle, access, ownsHandle, bufferSize, DefaultIsAsync) { } @@ -68,7 +64,7 @@ public FileStream(IntPtr handle, FileAccess access, bool ownsHandle, int bufferS } } - private static void ValidateHandle(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) + private static void ValidateHandle(SafeFileHandle handle, FileAccess access, int bufferSize) { if (handle.IsInvalid) { @@ -78,17 +74,27 @@ private static void ValidateHandle(SafeFileHandle handle, FileAccess access, int { throw new ArgumentOutOfRangeException(nameof(access), SR.ArgumentOutOfRange_Enum); } - else if (bufferSize <= 0) + else if (bufferSize < 0) { - throw new ArgumentOutOfRangeException(nameof(bufferSize), SR.ArgumentOutOfRange_NeedPosNum); + ThrowHelper.ThrowArgumentOutOfRangeException_NeedNonNegNum(nameof(bufferSize)); } else if (handle.IsClosed) { ThrowHelper.ThrowObjectDisposedException_FileClosed(); } - else if (handle.IsAsync.HasValue && isAsync != handle.IsAsync.GetValueOrDefault()) + } + + private static void ValidateHandle(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) + { + ValidateHandle(handle, access, bufferSize); + + if (isAsync && !handle.IsAsync) + { + ThrowHelper.ThrowArgumentException_HandleNotAsync(nameof(handle)); + } + else if (!isAsync && handle.IsAsync) { - throw new ArgumentException(SR.Arg_HandleNotAsync, nameof(handle)); + ThrowHelper.ThrowArgumentException_HandleNotSync(nameof(handle)); } } @@ -98,8 +104,10 @@ public FileStream(SafeFileHandle handle, FileAccess access) } public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize) - : this(handle, access, bufferSize, FileStreamHelpers.GetDefaultIsAsync(handle, DefaultIsAsync)) { + ValidateHandle(handle, access, bufferSize); + + _strategy = FileStreamHelpers.ChooseStrategy(this, handle, access, DefaultShare, bufferSize, handle.IsAsync); } public FileStream(SafeFileHandle handle, FileAccess access, int bufferSize, bool isAsync) @@ -188,10 +196,8 @@ public FileStream(string path, FileStreamOptions options) throw new ArgumentException(SR.Format(SR.Argument_InvalidFileModeAndAccessCombo, options.Mode, options.Access), nameof(options)); } } - else if ((options.Access & FileAccess.Write) == FileAccess.Write) - { - SerializationInfo.ThrowIfDeserializationInProgress("AllowFileWrites", ref s_cachedSerializationSwitch); - } + + FileStreamHelpers.SerializationGuard(options.Access); _strategy = FileStreamHelpers.ChooseStrategy( this, path, options.Mode, options.Access, options.Share, options.BufferSize, options.Options, options.PreallocationSize); @@ -199,69 +205,7 @@ public FileStream(string path, FileStreamOptions options) private FileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { - if (path == null) - { - throw new ArgumentNullException(nameof(path), SR.ArgumentNull_Path); - } - else if (path.Length == 0) - { - throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); - } - - // don't include inheritable in our bounds check for share - FileShare tempshare = share & ~FileShare.Inheritable; - string? badArg = null; - - if (mode < FileMode.CreateNew || mode > FileMode.Append) - { - badArg = nameof(mode); - } - else if (access < FileAccess.Read || access > FileAccess.ReadWrite) - { - badArg = nameof(access); - } - else if (tempshare < FileShare.None || tempshare > (FileShare.ReadWrite | FileShare.Delete)) - { - badArg = nameof(share); - } - - if (badArg != null) - { - throw new ArgumentOutOfRangeException(badArg, SR.ArgumentOutOfRange_Enum); - } - - // NOTE: any change to FileOptions enum needs to be matched here in the error validation - if (options != FileOptions.None && (options & ~(FileOptions.WriteThrough | FileOptions.Asynchronous | FileOptions.RandomAccess | FileOptions.DeleteOnClose | FileOptions.SequentialScan | FileOptions.Encrypted | (FileOptions)0x20000000 /* NoBuffering */)) != 0) - { - throw new ArgumentOutOfRangeException(nameof(options), SR.ArgumentOutOfRange_Enum); - } - else if (bufferSize < 0) - { - throw new ArgumentOutOfRangeException(nameof(bufferSize), SR.ArgumentOutOfRange_NeedPosNum); - } - else if (preallocationSize < 0) - { - throw new ArgumentOutOfRangeException(nameof(preallocationSize), SR.ArgumentOutOfRange_NeedNonNegNum); - } - - // Write access validation - if ((access & FileAccess.Write) == 0) - { - if (mode == FileMode.Truncate || mode == FileMode.CreateNew || mode == FileMode.Create || mode == FileMode.Append) - { - // No write access, mode and access disagree but flag access since mode comes first - throw new ArgumentException(SR.Format(SR.Argument_InvalidFileModeAndAccessCombo, mode, access), nameof(access)); - } - } - - if ((access & FileAccess.Read) != 0 && mode == FileMode.Append) - { - throw new ArgumentException(SR.Argument_InvalidAppendMode, nameof(access)); - } - else if ((access & FileAccess.Write) == FileAccess.Write) - { - SerializationInfo.ThrowIfDeserializationInProgress("AllowFileWrites", ref s_cachedSerializationSwitch); - } + FileStreamHelpers.ValidateArguments(path, mode, access, share, bufferSize, options, preallocationSize); _strategy = FileStreamHelpers.ChooseStrategy(this, path, mode, access, share, bufferSize, options, preallocationSize); } @@ -277,7 +221,7 @@ public virtual void Lock(long position, long length) { if (position < 0 || length < 0) { - throw new ArgumentOutOfRangeException(position < 0 ? nameof(position) : nameof(length), SR.ArgumentOutOfRange_NeedNonNegNum); + ThrowHelper.ThrowArgumentOutOfRangeException_NeedNonNegNum(position < 0 ? nameof(position) : nameof(length)); } else if (_strategy.IsClosed) { @@ -295,7 +239,7 @@ public virtual void Unlock(long position, long length) { if (position < 0 || length < 0) { - throw new ArgumentOutOfRangeException(position < 0 ? nameof(position) : nameof(length), SR.ArgumentOutOfRange_NeedNonNegNum); + ThrowHelper.ThrowArgumentOutOfRangeException_NeedNonNegNum(position < 0 ? nameof(position) : nameof(length)); } else if (_strategy.IsClosed) { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs new file mode 100644 index 00000000000000..22a3934fe4089e --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Generic; +using System.IO.Strategies; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + public static partial class RandomAccess + { + // IovStackThreshold matches Linux's UIO_FASTIOV, which is the number of 'struct iovec' + // that get stackalloced in the Linux kernel. + private const int IovStackThreshold = 8; + + internal static long GetFileLength(SafeFileHandle handle, string? path) + { + int result = Interop.Sys.FStat(handle, out Interop.Sys.FileStatus status); + FileStreamHelpers.CheckFileCall(result, path); + return status.Size; + } + + private static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer, long fileOffset) + { + fixed (byte* bufPtr = &MemoryMarshal.GetReference(buffer)) + { + int result = Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset); + FileStreamHelpers.CheckFileCall(result, path: null); + return result; + } + } + + private static unsafe long ReadScatterAtOffset(SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset) + { + MemoryHandle[] handles = new MemoryHandle[buffers.Count]; + Span vectors = buffers.Count <= IovStackThreshold ? stackalloc Interop.Sys.IOVector[IovStackThreshold] : new Interop.Sys.IOVector[buffers.Count]; + + long result; + try + { + int buffersCount = buffers.Count; + for (int i = 0; i < buffersCount; i++) + { + Memory buffer = buffers[i]; + MemoryHandle memoryHandle = buffer.Pin(); + vectors[i] = new Interop.Sys.IOVector { Base = (byte*)memoryHandle.Pointer, Count = (UIntPtr)buffer.Length }; + handles[i] = memoryHandle; + } + + fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(vectors)) + { + result = Interop.Sys.PReadV(handle, pinnedVectors, buffers.Count, fileOffset); + } + } + finally + { + foreach (MemoryHandle memoryHandle in handles) + { + memoryHandle.Dispose(); + } + } + + return FileStreamHelpers.CheckFileCall(result, path: null); + } + + private static ValueTask ReadAtOffsetAsync(SafeFileHandle handle, Memory buffer, long fileOffset, + CancellationToken cancellationToken) + { + return new ValueTask(Task.Factory.StartNew(static state => + { + var args = ((SafeFileHandle handle, Memory buffer, long fileOffset))state!; + return ReadAtOffset(args.handle, args.buffer.Span, args.fileOffset); + }, (handle, buffer, fileOffset), cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default)); + } + + private static ValueTask ReadScatterAtOffsetAsync(SafeFileHandle handle, IReadOnlyList> buffers, + long fileOffset, CancellationToken cancellationToken) + { + return new ValueTask(Task.Factory.StartNew(static state => + { + var args = ((SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset))state!; + return ReadScatterAtOffset(args.handle, args.buffers, args.fileOffset); + }, (handle, buffers, fileOffset), cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default)); + } + + private static unsafe int WriteAtOffset(SafeFileHandle handle, ReadOnlySpan buffer, long fileOffset) + { + fixed (byte* bufPtr = &MemoryMarshal.GetReference(buffer)) + { + int result = Interop.Sys.PWrite(handle, bufPtr, buffer.Length, fileOffset); + FileStreamHelpers.CheckFileCall(result, path: null); + return result; + } + } + + private static unsafe long WriteGatherAtOffset(SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset) + { + MemoryHandle[] handles = new MemoryHandle[buffers.Count]; + Span vectors = buffers.Count <= IovStackThreshold ? stackalloc Interop.Sys.IOVector[IovStackThreshold] : new Interop.Sys.IOVector[buffers.Count ]; + + long result; + try + { + int buffersCount = buffers.Count; + for (int i = 0; i < buffersCount; i++) + { + ReadOnlyMemory buffer = buffers[i]; + MemoryHandle memoryHandle = buffer.Pin(); + vectors[i] = new Interop.Sys.IOVector { Base = (byte*)memoryHandle.Pointer, Count = (UIntPtr)buffer.Length }; + handles[i] = memoryHandle; + } + + fixed (Interop.Sys.IOVector* pinnedVectors = &MemoryMarshal.GetReference(vectors)) + { + result = Interop.Sys.PWriteV(handle, pinnedVectors, buffers.Count, fileOffset); + } + } + finally + { + foreach (MemoryHandle memoryHandle in handles) + { + memoryHandle.Dispose(); + } + } + + return FileStreamHelpers.CheckFileCall(result, path: null); + } + + private static ValueTask WriteAtOffsetAsync(SafeFileHandle handle, ReadOnlyMemory buffer, long fileOffset, + CancellationToken cancellationToken) + { + return new ValueTask(Task.Factory.StartNew(static state => + { + var args = ((SafeFileHandle handle, ReadOnlyMemory buffer, long fileOffset))state!; + return WriteAtOffset(args.handle, args.buffer.Span, args.fileOffset); + }, (handle, buffer, fileOffset), cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default)); + } + + private static ValueTask WriteGatherAtOffsetAsync(SafeFileHandle handle, IReadOnlyList> buffers, + long fileOffset, CancellationToken cancellationToken) + { + return new ValueTask(Task.Factory.StartNew(static state => + { + var args = ((SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset))state!; + return WriteGatherAtOffset(args.handle, args.buffers, args.fileOffset); + }, (handle, buffers, fileOffset), cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default)); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs new file mode 100644 index 00000000000000..5d198ccb0785c5 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs @@ -0,0 +1,558 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.Strategies; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + public static partial class RandomAccess + { + internal static unsafe long GetFileLength(SafeFileHandle handle, string? path) + { + Interop.Kernel32.FILE_STANDARD_INFO info; + + if (!Interop.Kernel32.GetFileInformationByHandleEx(handle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))) + { + throw Win32Marshal.GetExceptionForLastWin32Error(path); + } + + return info.EndOfFile; + } + + internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer, long fileOffset, string? path = null) + { + NativeOverlapped nativeOverlapped = GetNativeOverlapped(fileOffset); + int r = ReadFileNative(handle, buffer, syncUsingOverlapped: true, &nativeOverlapped, out int errorCode); + + if (r == -1) + { + // For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe. + if (errorCode == Interop.Errors.ERROR_BROKEN_PIPE) + { + r = 0; + } + else + { + if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) + { + ThrowHelper.ThrowArgumentException_HandleNotSync(nameof(handle)); + } + + throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); + } + } + + return r; + } + + internal static unsafe int WriteAtOffset(SafeFileHandle handle, ReadOnlySpan buffer, long fileOffset, string? path = null) + { + NativeOverlapped nativeOverlapped = GetNativeOverlapped(fileOffset); + int r = WriteFileNative(handle, buffer, true, &nativeOverlapped, out int errorCode); + + if (r == -1) + { + // For pipes, ERROR_NO_DATA is not an error, but the pipe is closing. + if (errorCode == Interop.Errors.ERROR_NO_DATA) + { + r = 0; + } + else + { + // ERROR_INVALID_PARAMETER may be returned for writes + // where the position is too large or for synchronous writes + // to a handle opened asynchronously. + if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) + { + throw new IOException(SR.IO_FileTooLongOrHandleNotSync); + } + + throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); + } + } + + return r; + } + + internal static unsafe int ReadFileNative(SafeFileHandle handle, Span bytes, bool syncUsingOverlapped, NativeOverlapped* overlapped, out int errorCode) + { + Debug.Assert(handle != null, "handle != null"); + + int r; + int numBytesRead = 0; + + fixed (byte* p = &MemoryMarshal.GetReference(bytes)) + { + r = overlapped == null || syncUsingOverlapped ? + Interop.Kernel32.ReadFile(handle, p, bytes.Length, out numBytesRead, overlapped) : + Interop.Kernel32.ReadFile(handle, p, bytes.Length, IntPtr.Zero, overlapped); + } + + if (r == 0) + { + errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); + + if (syncUsingOverlapped && errorCode == Interop.Errors.ERROR_HANDLE_EOF) + { + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile#synchronization-and-file-position : + // "If lpOverlapped is not NULL, then when a synchronous read operation reaches the end of a file, + // ReadFile returns FALSE and GetLastError returns ERROR_HANDLE_EOF" + return numBytesRead; + } + + return -1; + } + else + { + errorCode = 0; + return numBytesRead; + } + } + + internal static unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan buffer, bool syncUsingOverlapped, NativeOverlapped* overlapped, out int errorCode) + { + Debug.Assert(handle != null, "handle != null"); + + int numBytesWritten = 0; + int r; + + fixed (byte* p = &MemoryMarshal.GetReference(buffer)) + { + r = overlapped == null || syncUsingOverlapped ? + Interop.Kernel32.WriteFile(handle, p, buffer.Length, out numBytesWritten, overlapped) : + Interop.Kernel32.WriteFile(handle, p, buffer.Length, IntPtr.Zero, overlapped); + } + + if (r == 0) + { + errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); + return -1; + } + else + { + errorCode = 0; + return numBytesWritten; + } + } + + private static ValueTask ReadAtOffsetAsync(SafeFileHandle handle, Memory buffer, long fileOffset, CancellationToken cancellationToken) + => Map(QueueAsyncReadFile(handle, buffer, fileOffset, cancellationToken)); + + private static ValueTask Map((SafeFileHandle.ValueTaskSource? vts, int errorCode) tuple) + => tuple.vts != null + ? new ValueTask(tuple.vts, tuple.vts.Version) + : tuple.errorCode == 0 ? ValueTask.FromResult(0) : ValueTask.FromException(Win32Marshal.GetExceptionForWin32Error(tuple.errorCode)); + + internal static unsafe (SafeFileHandle.ValueTaskSource? vts, int errorCode) QueueAsyncReadFile( + SafeFileHandle handle, Memory buffer, long fileOffset, CancellationToken cancellationToken) + { + SafeFileHandle.ValueTaskSource vts = handle.GetValueTaskSource(); + try + { + NativeOverlapped* nativeOverlapped = vts.PrepareForOperation(buffer, fileOffset); + Debug.Assert(vts._memoryHandle.Pointer != null); + + // Queue an async ReadFile operation. + if (Interop.Kernel32.ReadFile(handle, (byte*)vts._memoryHandle.Pointer, buffer.Length, IntPtr.Zero, nativeOverlapped) == 0) + { + // The operation failed, or it's pending. + int errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); + switch (errorCode) + { + case Interop.Errors.ERROR_IO_PENDING: + // Common case: IO was initiated, completion will be handled by callback. + // Register for cancellation now that the operation has been initiated. + vts.RegisterForCancellation(cancellationToken); + break; + + case Interop.Errors.ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file) + case Interop.Errors.ERROR_BROKEN_PIPE: + // EOF on a pipe. Callback will not be called. + // We clear the overlapped status bit for this special case (failure + // to do so looks like we are freeing a pending overlapped later). + nativeOverlapped->InternalLow = IntPtr.Zero; + vts.Dispose(); + return (null, 0); + + default: + // Error. Callback will not be called. + vts.Dispose(); + return (null, errorCode); + } + } + } + catch + { + vts.Dispose(); + throw; + } + + // Completion handled by callback. + vts.FinishedScheduling(); + return (vts, -1); + } + + private static ValueTask WriteAtOffsetAsync(SafeFileHandle handle, ReadOnlyMemory buffer, long fileOffset, CancellationToken cancellationToken) + => Map(QueueAsyncWriteFile(handle, buffer, fileOffset, cancellationToken)); + + internal static unsafe (SafeFileHandle.ValueTaskSource? vts, int errorCode) QueueAsyncWriteFile( + SafeFileHandle handle, ReadOnlyMemory buffer, long fileOffset, CancellationToken cancellationToken) + { + SafeFileHandle.ValueTaskSource vts = handle.GetValueTaskSource(); + try + { + NativeOverlapped* nativeOverlapped = vts.PrepareForOperation(buffer, fileOffset); + Debug.Assert(vts._memoryHandle.Pointer != null); + + // Queue an async WriteFile operation. + if (Interop.Kernel32.WriteFile(handle, (byte*)vts._memoryHandle.Pointer, buffer.Length, IntPtr.Zero, nativeOverlapped) == 0) + { + // The operation failed, or it's pending. + int errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); + switch (errorCode) + { + case Interop.Errors.ERROR_IO_PENDING: + // Common case: IO was initiated, completion will be handled by callback. + // Register for cancellation now that the operation has been initiated. + vts.RegisterForCancellation(cancellationToken); + break; + case Interop.Errors.ERROR_NO_DATA: // EOF on a pipe. IO callback will not be called. + vts.Dispose(); + return (null, 0); + default: + // Error. Callback will not be invoked. + vts.Dispose(); + return (null, errorCode); + } + } + } + catch + { + vts.Dispose(); + throw; + } + + // Completion handled by callback. + vts.FinishedScheduling(); + return (vts, -1); + } + + private static long ReadScatterAtOffset(SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset) + { + long total = 0; + + // ReadFileScatter does not support sync handles, so we just call ReadFile in a loop + for (int i = 0; i < buffers.Count; i++) + { + Span span = buffers[i].Span; + int read = ReadAtOffset(handle, span, fileOffset + total); + total += read; + + // We stop on the first incomplete read. + // Most probably there is no more data available and the next read is going to return 0 (EOF). + if (read != span.Length) + { + break; + } + } + + return total; + } + + private static long WriteGatherAtOffset(SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset) + { + long total = 0; + + // WriteFileGather does not support sync handles, so we just call WriteFile in a loop + for (int i = 0; i < buffers.Count; i++) + { + ReadOnlySpan span = buffers[i].Span; + int written = WriteAtOffset(handle, span, fileOffset + total); + total += written; + + // We stop on the first incomplete write. + // Most probably the disk became full and the next write is going to throw. + if (written != span.Length) + { + break; + } + } + + return total; + } + + private static ValueTask ReadScatterAtOffsetAsync(SafeFileHandle handle, IReadOnlyList> buffers, + long fileOffset, CancellationToken cancellationToken) + { + if (CanUseScatterGatherWindowsAPIs(handle)) + { + long totalBytes = 0; + for (int i = 0; i < buffers.Count; i++) + { + totalBytes += buffers[i].Length; + } + + if (totalBytes <= int.MaxValue) // the ReadFileScatter API uses int, not long + { + return ReadScatterAtOffsetSingleSyscallAsync(handle, buffers, fileOffset, (int)totalBytes, cancellationToken); + } + } + + return ReadScatterAtOffsetMultipleSyscallsAsync(handle, buffers, fileOffset, cancellationToken); + } + + // From https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfilescatter: + // "The file handle must be created with the GENERIC_READ right, and the FILE_FLAG_OVERLAPPED and FILE_FLAG_NO_BUFFERING flags." + private static bool CanUseScatterGatherWindowsAPIs(SafeFileHandle handle) + => handle.IsAsync && ((handle.GetFileOptions() & SafeFileHandle.NoBuffering) != 0); + + private static async ValueTask ReadScatterAtOffsetSingleSyscallAsync(SafeFileHandle handle, + IReadOnlyList> buffers, long fileOffset, int totalBytes, CancellationToken cancellationToken) + { + if (buffers.Count == 1) + { + // we have to await it because we can't cast a VT to VT + return await ReadAtOffsetAsync(handle, buffers[0], fileOffset, cancellationToken).ConfigureAwait(false); + } + + // "The array must contain enough elements to store nNumberOfBytesToWrite bytes of data, and one element for the terminating NULL. " + long[] fileSegments = new long[buffers.Count + 1]; + fileSegments[buffers.Count] = 0; + + MemoryHandle[] memoryHandles = new MemoryHandle[buffers.Count]; + MemoryHandle pinnedSegments = fileSegments.AsMemory().Pin(); + + try + { + for (int i = 0; i < buffers.Count; i++) + { + Memory buffer = buffers[i]; + MemoryHandle memoryHandle = buffer.Pin(); + memoryHandles[i] = memoryHandle; + + unsafe // awaits can't be in an unsafe context + { + fileSegments[i] = new IntPtr(memoryHandle.Pointer).ToInt64(); + } + } + + return await ReadFileScatterAsync(handle, pinnedSegments, totalBytes, fileOffset, cancellationToken).ConfigureAwait(false); + } + finally + { + foreach (MemoryHandle memoryHandle in memoryHandles) + { + memoryHandle.Dispose(); + } + pinnedSegments.Dispose(); + } + } + + private static unsafe ValueTask ReadFileScatterAsync(SafeFileHandle handle, MemoryHandle pinnedSegments, + int bytesToRead, long fileOffset, CancellationToken cancellationToken) + { + SafeFileHandle.ValueTaskSource vts = handle.GetValueTaskSource(); + try + { + NativeOverlapped* nativeOverlapped = vts.PrepareForOperation(Memory.Empty, fileOffset); + Debug.Assert(pinnedSegments.Pointer != null); + + if (Interop.Kernel32.ReadFileScatter(handle, (long*)pinnedSegments.Pointer, bytesToRead, IntPtr.Zero, nativeOverlapped) == 0) + { + // The operation failed, or it's pending. + int errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); + switch (errorCode) + { + case Interop.Errors.ERROR_IO_PENDING: + // Common case: IO was initiated, completion will be handled by callback. + // Register for cancellation now that the operation has been initiated. + vts.RegisterForCancellation(cancellationToken); + break; + + case Interop.Errors.ERROR_HANDLE_EOF: // logically success with 0 bytes read (read at end of file) + case Interop.Errors.ERROR_BROKEN_PIPE: + // EOF on a pipe. Callback will not be called. + // We clear the overlapped status bit for this special case (failure + // to do so looks like we are freeing a pending overlapped later). + nativeOverlapped->InternalLow = IntPtr.Zero; + vts.Dispose(); + return ValueTask.FromResult(0); + + default: + // Error. Callback will not be called. + vts.Dispose(); + return ValueTask.FromException(Win32Marshal.GetExceptionForWin32Error(errorCode)); + } + } + } + catch + { + vts.Dispose(); + throw; + } + + // Completion handled by callback. + vts.FinishedScheduling(); + return new ValueTask(vts, vts.Version); + } + + private static async ValueTask ReadScatterAtOffsetMultipleSyscallsAsync(SafeFileHandle handle, IReadOnlyList> buffers, + long fileOffset, CancellationToken cancellationToken) + { + long total = 0; + + for (int i = 0; i < buffers.Count; i++) + { + Memory buffer = buffers[i]; + int read = await ReadAtOffsetAsync(handle, buffer, fileOffset + total, cancellationToken).ConfigureAwait(false); + total += read; + + if (read != buffer.Length) + { + break; + } + } + + return total; + } + + private static ValueTask WriteGatherAtOffsetAsync(SafeFileHandle handle, IReadOnlyList> buffers, + long fileOffset, CancellationToken cancellationToken) + { + if (CanUseScatterGatherWindowsAPIs(handle)) + { + long totalBytes = 0; + for (int i = 0; i < buffers.Count; i++) + { + totalBytes += buffers[i].Length; + } + + if (totalBytes <= int.MaxValue) // the ReadFileScatter API uses int, not long + { + return WriteGatherAtOffsetSingleSyscallAsync(handle, buffers, fileOffset, (int)totalBytes, cancellationToken); + } + } + + return WriteGatherAtOffsetMultipleSyscallsAsync(handle, buffers, fileOffset, cancellationToken); + } + + private static async ValueTask WriteGatherAtOffsetMultipleSyscallsAsync(SafeFileHandle handle, IReadOnlyList> buffers, + long fileOffset, CancellationToken cancellationToken) + { + long total = 0; + + for (int i = 0; i < buffers.Count; i++) + { + ReadOnlyMemory buffer = buffers[i]; + int written = await WriteAtOffsetAsync(handle, buffer, fileOffset + total, cancellationToken).ConfigureAwait(false); + total += written; + + if (written != buffer.Length) + { + break; + } + } + + return total; + } + + private static async ValueTask WriteGatherAtOffsetSingleSyscallAsync(SafeFileHandle handle, IReadOnlyList> buffers, + long fileOffset, int totalBytes, CancellationToken cancellationToken) + { + if (buffers.Count == 1) + { + return await WriteAtOffsetAsync(handle, buffers[0], fileOffset, cancellationToken).ConfigureAwait(false); + } + + // "The array must contain enough elements to store nNumberOfBytesToWrite bytes of data, and one element for the terminating NULL. " + long[] fileSegments = new long[buffers.Count + 1]; + fileSegments[buffers.Count] = 0; + + MemoryHandle[] memoryHandles = new MemoryHandle[buffers.Count]; + MemoryHandle pinnedSegments = fileSegments.AsMemory().Pin(); + + try + { + for (int i = 0; i < buffers.Count; i++) + { + ReadOnlyMemory buffer = buffers[i]; + MemoryHandle memoryHandle = buffer.Pin(); + memoryHandles[i] = memoryHandle; + + unsafe // awaits can't be in an unsafe context + { + fileSegments[i] = new IntPtr(memoryHandle.Pointer).ToInt64(); + } + } + + return await WriteFileGatherAsync(handle, pinnedSegments, totalBytes, fileOffset, cancellationToken).ConfigureAwait(false); + } + finally + { + foreach (MemoryHandle memoryHandle in memoryHandles) + { + memoryHandle.Dispose(); + } + pinnedSegments.Dispose(); + } + } + + private static unsafe ValueTask WriteFileGatherAsync(SafeFileHandle handle, MemoryHandle pinnedSegments, + int bytesToWrite, long fileOffset, CancellationToken cancellationToken) + { + SafeFileHandle.ValueTaskSource vts = handle.GetValueTaskSource(); + try + { + NativeOverlapped* nativeOverlapped = vts.PrepareForOperation(ReadOnlyMemory.Empty, fileOffset); + Debug.Assert(pinnedSegments.Pointer != null); + + // Queue an async WriteFile operation. + if (Interop.Kernel32.WriteFileGather(handle, (long*)pinnedSegments.Pointer, bytesToWrite, IntPtr.Zero, nativeOverlapped) == 0) + { + // The operation failed, or it's pending. + int errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); + if (errorCode == Interop.Errors.ERROR_IO_PENDING) + { + // Common case: IO was initiated, completion will be handled by callback. + // Register for cancellation now that the operation has been initiated. + vts.RegisterForCancellation(cancellationToken); + } + else + { + // Error. Callback will not be invoked. + vts.Dispose(); + return errorCode == Interop.Errors.ERROR_NO_DATA // EOF on a pipe. IO callback will not be called. + ? ValueTask.FromResult(0) + : ValueTask.FromException(SafeFileHandle.ValueTaskSource.GetIOError(errorCode, path: null)); + } + } + } + catch + { + vts.Dispose(); + throw; + } + + // Completion handled by callback. + vts.FinishedScheduling(); + return new ValueTask(vts, vts.Version); + } + + private static NativeOverlapped GetNativeOverlapped(long fileOffset) + { + NativeOverlapped nativeOverlapped = default; + // For pipes the offsets are ignored by the OS + nativeOverlapped.OffsetLow = unchecked((int)fileOffset); + nativeOverlapped.OffsetHigh = (int)(fileOffset >> 32); + + return nativeOverlapped; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs new file mode 100644 index 00000000000000..199d0d4ea91c51 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs @@ -0,0 +1,283 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Win32.SafeHandles; + +namespace System.IO +{ + public static partial class RandomAccess + { + /// + /// Gets the length of the file in bytes. + /// + /// The file handle. + /// A long value representing the length of the file in bytes. + /// is . + /// is invalid. + /// The file is closed. + /// The file does not support seeking (pipe or socket). + public static long GetLength(SafeFileHandle handle) + { + ValidateInput(handle, fileOffset: 0); + + return GetFileLength(handle, path: null); + } + + /// + /// Reads a sequence of bytes from given file at given offset. + /// + /// The file handle. + /// A region of memory. When this method returns, the contents of this region are replaced by the bytes read from the file. + /// The file position to read from. + /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated in the buffer if that many bytes are not currently available, or zero (0) if the end of the file has been reached. + /// is . + /// is invalid. + /// The file is closed. + /// The file does not support seeking (pipe or socket). + /// was opened for async IO. + /// is negative. + /// was not opened for reading. + /// An I/O error occurred. + /// Position of the file is not advanced. + public static int Read(SafeFileHandle handle, Span buffer, long fileOffset) + { + ValidateInput(handle, fileOffset, mustBeSync: OperatingSystem.IsWindows()); + + return ReadAtOffset(handle, buffer, fileOffset); + } + + /// + /// Reads a sequence of bytes from given file at given offset. + /// + /// The file handle. + /// A list of memory buffers. When this method returns, the contents of the buffers are replaced by the bytes read from the file. + /// The file position to read from. + /// The total number of bytes read into the buffers. This can be less than the number of bytes allocated in the buffers if that many bytes are not currently available, or zero (0) if the end of the file has been reached. + /// or is . + /// is invalid. + /// The file is closed. + /// The file does not support seeking (pipe or socket). + /// was opened for async IO. + /// is negative. + /// was not opened for reading. + /// An I/O error occurred. + /// Position of the file is not advanced. + public static long Read(SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset) + { + ValidateInput(handle, fileOffset, mustBeSync: OperatingSystem.IsWindows()); + ValidateBuffers(buffers); + + return ReadScatterAtOffset(handle, buffers, fileOffset); + } + + /// + /// Reads a sequence of bytes from given file at given offset. + /// + /// The file handle. + /// A region of memory. When this method returns, the contents of this region are replaced by the bytes read from the file. + /// The file position to read from. + /// The token to monitor for cancellation requests. The default value is . + /// The total number of bytes read into the buffer. This can be less than the number of bytes allocated in the buffer if that many bytes are not currently available, or zero (0) if the end of the file has been reached. + /// is . + /// is invalid. + /// The file is closed. + /// The file does not support seeking (pipe or socket). + /// was not opened for async IO. + /// is negative. + /// was not opened for reading. + /// An I/O error occurred. + /// Position of the file is not advanced. + public static ValueTask ReadAsync(SafeFileHandle handle, Memory buffer, long fileOffset, CancellationToken cancellationToken = default) + { + ValidateInput(handle, fileOffset, mustBeAsync: OperatingSystem.IsWindows()); + + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + return ReadAtOffsetAsync(handle, buffer, fileOffset, cancellationToken); + } + + /// + /// Reads a sequence of bytes from given file at given offset. + /// + /// The file handle. + /// A list of memory buffers. When this method returns, the contents of these buffers are replaced by the bytes read from the file. + /// The file position to read from. + /// The token to monitor for cancellation requests. The default value is . + /// The total number of bytes read into the buffers. This can be less than the number of bytes allocated in the buffers if that many bytes are not currently available, or zero (0) if the end of the file has been reached. + /// or is . + /// is invalid. + /// The file is closed. + /// The file does not support seeking (pipe or socket). + /// was not opened for async IO. + /// is negative. + /// was not opened for reading. + /// An I/O error occurred. + /// Position of the file is not advanced. + public static ValueTask ReadAsync(SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset, CancellationToken cancellationToken = default) + { + ValidateInput(handle, fileOffset, mustBeAsync: OperatingSystem.IsWindows()); + ValidateBuffers(buffers); + + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + return ReadScatterAtOffsetAsync(handle, buffers, fileOffset, cancellationToken); + } + + /// + /// Writes a sequence of bytes from given buffer to given file at given offset. + /// + /// The file handle. + /// A region of memory. This method copies the contents of this region to the file. + /// The file position to write to. + /// The total number of bytes written into the file. This can be less than the number of bytes provided in the buffer and it's not an error. + /// is . + /// is invalid. + /// The file is closed. + /// The file does not support seeking (pipe or socket). + /// was opened for async IO. + /// is negative. + /// was not opened for writing. + /// An I/O error occurred. + /// Position of the file is not advanced. + public static int Write(SafeFileHandle handle, ReadOnlySpan buffer, long fileOffset) + { + ValidateInput(handle, fileOffset, mustBeSync: OperatingSystem.IsWindows()); + + return WriteAtOffset(handle, buffer, fileOffset); + } + + /// + /// Writes a sequence of bytes from given buffers to given file at given offset. + /// + /// The file handle. + /// A list of memory buffers. This method copies the contents of these buffers to the file. + /// The file position to write to. + /// The total number of bytes written into the file. This can be less than the number of bytes provided in the buffers and it's not an error. + /// or is . + /// is invalid. + /// The file is closed. + /// The file does not support seeking (pipe or socket). + /// was opened for async IO. + /// is negative. + /// was not opened for writing. + /// An I/O error occurred. + /// Position of the file is not advanced. + public static long Write(SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset) + { + ValidateInput(handle, fileOffset, mustBeSync: OperatingSystem.IsWindows()); + ValidateBuffers(buffers); + + return WriteGatherAtOffset(handle, buffers, fileOffset); + } + + /// + /// Writes a sequence of bytes from given buffer to given file at given offset. + /// + /// The file handle. + /// A region of memory. This method copies the contents of this region to the file. + /// The file position to write to. + /// The token to monitor for cancellation requests. The default value is . + /// The total number of bytes written into the file. This can be less than the number of bytes provided in the buffer and it's not an error. + /// is . + /// is invalid. + /// The file is closed. + /// The file does not support seeking (pipe or socket). + /// was not opened for async IO. + /// is negative. + /// was not opened for writing. + /// An I/O error occurred. + /// Position of the file is not advanced. + public static ValueTask WriteAsync(SafeFileHandle handle, ReadOnlyMemory buffer, long fileOffset, CancellationToken cancellationToken = default) + { + ValidateInput(handle, fileOffset, mustBeAsync: OperatingSystem.IsWindows()); + + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + return WriteAtOffsetAsync(handle, buffer, fileOffset, cancellationToken); + } + + /// + /// Writes a sequence of bytes from given buffers to given file at given offset. + /// + /// The file handle. + /// A list of memory buffers. This method copies the contents of these buffers to the file. + /// The file position to write to. + /// The token to monitor for cancellation requests. The default value is . + /// The total number of bytes written into the file. This can be less than the number of bytes provided in the buffers and it's not an error. + /// or is . + /// is invalid. + /// The file is closed. + /// The file does not support seeking (pipe or socket). + /// was not opened for async IO. + /// is negative. + /// was not opened for writing. + /// An I/O error occurred. + /// Position of the file is not advanced. + public static ValueTask WriteAsync(SafeFileHandle handle, IReadOnlyList> buffers, long fileOffset, CancellationToken cancellationToken = default) + { + ValidateInput(handle, fileOffset, mustBeAsync: OperatingSystem.IsWindows()); + ValidateBuffers(buffers); + + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + return WriteGatherAtOffsetAsync(handle, buffers, fileOffset, cancellationToken); + } + + private static void ValidateInput(SafeFileHandle handle, long fileOffset, bool mustBeSync = false, bool mustBeAsync = false) + { + if (handle is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.handle); + } + else if (handle.IsInvalid) + { + ThrowHelper.ThrowArgumentException_InvalidHandle(nameof(handle)); + } + else if (!handle.CanSeek) + { + // CanSeek calls IsClosed, we don't want to call it twice for valid handles + if (handle.IsClosed) + { + ThrowHelper.ThrowObjectDisposedException_FileClosed(); + } + + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); + } + else if (mustBeSync && handle.IsAsync) + { + ThrowHelper.ThrowArgumentException_HandleNotSync(nameof(handle)); + } + else if (mustBeAsync && !handle.IsAsync) + { + ThrowHelper.ThrowArgumentException_HandleNotAsync(nameof(handle)); + } + else if (fileOffset < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException_NeedPosNum(nameof(fileOffset)); + } + } + + private static void ValidateBuffers(IReadOnlyList buffers) + { + if (buffers is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.buffers); + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs index 6719dd9389566a..a194c4802d15c1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/AsyncWindowsFileStreamStrategy.cs @@ -10,8 +10,6 @@ namespace System.IO.Strategies { internal sealed partial class AsyncWindowsFileStreamStrategy : WindowsFileStreamStrategy { - private ValueTaskSource? _reusableValueTaskSource; // reusable ValueTaskSource that is currently NOT being used - internal AsyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access, FileShare share) : base(handle, access, share) { @@ -24,94 +22,6 @@ internal AsyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess a internal override bool IsAsync => true; - public override ValueTask DisposeAsync() - { - // the base class must dispose ThreadPoolBinding and FileHandle - // before _preallocatedOverlapped is disposed - ValueTask result = base.DisposeAsync(); - Debug.Assert(result.IsCompleted, "the method must be sync, as it performs no flushing"); - - Interlocked.Exchange(ref _reusableValueTaskSource, null)?.Dispose(); - - return result; - } - - protected override void Dispose(bool disposing) - { - // the base class must dispose ThreadPoolBinding and FileHandle - // before _preallocatedOverlapped is disposed - base.Dispose(disposing); - - Interlocked.Exchange(ref _reusableValueTaskSource, null)?.Dispose(); - } - - protected override void OnInitFromHandle(SafeFileHandle handle) - { - // This is necessary for async IO using IO Completion ports via our - // managed Threadpool API's. This calls the OS's - // BindIoCompletionCallback method, and passes in a stub for the - // LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped - // struct for this request and gets a delegate to a managed callback - // from there, which it then calls on a threadpool thread. (We allocate - // our native OVERLAPPED structs 2 pointers too large and store EE - // state & a handle to a delegate there.) - // - // If, however, we've already bound this file handle to our completion port, - // don't try to bind it again because it will fail. A handle can only be - // bound to a single completion port at a time. - if (handle.IsAsync != true) - { - try - { - handle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(handle); - } - catch (Exception ex) - { - // If you passed in a synchronous handle and told us to use - // it asynchronously, throw here. - throw new ArgumentException(SR.Arg_HandleNotAsync, nameof(handle), ex); - } - } - } - - protected override void OnInit() - { - // This is necessary for async IO using IO Completion ports via our - // managed Threadpool API's. This (theoretically) calls the OS's - // BindIoCompletionCallback method, and passes in a stub for the - // LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped - // struct for this request and gets a delegate to a managed callback - // from there, which it then calls on a threadpool thread. (We allocate - // our native OVERLAPPED structs 2 pointers too large and store EE state - // & GC handles there, one to an IAsyncResult, the other to a delegate.) - try - { - _fileHandle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(_fileHandle); - } - catch (ArgumentException ex) - { - throw new IOException(SR.IO_BindHandleFailed, ex); - } - finally - { - if (_fileHandle.ThreadPoolBinding == null) - { - // We should close the handle so that the handle is not open until SafeFileHandle GC - _fileHandle.Dispose(); - } - } - } - - private void TryToReuse(ValueTaskSource source) - { - source._source.Reset(); - - if (Interlocked.CompareExchange(ref _reusableValueTaskSource, source, null) is not null) - { - source._preallocatedOverlapped.Dispose(); - } - } - public override int Read(byte[] buffer, int offset, int count) { ValueTask vt = ReadAsyncInternal(new Memory(buffer, offset, count), CancellationToken.None); @@ -133,75 +43,27 @@ private unsafe ValueTask ReadAsyncInternal(Memory destination, Cancel ThrowHelper.ThrowNotSupportedException_UnreadableStream(); } - // Rent the reusable ValueTaskSource, or create a new one to use if we couldn't get one (which - // should only happen on first use or if the FileStream is being used concurrently). - ValueTaskSource vts = Interlocked.Exchange(ref _reusableValueTaskSource, null) ?? new ValueTaskSource(this); - try + long positionBefore = _filePosition; + if (CanSeek) { - NativeOverlapped* nativeOverlapped = vts.PrepareForOperation(destination); - Debug.Assert(vts._memoryHandle.Pointer != null); - - // Calculate position in the file we should be at after the read is done - long positionBefore = _filePosition; - if (CanSeek) + long len = Length; + if (positionBefore + destination.Length > len) { - long len = Length; - - if (positionBefore + destination.Length > len) - { - destination = positionBefore <= len ? - destination.Slice(0, (int)(len - positionBefore)) : - default; - } - - // Now set the position to read from in the NativeOverlapped struct - // For pipes, we should leave the offset fields set to 0. - nativeOverlapped->OffsetLow = unchecked((int)positionBefore); - nativeOverlapped->OffsetHigh = (int)(positionBefore >> 32); - - // When using overlapped IO, the OS is not supposed to - // touch the file pointer location at all. We will adjust it - // ourselves, but only in memory. This isn't threadsafe. - _filePosition += destination.Length; + destination = positionBefore <= len ? + destination.Slice(0, (int)(len - positionBefore)) : + default; } - // Queue an async ReadFile operation. - if (Interop.Kernel32.ReadFile(_fileHandle, (byte*)vts._memoryHandle.Pointer, destination.Length, IntPtr.Zero, nativeOverlapped) == 0) - { - // The operation failed, or it's pending. - int errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(_fileHandle); - switch (errorCode) - { - case Interop.Errors.ERROR_IO_PENDING: - // Common case: IO was initiated, completion will be handled by callback. - // Register for cancellation now that the operation has been initiated. - vts.RegisterForCancellation(cancellationToken); - break; - - case Interop.Errors.ERROR_BROKEN_PIPE: - // EOF on a pipe. Callback will not be called. - // We clear the overlapped status bit for this special case (failure - // to do so looks like we are freeing a pending overlapped later). - nativeOverlapped->InternalLow = IntPtr.Zero; - vts.Dispose(); - return ValueTask.FromResult(0); - - default: - // Error. Callback will not be called. - vts.Dispose(); - return ValueTask.FromException(HandleIOError(positionBefore, errorCode)); - } - } - } - catch - { - vts.Dispose(); - throw; + // When using overlapped IO, the OS is not supposed to + // touch the file pointer location at all. We will adjust it + // ourselves, but only in memory. This isn't threadsafe. + _filePosition += destination.Length; } - // Completion handled by callback. - vts.FinishedScheduling(); - return new ValueTask(vts, vts.Version); + (SafeFileHandle.ValueTaskSource? vts, int errorCode) = RandomAccess.QueueAsyncReadFile(_fileHandle, destination, positionBefore, cancellationToken); + return vts != null + ? new ValueTask(vts, vts.Version) + : (errorCode == 0) ? ValueTask.FromResult(0) : ValueTask.FromException(HandleIOError(positionBefore, errorCode)); } public override void Write(byte[] buffer, int offset, int count) @@ -220,59 +82,20 @@ private unsafe ValueTask WriteAsyncInternal(ReadOnlyMemory source, Cancell ThrowHelper.ThrowNotSupportedException_UnwritableStream(); } - // Rent the reusable ValueTaskSource, or create a new one to use if we couldn't get one (which - // should only happen on first use or if the FileStream is being used concurrently). - ValueTaskSource vts = Interlocked.Exchange(ref _reusableValueTaskSource, null) ?? new ValueTaskSource(this); - try - { - NativeOverlapped* nativeOverlapped = vts.PrepareForOperation(source); - Debug.Assert(vts._memoryHandle.Pointer != null); - - long positionBefore = _filePosition; - if (CanSeek) - { - // Now set the position to read from in the NativeOverlapped struct - // For pipes, we should leave the offset fields set to 0. - nativeOverlapped->OffsetLow = (int)positionBefore; - nativeOverlapped->OffsetHigh = (int)(positionBefore >> 32); - - // When using overlapped IO, the OS is not supposed to - // touch the file pointer location at all. We will adjust it - // ourselves, but only in memory. This isn't threadsafe. - _filePosition += source.Length; - UpdateLengthOnChangePosition(); - } - - // Queue an async WriteFile operation. - if (Interop.Kernel32.WriteFile(_fileHandle, (byte*)vts._memoryHandle.Pointer, source.Length, IntPtr.Zero, nativeOverlapped) == 0) - { - // The operation failed, or it's pending. - int errorCode = FileStreamHelpers.GetLastWin32ErrorAndDisposeHandleIfInvalid(_fileHandle); - if (errorCode == Interop.Errors.ERROR_IO_PENDING) - { - // Common case: IO was initiated, completion will be handled by callback. - // Register for cancellation now that the operation has been initiated. - vts.RegisterForCancellation(cancellationToken); - } - else - { - // Error. Callback will not be invoked. - vts.Dispose(); - return errorCode == Interop.Errors.ERROR_NO_DATA ? // EOF on a pipe. IO callback will not be called. - ValueTask.CompletedTask : - ValueTask.FromException(HandleIOError(positionBefore, errorCode)); - } - } - } - catch + long positionBefore = _filePosition; + if (CanSeek) { - vts.Dispose(); - throw; + // When using overlapped IO, the OS is not supposed to + // touch the file pointer location at all. We will adjust it + // ourselves, but only in memory. This isn't threadsafe. + _filePosition += source.Length; + UpdateLengthOnChangePosition(); } - // Completion handled by callback. - vts.FinishedScheduling(); - return new ValueTask(vts, vts.Version); + (SafeFileHandle.ValueTaskSource? vts, int errorCode) = RandomAccess.QueueAsyncWriteFile(_fileHandle, source, positionBefore, cancellationToken); + return vts != null + ? new ValueTask(vts, vts.Version) + : (errorCode == 0) ? ValueTask.CompletedTask : ValueTask.FromException(HandleIOError(positionBefore, errorCode)); } private Exception HandleIOError(long positionBefore, int errorCode) @@ -283,9 +106,7 @@ private Exception HandleIOError(long positionBefore, int errorCode) _filePosition = positionBefore; } - return errorCode == Interop.Errors.ERROR_HANDLE_EOF ? - ThrowHelper.CreateEndOfFileException() : - Win32Marshal.GetExceptionForWin32Error(errorCode, _path); + return SafeFileHandle.ValueTaskSource.GetIOError(errorCode, _path); } public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; // no buffering = nothing to flush diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs index 1fd6456f3eeb54..868ccb84fd7f21 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Unix.cs @@ -2,11 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Win32.SafeHandles; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Threading; -using System.Threading.Tasks; namespace System.IO.Strategies { @@ -20,88 +15,18 @@ private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, File private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) => new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize == 0 ? 1 : bufferSize, options, preallocationSize); - internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) + internal static long CheckFileCall(long result, string? path, bool ignoreNotSupported = false) { - // Translate the arguments into arguments for an open call. - Interop.Sys.OpenFlags openFlags = PreOpenConfigurationFromOptions(mode, access, share, options); - - // If the file gets created a new, we'll select the permissions for it. Most Unix utilities by default use 666 (read and - // write for all), so we do the same (even though this doesn't match Windows, where by default it's possible to write out - // a file and then execute it). No matter what we choose, it'll be subject to the umask applied by the system, such that the - // actual permissions will typically be less than what we select here. - const Interop.Sys.Permissions OpenPermissions = - Interop.Sys.Permissions.S_IRUSR | Interop.Sys.Permissions.S_IWUSR | - Interop.Sys.Permissions.S_IRGRP | Interop.Sys.Permissions.S_IWGRP | - Interop.Sys.Permissions.S_IROTH | Interop.Sys.Permissions.S_IWOTH; - - return SafeFileHandle.Open(path!, openFlags, (int)OpenPermissions); - } - - internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsync) => handle.IsAsync ?? defaultIsAsync; - - /// Translates the FileMode, FileAccess, and FileOptions values into flags to be passed when opening the file. - /// The FileMode provided to the stream's constructor. - /// The FileAccess provided to the stream's constructor - /// The FileShare provided to the stream's constructor - /// The FileOptions provided to the stream's constructor - /// The flags value to be passed to the open system call. - private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mode, FileAccess access, FileShare share, FileOptions options) - { - // Translate FileMode. Most of the values map cleanly to one or more options for open. - Interop.Sys.OpenFlags flags = default; - switch (mode) - { - default: - case FileMode.Open: // Open maps to the default behavior for open(...). No flags needed. - case FileMode.Truncate: // We truncate the file after getting the lock - break; - - case FileMode.Append: // Append is the same as OpenOrCreate, except that we'll also separately jump to the end later - case FileMode.OpenOrCreate: - case FileMode.Create: // We truncate the file after getting the lock - flags |= Interop.Sys.OpenFlags.O_CREAT; - break; - - case FileMode.CreateNew: - flags |= (Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL); - break; - } - - // Translate FileAccess. All possible values map cleanly to corresponding values for open. - switch (access) - { - case FileAccess.Read: - flags |= Interop.Sys.OpenFlags.O_RDONLY; - break; - - case FileAccess.ReadWrite: - flags |= Interop.Sys.OpenFlags.O_RDWR; - break; - - case FileAccess.Write: - flags |= Interop.Sys.OpenFlags.O_WRONLY; - break; - } - - // Handle Inheritable, other FileShare flags are handled by Init - if ((share & FileShare.Inheritable) == 0) - { - flags |= Interop.Sys.OpenFlags.O_CLOEXEC; - } - - // Translate some FileOptions; some just aren't supported, and others will be handled after calling open. - // - Asynchronous: Handled in ctor, setting _useAsync and SafeFileHandle.IsAsync to true - // - DeleteOnClose: Doesn't have a Unix equivalent, but we approximate it in Dispose - // - Encrypted: No equivalent on Unix and is ignored - // - RandomAccess: Implemented after open if posix_fadvise is available - // - SequentialScan: Implemented after open if posix_fadvise is available - // - WriteThrough: Handled here - if ((options & FileOptions.WriteThrough) != 0) + if (result < 0) { - flags |= Interop.Sys.OpenFlags.O_SYNC; + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (!(ignoreNotSupported && errorInfo.Error == Interop.Error.ENOTSUP)) + { + throw Interop.GetExceptionForIoErrno(errorInfo, path, isDirectory: false); + } } - return flags; + return result; } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs index f9f22b4d698f02..39275721010fb1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.Windows.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; @@ -61,202 +60,6 @@ private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, internal static FileStreamStrategy EnableBufferingIfNeeded(WindowsFileStreamStrategy strategy, int bufferSize) => bufferSize > 1 ? new BufferedFileStreamStrategy(strategy, bufferSize) : strategy; - internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) - => CreateFileOpenHandle(path, mode, access, share, options, preallocationSize); - - private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) - { - using (DisableMediaInsertionPrompt.Create()) - { - Debug.Assert(path != null); - - if (ShouldPreallocate(preallocationSize, access, mode)) - { - IntPtr fileHandle = NtCreateFile(path, mode, access, share, options, preallocationSize); - - return ValidateFileHandle(new SafeFileHandle(fileHandle, ownsHandle: true), path, (options & FileOptions.Asynchronous) != 0); - } - - Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = GetSecAttrs(share); - - int fAccess = - ((access & FileAccess.Read) == FileAccess.Read ? Interop.Kernel32.GenericOperations.GENERIC_READ : 0) | - ((access & FileAccess.Write) == FileAccess.Write ? Interop.Kernel32.GenericOperations.GENERIC_WRITE : 0); - - // Our Inheritable bit was stolen from Windows, but should be set in - // the security attributes class. Don't leave this bit set. - share &= ~FileShare.Inheritable; - - // Must use a valid Win32 constant here... - if (mode == FileMode.Append) - mode = FileMode.OpenOrCreate; - - int flagsAndAttributes = (int)options; - - // For mitigating local elevation of privilege attack through named pipes - // make sure we always call CreateFile with SECURITY_ANONYMOUS so that the - // named pipe server can't impersonate a high privileged client security context - // (note that this is the effective default on CreateFile2) - flagsAndAttributes |= (Interop.Kernel32.SecurityOptions.SECURITY_SQOS_PRESENT | Interop.Kernel32.SecurityOptions.SECURITY_ANONYMOUS); - - SafeFileHandle safeFileHandle = ValidateFileHandle( - Interop.Kernel32.CreateFile(path, fAccess, share, &secAttrs, mode, flagsAndAttributes, IntPtr.Zero), - path, - (options & FileOptions.Asynchronous) != 0); - - return safeFileHandle; - } - } - - private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) - { - uint ntStatus; - IntPtr fileHandle; - - const string mandatoryNtPrefix = @"\??\"; - if (fullPath.StartsWith(mandatoryNtPrefix, StringComparison.Ordinal)) - { - (ntStatus, fileHandle) = Interop.NtDll.CreateFile(fullPath, mode, access, share, options, preallocationSize); - } - else - { - var vsb = new ValueStringBuilder(stackalloc char[1024]); - vsb.Append(mandatoryNtPrefix); - - if (fullPath.StartsWith(@"\\?\", StringComparison.Ordinal)) // NtCreateFile does not support "\\?\" prefix, only "\??\" - { - vsb.Append(fullPath.AsSpan(4)); - } - else - { - vsb.Append(fullPath); - } - - (ntStatus, fileHandle) = Interop.NtDll.CreateFile(vsb.AsSpan(), mode, access, share, options, preallocationSize); - vsb.Dispose(); - } - - switch (ntStatus) - { - case 0: - return fileHandle; - case Interop.NtDll.NT_ERROR_STATUS_DISK_FULL: - throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, fullPath, preallocationSize)); - // NtCreateFile has a bug and it reports STATUS_INVALID_PARAMETER for files - // that are too big for the current file system. Example: creating a 4GB+1 file on a FAT32 drive. - case Interop.NtDll.NT_STATUS_INVALID_PARAMETER: - case Interop.NtDll.NT_ERROR_STATUS_FILE_TOO_LARGE: - throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, fullPath, preallocationSize)); - default: - int error = (int)Interop.NtDll.RtlNtStatusToDosError((int)ntStatus); - throw Win32Marshal.GetExceptionForWin32Error(error, fullPath); - } - } - - internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsync) - { - return handle.IsAsync ?? !IsHandleSynchronous(handle, ignoreInvalid: true) ?? defaultIsAsync; - } - - internal static unsafe bool? IsHandleSynchronous(SafeFileHandle fileHandle, bool ignoreInvalid) - { - if (fileHandle.IsInvalid) - return null; - - uint fileMode; - - int status = Interop.NtDll.NtQueryInformationFile( - FileHandle: fileHandle, - IoStatusBlock: out _, - FileInformation: &fileMode, - Length: sizeof(uint), - FileInformationClass: Interop.NtDll.FileModeInformation); - - switch (status) - { - case 0: - // We were successful - break; - case Interop.NtDll.STATUS_INVALID_HANDLE: - if (!ignoreInvalid) - { - throw Win32Marshal.GetExceptionForWin32Error(Interop.Errors.ERROR_INVALID_HANDLE); - } - else - { - return null; - } - default: - // Something else is preventing access - Debug.Fail("Unable to get the file mode information, status was" + status.ToString()); - return null; - } - - // If either of these two flags are set, the file handle is synchronous (not overlapped) - return (fileMode & (uint)(Interop.NtDll.CreateOptions.FILE_SYNCHRONOUS_IO_ALERT | Interop.NtDll.CreateOptions.FILE_SYNCHRONOUS_IO_NONALERT)) > 0; - } - - internal static void VerifyHandleIsSync(SafeFileHandle handle) - { - // As we can accurately check the handle type when we have access to NtQueryInformationFile we don't need to skip for - // any particular file handle type. - - // If the handle was passed in without an explicit async setting, we already looked it up in GetDefaultIsAsync - if (!handle.IsAsync.HasValue) - return; - - // If we can't check the handle, just assume it is ok. - if (!(IsHandleSynchronous(handle, ignoreInvalid: false) ?? true)) - ThrowHelper.ThrowArgumentException_HandleNotSync(nameof(handle)); - } - - private static unsafe Interop.Kernel32.SECURITY_ATTRIBUTES GetSecAttrs(FileShare share) - { - Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = default; - if ((share & FileShare.Inheritable) != 0) - { - secAttrs = new Interop.Kernel32.SECURITY_ATTRIBUTES - { - nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES), - bInheritHandle = Interop.BOOL.TRUE - }; - } - return secAttrs; - } - - private static SafeFileHandle ValidateFileHandle(SafeFileHandle fileHandle, string path, bool useAsyncIO) - { - if (fileHandle.IsInvalid) - { - // Return a meaningful exception with the full path. - - // NT5 oddity - when trying to open "C:\" as a Win32FileStream, - // we usually get ERROR_PATH_NOT_FOUND from the OS. We should - // probably be consistent w/ every other directory. - int errorCode = Marshal.GetLastPInvokeError(); - - if (errorCode == Interop.Errors.ERROR_PATH_NOT_FOUND && path!.Length == PathInternal.GetRootLength(path)) - errorCode = Interop.Errors.ERROR_ACCESS_DENIED; - - throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); - } - - fileHandle.IsAsync = useAsyncIO; - return fileHandle; - } - - internal static unsafe long GetFileLength(SafeFileHandle handle, string? path) - { - Interop.Kernel32.FILE_STANDARD_INFO info; - - if (!Interop.Kernel32.GetFileInformationByHandleEx(handle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))) - { - throw Win32Marshal.GetExceptionForLastWin32Error(path); - } - - return info.EndOfFile; - } - internal static void FlushToDisk(SafeFileHandle handle, string? path) { if (!Interop.Kernel32.FlushFileBuffers(handle)) @@ -347,7 +150,7 @@ internal static void ValidateFileTypeForNonExtendedPaths(SafeFileHandle handle, // we were explicitly passed a path that has \\?\. GetFullPath() will turn paths like C:\foo\con.txt into // \\.\CON, so we'll only allow the \\?\ syntax. - int fileType = Interop.Kernel32.GetFileType(handle); + int fileType = handle.GetFileType(); if (fileType != Interop.Kernel32.FileTypes.FILE_TYPE_DISK) { int errorCode = fileType == Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN @@ -365,18 +168,6 @@ internal static void ValidateFileTypeForNonExtendedPaths(SafeFileHandle handle, } } - internal static void GetFileTypeSpecificInformation(SafeFileHandle handle, out bool canSeek, out bool isPipe) - { - int handleType = Interop.Kernel32.GetFileType(handle); - Debug.Assert(handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK - || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE - || handleType == Interop.Kernel32.FileTypes.FILE_TYPE_CHAR, - "FileStream was passed an unknown file type!"); - - canSeek = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK; - isPipe = handleType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE; - } - internal static unsafe void SetFileLength(SafeFileHandle handle, string? path, long length) { var eofInfo = new Interop.Kernel32.FILE_END_OF_FILE_INFO @@ -397,70 +188,7 @@ internal static unsafe void SetFileLength(SafeFileHandle handle, string? path, l } } - internal static unsafe int ReadFileNative(SafeFileHandle handle, Span bytes, bool syncUsingOverlapped, NativeOverlapped* overlapped, out int errorCode) - { - Debug.Assert(handle != null, "handle != null"); - - int r; - int numBytesRead = 0; - - fixed (byte* p = &MemoryMarshal.GetReference(bytes)) - { - r = overlapped != null ? - (syncUsingOverlapped - ? Interop.Kernel32.ReadFile(handle, p, bytes.Length, out numBytesRead, overlapped) - : Interop.Kernel32.ReadFile(handle, p, bytes.Length, IntPtr.Zero, overlapped)) - : Interop.Kernel32.ReadFile(handle, p, bytes.Length, out numBytesRead, IntPtr.Zero); - } - - if (r == 0) - { - errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); - - if (syncUsingOverlapped && errorCode == Interop.Errors.ERROR_HANDLE_EOF) - { - // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-readfile#synchronization-and-file-position : - // "If lpOverlapped is not NULL, then when a synchronous read operation reaches the end of a file, - // ReadFile returns FALSE and GetLastError returns ERROR_HANDLE_EOF" - return numBytesRead; - } - return -1; - } - else - { - errorCode = 0; - return numBytesRead; - } - } - - internal static unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan buffer, bool syncUsingOverlapped, NativeOverlapped* overlapped, out int errorCode) - { - Debug.Assert(handle != null, "handle != null"); - - int numBytesWritten = 0; - int r; - - fixed (byte* p = &MemoryMarshal.GetReference(buffer)) - { - r = overlapped != null ? - (syncUsingOverlapped - ? Interop.Kernel32.WriteFile(handle, p, buffer.Length, out numBytesWritten, overlapped) - : Interop.Kernel32.WriteFile(handle, p, buffer.Length, IntPtr.Zero, overlapped)) - : Interop.Kernel32.WriteFile(handle, p, buffer.Length, out numBytesWritten, IntPtr.Zero); - } - - if (r == 0) - { - errorCode = GetLastWin32ErrorAndDisposeHandleIfInvalid(handle); - return -1; - } - else - { - errorCode = 0; - return numBytesWritten; - } - } internal static async Task AsyncModeCopyToAsync(SafeFileHandle handle, string? path, bool canSeek, long filePosition, Stream destination, int bufferSize, CancellationToken cancellationToken) { @@ -537,7 +265,7 @@ internal static async Task AsyncModeCopyToAsync(SafeFileHandle handle, string? p } // Kick off the read. - synchronousSuccess = ReadFileNative(handle, copyBuffer, false, readAwaitable._nativeOverlapped, out errorCode) >= 0; + synchronousSuccess = RandomAccess.ReadFileNative(handle, copyBuffer, false, readAwaitable._nativeOverlapped, out errorCode) >= 0; } // If the operation did not synchronously succeed, it either failed or initiated the asynchronous operation. diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs index 9fc329ed86bb09..965e9c09f04e15 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/FileStreamHelpers.cs @@ -1,12 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.Serialization; using Microsoft.Win32.SafeHandles; namespace System.IO.Strategies { internal static partial class FileStreamHelpers { + /// Caches whether Serialization Guard has been disabled for file writes + private static int s_cachedSerializationSwitch; + internal static bool UseNet5CompatStrategy { get; } = AppContextConfigHelper.GetBooleanConfig("System.IO.UseNet5CompatFileStream", "DOTNET_SYSTEM_IO_USENET5COMPATFILESTREAM"); internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) @@ -40,5 +44,78 @@ internal static bool ShouldPreallocate(long preallocationSize, FileAccess access => preallocationSize > 0 && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append; + + internal static void ValidateArguments(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path), SR.ArgumentNull_Path); + } + else if (path.Length == 0) + { + throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + } + + // don't include inheritable in our bounds check for share + FileShare tempshare = share & ~FileShare.Inheritable; + string? badArg = null; + + if (mode < FileMode.CreateNew || mode > FileMode.Append) + { + badArg = nameof(mode); + } + else if (access < FileAccess.Read || access > FileAccess.ReadWrite) + { + badArg = nameof(access); + } + else if (tempshare < FileShare.None || tempshare > (FileShare.ReadWrite | FileShare.Delete)) + { + badArg = nameof(share); + } + + if (badArg != null) + { + throw new ArgumentOutOfRangeException(badArg, SR.ArgumentOutOfRange_Enum); + } + + // NOTE: any change to FileOptions enum needs to be matched here in the error validation + if (options != FileOptions.None && (options & ~(FileOptions.WriteThrough | FileOptions.Asynchronous | FileOptions.RandomAccess | FileOptions.DeleteOnClose | FileOptions.SequentialScan | FileOptions.Encrypted | (FileOptions)0x20000000 /* NoBuffering */)) != 0) + { + throw new ArgumentOutOfRangeException(nameof(options), SR.ArgumentOutOfRange_Enum); + } + else if (bufferSize < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException_NeedNonNegNum(nameof(bufferSize)); + } + else if (preallocationSize < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException_NeedNonNegNum(nameof(preallocationSize)); + } + + // Write access validation + if ((access & FileAccess.Write) == 0) + { + if (mode == FileMode.Truncate || mode == FileMode.CreateNew || mode == FileMode.Create || mode == FileMode.Append) + { + // No write access, mode and access disagree but flag access since mode comes first + throw new ArgumentException(SR.Format(SR.Argument_InvalidFileModeAndAccessCombo, mode, access), nameof(access)); + } + } + + if ((access & FileAccess.Read) != 0 && mode == FileMode.Append) + { + throw new ArgumentException(SR.Argument_InvalidAppendMode, nameof(access)); + } + + SerializationGuard(access); + } + + internal static void SerializationGuard(FileAccess access) + { + if ((access & FileAccess.Write) == FileAccess.Write) + { + SerializationInfo.ThrowIfDeserializationInProgress("AllowFileWrites", ref s_cachedSerializationSwitch); + } + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs index f039c020433812..5ee047869c9a68 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs @@ -13,9 +13,6 @@ namespace System.IO.Strategies /// Provides an implementation of a file stream for Unix files. internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy { - /// File mode. - private FileMode _mode; - /// Advanced options requested when opening the file. private FileOptions _options; @@ -31,56 +28,16 @@ internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy /// private AsyncState? _asyncState; - /// Lazily-initialized value for whether the file supports seeking. - private bool? _canSeek; - - /// Initializes a stream for reading or writing a Unix file. - /// How the file should be opened. - /// What other access to the file should be allowed. This is currently ignored. - /// The original path specified for the FileStream. - /// Options, passed via arguments as we have no guarantee that _options field was already set. - /// passed to posix_fallocate - private void Init(FileMode mode, FileShare share, string originalPath, FileOptions options, long preallocationSize) + private void Init(FileMode mode, string originalPath, FileOptions options) { // FileStream performs most of the general argument validation. We can assume here that the arguments // are all checked and consistent (e.g. non-null-or-empty path; valid enums in mode, access, share, and options; etc.) // Store the arguments - _mode = mode; _options = options; if (_useAsyncIO) - _asyncState = new AsyncState(); - - _fileHandle.IsAsync = _useAsyncIO; - - // Lock the file if requested via FileShare. This is only advisory locking. FileShare.None implies an exclusive - // lock on the file and all other modes use a shared lock. While this is not as granular as Windows, not mandatory, - // and not atomic with file opening, it's better than nothing. - Interop.Sys.LockOperations lockOperation = (share == FileShare.None) ? Interop.Sys.LockOperations.LOCK_EX : Interop.Sys.LockOperations.LOCK_SH; - if (Interop.Sys.FLock(_fileHandle, lockOperation | Interop.Sys.LockOperations.LOCK_NB) < 0) - { - // The only error we care about is EWOULDBLOCK, which indicates that the file is currently locked by someone - // else and we would block trying to access it. Other errors, such as ENOTSUP (locking isn't supported) or - // EACCES (the file system doesn't allow us to lock), will only hamper FileStream's usage without providing value, - // given again that this is only advisory / best-effort. - Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); - if (errorInfo.Error == Interop.Error.EWOULDBLOCK) - { - throw Interop.GetExceptionForIoErrno(errorInfo, _path, isDirectory: false); - } - } - - // These provide hints around how the file will be accessed. Specifying both RandomAccess - // and Sequential together doesn't make sense as they are two competing options on the same spectrum, - // so if both are specified, we prefer RandomAccess (behavior on Windows is unspecified if both are provided). - Interop.Sys.FileAdvice fadv = - (options & FileOptions.RandomAccess) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_RANDOM : - (options & FileOptions.SequentialScan) != 0 ? Interop.Sys.FileAdvice.POSIX_FADV_SEQUENTIAL : - 0; - if (fadv != 0) { - CheckFileCall(Interop.Sys.PosixFAdvise(_fileHandle, 0, 0, fadv), - ignoreNotSupported: true); // just a hint. + _asyncState = new AsyncState(); } if (mode == FileMode.Append) @@ -88,41 +45,8 @@ private void Init(FileMode mode, FileShare share, string originalPath, FileOptio // Jump to the end of the file if opened as Append. _appendStart = SeekCore(_fileHandle, 0, SeekOrigin.End); } - else if (mode == FileMode.Create || mode == FileMode.Truncate) - { - // Truncate the file now if the file mode requires it. This ensures that the file only will be truncated - // if opened successfully. - if (Interop.Sys.FTruncate(_fileHandle, 0) < 0) - { - Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); - if (errorInfo.Error != Interop.Error.EBADF && errorInfo.Error != Interop.Error.EINVAL) - { - // We know the file descriptor is valid and we know the size argument to FTruncate is correct, - // so if EBADF or EINVAL is returned, it means we're dealing with a special file that can't be - // truncated. Ignore the error in such cases; in all others, throw. - throw Interop.GetExceptionForIoErrno(errorInfo, _path, isDirectory: false); - } - } - } - - // If preallocationSize has been provided for a creatable and writeable file - if (FileStreamHelpers.ShouldPreallocate(preallocationSize, _access, mode)) - { - int fallocateResult = Interop.Sys.PosixFAllocate(_fileHandle, 0, preallocationSize); - if (fallocateResult != 0) - { - _fileHandle.Dispose(); - Interop.Sys.Unlink(_path!); // remove the file to mimic Windows behaviour (atomic operation) - if (fallocateResult == -1) - { - throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, _path, preallocationSize)); - } - - Debug.Assert(fallocateResult == -2); - throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, _path, preallocationSize)); - } - } + Debug.Assert(_fileHandle.IsAsync == _useAsyncIO); } /// Initializes a stream from an already open file handle (file descriptor). @@ -131,42 +55,18 @@ private void InitFromHandle(SafeFileHandle handle, FileAccess access, bool useAs if (useAsyncIO) _asyncState = new AsyncState(); - if (CanSeekCore(handle)) // use non-virtual CanSeekCore rather than CanSeek to avoid making virtual call during ctor + if (handle.CanSeek) SeekCore(handle, 0, SeekOrigin.Current); } - /// Gets a value indicating whether the current stream supports seeking. - public override bool CanSeek => CanSeekCore(_fileHandle); - - /// Gets a value indicating whether the current stream supports seeking. - /// - /// Separated out of CanSeek to enable making non-virtual call to this logic. - /// We also pass in the file handle to allow the constructor to use this before it stashes the handle. - /// - private bool CanSeekCore(SafeFileHandle fileHandle) - { - if (fileHandle.IsClosed) - { - return false; - } - - if (!_canSeek.HasValue) - { - // Lazily-initialize whether we're able to seek, tested by seeking to our current location. - _canSeek = Interop.Sys.LSeek(fileHandle, 0, Interop.Sys.SeekWhence.SEEK_CUR) >= 0; - } - - return _canSeek.GetValueOrDefault(); - } + public override bool CanSeek => _fileHandle.CanSeek; public override long Length { get { // Get the length of the file as reported by the OS - Interop.Sys.FileStatus status; - CheckFileCall(Interop.Sys.FStat(_fileHandle, out status)); - long length = status.Size; + long length = RandomAccess.GetFileLength(_fileHandle, _path); // But we may have buffered some data to be written that puts our length // beyond what the OS is aware of. Update accordingly. @@ -710,31 +610,18 @@ public override long Seek(long offset, SeekOrigin origin) /// The new position in the stream. private long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin, bool closeInvalidHandle = false) { - Debug.Assert(!fileHandle.IsClosed && CanSeekCore(fileHandle)); + Debug.Assert(!fileHandle.IsInvalid); + Debug.Assert(fileHandle.CanSeek); Debug.Assert(origin >= SeekOrigin.Begin && origin <= SeekOrigin.End); - long pos = CheckFileCall(Interop.Sys.LSeek(fileHandle, offset, (Interop.Sys.SeekWhence)(int)origin)); // SeekOrigin values are the same as Interop.libc.SeekWhence values + long pos = FileStreamHelpers.CheckFileCall(Interop.Sys.LSeek(fileHandle, offset, (Interop.Sys.SeekWhence)(int)origin), _path); // SeekOrigin values are the same as Interop.libc.SeekWhence values _filePosition = pos; return pos; } - private long CheckFileCall(long result, bool ignoreNotSupported = false) - { - if (result < 0) - { - Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); - if (!(ignoreNotSupported && errorInfo.Error == Interop.Error.ENOTSUP)) - { - throw Interop.GetExceptionForIoErrno(errorInfo, _path, isDirectory: false); - } - } - - return result; - } - private int CheckFileCall(int result, bool ignoreNotSupported = false) { - CheckFileCall((long)result, ignoreNotSupported); + FileStreamHelpers.CheckFileCall(result, _path, ignoreNotSupported); return result; } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Windows.cs index ceb9bcc6e40527..ec74ccf1ac497d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.Windows.cs @@ -38,48 +38,17 @@ namespace System.IO.Strategies { internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy { - private bool _canSeek; - private bool _isPipe; // Whether to disable async buffering code. private long _appendStart; // When appending, prevent overwriting file. private Task _activeBufferOperation = Task.CompletedTask; // tracks in-progress async ops using the buffer private PreAllocatedOverlapped? _preallocatedOverlapped; // optimization for async ops to avoid per-op allocations private CompletionSource? _currentOverlappedOwner; // async op currently using the preallocated overlapped - private void Init(FileMode mode, FileShare share, string originalPath, FileOptions options, long preallocationSize) + private void Init(FileMode mode, string originalPath, FileOptions options) { FileStreamHelpers.ValidateFileTypeForNonExtendedPaths(_fileHandle, originalPath); - // This is necessary for async IO using IO Completion ports via our - // managed Threadpool API's. This (theoretically) calls the OS's - // BindIoCompletionCallback method, and passes in a stub for the - // LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped - // struct for this request and gets a delegate to a managed callback - // from there, which it then calls on a threadpool thread. (We allocate - // our native OVERLAPPED structs 2 pointers too large and store EE state - // & GC handles there, one to an IAsyncResult, the other to a delegate.) - if (_useAsyncIO) - { - try - { - _fileHandle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(_fileHandle); - } - catch (ArgumentException ex) - { - throw new IOException(SR.IO_BindHandleFailed, ex); - } - finally - { - if (_fileHandle.ThreadPoolBinding == null) - { - // We should close the handle so that the handle is not open until SafeFileHandle GC - Debug.Assert(!_exposedHandle, "Are we closing handle that we exposed/not own, how?"); - _fileHandle.Dispose(); - } - } - } - - _canSeek = true; + Debug.Assert(!_useAsyncIO || _fileHandle.ThreadPoolBinding != null); // For Append mode... if (mode == FileMode.Append) @@ -113,39 +82,9 @@ private void InitFromHandle(SafeFileHandle handle, FileAccess access, bool useAs private void InitFromHandleImpl(SafeFileHandle handle, bool useAsyncIO) { - FileStreamHelpers.GetFileTypeSpecificInformation(handle, out _canSeek, out _isPipe); - - // This is necessary for async IO using IO Completion ports via our - // managed Threadpool API's. This calls the OS's - // BindIoCompletionCallback method, and passes in a stub for the - // LPOVERLAPPED_COMPLETION_ROUTINE. This stub looks at the Overlapped - // struct for this request and gets a delegate to a managed callback - // from there, which it then calls on a threadpool thread. (We allocate - // our native OVERLAPPED structs 2 pointers too large and store EE - // state & a handle to a delegate there.) - // - // If, however, we've already bound this file handle to our completion port, - // don't try to bind it again because it will fail. A handle can only be - // bound to a single completion port at a time. - if (useAsyncIO && !(handle.IsAsync ?? false)) - { - try - { - handle.ThreadPoolBinding = ThreadPoolBoundHandle.BindHandle(handle); - } - catch (Exception ex) - { - // If you passed in a synchronous handle and told us to use - // it asynchronously, throw here. - throw new ArgumentException(SR.Arg_HandleNotAsync, nameof(handle), ex); - } - } - else if (!useAsyncIO) - { - FileStreamHelpers.VerifyHandleIsSync(handle); - } + handle.InitThreadPoolBindingIfNeeded(); - if (_canSeek) + if (handle.CanSeek) SeekCore(handle, 0, SeekOrigin.Current); else _filePosition = 0; @@ -153,13 +92,13 @@ private void InitFromHandleImpl(SafeFileHandle handle, bool useAsyncIO) private bool HasActiveBufferOperation => !_activeBufferOperation.IsCompleted; - public override bool CanSeek => _canSeek; + public override bool CanSeek => _fileHandle.CanSeek; public unsafe override long Length { get { - long len = FileStreamHelpers.GetFileLength(_fileHandle, _path); + long len = RandomAccess.GetFileLength(_fileHandle, _path); // If we're writing near the end of the file, we must include our // internal buffer in our Length calculation. Don't flush because @@ -208,7 +147,6 @@ protected override void Dispose(bool disposing) } _preallocatedOverlapped?.Dispose(); - _canSeek = false; // Don't set the buffer to null, to avoid a NullReferenceException // when users have a race condition in their code (i.e. they call @@ -236,7 +174,6 @@ public override async ValueTask DisposeAsync() } _preallocatedOverlapped?.Dispose(); - _canSeek = false; GC.SuppressFinalize(this); // the handle is closed; nothing further for the finalizer to do } } @@ -380,7 +317,7 @@ private int ReadSpan(Span destination) // If we are reading from a device with no clear EOF like a // serial port or a pipe, this will cause us to block incorrectly. - if (!_isPipe) + if (!_fileHandle.IsPipe) { // If we hit the end of the buffer and didn't have enough bytes, we must // read some more from the underlying stream. However, if we got @@ -533,7 +470,7 @@ public override long Seek(long offset, SeekOrigin origin) // internal position private long SeekCore(SafeFileHandle fileHandle, long offset, SeekOrigin origin, bool closeInvalidHandle = false) { - Debug.Assert(!fileHandle.IsClosed && _canSeek, "!fileHandle.IsClosed && _canSeek"); + Debug.Assert(fileHandle.CanSeek, "fileHandle.CanSeek"); return _filePosition = FileStreamHelpers.Seek(fileHandle, _path, offset, origin, closeInvalidHandle); } @@ -655,7 +592,7 @@ private unsafe void WriteCore(ReadOnlySpan source) Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); - if (_isPipe) + if (_fileHandle.IsPipe) { // Pipes are tricky, at least when you have 2 different pipes // that you want to use simultaneously. When redirecting stdout @@ -686,7 +623,7 @@ private unsafe void WriteCore(ReadOnlySpan source) } } - Debug.Assert(!_isPipe, "Should not be a pipe."); + Debug.Assert(!_fileHandle.IsPipe, "Should not be a pipe."); // Handle buffering. if (_writePos > 0) FlushWriteBuffer(); @@ -865,12 +802,12 @@ private ValueTask WriteAsyncInternal(ReadOnlyMemory source, CancellationTo { Debug.Assert(_useAsyncIO); Debug.Assert((_readPos == 0 && _readLength == 0 && _writePos >= 0) || (_writePos == 0 && _readPos <= _readLength), "We're either reading or writing, but not both."); - Debug.Assert(!_isPipe || (_readPos == 0 && _readLength == 0), "Win32FileStream must not have buffered data here! Pipes should be unidirectional."); + Debug.Assert(!_fileHandle.IsPipe || (_readPos == 0 && _readLength == 0), "Win32FileStream must not have buffered data here! Pipes should be unidirectional."); if (!CanWrite) ThrowHelper.ThrowNotSupportedException_UnwritableStream(); bool writeDataStoredInBuffer = false; - if (!_isPipe) // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadInternalAsyncCore) + if (!_fileHandle.IsPipe) // avoid async buffering with pipes, as doing so can lead to deadlocks (see comments in ReadInternalAsyncCore) { // Ensure the buffer is clear for writing if (_writePos == 0) @@ -1068,14 +1005,14 @@ private unsafe int ReadFileNative(SafeFileHandle handle, Span bytes, Nativ { Debug.Assert((_useAsyncIO && overlapped != null) || (!_useAsyncIO && overlapped == null), "Async IO and overlapped parameters inconsistent in call to ReadFileNative."); - return FileStreamHelpers.ReadFileNative(handle, bytes, false, overlapped, out errorCode); + return RandomAccess.ReadFileNative(handle, bytes, false, overlapped, out errorCode); } private unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan buffer, NativeOverlapped* overlapped, out int errorCode) { Debug.Assert((_useAsyncIO && overlapped != null) || (!_useAsyncIO && overlapped == null), "Async IO and overlapped parameters inconsistent in call to WriteFileNative."); - return FileStreamHelpers.WriteFileNative(handle, buffer, false, overlapped, out errorCode); + return RandomAccess.WriteFileNative(handle, buffer, false, overlapped, out errorCode); } public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs index d257df97722da8..9a88f5a65d4894 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs @@ -89,11 +89,11 @@ internal Net5CompatFileStreamStrategy(string path, FileMode mode, FileAccess acc if ((options & FileOptions.Asynchronous) != 0) _useAsyncIO = true; - _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options, preallocationSize); + _fileHandle = SafeFileHandle.Open(fullPath, mode, access, share, options, preallocationSize); try { - Init(mode, share, path, options, preallocationSize); + Init(mode, path, options); } catch { diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs index 30499b660ddf00..e3f111e2c71fa0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/SyncWindowsFileStreamStrategy.cs @@ -22,20 +22,6 @@ internal SyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess ac internal override bool IsAsync => false; - protected override void OnInitFromHandle(SafeFileHandle handle) - { - // As we can accurately check the handle type when we have access to NtQueryInformationFile we don't need to skip for - // any particular file handle type. - - // If the handle was passed in without an explicit async setting, we already looked it up in GetDefaultIsAsync - if (!handle.IsAsync.HasValue) - return; - - // If we can't check the handle, just assume it is ok. - if (!(FileStreamHelpers.IsHandleSynchronous(handle, ignoreInvalid: false) ?? true)) - ThrowHelper.ThrowArgumentException_HandleNotSync(nameof(handle)); - } - public override int Read(byte[] buffer, int offset, int count) => ReadSpan(new Span(buffer, offset, count)); public override int Read(Span buffer) => ReadSpan(buffer); @@ -104,25 +90,8 @@ private unsafe int ReadSpan(Span destination) Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); - NativeOverlapped nativeOverlapped = GetNativeOverlappedForCurrentPosition(); - int r = FileStreamHelpers.ReadFileNative(_fileHandle, destination, true, &nativeOverlapped, out int errorCode); - - if (r == -1) - { - // For pipes, ERROR_BROKEN_PIPE is the normal end of the pipe. - if (errorCode == Interop.Errors.ERROR_BROKEN_PIPE) - { - r = 0; - } - else - { - if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) - ThrowHelper.ThrowArgumentException_HandleNotSync(nameof(_fileHandle)); - - throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); - } - } - Debug.Assert(r >= 0, "FileStream's ReadNative is likely broken."); + int r = RandomAccess.ReadAtOffset(_fileHandle, destination, _filePosition, _path); + Debug.Assert(r >= 0, $"RandomAccess.ReadAtOffset returned {r}."); _filePosition += r; return r; @@ -137,39 +106,11 @@ private unsafe void WriteSpan(ReadOnlySpan source) Debug.Assert(!_fileHandle.IsClosed, "!_handle.IsClosed"); - NativeOverlapped nativeOverlapped = GetNativeOverlappedForCurrentPosition(); - int r = FileStreamHelpers.WriteFileNative(_fileHandle, source, true, &nativeOverlapped, out int errorCode); - - if (r == -1) - { - // For pipes, ERROR_NO_DATA is not an error, but the pipe is closing. - if (errorCode == Interop.Errors.ERROR_NO_DATA) - { - r = 0; - } - else - { - // ERROR_INVALID_PARAMETER may be returned for writes - // where the position is too large or for synchronous writes - // to a handle opened asynchronously. - if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) - throw new IOException(SR.IO_FileTooLongOrHandleNotSync); - throw Win32Marshal.GetExceptionForWin32Error(errorCode, _path); - } - } - Debug.Assert(r >= 0, "FileStream's WriteCore is likely broken."); + int r = RandomAccess.WriteAtOffset(_fileHandle, source, _filePosition, _path); + Debug.Assert(r >= 0, $"RandomAccess.WriteAtOffset returned {r}."); _filePosition += r; - UpdateLengthOnChangePosition(); - } - - private NativeOverlapped GetNativeOverlappedForCurrentPosition() - { - NativeOverlapped nativeOverlapped = default; - // For pipes the offsets are ignored by the OS - nativeOverlapped.OffsetLow = unchecked((int)_filePosition); - nativeOverlapped.OffsetHigh = (int)(_filePosition >> 32); - return nativeOverlapped; + UpdateLengthOnChangePosition(); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/WindowsFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/WindowsFileStreamStrategy.cs index a5ebdd6f5da599..dee1c5a954857b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/WindowsFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/WindowsFileStreamStrategy.cs @@ -14,8 +14,6 @@ internal abstract class WindowsFileStreamStrategy : FileStreamStrategy protected readonly string? _path; // The path to the opened file. private readonly FileAccess _access; // What file was opened for. private readonly FileShare _share; - private readonly bool _canSeek; // Whether can seek (file) or not (pipe). - private readonly bool _isPipe; // Whether to disable async buffering code. protected long _filePosition; private long _appendStart; // When appending, prevent overwriting file. @@ -24,16 +22,23 @@ internal abstract class WindowsFileStreamStrategy : FileStreamStrategy internal WindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access, FileShare share) { - InitFromHandle(handle, access, out _canSeek, out _isPipe); - - // Note: Cleaner to set the following fields in ValidateAndInitFromHandle, - // but we can't as they're readonly. _access = access; _share = share; _exposedHandle = true; - // As the handle was passed in, we must set the handle field at the very end to - // avoid the finalizer closing the handle when we throw errors. + handle.InitThreadPoolBindingIfNeeded(); + + if (handle.CanSeek) + { + // given strategy was created out of existing handle, so we have to perform + // a syscall to get the current handle offset + _filePosition = FileStreamHelpers.Seek(handle, _path, 0, SeekOrigin.Current); + } + else + { + _filePosition = 0; + } + _fileHandle = handle; } @@ -45,12 +50,10 @@ internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access _access = access; _share = share; - _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options, preallocationSize); + _fileHandle = SafeFileHandle.Open(fullPath, mode, access, share, options, preallocationSize); try { - _canSeek = true; - Init(mode, path); } catch @@ -63,7 +66,7 @@ internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access } } - public sealed override bool CanSeek => _canSeek; + public sealed override bool CanSeek => _fileHandle.CanSeek; public sealed override bool CanRead => !_fileHandle.IsClosed && (_access & FileAccess.Read) != 0; @@ -77,12 +80,12 @@ public unsafe sealed override long Length { if (_share > FileShare.Read || _exposedHandle) { - return FileStreamHelpers.GetFileLength(_fileHandle, _path); + return RandomAccess.GetFileLength(_fileHandle, _path); } if (_length < 0) { - _length = FileStreamHelpers.GetFileLength(_fileHandle, _path); + _length = RandomAccess.GetFileLength(_fileHandle, _path); } return _length; @@ -116,7 +119,8 @@ public override long Position internal sealed override bool IsClosed => _fileHandle.IsClosed; - internal sealed override bool IsPipe => _isPipe; + internal sealed override bool IsPipe => _fileHandle.IsPipe; + // Flushing is the responsibility of BufferedFileStreamStrategy internal sealed override SafeFileHandle SafeFileHandle { @@ -226,16 +230,10 @@ public sealed override long Seek(long offset, SeekOrigin origin) internal sealed override void Unlock(long position, long length) => FileStreamHelpers.Unlock(_fileHandle, _path, position, length); - protected abstract void OnInitFromHandle(SafeFileHandle handle); - - protected virtual void OnInit() { } - private void Init(FileMode mode, string originalPath) { FileStreamHelpers.ValidateFileTypeForNonExtendedPaths(_fileHandle, originalPath); - OnInit(); - // For Append mode... if (mode == FileMode.Append) { @@ -247,43 +245,6 @@ private void Init(FileMode mode, string originalPath) } } - private void InitFromHandle(SafeFileHandle handle, FileAccess access, out bool canSeek, out bool isPipe) - { -#if DEBUG - bool hadBinding = handle.ThreadPoolBinding != null; - - try - { -#endif - InitFromHandleImpl(handle, out canSeek, out isPipe); -#if DEBUG - } - catch - { - Debug.Assert(hadBinding || handle.ThreadPoolBinding == null, "We should never error out with a ThreadPoolBinding we've added"); - throw; - } -#endif - } - - private void InitFromHandleImpl(SafeFileHandle handle, out bool canSeek, out bool isPipe) - { - FileStreamHelpers.GetFileTypeSpecificInformation(handle, out canSeek, out isPipe); - - OnInitFromHandle(handle); - - if (_canSeek) - { - // given strategy was created out of existing handle, so we have to perform - // a syscall to get the current handle offset - _filePosition = FileStreamHelpers.Seek(handle, _path, 0, SeekOrigin.Current); - } - else - { - _filePosition = 0; - } - } - public sealed override void SetLength(long value) { if (_appendStart != -1 && value < _appendStart) diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/SafeHandle.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/SafeHandle.cs index 6d1bbf831773a1..227dbf0641f5ee 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/SafeHandle.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/SafeHandle.cs @@ -74,6 +74,8 @@ protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle) } #endif + internal bool OwnsHandle => _ownsHandle; + protected internal void SetHandle(IntPtr handle) => this.handle = handle; public IntPtr DangerousGetHandle() => handle; diff --git a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs index a79210cb523bbd..94b2e6d0b18d7e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs @@ -231,6 +231,12 @@ internal static void ThrowArgumentException_HandleNotSync(string paramName) throw new ArgumentException(SR.Arg_HandleNotSync, paramName); } + [DoesNotReturn] + internal static void ThrowArgumentException_HandleNotAsync(string paramName) + { + throw new ArgumentException(SR.Arg_HandleNotAsync, paramName); + } + [DoesNotReturn] internal static void ThrowArgumentNullException(ExceptionArgument argument) { @@ -396,6 +402,12 @@ internal static void ThrowArgumentException_Argument_InvalidArrayType() throw new ArgumentException(SR.Argument_InvalidArrayType); } + [DoesNotReturn] + internal static void ThrowArgumentException_InvalidHandle(string? paramName) + { + throw new ArgumentException(SR.Arg_InvalidHandle, paramName); + } + [DoesNotReturn] internal static void ThrowInvalidOperationException_InvalidOperation_EnumNotStarted() { @@ -474,6 +486,18 @@ internal static void ThrowArgumentOutOfRangeException_SymbolDoesNotFit() throw new ArgumentOutOfRangeException("symbol", SR.Argument_BadFormatSpecifier); } + [DoesNotReturn] + internal static void ThrowArgumentOutOfRangeException_NeedPosNum(string? paramName) + { + throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_NeedPosNum); + } + + [DoesNotReturn] + internal static void ThrowArgumentOutOfRangeException_NeedNonNegNum(string paramName) + { + throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_NeedNonNegNum); + } + [DoesNotReturn] internal static void ArgumentOutOfRangeException_Enum_Value() { @@ -739,6 +763,8 @@ private static string GetArgumentName(ExceptionArgument argument) return "destinationArray"; case ExceptionArgument.pHandle: return "pHandle"; + case ExceptionArgument.handle: + return "handle"; case ExceptionArgument.other: return "other"; case ExceptionArgument.newSize: @@ -785,6 +811,8 @@ private static string GetArgumentName(ExceptionArgument argument) return "suffix"; case ExceptionArgument.buffer: return "buffer"; + case ExceptionArgument.buffers: + return "buffers"; case ExceptionArgument.offset: return "offset"; case ExceptionArgument.stream: @@ -1028,6 +1056,7 @@ internal enum ExceptionArgument destinationIndex, destinationArray, pHandle, + handle, other, newSize, lowerBounds, @@ -1051,6 +1080,7 @@ internal enum ExceptionArgument prefix, suffix, buffer, + buffers, offset, stream } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 5c77a9476c8bb9..3de9ab495cc1b6 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -21,6 +21,7 @@ public sealed partial class SafeFileHandle : Microsoft.Win32.SafeHandles.SafeHan 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; } } protected override bool ReleaseHandle() { throw null; } } public abstract partial class SafeHandleMinusOneIsInvalid : System.Runtime.InteropServices.SafeHandle @@ -7579,6 +7580,7 @@ public static void Move(string sourceFileName, string destFileName, bool overwri public static System.IO.FileStream Open(string path, System.IO.FileMode mode, System.IO.FileAccess access) { throw null; } public static System.IO.FileStream Open(string path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share) { throw null; } public static System.IO.FileStream Open(string path, System.IO.FileStreamOptions options) { throw null; } + public static Microsoft.Win32.SafeHandles.SafeFileHandle OpenHandle(string path, System.IO.FileMode mode = System.IO.FileMode.Open, System.IO.FileAccess access = System.IO.FileAccess.Read, System.IO.FileShare share = System.IO.FileShare.Read, System.IO.FileOptions options = System.IO.FileOptions.None, long preallocationSize = 0) { throw null; } public static System.IO.FileStream OpenRead(string path) { throw null; } public static System.IO.StreamReader OpenText(string path) { throw null; } public static System.IO.FileStream OpenWrite(string path) { throw null; } @@ -8092,6 +8094,18 @@ public override void Write(System.ReadOnlySpan buffer) { } public override System.Threading.Tasks.ValueTask WriteAsync(System.ReadOnlyMemory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override void WriteByte(byte value) { } } + public static partial class RandomAccess + { + public static long GetLength(Microsoft.Win32.SafeHandles.SafeFileHandle handle) { throw null; } + public static int Read(Microsoft.Win32.SafeHandles.SafeFileHandle handle, System.Span buffer, long fileOffset) { throw null; } + public static long Read(Microsoft.Win32.SafeHandles.SafeFileHandle handle, System.Collections.Generic.IReadOnlyList> buffers, long fileOffset) { throw null; } + public static System.Threading.Tasks.ValueTask ReadAsync(Microsoft.Win32.SafeHandles.SafeFileHandle handle, System.Memory buffer, long fileOffset, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.ValueTask ReadAsync(Microsoft.Win32.SafeHandles.SafeFileHandle handle, System.Collections.Generic.IReadOnlyList> buffers, long fileOffset, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static int Write(Microsoft.Win32.SafeHandles.SafeFileHandle handle, System.ReadOnlySpan buffer, long fileOffset) { throw null; } + public static long Write(Microsoft.Win32.SafeHandles.SafeFileHandle handle, System.Collections.Generic.IReadOnlyList> buffers, long fileOffset) { throw null; } + public static System.Threading.Tasks.ValueTask WriteAsync(Microsoft.Win32.SafeHandles.SafeFileHandle handle, System.ReadOnlyMemory buffer, long fileOffset, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.ValueTask WriteAsync(Microsoft.Win32.SafeHandles.SafeFileHandle handle, System.Collections.Generic.IReadOnlyList> buffers, long fileOffset, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } } namespace System.IO.Enumeration {