From 9d771a26f058a9fa4a49850d4778bbab7aa79a22 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 15 Jun 2021 20:59:58 +0200 Subject: [PATCH] Set of offset-based APIs for thread-safe file IO (#53669) Co-authored-by: Stephen Toub --- .../Interop/Unix/System.Native/Interop.IOVector.cs | 16 + .../Unix/System.Native/Interop.MessageHeader.cs | 6 - .../Interop/Unix/System.Native/Interop.PRead.cs | 13 + .../Interop/Unix/System.Native/Interop.PReadV.cs | 13 + .../Interop/Unix/System.Native/Interop.PWrite.cs | 13 + .../Interop/Unix/System.Native/Interop.PWriteV.cs | 13 + .../Windows/Kernel32/Interop.FileScatterGather.cs | 28 ++ .../Interop/Windows/NtDll/Interop.NtCreateFile.cs | 19 +- .../NtDll/Interop.NtQueryInformationFile.cs | 1 - .../src/Interop/Windows/NtDll/Interop.NtStatus.cs | 2 + src/libraries/Native/Unix/Common/pal_config.h.in | 2 + .../Native/Unix/System.Native/entrypoints.c | 4 + src/libraries/Native/Unix/System.Native/pal_io.c | 105 ++++ src/libraries/Native/Unix/System.Native/pal_io.h | 36 ++ .../Native/Unix/System.Native/pal_networking.c | 5 +- .../Native/Unix/System.Native/pal_networking.h | 9 +- src/libraries/Native/Unix/configure.cmake | 10 + .../System.IO.FileSystem/tests/File/AppendAsync.cs | 2 +- .../System.IO.FileSystem/tests/File/Create.cs | 34 +- .../tests/File/EncryptDecrypt.cs | 2 +- .../System.IO.FileSystem/tests/File/OpenHandle.cs | 70 +++ .../tests/File/ReadWriteAllBytesAsync.cs | 2 +- .../tests/File/ReadWriteAllLinesAsync.cs | 2 +- .../tests/File/ReadWriteAllTextAsync.cs | 4 +- .../tests/FileStream/CopyToAsync.cs | 2 +- .../tests/FileStream/FileStreamConformanceTests.cs | 4 +- .../tests/FileStream/IsAsync.cs | 2 +- .../tests/FileStream/ReadAsync.cs | 4 +- .../tests/FileStream/SafeFileHandle.cs | 2 +- .../tests/FileStream/WriteAsync.cs | 4 +- .../tests/FileStream/ctor_options_as.cs | 2 +- .../tests/FileStream/ctor_sfh_fa.cs | 4 +- .../tests/FileStream/ctor_sfh_fa_buffer.cs | 12 +- .../tests/FileStream/ctor_sfh_fa_buffer_async.cs | 2 +- .../FileStream/ctor_str_fm_fa_fs_buffer_fo.cs | 4 +- .../System.IO.FileSystem.Net5Compat.Tests.csproj | 10 +- .../tests/RandomAccess/Base.cs | 110 ++++ .../tests/RandomAccess/GetLength.cs | 38 ++ .../tests/RandomAccess/NoBuffering.Windows.cs | 153 ++++++ .../tests/RandomAccess/Read.cs | 70 +++ .../tests/RandomAccess/ReadAsync.cs | 89 ++++ .../tests/RandomAccess/ReadScatter.cs | 103 ++++ .../tests/RandomAccess/ReadScatterAsync.cs | 121 +++++ .../RandomAccess/SectorAlignedMemory.Windows.cs | 102 ++++ .../tests/RandomAccess/Write.cs | 63 +++ .../tests/RandomAccess/WriteAsync.cs | 82 +++ .../tests/RandomAccess/WriteGather.cs | 103 ++++ .../tests/RandomAccess/WriteGatherAsync.cs | 121 +++++ .../tests/System.IO.FileSystem.Tests.csproj | 23 +- .../src/System.Net.Sockets.csproj | 2 + .../Win32/SafeHandles/SafeFileHandle.Unix.cs | 203 +++++++- .../SafeFileHandle.ValueTaskSource.Windows.cs} | 59 ++- .../Win32/SafeHandles/SafeFileHandle.Windows.cs | 183 ++++++- .../src/System.Private.CoreLib.Shared.projitems | 26 +- .../System.Private.CoreLib/src/System/IO/File.cs | 51 +- .../src/System/IO/File.netcoreapp.cs | 91 +++- .../src/System/IO/FileStream.cs | 108 +--- .../src/System/IO/RandomAccess.Unix.cs | 153 ++++++ .../src/System/IO/RandomAccess.Windows.cs | 558 +++++++++++++++++++++ .../src/System/IO/RandomAccess.cs | 283 +++++++++++ .../Strategies/AsyncWindowsFileStreamStrategy.cs | 233 +-------- .../System/IO/Strategies/FileStreamHelpers.Unix.cs | 91 +--- .../IO/Strategies/FileStreamHelpers.Windows.cs | 276 +--------- .../src/System/IO/Strategies/FileStreamHelpers.cs | 77 +++ .../Net5CompatFileStreamStrategy.Unix.cs | 133 +---- .../Net5CompatFileStreamStrategy.Windows.cs | 91 +--- .../IO/Strategies/Net5CompatFileStreamStrategy.cs | 4 +- .../IO/Strategies/SyncWindowsFileStreamStrategy.cs | 69 +-- .../IO/Strategies/WindowsFileStreamStrategy.cs | 77 +-- .../System/Runtime/InteropServices/SafeHandle.cs | 2 + .../src/System/ThrowHelper.cs | 30 ++ src/libraries/System.Runtime/ref/System.Runtime.cs | 14 + 72 files changed, 3340 insertions(+), 1115 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.IOVector.cs create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.PRead.cs create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.PReadV.cs create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.PWrite.cs create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.PWriteV.cs create mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FileScatterGather.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/Base.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/GetLength.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/NoBuffering.Windows.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/Read.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadAsync.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadScatter.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/ReadScatterAsync.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/SectorAlignedMemory.Windows.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/Write.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteAsync.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteGather.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/RandomAccess/WriteGatherAsync.cs rename src/libraries/System.Private.CoreLib/src/{System/IO/Strategies/AsyncWindowsFileStreamStrategy.ValueTaskSource.cs => Microsoft/Win32/SafeHandles/SafeFileHandle.ValueTaskSource.Windows.cs} (77%) create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.cs 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 0000000..9cbf1ee --- /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 5e327a6..4b71e0e 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 0000000..664da01 --- /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 0000000..5d93078 --- /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 0000000..721a1c8 --- /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 0000000..c17e996 --- /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 0000000..8e61970 --- /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 9ce82c7..1a89d91 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 partial class Interop } } - 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 @@ internal static partial class Interop 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 @@ internal static partial class Interop { 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 @@ internal static partial class Interop } 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 644315f..402443b 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 d79653a..76715c8 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 partial class Interop 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 b7bd484..034d57b 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 0f7b1a12..8a1438b 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 c63eb88..d7eb6c4 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 e3d402d..1dc7038 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 11124f3..3236fe2 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 bbb0bc0..4bf9de6 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 7c0ed7a..9d40db9 100644 --- a/src/libraries/Native/Unix/configure.cmake +++ b/src/libraries/Native/Unix/configure.cmake @@ -220,6 +220,16 @@ check_symbol_exists( 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 HAVE_IOCTL) diff --git a/src/libraries/System.IO.FileSystem/tests/File/AppendAsync.cs b/src/libraries/System.IO.FileSystem/tests/File/AppendAsync.cs index 04d7ebe..4573662 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 @@ 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_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 a4a8de3..bf0417d 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 @@ namespace System.IO.Tests } } + [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 @@ namespace System.IO.Tests 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 624eb2a..984afef 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 @@ using Xunit; 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 0000000..a283b01 --- /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 fdb0ec6..111ecb5 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 @@ using Xunit; 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 f5cbd0f..76d1541 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 @@ using Xunit; 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 fe67d88..3481bff 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 @@ using Xunit; 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 @@ namespace System.IO.Tests #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 f059c49..3a48354 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 @@ using Xunit; 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 0fbc771..d1fd4fe 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 @@ namespace System.IO.Tests 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 @@ namespace System.IO.Tests #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 b090076..e187468 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 @@ using Xunit; 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 ef4458e..da429bb 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 @@ 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_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 40064cd..0740eed 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 @@ using Xunit; 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 9e4db63..7f7fd8a 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 @@ 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_WriteAsync_AsyncWrites : FileStream_AsyncWrites { protected override Task WriteAsync(FileStream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) => @@ -371,7 +371,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_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 3190e66..f36e55e 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 @@ namespace System.IO.Tests 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 49881c4..5ad232a 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 @@ namespace System.IO.Tests [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 @@ namespace System.IO.Tests [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 745ff57..0c480d4 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 @@ namespace System.IO.Tests 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 f05cbce..b2a8ba1 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 @@ using Xunit; 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 f325fc2..61efd82 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 @@ using Xunit; 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 @@ namespace System.IO.Tests [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 dfabeb0..628bbb4 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 0000000..8d876f3 --- /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 0000000..46e2914 --- /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 0000000..ea99826 --- /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 0000000..81021d0 --- /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 0000000..c18da9e --- /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 0000000..68058b6 --- /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 0000000..68d631a --- /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 0000000..593cc6e --- /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 0000000..abcac00 --- /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 0000000..074f5ba --- /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 0000000..a9483d6 --- /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 0000000..f8369eb --- /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 0bc213c..a271109 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 9f56ca5..35d10bb 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 @@ namespace Microsoft.Win32.SafeHandles 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 @@ namespace Microsoft.Win32.SafeHandles 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 f1153a5..69371a4 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 @@ namespace System.IO.Strategies /// 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 @@ namespace System.IO.Strategies _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 @@ namespace System.IO.Strategies finally { // The instance is ready to be reused - _strategy.TryToReuse(this); + _fileHandle.TryToReuse(this); } } @@ -79,11 +113,11 @@ namespace System.IO.Strategies _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 @@ namespace System.IO.Strategies // Free the overlapped. if (_overlapped != null) { - _strategy._fileHandle.ThreadPoolBinding!.FreeNativeOverlapped(_overlapped); + _fileHandle.ThreadPoolBinding!.FreeNativeOverlapped(_overlapped); _overlapped = null; } } @@ -161,6 +195,7 @@ namespace System.IO.Strategies 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 dfcce3c..36710dc 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 c37a7824..a189be9 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 c387630..626dc76 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; using System.Threading.Tasks; #if MS_IO_REDIST +using System; +using System.IO; + namespace Microsoft.IO #else namespace System.IO @@ -363,52 +364,6 @@ namespace System.IO } } -#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 1996833..b5502ff 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 f892c57..9653bd0 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 @@ namespace System.IO 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 @@ namespace System.IO } } - 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 @@ namespace System.IO { 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 @@ namespace System.IO } 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 @@ namespace System.IO 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 @@ namespace System.IO 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 @@ namespace System.IO { 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 @@ namespace System.IO { 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 0000000..22a3934 --- /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 0000000..5d198cc --- /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 0000000..199d0d4 --- /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 6719dd9..a194c48 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies _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 1fd6456..868ccb8 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 @@ namespace System.IO.Strategies 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 f9f22b4..3927572 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.Buffers; 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies // 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 @@ namespace System.IO.Strategies } } - 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 @@ namespace System.IO.Strategies } } - 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 @@ namespace System.IO.Strategies } // 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 9fc329e..965e9c0 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 @@ namespace System.IO.Strategies => 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 f039c02..5ee0478 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 @@ namespace System.IO.Strategies /// 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 @@ namespace System.IO.Strategies // 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies /// 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 ceb9bcc..ec74ccf 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies } _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 @@ namespace System.IO.Strategies } _preallocatedOverlapped?.Dispose(); - _canSeek = false; GC.SuppressFinalize(this); // the handle is closed; nothing further for the finalizer to do } } @@ -380,7 +317,7 @@ namespace System.IO.Strategies // 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 @@ namespace System.IO.Strategies // 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies } } - 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 @@ namespace System.IO.Strategies { 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 @@ namespace System.IO.Strategies { 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 d257df9..9a88f5a 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 @@ namespace System.IO.Strategies 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 30499b6..e3f111e 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies 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 a5ebdd6..dee1c5a 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies _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 @@ namespace System.IO.Strategies } } - 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 @@ namespace System.IO.Strategies { 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies 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 @@ namespace System.IO.Strategies } } - 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 6d1bbf8..227dbf0 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 @@ namespace System.Runtime.InteropServices } #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 a79210c..94b2e6d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs @@ -232,6 +232,12 @@ namespace System } [DoesNotReturn] + internal static void ThrowArgumentException_HandleNotAsync(string paramName) + { + throw new ArgumentException(SR.Arg_HandleNotAsync, paramName); + } + + [DoesNotReturn] internal static void ThrowArgumentNullException(ExceptionArgument argument) { throw new ArgumentNullException(GetArgumentName(argument)); @@ -397,6 +403,12 @@ namespace System } [DoesNotReturn] + internal static void ThrowArgumentException_InvalidHandle(string? paramName) + { + throw new ArgumentException(SR.Arg_InvalidHandle, paramName); + } + + [DoesNotReturn] internal static void ThrowInvalidOperationException_InvalidOperation_EnumNotStarted() { throw new InvalidOperationException(SR.InvalidOperation_EnumNotStarted); @@ -475,6 +487,18 @@ namespace System } [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() { throw new ArgumentOutOfRangeException("value", SR.ArgumentOutOfRange_Enum); @@ -739,6 +763,8 @@ namespace System return "destinationArray"; case ExceptionArgument.pHandle: return "pHandle"; + case ExceptionArgument.handle: + return "handle"; case ExceptionArgument.other: return "other"; case ExceptionArgument.newSize: @@ -785,6 +811,8 @@ namespace System return "suffix"; case ExceptionArgument.buffer: return "buffer"; + case ExceptionArgument.buffers: + return "buffers"; case ExceptionArgument.offset: return "offset"; case ExceptionArgument.stream: @@ -1028,6 +1056,7 @@ namespace System destinationIndex, destinationArray, pHandle, + handle, other, newSize, lowerBounds, @@ -1051,6 +1080,7 @@ namespace System 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 da83529..a251145 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -21,6 +21,7 @@ namespace Microsoft.Win32.SafeHandles 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 @@ namespace System.IO 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 @@ namespace System.IO 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 { -- 2.7.4