From fe24ee2f3de431ea20a06cedb296ffd025fbe02f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 12 Apr 2021 13:22:32 +0200 Subject: [PATCH 01/37] add the new ctor to the ref assembly --- src/libraries/System.Runtime/ref/System.Runtime.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 6292cc6b7e7db0..b51595cca8d842 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -7163,6 +7163,7 @@ public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess acc public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share, int bufferSize) { } public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share, int bufferSize, bool useAsync) { } public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share, int bufferSize, System.IO.FileOptions options) { } + public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share = System.IO.FileShare.Read, int bufferSize = 4096, System.IO.FileOptions options = System.IO.FileOptions.None, long allocationSize = 0) { } public override bool CanRead { get { throw null; } } public override bool CanSeek { get { throw null; } } public override bool CanWrite { get { throw null; } } From c127580fdf2a353b54793eb3434e0995d8c7ef01 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 12 Apr 2021 13:22:56 +0200 Subject: [PATCH 02/37] add tests --- .../FileStream/FileStreamConformanceTests.cs | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs index 82b10a882c3d38..680859d44480ee 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs @@ -14,6 +14,7 @@ public abstract class FileStreamStandaloneConformanceTests : StandaloneStreamCon { protected abstract FileOptions Options { get; } protected abstract int BufferSize { get; } + protected abstract long AllocationSize { get; } private Task CreateStream(byte[] initialData, FileAccess access) { @@ -23,7 +24,7 @@ private Task CreateStream(byte[] initialData, FileAccess access) File.WriteAllBytes(path, initialData); } - return Task.FromResult(new FileStream(path, FileMode.OpenOrCreate, access, FileShare.None, BufferSize, Options)); + return Task.FromResult(new FileStream(path, FileMode.OpenOrCreate, access, FileShare.None, BufferSize, Options, AllocationSize)); } protected override Task CreateReadOnlyStreamCore(byte[] initialData) => CreateStream(initialData, FileAccess.Read); @@ -161,18 +162,48 @@ public async Task LengthIsNotCachedAfterHandleHasBeenExposed(FileAccess fileAcce Assert.Equal(2, stream.Length); Assert.Equal(2, createdFromHandle.Length); } + + [Fact] + public void WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllTheDetails() + { + const long tooMuch = 1024L * 1024L * 1024L * 1024L; // 1 TB + + string filePath = GetTestFilePath(); + IOException ex = Assert.Throws(() => new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, BufferSize, Options, tooMuch)); + Assert.Contains("disk was full", ex.Message); + Assert.Contains(filePath, ex.Message); + Assert.Contains(AllocationSize.ToString(), ex.Message); + } } public class UnbufferedSyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests { protected override FileOptions Options => FileOptions.None; protected override int BufferSize => 1; + protected override long AllocationSize => 0; + } + + public class UnbufferedPreallocatedSyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests + { + protected override FileOptions Options => FileOptions.None; + protected override int BufferSize => 1; + + // any AllocationSize > 0 executes the code path where we try to pre-allocate the disk space + protected override long AllocationSize => 1; } public class BufferedSyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests { protected override FileOptions Options => FileOptions.None; protected override int BufferSize => 10; + protected override long AllocationSize => 0; + } + + public class BufferedPreallocatedSyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests + { + protected override FileOptions Options => FileOptions.None; + protected override int BufferSize => 10; + protected override long AllocationSize => 1; } [ActiveIssue("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/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] @@ -181,6 +212,16 @@ public class UnbufferedAsyncFileStreamStandaloneConformanceTests : FileStreamSta { protected override FileOptions Options => FileOptions.Asynchronous; protected override int BufferSize => 1; + protected override long AllocationSize => 0; + } + + [ActiveIssue("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/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [PlatformSpecific(~TestPlatforms.Browser)] // copied from base class due to https://github.com/xunit/xunit/issues/2186 + public class UnbufferedPreallocatedAsyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests + { + protected override FileOptions Options => FileOptions.Asynchronous; + protected override int BufferSize => 1; + protected override long AllocationSize => 1; } [ActiveIssue("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/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] @@ -189,6 +230,16 @@ public class BufferedAsyncFileStreamStandaloneConformanceTests : FileStreamStand { protected override FileOptions Options => FileOptions.Asynchronous; protected override int BufferSize => 10; + protected override long AllocationSize => 0; + } + + [ActiveIssue("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/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [PlatformSpecific(~TestPlatforms.Browser)] // copied from base class due to https://github.com/xunit/xunit/issues/2186 + public class BufferedPreallocatedAsyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests + { + protected override FileOptions Options => FileOptions.Asynchronous; + protected override int BufferSize => 10; + protected override long AllocationSize => 1; } public class AnonymousPipeFileStreamConnectedConformanceTests : ConnectedStreamConformanceTests From f3af047e5109e04afe5d80ff04b79300a5a204c2 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 12 Apr 2021 13:41:22 +0200 Subject: [PATCH 03/37] add new ctor --- .../src/System/IO/FileStream.cs | 7 ++++++- .../Strategies/AsyncWindowsFileStreamStrategy.cs | 4 ++-- .../IO/Strategies/FileStreamHelpers.Unix.cs | 2 +- .../IO/Strategies/FileStreamHelpers.Windows.cs | 16 ++++++++-------- .../System/IO/Strategies/FileStreamHelpers.cs | 4 ++-- .../Strategies/Net5CompatFileStreamStrategy.cs | 4 ++-- .../Strategies/SyncWindowsFileStreamStrategy.cs | 4 ++-- .../IO/Strategies/WindowsFileStreamStrategy.cs | 4 ++-- 8 files changed, 25 insertions(+), 20 deletions(-) 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 eb9d98750f758d..614775610412a5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -130,6 +130,11 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share } public FileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + : this(path, mode, access, share, bufferSize, options, 0) + { + } + + public FileStream(string path, FileMode mode, FileAccess access, FileShare share = DefaultShare, int bufferSize = DefaultBufferSize, FileOptions options = FileOptions.None, long allocationSize = 0) { if (path == null) { @@ -191,7 +196,7 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share SerializationInfo.ThrowIfDeserializationInProgress("AllowFileWrites", ref s_cachedSerializationSwitch); } - _strategy = FileStreamHelpers.ChooseStrategy(this, path, mode, access, share, bufferSize, options); + _strategy = FileStreamHelpers.ChooseStrategy(this, path, mode, access, share, bufferSize, options, allocationSize); } [Obsolete("This property has been deprecated. Please use FileStream's SafeFileHandle property instead. https://go.microsoft.com/fwlink/?linkid=14202")] 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 b7f24351a0bda4..e7925787bd8270 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 @@ -18,8 +18,8 @@ internal AsyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access { } - internal AsyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) - : base(path, mode, access, share, options) + internal AsyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) + : base(path, mode, access, share, options, allocationSize) { } 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 0d2c5df52be3aa..64522ea320fee6 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 @@ -20,7 +20,7 @@ private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, File private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) => new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize, options); - internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) { // Translate the arguments into arguments for an open call. Interop.Sys.OpenFlags openFlags = PreOpenConfigurationFromOptions(mode, access, share, options); 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 fd3218f6eb4c3e..4e5543d7af9e3f 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 @@ -18,6 +18,7 @@ internal static partial class FileStreamHelpers internal const int ERROR_NO_DATA = 232; private const int ERROR_HANDLE_EOF = 38; private const int ERROR_IO_PENDING = 997; + private const uint ERROR_STATUS_DISK_FULL = 0xC000007F; private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) { @@ -33,28 +34,27 @@ private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, File return EnableBufferingIfNeeded(strategy, bufferSize); } - private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long allocationSize) { if (UseNet5CompatStrategy) { - return new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize, options); + return new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize, options, allocationSize); } WindowsFileStreamStrategy strategy = (options & FileOptions.Asynchronous) != 0 - ? new AsyncWindowsFileStreamStrategy(path, mode, access, share, options) - : new SyncWindowsFileStreamStrategy(path, mode, access, share, options); + ? new AsyncWindowsFileStreamStrategy(path, mode, access, share, options, allocationSize) + : new SyncWindowsFileStreamStrategy(path, mode, access, share, options, allocationSize); return EnableBufferingIfNeeded(strategy, bufferSize); } - // TODO: we might want to consider strategy.IsPipe here and never enable buffering for async pipes internal static FileStreamStrategy EnableBufferingIfNeeded(WindowsFileStreamStrategy strategy, int bufferSize) => bufferSize == 1 ? strategy : new BufferedFileStreamStrategy(strategy, bufferSize); - internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) - => CreateFileOpenHandle(path, mode, access, share, options); + internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) + => CreateFileOpenHandle(path, mode, access, share, options, allocationSize); - private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) { Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = GetSecAttrs(share); 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 1ca2e563998caa..65e8c35929527f 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 @@ -12,8 +12,8 @@ internal static partial class FileStreamHelpers internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) => WrapIfDerivedType(fileStream, ChooseStrategyCore(handle, access, share, bufferSize, isAsync)); - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - => WrapIfDerivedType(fileStream, ChooseStrategyCore(path, mode, access, share, bufferSize, options)); + internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long allocationSize) + => WrapIfDerivedType(fileStream, ChooseStrategyCore(path, mode, access, share, bufferSize, options, allocationSize)); private static FileStreamStrategy WrapIfDerivedType(FileStream fileStream, FileStreamStrategy strategy) => fileStream.GetType() == typeof(FileStream) 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 03ca3533e717b7..58105ad890f294 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 @@ -78,7 +78,7 @@ internal Net5CompatFileStreamStrategy(SafeFileHandle handle, FileAccess access, _fileHandle = handle; } - internal Net5CompatFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + internal Net5CompatFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long allocationSize) { string fullPath = Path.GetFullPath(path); @@ -89,7 +89,7 @@ internal Net5CompatFileStreamStrategy(string path, FileMode mode, FileAccess acc if ((options & FileOptions.Asynchronous) != 0) _useAsyncIO = true; - _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options); + _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options, allocationSize); try { 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 3639b4b5fb4daf..6b6546d91aed75 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 @@ -15,8 +15,8 @@ internal SyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access, { } - internal SyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) - : base(path, mode, access, share, options) + internal SyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) + : base(path, mode, access, share, options, allocationSize) { } 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 d42494c19086d2..0d77839e8ca13d 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 @@ -44,7 +44,7 @@ internal WindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access, Fil _fileHandle = handle; } - internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) { string fullPath = Path.GetFullPath(path); @@ -52,7 +52,7 @@ internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access _access = access; _share = share; - _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options); + _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options, allocationSize); try { From 5d004fd23893ae1692fef6b13b3d3ac07a48f852 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 12 Apr 2021 14:12:01 +0200 Subject: [PATCH 04/37] initial Windows implementation --- .../Windows/Kernel32/Interop.CreateFile.cs | 173 ++++++++++++++++++ .../Kernel32/Interop.IO_STATUS_BLOCK.cs | 18 ++ .../Kernel32/Interop.OBJECT_ATTRIBUTES.cs | 33 ++++ .../Kernel32/Interop.UNICODE_STRING.cs | 27 +++ .../src/Resources/Strings.resx | 3 + .../System.Private.CoreLib.Shared.projitems | 9 + .../src/System/IO/FileStream.cs | 30 +++ .../Strategies/FileStreamHelpers.Windows.cs | 62 ++++--- 8 files changed, 332 insertions(+), 23 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.IO_STATUS_BLOCK.cs create mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.OBJECT_ATTRIBUTES.cs create mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.UNICODE_STRING.cs diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateFile.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateFile.cs index ce5bfd9fd5321f..6e88ab929e1108 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateFile.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateFile.cs @@ -3,6 +3,7 @@ using Microsoft.Win32.SafeHandles; using System; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; @@ -23,6 +24,22 @@ private static extern unsafe SafeFileHandle CreateFilePrivate( int dwFlagsAndAttributes, IntPtr hTemplateFile); + // https://docs.microsoft.com/en-us/windows/desktop/api/winternl/nf-winternl-ntcreatefile + // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntcreatefile + [DllImport(Libraries.NtDll, CharSet = CharSet.Unicode, ExactSpelling = true)] + private static unsafe extern uint NtCreateFile( + out IntPtr fileHandle, + int desiredAccess, + ref OBJECT_ATTRIBUTES objectAttributes, + out IO_STATUS_BLOCK ioStatusBlock, + long* allocationSize, + uint fileAttributes, + uint shareAccess, + uint createDisposition, + uint createOptions, + void* extendedAttributesBuffer, + uint extendedAttributesLength); + internal static unsafe SafeFileHandle CreateFile( string lpFileName, int dwDesiredAccess, @@ -46,5 +63,161 @@ internal static unsafe SafeFileHandle CreateFile( lpFileName = PathInternal.EnsureExtendedPrefixIfNeeded(lpFileName); return CreateFilePrivate(lpFileName, dwDesiredAccess, dwShareMode, null, dwCreationDisposition, dwFlagsAndAttributes, IntPtr.Zero); } + + internal static unsafe uint NtCreateFile(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize, out IntPtr fileHandle) + { + string prefixedAbsolutePath = PathInternal.IsExtended(path) + ? path + : @"\??\" + Path.GetFullPath(path); // TODO: we might consider getting rid of this managed allocation + + fixed (char* filePath = prefixedAbsolutePath) + { + UNICODE_STRING unicodeString = new UNICODE_STRING(filePath, prefixedAbsolutePath.Length); + OBJECT_ATTRIBUTES objectAttributes = new OBJECT_ATTRIBUTES(&unicodeString, GetObjectAttributes(share)); + + return NtCreateFile( + fileHandle: out fileHandle, + desiredAccess: GetDesiredAccess(access, mode, options), + objectAttributes: ref objectAttributes, + ioStatusBlock: out _, + allocationSize: &allocationSize, + fileAttributes: GetFileAttributes(options), + shareAccess: GetShareAccess(share), + createDisposition: GetCreateDisposition(mode), + createOptions: GetCreateOptions(options), + extendedAttributesBuffer: default, + extendedAttributesLength: default); + } + } + + private static uint GetObjectAttributes(FileShare share) + { + uint result = 0;// 0x00000040; // Lookups for this object should be case insensitive. [OBJ_CASE_INSENSITIVE] + + if ((share & FileShare.Inheritable) != 0 ) + { + result |= 0x00000002; + } + + return result; + } + + private static int GetDesiredAccess(FileAccess access, FileMode fileMode, FileOptions options) + { + int result = 0; + + if ((access & FileAccess.Read) != 0) + { + result |= GenericOperations.GENERIC_READ; + } + if ((access & FileAccess.Write) != 0) + { + result |= GenericOperations.GENERIC_WRITE; + } + if (fileMode == FileMode.Append) + { + result |= 0x0004; // FILE_APPEND_DATA + } + if ((options & FileOptions.Asynchronous) == 0) + { + result |= 0x00100000; // SYNCHRONIZE, requried by FILE_SYNCHRONOUS_IO_NONALERT + } + + return result; + } + + private static uint GetFileAttributes(FileOptions options) + { + uint result = 0; + + if ((options & FileOptions.Encrypted) != 0) + { + result |= 0x00004000; // FILE_ATTRIBUTE_ENCRYPTED + } + + return result; + } + + // FileShare.Inheritable is handled in GetObjectAttributes + private static uint GetShareAccess(FileShare share) + { + uint result = 0; + + if ((share & FileShare.Read) != 0) + { + result |= 1; // FILE_SHARE_READ + } + if ((share & FileShare.Write) != 0) + { + result |= 2; // FILE_SHARE_WRITE + } + if ((share & FileShare.Delete) != 0) + { + result |= 4; // FILE_SHARE_DELETE + } + + // https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile + // "If the original caller of NtCreateFile does not specify FILE_SHARE_READ, FILE_SHARE_WRITE, or FILE_SHARE_DELETE, + // no other open operations can be performed on the file; that is, the original caller is given exclusive access to the file." + // which is how we get FileShare.None working + + return result; + } + + private static uint GetCreateDisposition(FileMode mode) + { + switch (mode) + { + case FileMode.CreateNew: + return 2; // FILE_CREATE + case FileMode.Create: + return 0; // FILE_SUPERSEDE + case FileMode.OpenOrCreate: + case FileMode.Append: // has extra handling in GetDesiredAccess + return 3; // FILE_OPEN_IF + case FileMode.Truncate: + return 4; // FILE_OVERWRITE + default: + Debug.Assert(mode == FileMode.Open); // the enum value is validated in FileStream ctor + return 1; // FILE_OPEN + } + } + + // FileOptions.Encryptend is handled in GetFileAttributes + private static uint GetCreateOptions(FileOptions options) + { + // Every directory is just a directory FILE. + // FileStream does not allow for opening directories on purpose. + // FILE_NON_DIRECTORY_FILE is used to ensure that + uint result = 0x00000040; // FILE_NON_DIRECTORY_FILE + + if ((options & FileOptions.WriteThrough) != 0) + { + result |= 0x00000002; // FILE_WRITE_THROUGH + } + if ((options & FileOptions.RandomAccess) != 0) + { + result |= 0x00000800; // FILE_RANDOM_ACCESS + } + if ((options & FileOptions.SequentialScan) != 0) + { + result |= 0x00000004; // FILE_SEQUENTIAL_ONLY + } + if ((options & FileOptions.DeleteOnClose) != 0) + { + result |= 0x00001000; // FILE_DELETE_ON_CLOSE + } + if ((options & FileOptions.Asynchronous) == 0) + { + // it's async by default, so we need to disable it when async was not requested + result |= 0x00000020; // FILE_SYNCHRONOUS_IO_NONALERT, has extra handling in GetDesiredAccess + } + if (((int)options & 0x20000000) != 0) // NoBuffering + { + result |= 0x00000008; // FILE_NO_INTERMEDIATE_BUFFERING + } + + return result; + } } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.IO_STATUS_BLOCK.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.IO_STATUS_BLOCK.cs new file mode 100644 index 00000000000000..129395f0de84c6 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.IO_STATUS_BLOCK.cs @@ -0,0 +1,18 @@ +// 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; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + [StructLayout(LayoutKind.Sequential)] + internal struct IO_STATUS_BLOCK + { + internal uint Status; + internal IntPtr Information; + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.OBJECT_ATTRIBUTES.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.OBJECT_ATTRIBUTES.cs new file mode 100644 index 00000000000000..535976bac5ab17 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.OBJECT_ATTRIBUTES.cs @@ -0,0 +1,33 @@ +// 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; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + // https://msdn.microsoft.com/en-us/library/windows/hardware/ff557749.aspx + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct OBJECT_ATTRIBUTES + { + internal uint Length; + internal IntPtr RootDirectory; + internal UNICODE_STRING* ObjectName; + internal uint Attributes; + internal IntPtr SecurityDescriptor; + internal IntPtr SecurityQualityOfService; + + internal unsafe OBJECT_ATTRIBUTES(UNICODE_STRING* objectName, uint attributes) + { + Length = (uint)sizeof(OBJECT_ATTRIBUTES); + RootDirectory = IntPtr.Zero; + ObjectName = objectName; + Attributes = attributes; + SecurityDescriptor = IntPtr.Zero; + SecurityQualityOfService = IntPtr.Zero; + } + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.UNICODE_STRING.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.UNICODE_STRING.cs new file mode 100644 index 00000000000000..9c56fcb77adff1 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.UNICODE_STRING.cs @@ -0,0 +1,27 @@ +// 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 Kernel32 + { + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa380518.aspx + // https://msdn.microsoft.com/en-us/library/windows/hardware/ff564879.aspx + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal unsafe struct UNICODE_STRING + { + internal ushort Length; + internal ushort MaximumLength; + internal char* Buffer; + + internal unsafe UNICODE_STRING(char* buffer, int lengthInChars) + { + Length = checked((ushort)(lengthInChars * sizeof(char))); + MaximumLength = Length; + Buffer = buffer; + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 807cca91a37ae5..f3d66f94d4cfbe 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -2614,6 +2614,9 @@ Cannot create '{0}' because a file or directory with the same name already exists. + + Failed to create '{0}' with allocation size '{1}' because the disk was full. + BindHandle for ThreadPool failed on this handle. 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 9256e41f8a6332..69eb92ba634e44 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 @@ -1509,6 +1509,15 @@ Common\Interop\Windows\Kernel32\Interop.SECURITY_ATTRIBUTES.cs + + Common\Interop\Windows\Kernel32\Interop.UNICODE_STRING.cs + + + Common\Interop\Windows\Kernel32\Interop.OBJECT_ATTRIBUTES.cs + + + Common\Interop\Windows\Kernel32\Interop.IO_STATUS_BLOCK.cs + Common\Interop\Windows\Kernel32\Interop.SecurityOptions.cs 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 614775610412a5..d1eaf1f11f3cbb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -134,6 +134,36 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share { } + /// + /// 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. + /// + /// A relative or absolute path for the file that the current object will encapsulate. + /// One of the enumeration values that determines how to open or create the file. + /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. This also determines the values returned by the and properties of the object. is if specifies a disk file. + /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. The default value is Read. + /// A positive value greater than 0 indicating the buffer size. The default buffer size is 4096. + /// A bitwise combination of the enumeration values that specifies additional file options. The default value is None which means synchronous IO. + /// The initial allocation size in bytes for the file. A nonzero value has no effect unless the file is being created, overwritten, or superseded. + /// 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 zero. + /// -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 stream has been closed. + /// -or- + /// The disk was full. + /// 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 FileStream(string path, FileMode mode, FileAccess access, FileShare share = DefaultShare, int bufferSize = DefaultBufferSize, FileOptions options = FileOptions.None, long allocationSize = 0) { if (path == null) 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 4e5543d7af9e3f..d207a577255c6e 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 @@ -56,35 +56,51 @@ internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) { - Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = GetSecAttrs(share); + using (DisableMediaInsertionPrompt.Create()) + { + Debug.Assert(path != null); - int fAccess = - ((access & FileAccess.Read) == FileAccess.Read ? Interop.Kernel32.GenericOperations.GENERIC_READ : 0) | - ((access & FileAccess.Write) == FileAccess.Write ? Interop.Kernel32.GenericOperations.GENERIC_WRITE : 0); + if (allocationSize > 0) + { + uint ntCreateFileResult = Interop.Kernel32.NtCreateFile(path, mode, access, share, options, allocationSize, out IntPtr fileHandle); + if (ntCreateFileResult == 0) + { + return ValidateFileHandle(new SafeFileHandle(fileHandle, ownsHandle: true), path, (options & FileOptions.Asynchronous) != 0); + } + else if (ntCreateFileResult == ERROR_STATUS_DISK_FULL) + { + throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, path, allocationSize)); + } - // 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; + // NtCreateFile has failed for some other reason than a full disk. + // Instead of implementing the mapping for every NS Status value (there are plenty of them: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55) + // increasing both the code complexity and the chance for breaking backward compatibility (by throwing a different exception than CreateFileW) + // the code falls back to CreateFileW that just throws the right exception. + } - // Must use a valid Win32 constant here... - if (mode == FileMode.Append) - mode = FileMode.OpenOrCreate; + Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = GetSecAttrs(share); - int flagsAndAttributes = (int)options; + int fAccess = + ((access & FileAccess.Read) == FileAccess.Read ? Interop.Kernel32.GenericOperations.GENERIC_READ : 0) | + ((access & FileAccess.Write) == FileAccess.Write ? Interop.Kernel32.GenericOperations.GENERIC_WRITE : 0); - // 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); + // 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; - using (DisableMediaInsertionPrompt.Create()) - { - Debug.Assert(path != null); - return ValidateFileHandle( - Interop.Kernel32.CreateFile(path, fAccess, share, &secAttrs, mode, flagsAndAttributes, IntPtr.Zero), - path, - (options & FileOptions.Asynchronous) != 0); + // 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); + + return ValidateFileHandle(Interop.Kernel32.CreateFile(path, fAccess, share, &secAttrs, mode, flagsAndAttributes, IntPtr.Zero), path, (options & FileOptions.Asynchronous) != 0); } } From bbac6d76a2c54fd8cda40a017ad7afad521b6fd6 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Mon, 12 Apr 2021 15:07:34 +0200 Subject: [PATCH 05/37] fix the build --- .../Windows/Kernel32/Interop.CreateFile.cs | 173 ---------------- .../Windows/Kernel32/Interop.NtCreateFile.cs | 186 ++++++++++++++++++ .../System.Private.CoreLib.Shared.projitems | 3 + .../IO/Strategies/FileStreamHelpers.Unix.cs | 4 +- 4 files changed, 191 insertions(+), 175 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.NtCreateFile.cs diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateFile.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateFile.cs index 6e88ab929e1108..ce5bfd9fd5321f 100644 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateFile.cs +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateFile.cs @@ -3,7 +3,6 @@ using Microsoft.Win32.SafeHandles; using System; -using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; @@ -24,22 +23,6 @@ private static extern unsafe SafeFileHandle CreateFilePrivate( int dwFlagsAndAttributes, IntPtr hTemplateFile); - // https://docs.microsoft.com/en-us/windows/desktop/api/winternl/nf-winternl-ntcreatefile - // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntcreatefile - [DllImport(Libraries.NtDll, CharSet = CharSet.Unicode, ExactSpelling = true)] - private static unsafe extern uint NtCreateFile( - out IntPtr fileHandle, - int desiredAccess, - ref OBJECT_ATTRIBUTES objectAttributes, - out IO_STATUS_BLOCK ioStatusBlock, - long* allocationSize, - uint fileAttributes, - uint shareAccess, - uint createDisposition, - uint createOptions, - void* extendedAttributesBuffer, - uint extendedAttributesLength); - internal static unsafe SafeFileHandle CreateFile( string lpFileName, int dwDesiredAccess, @@ -63,161 +46,5 @@ internal static unsafe SafeFileHandle CreateFile( lpFileName = PathInternal.EnsureExtendedPrefixIfNeeded(lpFileName); return CreateFilePrivate(lpFileName, dwDesiredAccess, dwShareMode, null, dwCreationDisposition, dwFlagsAndAttributes, IntPtr.Zero); } - - internal static unsafe uint NtCreateFile(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize, out IntPtr fileHandle) - { - string prefixedAbsolutePath = PathInternal.IsExtended(path) - ? path - : @"\??\" + Path.GetFullPath(path); // TODO: we might consider getting rid of this managed allocation - - fixed (char* filePath = prefixedAbsolutePath) - { - UNICODE_STRING unicodeString = new UNICODE_STRING(filePath, prefixedAbsolutePath.Length); - OBJECT_ATTRIBUTES objectAttributes = new OBJECT_ATTRIBUTES(&unicodeString, GetObjectAttributes(share)); - - return NtCreateFile( - fileHandle: out fileHandle, - desiredAccess: GetDesiredAccess(access, mode, options), - objectAttributes: ref objectAttributes, - ioStatusBlock: out _, - allocationSize: &allocationSize, - fileAttributes: GetFileAttributes(options), - shareAccess: GetShareAccess(share), - createDisposition: GetCreateDisposition(mode), - createOptions: GetCreateOptions(options), - extendedAttributesBuffer: default, - extendedAttributesLength: default); - } - } - - private static uint GetObjectAttributes(FileShare share) - { - uint result = 0;// 0x00000040; // Lookups for this object should be case insensitive. [OBJ_CASE_INSENSITIVE] - - if ((share & FileShare.Inheritable) != 0 ) - { - result |= 0x00000002; - } - - return result; - } - - private static int GetDesiredAccess(FileAccess access, FileMode fileMode, FileOptions options) - { - int result = 0; - - if ((access & FileAccess.Read) != 0) - { - result |= GenericOperations.GENERIC_READ; - } - if ((access & FileAccess.Write) != 0) - { - result |= GenericOperations.GENERIC_WRITE; - } - if (fileMode == FileMode.Append) - { - result |= 0x0004; // FILE_APPEND_DATA - } - if ((options & FileOptions.Asynchronous) == 0) - { - result |= 0x00100000; // SYNCHRONIZE, requried by FILE_SYNCHRONOUS_IO_NONALERT - } - - return result; - } - - private static uint GetFileAttributes(FileOptions options) - { - uint result = 0; - - if ((options & FileOptions.Encrypted) != 0) - { - result |= 0x00004000; // FILE_ATTRIBUTE_ENCRYPTED - } - - return result; - } - - // FileShare.Inheritable is handled in GetObjectAttributes - private static uint GetShareAccess(FileShare share) - { - uint result = 0; - - if ((share & FileShare.Read) != 0) - { - result |= 1; // FILE_SHARE_READ - } - if ((share & FileShare.Write) != 0) - { - result |= 2; // FILE_SHARE_WRITE - } - if ((share & FileShare.Delete) != 0) - { - result |= 4; // FILE_SHARE_DELETE - } - - // https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile - // "If the original caller of NtCreateFile does not specify FILE_SHARE_READ, FILE_SHARE_WRITE, or FILE_SHARE_DELETE, - // no other open operations can be performed on the file; that is, the original caller is given exclusive access to the file." - // which is how we get FileShare.None working - - return result; - } - - private static uint GetCreateDisposition(FileMode mode) - { - switch (mode) - { - case FileMode.CreateNew: - return 2; // FILE_CREATE - case FileMode.Create: - return 0; // FILE_SUPERSEDE - case FileMode.OpenOrCreate: - case FileMode.Append: // has extra handling in GetDesiredAccess - return 3; // FILE_OPEN_IF - case FileMode.Truncate: - return 4; // FILE_OVERWRITE - default: - Debug.Assert(mode == FileMode.Open); // the enum value is validated in FileStream ctor - return 1; // FILE_OPEN - } - } - - // FileOptions.Encryptend is handled in GetFileAttributes - private static uint GetCreateOptions(FileOptions options) - { - // Every directory is just a directory FILE. - // FileStream does not allow for opening directories on purpose. - // FILE_NON_DIRECTORY_FILE is used to ensure that - uint result = 0x00000040; // FILE_NON_DIRECTORY_FILE - - if ((options & FileOptions.WriteThrough) != 0) - { - result |= 0x00000002; // FILE_WRITE_THROUGH - } - if ((options & FileOptions.RandomAccess) != 0) - { - result |= 0x00000800; // FILE_RANDOM_ACCESS - } - if ((options & FileOptions.SequentialScan) != 0) - { - result |= 0x00000004; // FILE_SEQUENTIAL_ONLY - } - if ((options & FileOptions.DeleteOnClose) != 0) - { - result |= 0x00001000; // FILE_DELETE_ON_CLOSE - } - if ((options & FileOptions.Asynchronous) == 0) - { - // it's async by default, so we need to disable it when async was not requested - result |= 0x00000020; // FILE_SYNCHRONOUS_IO_NONALERT, has extra handling in GetDesiredAccess - } - if (((int)options & 0x20000000) != 0) // NoBuffering - { - result |= 0x00000008; // FILE_NO_INTERMEDIATE_BUFFERING - } - - return result; - } } } diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.NtCreateFile.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.NtCreateFile.cs new file mode 100644 index 00000000000000..7d0417492651be --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.NtCreateFile.cs @@ -0,0 +1,186 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32.SafeHandles; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Kernel32 + { + // https://docs.microsoft.com/en-us/windows/desktop/api/winternl/nf-winternl-ntcreatefile + // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntcreatefile + [DllImport(Libraries.NtDll, CharSet = CharSet.Unicode, ExactSpelling = true)] + private static unsafe extern uint NtCreateFile( + out IntPtr fileHandle, + int desiredAccess, + ref OBJECT_ATTRIBUTES objectAttributes, + out IO_STATUS_BLOCK ioStatusBlock, + long* allocationSize, + uint fileAttributes, + uint shareAccess, + uint createDisposition, + uint createOptions, + void* extendedAttributesBuffer, + uint extendedAttributesLength); + + internal static unsafe uint NtCreateFile(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize, out IntPtr fileHandle) + { + string prefixedAbsolutePath = PathInternal.IsExtended(path) + ? path + : @"\??\" + Path.GetFullPath(path); // TODO: we might consider getting rid of this managed allocation + + fixed (char* filePath = prefixedAbsolutePath) + { + UNICODE_STRING unicodeString = new UNICODE_STRING(filePath, prefixedAbsolutePath.Length); + OBJECT_ATTRIBUTES objectAttributes = new OBJECT_ATTRIBUTES(&unicodeString, GetObjectAttributes(share)); + + return NtCreateFile( + fileHandle: out fileHandle, + desiredAccess: GetDesiredAccess(access, mode, options), + objectAttributes: ref objectAttributes, + ioStatusBlock: out _, + allocationSize: &allocationSize, + fileAttributes: GetFileAttributes(options), + shareAccess: GetShareAccess(share), + createDisposition: GetCreateDisposition(mode), + createOptions: GetCreateOptions(options), + extendedAttributesBuffer: default, + extendedAttributesLength: default); + } + } + + private static uint GetObjectAttributes(FileShare share) + { + uint result = 0;// 0x00000040; // Lookups for this object should be case insensitive. [OBJ_CASE_INSENSITIVE] + + if ((share & FileShare.Inheritable) != 0 ) + { + result |= 0x00000002; + } + + return result; + } + + private static int GetDesiredAccess(FileAccess access, FileMode fileMode, FileOptions options) + { + int result = 0; + + if ((access & FileAccess.Read) != 0) + { + result |= GenericOperations.GENERIC_READ; + } + if ((access & FileAccess.Write) != 0) + { + result |= GenericOperations.GENERIC_WRITE; + } + if (fileMode == FileMode.Append) + { + result |= 0x0004; // FILE_APPEND_DATA + } + if ((options & FileOptions.Asynchronous) == 0) + { + result |= 0x00100000; // SYNCHRONIZE, requried by FILE_SYNCHRONOUS_IO_NONALERT + } + + return result; + } + + private static uint GetFileAttributes(FileOptions options) + { + uint result = 0; + + if ((options & FileOptions.Encrypted) != 0) + { + result |= 0x00004000; // FILE_ATTRIBUTE_ENCRYPTED + } + + return result; + } + + // FileShare.Inheritable is handled in GetObjectAttributes + private static uint GetShareAccess(FileShare share) + { + uint result = 0; + + if ((share & FileShare.Read) != 0) + { + result |= 1; // FILE_SHARE_READ + } + if ((share & FileShare.Write) != 0) + { + result |= 2; // FILE_SHARE_WRITE + } + if ((share & FileShare.Delete) != 0) + { + result |= 4; // FILE_SHARE_DELETE + } + + // https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile + // "If the original caller of NtCreateFile does not specify FILE_SHARE_READ, FILE_SHARE_WRITE, or FILE_SHARE_DELETE, + // no other open operations can be performed on the file; that is, the original caller is given exclusive access to the file." + // which is how we get FileShare.None working + + return result; + } + + private static uint GetCreateDisposition(FileMode mode) + { + switch (mode) + { + case FileMode.CreateNew: + return 2; // FILE_CREATE + case FileMode.Create: + return 0; // FILE_SUPERSEDE + case FileMode.OpenOrCreate: + case FileMode.Append: // has extra handling in GetDesiredAccess + return 3; // FILE_OPEN_IF + case FileMode.Truncate: + return 4; // FILE_OVERWRITE + default: + Debug.Assert(mode == FileMode.Open); // the enum value is validated in FileStream ctor + return 1; // FILE_OPEN + } + } + + // FileOptions.Encryptend is handled in GetFileAttributes + private static uint GetCreateOptions(FileOptions options) + { + // Every directory is just a directory FILE. + // FileStream does not allow for opening directories on purpose. + // FILE_NON_DIRECTORY_FILE is used to ensure that + uint result = 0x00000040; // FILE_NON_DIRECTORY_FILE + + if ((options & FileOptions.WriteThrough) != 0) + { + result |= 0x00000002; // FILE_WRITE_THROUGH + } + if ((options & FileOptions.RandomAccess) != 0) + { + result |= 0x00000800; // FILE_RANDOM_ACCESS + } + if ((options & FileOptions.SequentialScan) != 0) + { + result |= 0x00000004; // FILE_SEQUENTIAL_ONLY + } + if ((options & FileOptions.DeleteOnClose) != 0) + { + result |= 0x00001000; // FILE_DELETE_ON_CLOSE + } + if ((options & FileOptions.Asynchronous) == 0) + { + // it's async by default, so we need to disable it when async was not requested + result |= 0x00000020; // FILE_SYNCHRONOUS_IO_NONALERT, has extra handling in GetDesiredAccess + } + if (((int)options & 0x20000000) != 0) // NoBuffering + { + result |= 0x00000008; // FILE_NO_INTERMEDIATE_BUFFERING + } + + return result; + } + } +} 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 69eb92ba634e44..4b8c4e1af160ab 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 @@ -1344,6 +1344,9 @@ Common\Interop\Windows\Kernel32\Interop.CreateFile.cs + + Common\Interop\Windows\Kernel32\Interop.NtCreateFile.cs + Common\Interop\Windows\Kernel32\Interop.CriticalSection.cs 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 64522ea320fee6..418dadc99378fd 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 @@ -17,8 +17,8 @@ internal static partial class FileStreamHelpers private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) => new Net5CompatFileStreamStrategy(handle, access, bufferSize, isAsync); - private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - => new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize, options); + private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long allocationSize) + => new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize, options, allocationSize); internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) { From 545143968a67214dc09f4783f443be53574bf853 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 13 Apr 2021 12:31:40 +0200 Subject: [PATCH 06/37] remove duplicated Interop code --- .../Kernel32/Interop.IO_STATUS_BLOCK.cs | 18 -- .../Windows/Kernel32/Interop.NtCreateFile.cs | 186 ------------------ .../Kernel32/Interop.OBJECT_ATTRIBUTES.cs | 33 ---- .../Kernel32/Interop.UNICODE_STRING.cs | 27 --- .../Windows/NtDll/Interop.NtCreateFile.cs | 122 +++++++++++- .../Enumeration/FileSystemEnumerator.Win32.cs | 6 +- .../System.Private.CoreLib.Shared.projitems | 15 +- .../Strategies/FileStreamHelpers.Windows.cs | 8 +- 8 files changed, 126 insertions(+), 289 deletions(-) delete mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.IO_STATUS_BLOCK.cs delete mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.NtCreateFile.cs delete mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.OBJECT_ATTRIBUTES.cs delete mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.UNICODE_STRING.cs diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.IO_STATUS_BLOCK.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.IO_STATUS_BLOCK.cs deleted file mode 100644 index 129395f0de84c6..00000000000000 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.IO_STATUS_BLOCK.cs +++ /dev/null @@ -1,18 +0,0 @@ -// 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; - -internal static partial class Interop -{ - internal static partial class Kernel32 - { - [StructLayout(LayoutKind.Sequential)] - internal struct IO_STATUS_BLOCK - { - internal uint Status; - internal IntPtr Information; - } - } -} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.NtCreateFile.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.NtCreateFile.cs deleted file mode 100644 index 7d0417492651be..00000000000000 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.NtCreateFile.cs +++ /dev/null @@ -1,186 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Win32.SafeHandles; -using System; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; - -internal static partial class Interop -{ - internal static partial class Kernel32 - { - // https://docs.microsoft.com/en-us/windows/desktop/api/winternl/nf-winternl-ntcreatefile - // https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-ntcreatefile - [DllImport(Libraries.NtDll, CharSet = CharSet.Unicode, ExactSpelling = true)] - private static unsafe extern uint NtCreateFile( - out IntPtr fileHandle, - int desiredAccess, - ref OBJECT_ATTRIBUTES objectAttributes, - out IO_STATUS_BLOCK ioStatusBlock, - long* allocationSize, - uint fileAttributes, - uint shareAccess, - uint createDisposition, - uint createOptions, - void* extendedAttributesBuffer, - uint extendedAttributesLength); - - internal static unsafe uint NtCreateFile(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize, out IntPtr fileHandle) - { - string prefixedAbsolutePath = PathInternal.IsExtended(path) - ? path - : @"\??\" + Path.GetFullPath(path); // TODO: we might consider getting rid of this managed allocation - - fixed (char* filePath = prefixedAbsolutePath) - { - UNICODE_STRING unicodeString = new UNICODE_STRING(filePath, prefixedAbsolutePath.Length); - OBJECT_ATTRIBUTES objectAttributes = new OBJECT_ATTRIBUTES(&unicodeString, GetObjectAttributes(share)); - - return NtCreateFile( - fileHandle: out fileHandle, - desiredAccess: GetDesiredAccess(access, mode, options), - objectAttributes: ref objectAttributes, - ioStatusBlock: out _, - allocationSize: &allocationSize, - fileAttributes: GetFileAttributes(options), - shareAccess: GetShareAccess(share), - createDisposition: GetCreateDisposition(mode), - createOptions: GetCreateOptions(options), - extendedAttributesBuffer: default, - extendedAttributesLength: default); - } - } - - private static uint GetObjectAttributes(FileShare share) - { - uint result = 0;// 0x00000040; // Lookups for this object should be case insensitive. [OBJ_CASE_INSENSITIVE] - - if ((share & FileShare.Inheritable) != 0 ) - { - result |= 0x00000002; - } - - return result; - } - - private static int GetDesiredAccess(FileAccess access, FileMode fileMode, FileOptions options) - { - int result = 0; - - if ((access & FileAccess.Read) != 0) - { - result |= GenericOperations.GENERIC_READ; - } - if ((access & FileAccess.Write) != 0) - { - result |= GenericOperations.GENERIC_WRITE; - } - if (fileMode == FileMode.Append) - { - result |= 0x0004; // FILE_APPEND_DATA - } - if ((options & FileOptions.Asynchronous) == 0) - { - result |= 0x00100000; // SYNCHRONIZE, requried by FILE_SYNCHRONOUS_IO_NONALERT - } - - return result; - } - - private static uint GetFileAttributes(FileOptions options) - { - uint result = 0; - - if ((options & FileOptions.Encrypted) != 0) - { - result |= 0x00004000; // FILE_ATTRIBUTE_ENCRYPTED - } - - return result; - } - - // FileShare.Inheritable is handled in GetObjectAttributes - private static uint GetShareAccess(FileShare share) - { - uint result = 0; - - if ((share & FileShare.Read) != 0) - { - result |= 1; // FILE_SHARE_READ - } - if ((share & FileShare.Write) != 0) - { - result |= 2; // FILE_SHARE_WRITE - } - if ((share & FileShare.Delete) != 0) - { - result |= 4; // FILE_SHARE_DELETE - } - - // https://docs.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntcreatefile - // "If the original caller of NtCreateFile does not specify FILE_SHARE_READ, FILE_SHARE_WRITE, or FILE_SHARE_DELETE, - // no other open operations can be performed on the file; that is, the original caller is given exclusive access to the file." - // which is how we get FileShare.None working - - return result; - } - - private static uint GetCreateDisposition(FileMode mode) - { - switch (mode) - { - case FileMode.CreateNew: - return 2; // FILE_CREATE - case FileMode.Create: - return 0; // FILE_SUPERSEDE - case FileMode.OpenOrCreate: - case FileMode.Append: // has extra handling in GetDesiredAccess - return 3; // FILE_OPEN_IF - case FileMode.Truncate: - return 4; // FILE_OVERWRITE - default: - Debug.Assert(mode == FileMode.Open); // the enum value is validated in FileStream ctor - return 1; // FILE_OPEN - } - } - - // FileOptions.Encryptend is handled in GetFileAttributes - private static uint GetCreateOptions(FileOptions options) - { - // Every directory is just a directory FILE. - // FileStream does not allow for opening directories on purpose. - // FILE_NON_DIRECTORY_FILE is used to ensure that - uint result = 0x00000040; // FILE_NON_DIRECTORY_FILE - - if ((options & FileOptions.WriteThrough) != 0) - { - result |= 0x00000002; // FILE_WRITE_THROUGH - } - if ((options & FileOptions.RandomAccess) != 0) - { - result |= 0x00000800; // FILE_RANDOM_ACCESS - } - if ((options & FileOptions.SequentialScan) != 0) - { - result |= 0x00000004; // FILE_SEQUENTIAL_ONLY - } - if ((options & FileOptions.DeleteOnClose) != 0) - { - result |= 0x00001000; // FILE_DELETE_ON_CLOSE - } - if ((options & FileOptions.Asynchronous) == 0) - { - // it's async by default, so we need to disable it when async was not requested - result |= 0x00000020; // FILE_SYNCHRONOUS_IO_NONALERT, has extra handling in GetDesiredAccess - } - if (((int)options & 0x20000000) != 0) // NoBuffering - { - result |= 0x00000008; // FILE_NO_INTERMEDIATE_BUFFERING - } - - return result; - } - } -} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.OBJECT_ATTRIBUTES.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.OBJECT_ATTRIBUTES.cs deleted file mode 100644 index 535976bac5ab17..00000000000000 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.OBJECT_ATTRIBUTES.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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; - -internal static partial class Interop -{ - internal static partial class Kernel32 - { - // https://msdn.microsoft.com/en-us/library/windows/hardware/ff557749.aspx - [StructLayout(LayoutKind.Sequential)] - internal unsafe struct OBJECT_ATTRIBUTES - { - internal uint Length; - internal IntPtr RootDirectory; - internal UNICODE_STRING* ObjectName; - internal uint Attributes; - internal IntPtr SecurityDescriptor; - internal IntPtr SecurityQualityOfService; - - internal unsafe OBJECT_ATTRIBUTES(UNICODE_STRING* objectName, uint attributes) - { - Length = (uint)sizeof(OBJECT_ATTRIBUTES); - RootDirectory = IntPtr.Zero; - ObjectName = objectName; - Attributes = attributes; - SecurityDescriptor = IntPtr.Zero; - SecurityQualityOfService = IntPtr.Zero; - } - } - } -} diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.UNICODE_STRING.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.UNICODE_STRING.cs deleted file mode 100644 index 9c56fcb77adff1..00000000000000 --- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.UNICODE_STRING.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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 Kernel32 - { - // https://msdn.microsoft.com/en-us/library/windows/desktop/aa380518.aspx - // https://msdn.microsoft.com/en-us/library/windows/hardware/ff564879.aspx - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - internal unsafe struct UNICODE_STRING - { - internal ushort Length; - internal ushort MaximumLength; - internal char* Buffer; - - internal unsafe UNICODE_STRING(char* buffer, int lengthInChars) - { - Length = checked((ushort)(lengthInChars * sizeof(char))); - MaximumLength = Length; - Buffer = buffer; - } - } - } -} 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 e2f518e409dca0..48415c06e82403 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics; +using System.IO; using System.Runtime.InteropServices; internal static partial class Interop @@ -11,30 +13,31 @@ internal static partial class NtDll // 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)] - private static extern unsafe int NtCreateFile( + private static extern unsafe uint NtCreateFile( out IntPtr FileHandle, DesiredAccess DesiredAccess, ref OBJECT_ATTRIBUTES ObjectAttributes, out IO_STATUS_BLOCK IoStatusBlock, long* AllocationSize, - System.IO.FileAttributes FileAttributes, - System.IO.FileShare ShareAccess, + FileAttributes FileAttributes, + FileShare ShareAccess, CreateDisposition CreateDisposition, CreateOptions CreateOptions, void* EaBuffer, uint EaLength); - internal static unsafe (int status, IntPtr handle) CreateFile( + internal static unsafe (uint status, IntPtr handle) CreateFile( ReadOnlySpan path, IntPtr rootDirectory, CreateDisposition createDisposition, DesiredAccess desiredAccess = DesiredAccess.FILE_GENERIC_READ | DesiredAccess.SYNCHRONIZE, - System.IO.FileShare shareAccess = System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete, - System.IO.FileAttributes fileAttributes = 0, + FileShare shareAccess = FileShare.ReadWrite | FileShare.Delete, + FileAttributes fileAttributes = 0, CreateOptions createOptions = CreateOptions.FILE_SYNCHRONOUS_IO_NONALERT, ObjectAttributes objectAttributes = ObjectAttributes.OBJ_CASE_INSENSITIVE, void* eaBuffer = null, - uint eaLength = 0) + uint eaLength = 0, + long* allocationSize = null) { fixed (char* c = &MemoryMarshal.GetReference(path)) { @@ -50,12 +53,12 @@ internal static unsafe (int status, IntPtr handle) CreateFile( objectAttributes, rootDirectory); - int status = NtCreateFile( + uint status = NtCreateFile( out IntPtr handle, desiredAccess, ref attributes, out IO_STATUS_BLOCK statusBlock, - AllocationSize: null, + AllocationSize: allocationSize, fileAttributes, shareAccess, createDisposition, @@ -67,6 +70,107 @@ internal static unsafe (int status, IntPtr handle) CreateFile( } } + internal static unsafe (uint status, IntPtr handle) CreateFile(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) + => CreateFile( + path: PathInternal.IsExtended(path) ? path : @"\??\" + Path.GetFullPath(path), // TODO: we might consider getting rid of this managed allocation, + rootDirectory: IntPtr.Zero, + createDisposition: GetCreateDisposition(mode), + desiredAccess: GetDesiredAccess(access, mode, options), + shareAccess: GetShareAccess(share), + fileAttributes: GetFileAttributes(options), + createOptions: GetCreateOptions(options), + objectAttributes: GetObjectAttributes(share), + allocationSize: &allocationSize); + + private static CreateDisposition GetCreateDisposition(FileMode mode) + { + switch (mode) + { + case FileMode.CreateNew: + return CreateDisposition.FILE_CREATE; + case FileMode.Create: + return CreateDisposition.FILE_SUPERSEDE; + case FileMode.OpenOrCreate: + case FileMode.Append: // has extra handling in GetDesiredAccess + return CreateDisposition.FILE_OPEN_IF; + case FileMode.Truncate: + return CreateDisposition.FILE_OVERWRITE; + default: + Debug.Assert(mode == FileMode.Open); // the enum value is validated in FileStream ctor + return CreateDisposition.FILE_OPEN; + } + } + + private static DesiredAccess GetDesiredAccess(FileAccess access, FileMode fileMode, FileOptions options) + { + DesiredAccess result = 0; + + if ((access & FileAccess.Read) != 0) + { + result |= DesiredAccess.FILE_GENERIC_READ; + } + if ((access & FileAccess.Write) != 0) + { + result |= DesiredAccess.FILE_GENERIC_WRITE; + } + if (fileMode == FileMode.Append) + { + result |= DesiredAccess.FILE_APPEND_DATA; + } + if ((options & FileOptions.Asynchronous) == 0) + { + result |= DesiredAccess.SYNCHRONIZE; // requried by FILE_SYNCHRONOUS_IO_NONALERT + } + + return result; + } + + private static FileShare GetShareAccess(FileShare share) + => share & ~FileShare.Inheritable; // FileShare.Inheritable is handled in GetObjectAttributes + + private static FileAttributes GetFileAttributes(FileOptions options) + => (options & FileOptions.Encrypted) != 0 ? FileAttributes.Encrypted : 0; + + // FileOptions.Encrypted is handled in GetFileAttributes + private static CreateOptions GetCreateOptions(FileOptions options) + { + // Every directory is just a directory FILE. + // FileStream does not allow for opening directories on purpose. + // FILE_NON_DIRECTORY_FILE is used to ensure that + CreateOptions result = CreateOptions.FILE_NON_DIRECTORY_FILE; + + if ((options & FileOptions.WriteThrough) != 0) + { + result |= CreateOptions.FILE_WRITE_THROUGH; + } + if ((options & FileOptions.RandomAccess) != 0) + { + result |= CreateOptions.FILE_RANDOM_ACCESS; + } + if ((options & FileOptions.SequentialScan) != 0) + { + result |= CreateOptions.FILE_SEQUENTIAL_ONLY; + } + if ((options & FileOptions.DeleteOnClose) != 0) + { + result |= CreateOptions.FILE_DELETE_ON_CLOSE; + } + if ((options & FileOptions.Asynchronous) == 0) + { + // it's async by default, so we need to disable it when async was not requested + result |= CreateOptions.FILE_SYNCHRONOUS_IO_NONALERT; // has extra handling in GetDesiredAccess + } + if (((int)options & 0x20000000) != 0) // NoBuffering + { + result |= CreateOptions.FILE_NO_INTERMEDIATE_BUFFERING; + } + + return result; + } + + private static ObjectAttributes GetObjectAttributes(FileShare share) + => (share & FileShare.Inheritable) != 0 ? ObjectAttributes.OBJ_INHERIT : 0; + /// /// File creation disposition when calling directly to NT APIs. /// diff --git a/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Win32.cs b/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Win32.cs index 44c8cec1317d26..22e990194f5d88 100644 --- a/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Win32.cs +++ b/src/libraries/System.IO.FileSystem/src/System/IO/Enumeration/FileSystemEnumerator.Win32.cs @@ -60,7 +60,7 @@ private unsafe bool GetData() private unsafe IntPtr CreateRelativeDirectoryHandle(ReadOnlySpan relativePath, string fullPath) { - (int status, IntPtr handle) = Interop.NtDll.CreateFile( + (uint status, IntPtr handle) = Interop.NtDll.CreateFile( relativePath, _directoryHandle, Interop.NtDll.CreateDisposition.FILE_OPEN, @@ -68,7 +68,7 @@ private unsafe IntPtr CreateRelativeDirectoryHandle(ReadOnlySpan relativeP createOptions: Interop.NtDll.CreateOptions.FILE_SYNCHRONOUS_IO_NONALERT | Interop.NtDll.CreateOptions.FILE_DIRECTORY_FILE | Interop.NtDll.CreateOptions.FILE_OPEN_FOR_BACKUP_INTENT); - switch ((uint)status) + switch (status) { case Interop.StatusOptions.STATUS_SUCCESS: return handle; @@ -77,7 +77,7 @@ private unsafe IntPtr CreateRelativeDirectoryHandle(ReadOnlySpan relativeP // such as ERROR_ACCESS_DENIED. As we want to replicate Win32 handling/reporting and the mapping isn't documented, // we should always do our logic on the converted code, not the NTSTATUS. - int error = (int)Interop.NtDll.RtlNtStatusToDosError(status); + int error = (int)Interop.NtDll.RtlNtStatusToDosError((int)status); if (ContinueOnDirectoryError(error, ignoreNotFound: true)) { 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 4b8c4e1af160ab..8894c8389c1948 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 @@ -1344,8 +1344,8 @@ Common\Interop\Windows\Kernel32\Interop.CreateFile.cs - - Common\Interop\Windows\Kernel32\Interop.NtCreateFile.cs + + Common\Interop\Windows\Kernel32\NtDll\Interop.NtCreateFile.cs Common\Interop\Windows\Kernel32\Interop.CriticalSection.cs @@ -1512,14 +1512,11 @@ Common\Interop\Windows\Kernel32\Interop.SECURITY_ATTRIBUTES.cs - - Common\Interop\Windows\Kernel32\Interop.UNICODE_STRING.cs + + Common\Interop\Windows\Interop.UNICODE_STRING.cs - - Common\Interop\Windows\Kernel32\Interop.OBJECT_ATTRIBUTES.cs - - - Common\Interop\Windows\Kernel32\Interop.IO_STATUS_BLOCK.cs + + Common\Interop\Windows\Interop.OBJECT_ATTRIBUTES.cs Common\Interop\Windows\Kernel32\Interop.SecurityOptions.cs 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 d207a577255c6e..525a52c85332c3 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 @@ -62,19 +62,19 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode if (allocationSize > 0) { - uint ntCreateFileResult = Interop.Kernel32.NtCreateFile(path, mode, access, share, options, allocationSize, out IntPtr fileHandle); - if (ntCreateFileResult == 0) + (uint ntStatus, IntPtr fileHandle) = Interop.NtDll.CreateFile(path, mode, access, share, options, allocationSize); + if (ntStatus == 0) { return ValidateFileHandle(new SafeFileHandle(fileHandle, ownsHandle: true), path, (options & FileOptions.Asynchronous) != 0); } - else if (ntCreateFileResult == ERROR_STATUS_DISK_FULL) + else if (ntStatus == ERROR_STATUS_DISK_FULL) { throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, path, allocationSize)); } // NtCreateFile has failed for some other reason than a full disk. // Instead of implementing the mapping for every NS Status value (there are plenty of them: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55) - // increasing both the code complexity and the chance for breaking backward compatibility (by throwing a different exception than CreateFileW) + // or using RtlNtStatusToDosError & GetExceptionForWin32Error // the code falls back to CreateFileW that just throws the right exception. } From 07cfc752eb8b54c11d3e992ea7d56aac490d772f Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 13 Apr 2021 16:11:01 +0200 Subject: [PATCH 07/37] fix the full framework build --- .../Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs | 4 ++-- .../src/System.Private.CoreLib.Shared.projitems | 2 +- .../src/System/IO/Strategies/FileStreamHelpers.Windows.cs | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) 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 48415c06e82403..bdc85f848e9167 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs @@ -70,9 +70,9 @@ internal static unsafe (uint status, IntPtr handle) CreateFile( } } - internal static unsafe (uint status, IntPtr handle) CreateFile(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) + internal static unsafe (uint status, IntPtr handle) CreateFile(ReadOnlySpan path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) => CreateFile( - path: PathInternal.IsExtended(path) ? path : @"\??\" + Path.GetFullPath(path), // TODO: we might consider getting rid of this managed allocation, + path: path, rootDirectory: IntPtr.Zero, createDisposition: GetCreateDisposition(mode), desiredAccess: GetDesiredAccess(access, mode, options), 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 5a130a543797f7..75c8c0f88d260a 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 @@ -1346,7 +1346,7 @@ Common\Interop\Windows\Kernel32\Interop.CreateFile.cs - Common\Interop\Windows\Kernel32\NtDll\Interop.NtCreateFile.cs + Common\Interop\Windows\NtDll\Interop.NtCreateFile.cs Common\Interop\Windows\Kernel32\Interop.CriticalSection.cs 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 525a52c85332c3..5c2271a8bf9031 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 @@ -62,7 +62,8 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode if (allocationSize > 0) { - (uint ntStatus, IntPtr fileHandle) = Interop.NtDll.CreateFile(path, mode, access, share, options, allocationSize); + string prefixedAbsolutePath = PathInternal.IsExtended(path) ? path : @"\??\" + Path.GetFullPath(path); // TODO: we might consider getting rid of this managed allocation, + (uint ntStatus, IntPtr fileHandle) = Interop.NtDll.CreateFile(prefixedAbsolutePath, mode, access, share, options, allocationSize); if (ntStatus == 0) { return ValidateFileHandle(new SafeFileHandle(fileHandle, ownsHandle: true), path, (options & FileOptions.Asynchronous) != 0); From 945c917a5a5c0d51583165651bfb41790aa9ce4d Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 20 Apr 2021 16:12:57 +0200 Subject: [PATCH 08/37] initial Unix implementation --- .../Unix/System.Native/Interop.FAllocate.cs | 14 +++++ .../Native/Unix/Common/pal_config.h.in | 2 + .../Native/Unix/System.Native/entrypoints.c | 1 + .../Native/Unix/System.Native/pal_io.c | 56 ++++++++++++++++++- .../Native/Unix/System.Native/pal_io.h | 7 +++ src/libraries/Native/Unix/configure.cmake | 10 ++++ .../FileStream/FileStreamConformanceTests.cs | 11 +++- .../System.Private.CoreLib.Shared.projitems | 5 +- .../IO/Strategies/FileStreamHelpers.Unix.cs | 20 ++++++- 9 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs new file mode 100644 index 00000000000000..8554f1fe66a53a --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs @@ -0,0 +1,14 @@ +// 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; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Sys + { + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_FAllocate", SetLastError = false /* this is explicitly called out in the man page */)] + internal static extern int FAllocate(SafeFileHandle fd, long offset, long length); + } +} diff --git a/src/libraries/Native/Unix/Common/pal_config.h.in b/src/libraries/Native/Unix/Common/pal_config.h.in index 67158f915a7a48..b7bd484e629ffa 100644 --- a/src/libraries/Native/Unix/Common/pal_config.h.in +++ b/src/libraries/Native/Unix/Common/pal_config.h.in @@ -34,6 +34,8 @@ #cmakedefine01 HAVE_STRLCAT #cmakedefine01 HAVE_SHM_OPEN_THAT_WORKS_WELL_ENOUGH_WITH_MMAP #cmakedefine01 HAVE_POSIX_ADVISE +#cmakedefine01 HAVE_POSIX_FALLOCATE +#cmakedefine01 HAVE_POSIX_FALLOCATE64 #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 5abcf2a91074dd..107c089c2cbbd0 100644 --- a/src/libraries/Native/Unix/System.Native/entrypoints.c +++ b/src/libraries/Native/Unix/System.Native/entrypoints.c @@ -89,6 +89,7 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_FTruncate) DllImportEntry(SystemNative_Poll) DllImportEntry(SystemNative_PosixFAdvise) + DllImportEntry(SystemNative_FAllocate) DllImportEntry(SystemNative_Read) DllImportEntry(SystemNative_ReadLink) DllImportEntry(SystemNative_Rename) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.c b/src/libraries/Native/Unix/System.Native/pal_io.c index 96b69525471987..89fa7c2108fac5 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.c +++ b/src/libraries/Native/Unix/System.Native/pal_io.c @@ -662,7 +662,7 @@ int32_t SystemNative_FSync(intptr_t fd) int fileDescriptor = ToFileDescriptor(fd); int32_t result; - while ((result = + while ((result = #if defined(TARGET_OSX) && HAVE_F_FULLFSYNC fcntl(fileDescriptor, F_FULLFSYNC) #else @@ -991,6 +991,58 @@ int32_t SystemNative_PosixFAdvise(intptr_t fd, int64_t offset, int64_t length, i #endif } +int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length) +{ + int fileDescriptor = ToFileDescriptor(fd); + int32_t result; +#if HAVE_POSIX_FALLOCATE64 // 64-bit Linux + while ((result = posix_fallocate64(fileDescriptor, (off64_t)offset, (off64_t)length)) == EINTR); +#elif HAVE_POSIX_FALLOCATE // 32-bit Linux + while ((result = posix_fallocate(fileDescriptor, (off_t)offset, (off_t)length)) == EINTR); +#elif defined(F_PREALLOCATE) // macOS + fstore_t fstore; + fstore.fst_flags = F_ALLOCATECONTIG; // ensure contiguous space + fstore.fst_posmode = F_PEOFPOSMODE; // allocate from the physical end of file, as offset MUST NOT be 0 for F_VOLPOSMODE + fstore.fst_offset = (off_t)offset; + fstore.fst_length = (off_t)length; + fstore.fst_bytesalloc = 0; // output size, can be > length + + while ((result = fcntl(fileDescriptor, F_PREALLOCATE, &fstore)) == -1 && errno == EINTR) ; + + if (result == -1) + { + // we have failed to allocate contiguous space, let's try non-contiguous + fstore.fst_flags = F_ALLOCATEALL; // all or nothing + while ((result = fcntl(fileDescriptor, F_PREALLOCATE, &fstore)) == -1 && errno == EINTR) ; + } +#elif defined(F_ALLOCSP) || defined(F_ALLOCSP64) // FreeBSD + #if HAVE_FLOCK64 + struct flock64 lockArgs; + int command = F_ALLOCSP64; + #else + struct flock lockArgs; + int command = F_ALLOCSP; + #endif + + lockArgs.l_whence = SEEK_SET; + lockArgs.l_start = (off_t)offset; + lockArgs.l_len = (off_t)length; + + while ((result = fcntl(fileDescriptor, command, &lockArgs)) == -1 && errno == EINTR) ; +#endif + +#if defined(F_PREALLOCATE) || defined(F_ALLOCSP) || defined(F_ALLOCSP64) + // most of the Unixes implement posix_fallocate which does NOT set the last error + // fctnl does, but to mimic the posix_fallocate behaviour we just return error + if (result == -1) + { + result = errno; + } +#endif + + return result; +} + int32_t SystemNative_Read(intptr_t fd, void* buffer, int32_t bufferSize) { return Common_Read(fd, buffer, bufferSize); @@ -1184,7 +1236,7 @@ int32_t SystemNative_CopyFile(intptr_t sourceFd, intptr_t destinationFd) #endif } // If we copied to a filesystem (eg EXFAT) that does not preserve POSIX ownership, all files appear - // to be owned by root. If we aren't running as root, then we won't be an owner of our new file, and + // to be owned by root. If we aren't running as root, then we won't be an owner of our new file, and // attempting to copy metadata to it will fail with EPERM. We have copied successfully, we just can't // copy metadata. The best thing we can do is skip copying the metadata. if (ret != 0 && errno != EPERM) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.h b/src/libraries/Native/Unix/System.Native/pal_io.h index e82cffe8a8c3a9..313aa5a6179f6d 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.h +++ b/src/libraries/Native/Unix/System.Native/pal_io.h @@ -604,6 +604,13 @@ PALEXPORT int32_t SystemNative_Poll(PollEvent* pollEvents, uint32_t eventCount, */ PALEXPORT int32_t SystemNative_PosixFAdvise(intptr_t fd, int64_t offset, int64_t length, int32_t advice); +/** + * Ensures that disk space is allocated. + * + * Returns 0 on success; otherwise, the error code is returned and errno is NOT set. + */ +PALEXPORT int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length); + /** * Reads the number of bytes specified into the provided buffer from the specified, opened file descriptor. * diff --git a/src/libraries/Native/Unix/configure.cmake b/src/libraries/Native/Unix/configure.cmake index 674375cdd9ec52..0cdc12cca9f0e3 100644 --- a/src/libraries/Native/Unix/configure.cmake +++ b/src/libraries/Native/Unix/configure.cmake @@ -209,6 +209,16 @@ check_symbol_exists( fcntl.h HAVE_POSIX_ADVISE) +check_symbol_exists( + posix_fallocate + fcntl.h + HAVE_POSIX_FALLOCATE) + +check_symbol_exists( + posix_fallocate64 + fcntl.h + HAVE_POSIX_FALLOCATE64) + check_symbol_exists( ioctl sys/ioctl.h diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs index 5ab668f760724c..a8ac77595566d8 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs @@ -62,7 +62,7 @@ public async Task FileOffsetIsPreservedWhenFileStreamIsCreatedFromSafeFileHandle using FileStream createdFromHandle = new FileStream(stream.SafeFileHandle, FileAccess.Write); - Assert.Equal(buffer.Length, stream.Position); + Assert.Equal(buffer.Length, stream.Position); Assert.Equal(stream.Position, createdFromHandle.Position); } @@ -187,17 +187,22 @@ public async Task WriteByteFlushesTheBufferWhenItBecomesFull() byte[] allBytes = File.ReadAllBytes(filePath); Assert.Equal(writtenBytes.ToArray(), allBytes); } - + [Fact] public void WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllTheDetails() { const long tooMuch = 1024L * 1024L * 1024L * 1024L; // 1 TB string filePath = GetTestFilePath(); - IOException ex = Assert.Throws(() => new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, BufferSize, Options, tooMuch)); + + Assert.False(File.Exists(filePath)); + + IOException ex = Assert.Throws(() => new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, BufferSize, Options, tooMuch)); Assert.Contains("disk was full", ex.Message); Assert.Contains(filePath, ex.Message); Assert.Contains(AllocationSize.ToString(), ex.Message); + + Assert.False(File.Exists(filePath)); } } 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 75c8c0f88d260a..ebd67d49a9c3d9 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 @@ -1,4 +1,4 @@ - + true c5ed3c1d-b572-46f1-8f96-522a85ce1179 @@ -1821,6 +1821,9 @@ Common\Interop\Unix\System.Native\Interop.PosixFAdvise.cs + + Common\Interop\Unix\System.Native\Interop.FAllocate.cs + Common\Interop\Unix\System.Native\Interop.Read.cs 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 418dadc99378fd..5138b36c10b0ea 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 @@ -7,12 +7,15 @@ using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; +using Internal.IO; namespace System.IO.Strategies { // this type defines a set of stateless FileStream/FileStreamStrategy helper methods internal static partial class FileStreamHelpers { + private const int ENOSPC_Linux = 28; + // in the future we are most probably going to introduce more strategies (io_uring etc) private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) => new Net5CompatFileStreamStrategy(handle, access, bufferSize, isAsync); @@ -35,7 +38,22 @@ internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess Interop.Sys.Permissions.S_IROTH | Interop.Sys.Permissions.S_IWOTH; // Open the file and store the safe handle. - return SafeFileHandle.Open(path!, openFlags, (int)OpenPermissions); + SafeFileHandle handle = SafeFileHandle.Open(path!, openFlags, (int)OpenPermissions); + // If allocationSize has been provided for a creatable and writeable file + if (allocationSize > 0 && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append) + { + int allocationResult = Interop.Sys.FAllocate(handle, 0, allocationSize); + if (allocationResult == (int)Interop.Error.ENOSPC || allocationResult == ENOSPC_Linux) + { + handle.Dispose(); + Interop.Sys.Unlink(path); // remove the file to mimic Windows behaviour (atomic operation) + + throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, path, allocationSize)); + } + // ignore not supported and other failures (pipe etc) + } + + return handle; } internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsync) => handle.IsAsync ?? defaultIsAsync; From 3bf1f95ad2e4670dfe34ec1baaed0aa98d8e81af Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 21 Apr 2021 09:52:28 +0200 Subject: [PATCH 09/37] some polishing --- src/libraries/Native/Unix/System.Native/pal_io.c | 6 +++--- .../src/System/IO/Strategies/FileStreamHelpers.Unix.cs | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.c b/src/libraries/Native/Unix/System.Native/pal_io.c index 89fa7c2108fac5..c836e9b38d97c6 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.c +++ b/src/libraries/Native/Unix/System.Native/pal_io.c @@ -1007,13 +1007,13 @@ int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length) fstore.fst_length = (off_t)length; fstore.fst_bytesalloc = 0; // output size, can be > length - while ((result = fcntl(fileDescriptor, F_PREALLOCATE, &fstore)) == -1 && errno == EINTR) ; + while ((result = fcntl(fileDescriptor, F_PREALLOCATE, &fstore)) == -1 && errno == EINTR); if (result == -1) { // we have failed to allocate contiguous space, let's try non-contiguous fstore.fst_flags = F_ALLOCATEALL; // all or nothing - while ((result = fcntl(fileDescriptor, F_PREALLOCATE, &fstore)) == -1 && errno == EINTR) ; + while ((result = fcntl(fileDescriptor, F_PREALLOCATE, &fstore)) == -1 && errno == EINTR); } #elif defined(F_ALLOCSP) || defined(F_ALLOCSP64) // FreeBSD #if HAVE_FLOCK64 @@ -1028,7 +1028,7 @@ int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length) lockArgs.l_start = (off_t)offset; lockArgs.l_len = (off_t)length; - while ((result = fcntl(fileDescriptor, command, &lockArgs)) == -1 && errno == EINTR) ; + while ((result = fcntl(fileDescriptor, command, &lockArgs)) == -1 && errno == EINTR); #endif #if defined(F_PREALLOCATE) || defined(F_ALLOCSP) || defined(F_ALLOCSP64) 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 5138b36c10b0ea..37e2d69eea05fb 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 @@ -14,7 +14,7 @@ namespace System.IO.Strategies // this type defines a set of stateless FileStream/FileStreamStrategy helper methods internal static partial class FileStreamHelpers { - private const int ENOSPC_Linux = 28; + private static readonly int ENOSPC = OperatingSystem.IsLinux() ? 28 : (int)Interop.Error.ENOSPC; // Linux error code != Unix error code // in the future we are most probably going to introduce more strategies (io_uring etc) private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) @@ -42,8 +42,7 @@ internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess // If allocationSize has been provided for a creatable and writeable file if (allocationSize > 0 && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append) { - int allocationResult = Interop.Sys.FAllocate(handle, 0, allocationSize); - if (allocationResult == (int)Interop.Error.ENOSPC || allocationResult == ENOSPC_Linux) + if (Interop.Sys.FAllocate(handle, 0, allocationSize) == ENOSPC) { handle.Dispose(); Interop.Sys.Unlink(path); // remove the file to mimic Windows behaviour (atomic operation) From ed1f18dd19fc73266cd4c71bc7149fd0a9327e57 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 21 Apr 2021 09:59:14 +0200 Subject: [PATCH 10/37] remove unused using and extra empty lines from existing tests --- src/libraries/System.IO.FileSystem/tests/File/Open.cs | 4 ---- .../System.IO.FileSystem/tests/FileStream/Dispose.cs | 1 - src/libraries/System.IO.FileSystem/tests/FileStream/Flush.cs | 1 - src/libraries/System.IO.FileSystem/tests/FileStream/Name.cs | 4 ---- .../System.IO.FileSystem/tests/FileStream/ToString.cs | 2 -- .../tests/FileStream/ctor_sfh_fa_buffer.cs | 1 - .../System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs | 1 - .../System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa.cs | 2 -- .../tests/FileStream/ctor_str_fm_fa_fs.read.cs | 2 -- .../tests/FileStream/ctor_str_fm_fa_fs.write.cs | 2 -- .../tests/FileStream/ctor_str_fm_fa_fs_buffer.cs | 2 -- .../tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs | 3 --- .../tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs | 3 --- 13 files changed, 28 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/File/Open.cs b/src/libraries/System.IO.FileSystem/tests/File/Open.cs index f0a7897725d10f..3a042d8a65b9ef 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/Open.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/Open.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; - namespace System.IO.Tests { public class File_Open_str_fm : FileStream_ctor_str_fm @@ -11,7 +9,6 @@ protected override FileStream CreateFileStream(string path, FileMode mode) { return File.Open(path, mode); } - } public class File_Open_str_fm_fa : FileStream_ctor_str_fm_fa @@ -86,5 +83,4 @@ protected override string Read(string path) return reader.ReadToEnd(); } } - } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/Dispose.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/Dispose.cs index 283cb3e8eddc97..2a94247dc04144 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/Dispose.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/Dispose.cs @@ -48,7 +48,6 @@ protected override void Dispose(bool disposing) } } - [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public void Dispose_CallsVirtualDisposeTrueArg_ThrowsDuringFlushWriteBuffer_DisposeThrows() { diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/Flush.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/Flush.cs index 25dd6422410aa5..be175c6efa3015 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/Flush.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/Flush.cs @@ -183,6 +183,5 @@ public override void Flush(bool flushToDisk) base.Flush(flushToDisk); } } - } } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/Name.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/Name.cs index 1b088a15062a2b..cfe0c62d6c30f1 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/Name.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/Name.cs @@ -1,11 +1,8 @@ // 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.Globalization; -using System.IO; using System.Tests; -using Microsoft.DotNet.RemoteExecutor; using Xunit; namespace System.IO.Tests @@ -37,7 +34,6 @@ public void NameNormalizesPath() } } - [Fact] public void NameReturnsUnknownForHandle() { diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ToString.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ToString.cs index 647cccf60a575a..8682f5a75b9986 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ToString.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ToString.cs @@ -1,8 +1,6 @@ // 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.IO; using Xunit; namespace System.IO.Tests 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 aa263624d18909..745ff57850888a 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,7 +18,6 @@ protected virtual FileStream CreateFileStream(SafeFileHandle handle, FileAccess return new FileStream(handle, access, bufferSize); } - [Theory, InlineData(0), InlineData(-1)] diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs index c1b97f00cc4340..0b4b1345ca907a 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs @@ -50,7 +50,6 @@ public void MissingDirectory_ThrowsDirectoryNotFound(char trailingChar) Assert.Throws(() => CreateFileStream(path, FileMode.Open)); } - public static TheoryData StreamSpecifiers { get diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa.cs index 41d7263c6f81e6..6ab972644c3000 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa.cs @@ -1,8 +1,6 @@ // 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.IO; using Xunit; namespace System.IO.Tests diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs.read.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs.read.cs index c1d989f12e2436..ba8c402925f2cf 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs.read.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs.read.cs @@ -1,8 +1,6 @@ // 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.IO; using Xunit; namespace System.IO.Tests diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs.write.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs.write.cs index e0d619187c3999..ae93555bbe8316 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs.write.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs.write.cs @@ -1,8 +1,6 @@ // 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.IO; using Xunit; namespace System.IO.Tests diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer.cs index a24a4fd16d8db6..3885149161cadd 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer.cs @@ -1,8 +1,6 @@ // 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.IO; using Xunit; namespace System.IO.Tests diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs index c3d8f0077dc442..4e5c38229180b7 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs @@ -1,8 +1,6 @@ // 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.IO; using Xunit; namespace System.IO.Tests @@ -29,6 +27,5 @@ public void ValidUseAsync() using (CreateFileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, c_DefaultBufferSize, false)) { } } - } } 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 3c99a8005d2b17..561f1ebf071e32 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 @@ -1,8 +1,6 @@ // 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.IO; using Xunit; namespace System.IO.Tests @@ -87,6 +85,5 @@ public void DeleteOnClose_FileDeletedAfterClose(FileOptions options) } Assert.False(File.Exists(path)); } - } } From f645e677069aa66807fda56958a6909543b37872 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 21 Apr 2021 10:00:20 +0200 Subject: [PATCH 11/37] the active issue link should point to a valid issue --- .../tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs index 4e5c38229180b7..06f507616c0095 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs @@ -5,7 +5,7 @@ namespace System.IO.Tests { - [ActiveIssue("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/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + [ActiveIssue("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/dotnet/runtime/issues/34582", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] public class FileStream_ctor_str_fm_fa_fs_buffer_async : FileStream_ctor_str_fm_fa_fs_buffer { protected sealed override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize) From 909c86e236b344a6058a0ef52ec15715e66a1bf6 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 21 Apr 2021 10:02:02 +0200 Subject: [PATCH 12/37] the tests should actually verify whether FileStream is async or not --- .../FileStream/ctor_str_fm_fa_fs_buffer_async.cs | 15 ++++++++------- .../FileStream/ctor_str_fm_fa_fs_buffer_fo.cs | 2 ++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs index 06f507616c0095..a93eb4acb08159 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs @@ -18,14 +18,15 @@ protected virtual FileStream CreateFileStream(string path, FileMode mode, FileAc return new FileStream(path, mode, access, share, bufferSize, useAsync); } - [Fact] - public void ValidUseAsync() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ValidUseAsync(bool isAsync) { - using (CreateFileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, c_DefaultBufferSize, true)) - { } - - using (CreateFileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, c_DefaultBufferSize, false)) - { } + using (FileStream fs = CreateFileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, c_DefaultBufferSize, isAsync)) + { + Assert.Equal(isAsync, fs.IsAsync); + } } } } 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 561f1ebf071e32..26f3a2fc55d2b0 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 @@ -45,6 +45,8 @@ public void ValidFileOptions(FileOptions option) using (FileStream fs = CreateFileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, c_DefaultBufferSize, option)) { + Assert.Equal((option & FileOptions.Asynchronous) != 0, fs.IsAsync); + // make sure we can write, seek, and read data with this option set fs.Write(data, 0, data.Length); fs.Position = 0; From 6789382e521bf16ef554480e196665ab2d203ebe Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 21 Apr 2021 11:38:29 +0200 Subject: [PATCH 13/37] remove duplicated consts --- .../src/Interop/Windows/NtDll/Interop.NtQueryInformationFile.cs | 2 -- .../src/System/IO/Strategies/FileStreamHelpers.Windows.cs | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) 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 ccec236d394e21..a9c79cd1d68692 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtQueryInformationFile.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtQueryInformationFile.cs @@ -25,8 +25,6 @@ internal struct IO_STATUS_BLOCK } internal const uint FileModeInformation = 16; - internal const uint FILE_SYNCHRONOUS_IO_ALERT = 0x00000010; - internal const uint FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020; internal const int STATUS_INVALID_HANDLE = unchecked((int)0xC0000008); } 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 e0fbad1f55b2f6..6be87f7cbccb3a 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 @@ -154,7 +154,7 @@ internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsyn } // If either of these two flags are set, the file handle is synchronous (not overlapped) - return (fileMode & (Interop.NtDll.FILE_SYNCHRONOUS_IO_ALERT | Interop.NtDll.FILE_SYNCHRONOUS_IO_NONALERT)) > 0; + return (fileMode & (uint)(Interop.NtDll.CreateOptions.FILE_SYNCHRONOUS_IO_ALERT | Interop.NtDll.CreateOptions.FILE_SYNCHRONOUS_IO_NONALERT)) > 0; } internal static void VerifyHandleIsSync(SafeFileHandle handle) From dbe44d40daecc0a4d8196dabbb9c83dadbe97dd2 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 21 Apr 2021 12:13:20 +0200 Subject: [PATCH 14/37] proper tests for Windows --- .../FileStream/FileStreamConformanceTests.cs | 60 +----- .../tests/FileStream/ctor_str_fm.cs | 2 + ..._fa_fs_buffer_fo_AllocationSize.Windows.cs | 171 ++++++++++++++++++ ...r_str_fm_fa_fs_buffer_fo_AllocationSize.cs | 35 ++++ ...stem.IO.FileSystem.Net5Compat.Tests.csproj | 4 + .../tests/System.IO.FileSystem.Tests.csproj | 6 + .../Strategies/FileStreamHelpers.Windows.cs | 4 +- 7 files changed, 222 insertions(+), 60 deletions(-) create mode 100644 src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs create mode 100644 src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs index a8ac77595566d8..f086c2cc23609f 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/FileStreamConformanceTests.cs @@ -14,7 +14,6 @@ public abstract class FileStreamStandaloneConformanceTests : StandaloneStreamCon { protected abstract FileOptions Options { get; } protected abstract int BufferSize { get; } - protected abstract long AllocationSize { get; } private Task CreateStream(byte[] initialData, FileAccess access) { @@ -24,7 +23,7 @@ private Task CreateStream(byte[] initialData, FileAccess access) File.WriteAllBytes(path, initialData); } - return Task.FromResult(new FileStream(path, FileMode.OpenOrCreate, access, FileShare.None, BufferSize, Options, AllocationSize)); + return Task.FromResult(new FileStream(path, FileMode.OpenOrCreate, access, FileShare.None, BufferSize, Options)); } protected override Task CreateReadOnlyStreamCore(byte[] initialData) => CreateStream(initialData, FileAccess.Read); @@ -62,7 +61,7 @@ public async Task FileOffsetIsPreservedWhenFileStreamIsCreatedFromSafeFileHandle using FileStream createdFromHandle = new FileStream(stream.SafeFileHandle, FileAccess.Write); - Assert.Equal(buffer.Length, stream.Position); + Assert.Equal(buffer.Length, stream.Position); Assert.Equal(stream.Position, createdFromHandle.Position); } @@ -187,53 +186,18 @@ public async Task WriteByteFlushesTheBufferWhenItBecomesFull() byte[] allBytes = File.ReadAllBytes(filePath); Assert.Equal(writtenBytes.ToArray(), allBytes); } - - [Fact] - public void WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllTheDetails() - { - const long tooMuch = 1024L * 1024L * 1024L * 1024L; // 1 TB - - string filePath = GetTestFilePath(); - - Assert.False(File.Exists(filePath)); - - IOException ex = Assert.Throws(() => new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, BufferSize, Options, tooMuch)); - Assert.Contains("disk was full", ex.Message); - Assert.Contains(filePath, ex.Message); - Assert.Contains(AllocationSize.ToString(), ex.Message); - - Assert.False(File.Exists(filePath)); - } } public class UnbufferedSyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests { protected override FileOptions Options => FileOptions.None; protected override int BufferSize => 1; - protected override long AllocationSize => 0; - } - - public class UnbufferedPreallocatedSyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests - { - protected override FileOptions Options => FileOptions.None; - protected override int BufferSize => 1; - - // any AllocationSize > 0 executes the code path where we try to pre-allocate the disk space - protected override long AllocationSize => 1; } public class BufferedSyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests { protected override FileOptions Options => FileOptions.None; protected override int BufferSize => 10; - protected override long AllocationSize => 0; - } - - public class BufferedPreallocatedSyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests - { - protected override FileOptions Options => FileOptions.None; - protected override int BufferSize => 10; - protected override long AllocationSize => 1; } [ActiveIssue("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/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] @@ -242,16 +206,6 @@ public class UnbufferedAsyncFileStreamStandaloneConformanceTests : FileStreamSta { protected override FileOptions Options => FileOptions.Asynchronous; protected override int BufferSize => 1; - protected override long AllocationSize => 0; - } - - [ActiveIssue("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/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] - [PlatformSpecific(~TestPlatforms.Browser)] // copied from base class due to https://github.com/xunit/xunit/issues/2186 - public class UnbufferedPreallocatedAsyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests - { - protected override FileOptions Options => FileOptions.Asynchronous; - protected override int BufferSize => 1; - protected override long AllocationSize => 1; } [ActiveIssue("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/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] @@ -260,16 +214,6 @@ public class BufferedAsyncFileStreamStandaloneConformanceTests : FileStreamStand { protected override FileOptions Options => FileOptions.Asynchronous; protected override int BufferSize => 10; - protected override long AllocationSize => 0; - } - - [ActiveIssue("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/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] - [PlatformSpecific(~TestPlatforms.Browser)] // copied from base class due to https://github.com/xunit/xunit/issues/2186 - public class BufferedPreallocatedAsyncFileStreamStandaloneConformanceTests : FileStreamStandaloneConformanceTests - { - protected override FileOptions Options => FileOptions.Asynchronous; - protected override int BufferSize => 10; - protected override long AllocationSize => 1; } public class AnonymousPipeFileStreamConnectedConformanceTests : ConnectedStreamConformanceTests diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs index 0b4b1345ca907a..b61e4d28a7274e 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs @@ -12,6 +12,8 @@ protected virtual FileStream CreateFileStream(string path, FileMode mode) return new FileStream(path, mode); } + protected virtual long InitialLength => 0; + [Fact] public void NullPathThrows() { diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs new file mode 100644 index 00000000000000..22c2f2371e9086 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.IO.Tests +{ + public partial class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Windows : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize + { + protected override long AllocationSize => 10; + + protected override long InitialLength => 0; // Windows modifies AllocationSize, but not EndOfFile (file length) + + [Theory] + [InlineData(FileMode.Create, 0L)] + [InlineData(FileMode.Create, -1L)] + [InlineData(FileMode.CreateNew, 0L)] + [InlineData(FileMode.CreateNew, -1L)] + [InlineData(FileMode.OpenOrCreate, 0L)] + [InlineData(FileMode.OpenOrCreate, -1L)] + public unsafe void WhenFileIsCreatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet(FileMode mode, long allocationSize) + { + using (var fs = new FileStream(GetPathToNonExistingFile(), mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + { + Interop.Kernel32.FILE_STANDARD_INFO info; + + Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); + Assert.Equal(0, info.AllocationSize); + Assert.Equal(0, info.EndOfFile); + + Assert.Equal(0, fs.Length); + Assert.Equal(0, fs.Position); + } + } + + [Theory] + [InlineData(FileMode.Open, 0L)] + [InlineData(FileMode.Open, -1L)] + [InlineData(FileMode.Open, 1L)] + [InlineData(FileMode.OpenOrCreate, 0L)] + [InlineData(FileMode.OpenOrCreate, -1L)] + [InlineData(FileMode.OpenOrCreate, 1L)] + [InlineData(FileMode.Append, 0L)] + [InlineData(FileMode.Append, -1L)] + [InlineData(FileMode.Append, 1L)] + public unsafe void WhenExistingFileIsBeingOpenedWithAllocationSizeSpecifiedTheAllocationSizeIsNotChanged(FileMode mode, long allocationSize) + { + const int initialSize = 1; + string filePath = GetPathToNonExistingFile(); + File.WriteAllBytes(filePath, new byte[initialSize]); + long initialAllocationSize; + + using (var fs = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None)) // allocationSize NOT provided + { + Interop.Kernel32.FILE_STANDARD_INFO info; + + Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); + initialAllocationSize = info.AllocationSize; // just read it to ensure it's not being changed + } + + using (var fs = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + { + Interop.Kernel32.FILE_STANDARD_INFO info; + + Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); + Assert.Equal(initialAllocationSize, info.AllocationSize); // it has NOT been changed + Assert.Equal(initialSize, info.EndOfFile); + + Assert.Equal(initialSize, fs.Length); + Assert.Equal(mode == FileMode.Append ? initialSize : 0, fs.Position); + } + } + + [Theory] + [InlineData(FileMode.Create)] + [InlineData(FileMode.CreateNew)] + [InlineData(FileMode.OpenOrCreate)] + public unsafe void WhenFileIsCreatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet(FileMode mode) + { + const long allocationSize = 123; + + using (var fs = new FileStream(GetPathToNonExistingFile(), mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + { + Interop.Kernel32.FILE_STANDARD_INFO info; + + Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); + Assert.True(info.AllocationSize >= allocationSize); // Windows might allocate MORE than we have requested + Assert.Equal(0, info.EndOfFile); // Windows modifies AllocationSize, but not EndOfFile (file length) + + Assert.Equal(0, fs.Length); + Assert.Equal(0, fs.Position); + } + } + + [Theory] + [InlineData(FileMode.Create)] + [InlineData(FileMode.CreateNew)] + [InlineData(FileMode.OpenOrCreate)] + public void WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllDetails(FileMode mode) + { + const long tooMuch = 1024L * 1024L * 1024L * 1024L; // 1 TB + + string filePath = GetPathToNonExistingFile(); + + IOException ex = Assert.Throws(() => new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, tooMuch)); + Assert.Contains("disk was full", ex.Message); + Assert.Contains(filePath, ex.Message); + Assert.Contains(AllocationSize.ToString(), ex.Message); + + Assert.False(File.Exists(filePath)); // ensure it was NOT created + } + + [Theory] + [InlineData(0L)] + [InlineData(-1L)] + public unsafe void WhenFileIsTruncatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet(int allocationSize) + { + const int initialSize = 10_000; + + string filePath = GetPathToNonExistingFile(); + File.WriteAllBytes(filePath, new byte[initialSize]); + + using (var fs = new FileStream(filePath, FileMode.Truncate, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + { + Interop.Kernel32.FILE_STANDARD_INFO info; + + Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); + Assert.Equal(0, info.AllocationSize); + Assert.Equal(0, info.EndOfFile); + + Assert.Equal(0, fs.Length); + Assert.Equal(0, fs.Position); + } + } + + [Fact] + public unsafe void WhenFileIsTruncatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet() + { + const int initialSize = 10_000; // this must be more than 4kb which seems to be minimum allocaiton size on Windows + const long allocationSize = 100; + + string filePath = GetPathToNonExistingFile(); + File.WriteAllBytes(filePath, new byte[initialSize]); + + using (var fs = new FileStream(filePath, FileMode.Truncate, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + { + Interop.Kernel32.FILE_STANDARD_INFO info; + + Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); + Assert.True(info.AllocationSize >= allocationSize); + Assert.True(info.AllocationSize < initialSize); // less than initial file size (file got truncated) + Assert.Equal(0, info.EndOfFile); + + Assert.Equal(0, fs.Length); + Assert.Equal(0, fs.Position); + } + } + + private string GetPathToNonExistingFile() + { + string filePath = GetTestFilePath(); + + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + + return filePath; + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs new file mode 100644 index 00000000000000..365af5994cd5e6 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Tests +{ + public abstract class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize : FileStream_ctor_str_fm_fa_fs_buffer_fo + { + protected abstract long AllocationSize { get; } + + protected override long InitialLength => 0; + + protected override FileStream CreateFileStream(string path, FileMode mode) + => new FileStream(path, mode, mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite, allocationSize: AllocationSize); + + protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access) + => new FileStream(path, mode, access, allocationSize: AllocationSize); + + protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + => new FileStream(path, mode, access, share, bufferSize, options, allocationSize: AllocationSize); + } + + public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Default : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize + { + protected override long AllocationSize => 0; // specyfing 0 should have no effect + + protected override long InitialLength => 0; + } + + public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Negative : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize + { + protected override long AllocationSize => -1; // specyfing negative value should have no effect + + protected override long InitialLength => 0; + } +} 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 1389ee3462649e..2a56c825b5f7e4 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 @@ -17,6 +17,10 @@ + + + + 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 f22b4500be176b..8d7a83ad0c9572 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 @@ -18,6 +18,7 @@ + @@ -52,6 +53,11 @@ + + + + + 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 6be87f7cbccb3a..c5e2b589612e06 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 @@ -69,9 +69,9 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode { Debug.Assert(path != null); - if (allocationSize > 0) + if (allocationSize > 0 && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append) { - string prefixedAbsolutePath = PathInternal.IsExtended(path) ? path : @"\??\" + Path.GetFullPath(path); // TODO: we might consider getting rid of this managed allocation, + string prefixedAbsolutePath = PathInternal.IsExtended(path) ? path : @"\??\" + Path.GetFullPath(path); // we might consider getting rid of this managed allocation (uint ntStatus, IntPtr fileHandle) = Interop.NtDll.CreateFile(prefixedAbsolutePath, mode, access, share, options, allocationSize); if (ntStatus == 0) { From 3571a238fe7922ccc88386aae60ba17d1d2beff5 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 21 Apr 2021 15:26:35 +0200 Subject: [PATCH 15/37] implement Unix tests and make them pass by moving fallocate call after lock and after truncate --- .../tests/FileStream/ctor_str_fm.cs | 12 +- ..._fm_fa_fs_buffer_fo_AllocationSize.Unix.cs | 29 ++++ ..._fa_fs_buffer_fo_AllocationSize.Windows.cs | 160 +----------------- ...r_str_fm_fa_fs_buffer_fo_AllocationSize.cs | 155 ++++++++++++++++- ...stem.IO.FileSystem.Net5Compat.Tests.csproj | 4 +- .../tests/System.IO.FileSystem.Tests.csproj | 5 +- .../IO/Strategies/FileStreamHelpers.Unix.cs | 21 +-- .../Net5CompatFileStreamStrategy.Unix.cs | 17 +- .../Net5CompatFileStreamStrategy.Windows.cs | 2 +- .../Net5CompatFileStreamStrategy.cs | 4 +- 10 files changed, 221 insertions(+), 188 deletions(-) create mode 100644 src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs index b61e4d28a7274e..f442fbd4322190 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm.cs @@ -92,7 +92,7 @@ public void FileModeCreateExisting(string streamSpecifier) using (FileStream fs = CreateFileStream(fileName, FileMode.Create)) { // Ensure that the file was re-created - Assert.Equal(0L, fs.Length); + Assert.Equal(InitialLength, fs.Length); Assert.Equal(0L, fs.Position); Assert.True(fs.CanRead); Assert.True(fs.CanWrite); @@ -143,7 +143,7 @@ public void FileModeOpenExisting(string streamSpecifier) using (FileStream fs = CreateFileStream(fileName, FileMode.Open)) { // Ensure that the file was re-opened - Assert.Equal(1L, fs.Length); + Assert.Equal(Math.Max(1L, InitialLength), fs.Length); Assert.Equal(0L, fs.Position); Assert.True(fs.CanRead); Assert.True(fs.CanWrite); @@ -172,7 +172,7 @@ public void FileModeOpenOrCreateExisting(string streamSpecifier) using (FileStream fs = CreateFileStream(fileName, FileMode.OpenOrCreate)) { // Ensure that the file was re-opened - Assert.Equal(1L, fs.Length); + Assert.Equal(Math.Max(1L, InitialLength), fs.Length); Assert.Equal(0L, fs.Position); Assert.True(fs.CanRead); Assert.True(fs.CanWrite); @@ -199,7 +199,7 @@ public void FileModeTruncateExisting(string streamSpecifier) using (FileStream fs = CreateFileStream(fileName, FileMode.Truncate)) { // Ensure that the file was re-opened and truncated - Assert.Equal(0L, fs.Length); + Assert.Equal(InitialLength, fs.Length); Assert.Equal(0L, fs.Position); Assert.True(fs.CanRead); Assert.True(fs.CanWrite); @@ -228,8 +228,8 @@ public virtual void FileModeAppendExisting(string streamSpecifier) using (FileStream fs = CreateFileStream(fileName, FileMode.Append)) { // Ensure that the file was re-opened and position set to end - Assert.Equal(1L, fs.Length); - Assert.Equal(1L, fs.Position); + Assert.Equal(Math.Max(1L, InitialLength), fs.Length); + Assert.Equal(fs.Length, fs.Position); Assert.False(fs.CanRead); Assert.True(fs.CanSeek); Assert.True(fs.CanWrite); diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs new file mode 100644 index 00000000000000..d1fe7bd189dea1 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO.Tests +{ + public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Unix : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_OS + { + protected override long AllocationSize => 10; + + protected override long InitialLength => 10; + + protected override long GetExpectedFileLength(long allocationSize) + => OperatingSystem.IsLinux() + ? allocationSize // posix_fallocate sets file Length as well + : 0; // macOS and other Unixes + + protected override long GetAllocationSize(FileStream fileStream) + { + if (OperatingSystem.IsLinux()) + { + // posix_fallocate sets file Length as well + Interop.Sys.FStat(fileStream.SafeFileHandle, out Interop.Sys.FileStatus fileStatus); + return fileStatus.Size; + } + + throw new NotImplementedException("Need to implement it for macOS and other Unixes"); + } + } +} diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs index 22c2f2371e9086..49d32a48250733 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs @@ -5,167 +5,21 @@ namespace System.IO.Tests { - public partial class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Windows : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize + public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Windows : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_OS { protected override long AllocationSize => 10; - - protected override long InitialLength => 0; // Windows modifies AllocationSize, but not EndOfFile (file length) - - [Theory] - [InlineData(FileMode.Create, 0L)] - [InlineData(FileMode.Create, -1L)] - [InlineData(FileMode.CreateNew, 0L)] - [InlineData(FileMode.CreateNew, -1L)] - [InlineData(FileMode.OpenOrCreate, 0L)] - [InlineData(FileMode.OpenOrCreate, -1L)] - public unsafe void WhenFileIsCreatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet(FileMode mode, long allocationSize) - { - using (var fs = new FileStream(GetPathToNonExistingFile(), mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) - { - Interop.Kernel32.FILE_STANDARD_INFO info; - - Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); - Assert.Equal(0, info.AllocationSize); - Assert.Equal(0, info.EndOfFile); - - Assert.Equal(0, fs.Length); - Assert.Equal(0, fs.Position); - } - } - - [Theory] - [InlineData(FileMode.Open, 0L)] - [InlineData(FileMode.Open, -1L)] - [InlineData(FileMode.Open, 1L)] - [InlineData(FileMode.OpenOrCreate, 0L)] - [InlineData(FileMode.OpenOrCreate, -1L)] - [InlineData(FileMode.OpenOrCreate, 1L)] - [InlineData(FileMode.Append, 0L)] - [InlineData(FileMode.Append, -1L)] - [InlineData(FileMode.Append, 1L)] - public unsafe void WhenExistingFileIsBeingOpenedWithAllocationSizeSpecifiedTheAllocationSizeIsNotChanged(FileMode mode, long allocationSize) - { - const int initialSize = 1; - string filePath = GetPathToNonExistingFile(); - File.WriteAllBytes(filePath, new byte[initialSize]); - long initialAllocationSize; - - using (var fs = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None)) // allocationSize NOT provided - { - Interop.Kernel32.FILE_STANDARD_INFO info; - - Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); - initialAllocationSize = info.AllocationSize; // just read it to ensure it's not being changed - } - - using (var fs = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) - { - Interop.Kernel32.FILE_STANDARD_INFO info; - - Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); - Assert.Equal(initialAllocationSize, info.AllocationSize); // it has NOT been changed - Assert.Equal(initialSize, info.EndOfFile); - - Assert.Equal(initialSize, fs.Length); - Assert.Equal(mode == FileMode.Append ? initialSize : 0, fs.Position); - } - } - - [Theory] - [InlineData(FileMode.Create)] - [InlineData(FileMode.CreateNew)] - [InlineData(FileMode.OpenOrCreate)] - public unsafe void WhenFileIsCreatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet(FileMode mode) - { - const long allocationSize = 123; - - using (var fs = new FileStream(GetPathToNonExistingFile(), mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) - { - Interop.Kernel32.FILE_STANDARD_INFO info; - - Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); - Assert.True(info.AllocationSize >= allocationSize); // Windows might allocate MORE than we have requested - Assert.Equal(0, info.EndOfFile); // Windows modifies AllocationSize, but not EndOfFile (file length) - - Assert.Equal(0, fs.Length); - Assert.Equal(0, fs.Position); - } - } - [Theory] - [InlineData(FileMode.Create)] - [InlineData(FileMode.CreateNew)] - [InlineData(FileMode.OpenOrCreate)] - public void WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllDetails(FileMode mode) - { - const long tooMuch = 1024L * 1024L * 1024L * 1024L; // 1 TB - - string filePath = GetPathToNonExistingFile(); - - IOException ex = Assert.Throws(() => new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, tooMuch)); - Assert.Contains("disk was full", ex.Message); - Assert.Contains(filePath, ex.Message); - Assert.Contains(AllocationSize.ToString(), ex.Message); - - Assert.False(File.Exists(filePath)); // ensure it was NOT created - } - - [Theory] - [InlineData(0L)] - [InlineData(-1L)] - public unsafe void WhenFileIsTruncatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet(int allocationSize) - { - const int initialSize = 10_000; - - string filePath = GetPathToNonExistingFile(); - File.WriteAllBytes(filePath, new byte[initialSize]); - - using (var fs = new FileStream(filePath, FileMode.Truncate, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) - { - Interop.Kernel32.FILE_STANDARD_INFO info; - - Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); - Assert.Equal(0, info.AllocationSize); - Assert.Equal(0, info.EndOfFile); - - Assert.Equal(0, fs.Length); - Assert.Equal(0, fs.Position); - } - } - - [Fact] - public unsafe void WhenFileIsTruncatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet() - { - const int initialSize = 10_000; // this must be more than 4kb which seems to be minimum allocaiton size on Windows - const long allocationSize = 100; - - string filePath = GetPathToNonExistingFile(); - File.WriteAllBytes(filePath, new byte[initialSize]); - - using (var fs = new FileStream(filePath, FileMode.Truncate, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) - { - Interop.Kernel32.FILE_STANDARD_INFO info; - - Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fs.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); - Assert.True(info.AllocationSize >= allocationSize); - Assert.True(info.AllocationSize < initialSize); // less than initial file size (file got truncated) - Assert.Equal(0, info.EndOfFile); + protected override long InitialLength => 0; // Windows modifies AllocationSize, but not EndOfFile (file length) - Assert.Equal(0, fs.Length); - Assert.Equal(0, fs.Position); - } - } + protected override long GetExpectedFileLength(long allocationSize) => 0; // Windows modifies AllocationSize, but not EndOfFile (file length) - private string GetPathToNonExistingFile() + protected override unsafe Int64 GetAllocationSize(FileStream fileStream) { - string filePath = GetTestFilePath(); + Interop.Kernel32.FILE_STANDARD_INFO info; - if (File.Exists(filePath)) - { - File.Delete(filePath); - } + Assert.True(Interop.Kernel32.GetFileInformationByHandleEx(fileStream.SafeFileHandle, Interop.Kernel32.FileStandardInfo, &info, (uint)sizeof(Interop.Kernel32.FILE_STANDARD_INFO))); - return filePath; + return info.AllocationSize; } } } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs index 365af5994cd5e6..6c92f2cf864c4c 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs @@ -1,14 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Xunit; + namespace System.IO.Tests { public abstract class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize : FileStream_ctor_str_fm_fa_fs_buffer_fo { protected abstract long AllocationSize { get; } - protected override long InitialLength => 0; - protected override FileStream CreateFileStream(string path, FileMode mode) => new FileStream(path, mode, mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite, allocationSize: AllocationSize); @@ -21,15 +21,162 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Default : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize { - protected override long AllocationSize => 0; // specyfing 0 should have no effect + protected override long AllocationSize => 0; // specifying 0 should have no effect protected override long InitialLength => 0; } public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Negative : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize { - protected override long AllocationSize => -1; // specyfing negative value should have no effect + protected override long AllocationSize => -1; // specifying negative value should have no effect protected override long InitialLength => 0; } + + [CollectionDefinition("NoParallelTests", DisableParallelization = true)] + public partial class NoParallelTests { } + + // Don't run in parallel as the WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllDetails test + // consumes entire available free space on the disk and if we try to run other disk-writing test in the meantime + // we are going to get "No space left on device" exception. + [Collection("NoParallelTests")] + public abstract class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_OS : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize + { + protected abstract long GetAllocationSize(FileStream fileStream); + + protected abstract long GetExpectedFileLength(long allocationSize); + + [Theory] + [InlineData(FileMode.Create, 0L)] + [InlineData(FileMode.Create, -1L)] + [InlineData(FileMode.CreateNew, 0L)] + [InlineData(FileMode.CreateNew, -1L)] + [InlineData(FileMode.OpenOrCreate, 0L)] + [InlineData(FileMode.OpenOrCreate, -1L)] + public void WhenFileIsCreatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet(FileMode mode, long allocationSize) + { + using (var fs = new FileStream(GetPathToNonExistingFile(), mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + { + Assert.Equal(0, GetAllocationSize(fs)); + + Assert.Equal(0, fs.Length); + Assert.Equal(0, fs.Position); + } + } + + [Theory] + [InlineData(FileMode.Open, 0L)] + [InlineData(FileMode.Open, -1L)] + [InlineData(FileMode.Open, 1L)] + [InlineData(FileMode.OpenOrCreate, 0L)] + [InlineData(FileMode.OpenOrCreate, -1L)] + [InlineData(FileMode.OpenOrCreate, 1L)] + [InlineData(FileMode.Append, 0L)] + [InlineData(FileMode.Append, -1L)] + [InlineData(FileMode.Append, 1L)] + public void WhenExistingFileIsBeingOpenedWithAllocationSizeSpecifiedTheAllocationSizeIsNotChanged(FileMode mode, long allocationSize) + { + const int initialSize = 1; + string filePath = GetPathToNonExistingFile(); + File.WriteAllBytes(filePath, new byte[initialSize]); + long initialAllocationSize; + + using (var fs = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None)) // allocationSize NOT provided + { + initialAllocationSize = GetAllocationSize(fs); // just read it to ensure it's not being changed + } + + using (var fs = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + { + Assert.Equal(initialAllocationSize, GetAllocationSize(fs)); // it has NOT been changed + + Assert.Equal(initialSize, fs.Length); + Assert.Equal(mode == FileMode.Append ? initialSize : 0, fs.Position); + } + } + + [Theory] + [InlineData(FileMode.Create)] + [InlineData(FileMode.CreateNew)] + [InlineData(FileMode.OpenOrCreate)] + public void WhenFileIsCreatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet(FileMode mode) + { + const long allocationSize = 123; + + using (var fs = new FileStream(GetPathToNonExistingFile(), mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + { + Assert.True(GetAllocationSize(fs) >= allocationSize, $"Provided {allocationSize}, actual: {GetAllocationSize(fs)}"); // OS might allocate MORE than we have requested + + Assert.Equal(GetExpectedFileLength(allocationSize), fs.Length); + Assert.Equal(0, fs.Position); + } + } + + [Theory] + [InlineData(FileMode.Create)] + [InlineData(FileMode.CreateNew)] + [InlineData(FileMode.OpenOrCreate)] + public void WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllDetails(FileMode mode) + { + const long tooMuch = 1024L * 1024L * 1024L * 1024L; // 1 TB + + string filePath = GetPathToNonExistingFile(); + + IOException ex = Assert.Throws(() => new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, tooMuch)); + Assert.Contains("disk was full", ex.Message); + Assert.Contains(filePath, ex.Message); + Assert.Contains(AllocationSize.ToString(), ex.Message); + + Assert.False(File.Exists(filePath)); // ensure it was NOT created + } + + [Theory] + [InlineData(0L)] + [InlineData(-1L)] + public void WhenFileIsTruncatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet(int allocationSize) + { + const int initialSize = 10_000; + + string filePath = GetPathToNonExistingFile(); + File.WriteAllBytes(filePath, new byte[initialSize]); + + using (var fs = new FileStream(filePath, FileMode.Truncate, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + { + Assert.Equal(0, GetAllocationSize(fs)); + Assert.Equal(0, fs.Length); + Assert.Equal(0, fs.Position); + } + } + + [Fact] + public void WhenFileIsTruncatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet() + { + const int initialSize = 10_000; // this must be more than 4kb which seems to be minimum allocaiton size on Windows + const long allocationSize = 100; + + string filePath = GetPathToNonExistingFile(); + File.WriteAllBytes(filePath, new byte[initialSize]); + + using (var fs = new FileStream(filePath, FileMode.Truncate, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + { + Assert.True(GetAllocationSize(fs) >= allocationSize, $"Provided {allocationSize}, actual: {GetAllocationSize(fs)}"); + Assert.True(GetAllocationSize(fs) < initialSize); // less than initial file size (file got truncated) + + Assert.Equal(GetExpectedFileLength(allocationSize), fs.Length); + Assert.Equal(0, fs.Position); + } + } + + private string GetPathToNonExistingFile() + { + string filePath = GetTestFilePath(); + + if (File.Exists(filePath)) + { + File.Delete(filePath); + } + + return filePath; + } + } } 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 2a56c825b5f7e4..dfabeb02694b11 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 @@ -13,6 +13,8 @@ + + @@ -25,7 +27,7 @@ - + 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 8d7a83ad0c9572..be5ea5c06366ad 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 @@ -50,6 +50,9 @@ + + + @@ -61,7 +64,7 @@ - + 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 37e2d69eea05fb..a4b088bbab82a3 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 @@ -14,8 +14,6 @@ namespace System.IO.Strategies // this type defines a set of stateless FileStream/FileStreamStrategy helper methods internal static partial class FileStreamHelpers { - private static readonly int ENOSPC = OperatingSystem.IsLinux() ? 28 : (int)Interop.Error.ENOSPC; // Linux error code != Unix error code - // in the future we are most probably going to introduce more strategies (io_uring etc) private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) => new Net5CompatFileStreamStrategy(handle, access, bufferSize, isAsync); @@ -23,7 +21,7 @@ private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, File private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long allocationSize) => new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize, options, allocationSize); - internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) + internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) { // Translate the arguments into arguments for an open call. Interop.Sys.OpenFlags openFlags = PreOpenConfigurationFromOptions(mode, access, share, options); @@ -37,22 +35,7 @@ internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess Interop.Sys.Permissions.S_IRGRP | Interop.Sys.Permissions.S_IWGRP | Interop.Sys.Permissions.S_IROTH | Interop.Sys.Permissions.S_IWOTH; - // Open the file and store the safe handle. - SafeFileHandle handle = SafeFileHandle.Open(path!, openFlags, (int)OpenPermissions); - // If allocationSize has been provided for a creatable and writeable file - if (allocationSize > 0 && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append) - { - if (Interop.Sys.FAllocate(handle, 0, allocationSize) == ENOSPC) - { - handle.Dispose(); - Interop.Sys.Unlink(path); // remove the file to mimic Windows behaviour (atomic operation) - - throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, path, allocationSize)); - } - // ignore not supported and other failures (pipe etc) - } - - return handle; + return SafeFileHandle.Open(path!, openFlags, (int)OpenPermissions); } internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsync) => handle.IsAsync ?? defaultIsAsync; 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 eb1cebc37a7f89..b414fd7097c905 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 @@ -39,7 +39,8 @@ internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy /// 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. - private void Init(FileMode mode, FileShare share, string originalPath, FileOptions options) + /// passed to posix_fallocate + private void Init(FileMode mode, FileShare share, string originalPath, FileOptions options, long allocationSize) { // 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.) @@ -103,6 +104,20 @@ private void Init(FileMode mode, FileShare share, string originalPath, FileOptio } } } + + // If allocationSize has been provided for a creatable and writeable file + if (allocationSize > 0 && (_access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append) + { + int ENOSPC = OperatingSystem.IsLinux() ? 28 : (int)Interop.Error.ENOSPC; // Linux error code != Unix error code + if (Interop.Sys.FAllocate(_fileHandle, 0, allocationSize) == ENOSPC) + { + _fileHandle.Dispose(); + Interop.Sys.Unlink(_path!); // remove the file to mimic Windows behaviour (atomic operation) + + throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, _path, allocationSize)); + } + // ignore not supported and other failures (pipe etc) + } } /// Initializes a stream from an already open file handle (file descriptor). 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 fead01218fb0c3..acb952a4e1dad7 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 @@ -46,7 +46,7 @@ internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy 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) + private void Init(FileMode mode, FileShare share, string originalPath, FileOptions options, long allocationSize) { FileStreamHelpers.ValidateFileTypeForNonExtendedPaths(_fileHandle, originalPath); 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 58105ad890f294..f933c8c65b9189 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs @@ -89,11 +89,11 @@ internal Net5CompatFileStreamStrategy(string path, FileMode mode, FileAccess acc if ((options & FileOptions.Asynchronous) != 0) _useAsyncIO = true; - _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options, allocationSize); + _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options); try { - Init(mode, share, path, options); + Init(mode, share, path, options, allocationSize); } catch { From cbe22777dba7f949352f64e8da9ebefb48d698e4 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 21 Apr 2021 18:02:29 +0200 Subject: [PATCH 16/37] fix the Windows build... --- .../src/System/IO/Strategies/FileStreamHelpers.Unix.cs | 2 +- .../src/System/IO/Strategies/Net5CompatFileStreamStrategy.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 a4b088bbab82a3..a82303b10d8ee2 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 @@ -21,7 +21,7 @@ private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, File private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long allocationSize) => new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize, options, allocationSize); - internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options) + internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) { // Translate the arguments into arguments for an open call. Interop.Sys.OpenFlags openFlags = PreOpenConfigurationFromOptions(mode, access, share, options); 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 f933c8c65b9189..056c1ab443e2fc 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,7 +89,7 @@ internal Net5CompatFileStreamStrategy(string path, FileMode mode, FileAccess acc if ((options & FileOptions.Asynchronous) != 0) _useAsyncIO = true; - _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options); + _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options, allocationSize); try { From c490439de1e629ca9098d271007622fc71626d65 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 21 Apr 2021 19:10:53 +0200 Subject: [PATCH 17/37] align macOS implementation with Linux --- src/libraries/Native/Unix/System.Native/pal_io.c | 5 +++++ ..._str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs | 15 +++------------ .../Net5CompatFileStreamStrategy.Unix.cs | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.c b/src/libraries/Native/Unix/System.Native/pal_io.c index c836e9b38d97c6..65848b1827e534 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.c +++ b/src/libraries/Native/Unix/System.Native/pal_io.c @@ -1038,6 +1038,11 @@ int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length) { result = errno; } + else + { + // align the behaviour with what posix_fallocate does + ftruncate(fileDescriptor, length); + } #endif return result; diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs index d1fe7bd189dea1..8e5d5a170485f0 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs @@ -9,21 +9,12 @@ public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Unix : FileSt protected override long InitialLength => 10; - protected override long GetExpectedFileLength(long allocationSize) - => OperatingSystem.IsLinux() - ? allocationSize // posix_fallocate sets file Length as well - : 0; // macOS and other Unixes + protected override long GetExpectedFileLength(long allocationSize) => allocationSize; protected override long GetAllocationSize(FileStream fileStream) { - if (OperatingSystem.IsLinux()) - { - // posix_fallocate sets file Length as well - Interop.Sys.FStat(fileStream.SafeFileHandle, out Interop.Sys.FileStatus fileStatus); - return fileStatus.Size; - } - - throw new NotImplementedException("Need to implement it for macOS and other Unixes"); + Interop.Sys.FStat(fileStream.SafeFileHandle, out Interop.Sys.FileStatus fileStatus); + return fileStatus.Size; } } } 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 b414fd7097c905..c521a3ec546425 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 @@ -108,7 +108,7 @@ private void Init(FileMode mode, FileShare share, string originalPath, FileOptio // If allocationSize has been provided for a creatable and writeable file if (allocationSize > 0 && (_access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append) { - int ENOSPC = OperatingSystem.IsLinux() ? 28 : (int)Interop.Error.ENOSPC; // Linux error code != Unix error code + const int ENOSPC = 28; // != Interop.Error.ENOSPC if (Interop.Sys.FAllocate(_fileHandle, 0, allocationSize) == ENOSPC) { _fileHandle.Dispose(); From 7842eba408c770493555770bb0f686e78cca852d Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 22 Apr 2021 10:45:41 +0200 Subject: [PATCH 18/37] polishing --- ...=> ctor_str_fm_fa_fs_buffer_fo_as.Unix.cs} | 7 +-- ...ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs} | 6 +-- ...e.cs => ctor_str_fm_fa_fs_buffer_fo_as.cs} | 43 +++++++++---------- .../tests/System.IO.FileSystem.Tests.csproj | 6 +-- .../src/System/IO/FileStream.cs | 4 +- .../IO/Strategies/FileStreamHelpers.Unix.cs | 1 - 6 files changed, 33 insertions(+), 34 deletions(-) rename src/libraries/System.IO.FileSystem/tests/FileStream/{ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs => ctor_str_fm_fa_fs_buffer_fo_as.Unix.cs} (54%) rename src/libraries/System.IO.FileSystem/tests/FileStream/{ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs => ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs} (64%) rename src/libraries/System.IO.FileSystem/tests/FileStream/{ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs => ctor_str_fm_fa_fs_buffer_fo_as.cs} (76%) diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Unix.cs similarity index 54% rename from src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs rename to src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Unix.cs index 8e5d5a170485f0..7a663aba85ebfb 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Unix.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Unix.cs @@ -3,16 +3,17 @@ namespace System.IO.Tests { - public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Unix : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_OS + public partial class FileStream_ctor_str_fm_fa_fs_buffer_fo_as : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base { protected override long AllocationSize => 10; protected override long InitialLength => 10; - protected override long GetExpectedFileLength(long allocationSize) => allocationSize; + private long GetExpectedFileLength(long allocationSize) => allocationSize; - protected override long GetAllocationSize(FileStream fileStream) + private long GetActualAllocationSize(FileStream fileStream) { + // On Unix posix_fallocate modifies file length and we are using fstat to get it for verificaiton Interop.Sys.FStat(fileStream.SafeFileHandle, out Interop.Sys.FileStatus fileStatus); return fileStatus.Size; } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs similarity index 64% rename from src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs rename to src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs index 49d32a48250733..8475f31aa48ae4 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.Windows.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs @@ -5,15 +5,15 @@ namespace System.IO.Tests { - public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Windows : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_OS + public partial class FileStream_ctor_str_fm_fa_fs_buffer_fo_as : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base { protected override long AllocationSize => 10; protected override long InitialLength => 0; // Windows modifies AllocationSize, but not EndOfFile (file length) - protected override long GetExpectedFileLength(long allocationSize) => 0; // Windows modifies AllocationSize, but not EndOfFile (file length) + private long GetExpectedFileLength(long allocationSize) => 0; // Windows modifies AllocationSize, but not EndOfFile (file length) - protected override unsafe Int64 GetAllocationSize(FileStream fileStream) + private unsafe long GetActualAllocationSize(FileStream fileStream) { Interop.Kernel32.FILE_STANDARD_INFO info; diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs similarity index 76% rename from src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs rename to src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs index 6c92f2cf864c4c..367cd46fa843ef 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_AllocationSize.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs @@ -5,7 +5,7 @@ namespace System.IO.Tests { - public abstract class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize : FileStream_ctor_str_fm_fa_fs_buffer_fo + public abstract class FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base : FileStream_ctor_str_fm_fa_fs_buffer_fo { protected abstract long AllocationSize { get; } @@ -19,14 +19,14 @@ protected override FileStream CreateFileStream(string path, FileMode mode, FileA => new FileStream(path, mode, access, share, bufferSize, options, allocationSize: AllocationSize); } - public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Default : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize + public class FileStream_ctor_str_fm_fa_fs_buffer_fo_as_zero : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base { protected override long AllocationSize => 0; // specifying 0 should have no effect protected override long InitialLength => 0; } - public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Negative : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize + public class FileStream_ctor_str_fm_fa_fs_buffer_fo_as_negative : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base { protected override long AllocationSize => -1; // specifying negative value should have no effect @@ -37,15 +37,11 @@ public class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_Negative : Fi public partial class NoParallelTests { } // Don't run in parallel as the WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllDetails test - // consumes entire available free space on the disk and if we try to run other disk-writing test in the meantime - // we are going to get "No space left on device" exception. + // consumes entire available free space on the disk (only on Linux, this is how posix_fallocate works) + // and if we try to run other disk-writing test in the meantime we are going to get "No space left on device" exception. [Collection("NoParallelTests")] - public abstract class FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize_OS : FileStream_ctor_str_fm_fa_fs_buffer_fo_AllocationSize + public partial class FileStream_ctor_str_fm_fa_fs_buffer_fo_as : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base { - protected abstract long GetAllocationSize(FileStream fileStream); - - protected abstract long GetExpectedFileLength(long allocationSize); - [Theory] [InlineData(FileMode.Create, 0L)] [InlineData(FileMode.Create, -1L)] @@ -57,8 +53,7 @@ public void WhenFileIsCreatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNo { using (var fs = new FileStream(GetPathToNonExistingFile(), mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) { - Assert.Equal(0, GetAllocationSize(fs)); - + Assert.Equal(0, GetActualAllocationSize(fs)); Assert.Equal(0, fs.Length); Assert.Equal(0, fs.Position); } @@ -83,13 +78,12 @@ public void WhenExistingFileIsBeingOpenedWithAllocationSizeSpecifiedTheAllocatio using (var fs = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None)) // allocationSize NOT provided { - initialAllocationSize = GetAllocationSize(fs); // just read it to ensure it's not being changed + initialAllocationSize = GetActualAllocationSize(fs); // just read it to ensure it's not being changed } using (var fs = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) { - Assert.Equal(initialAllocationSize, GetAllocationSize(fs)); // it has NOT been changed - + Assert.Equal(initialAllocationSize, GetActualAllocationSize(fs)); // it has NOT been changed Assert.Equal(initialSize, fs.Length); Assert.Equal(mode == FileMode.Append ? initialSize : 0, fs.Position); } @@ -105,13 +99,18 @@ public void WhenFileIsCreatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet(F using (var fs = new FileStream(GetPathToNonExistingFile(), mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) { - Assert.True(GetAllocationSize(fs) >= allocationSize, $"Provided {allocationSize}, actual: {GetAllocationSize(fs)}"); // OS might allocate MORE than we have requested - + // OS might allocate MORE than we have requested + Assert.True(GetActualAllocationSize(fs) >= allocationSize, $"Provided {allocationSize}, actual: {GetActualAllocationSize(fs)}"); Assert.Equal(GetExpectedFileLength(allocationSize), fs.Length); Assert.Equal(0, fs.Position); } } + [OuterLoop("Might allocate 1 TB file if there is enough space on the disk")] + // macOS fcntl doc does not mention ENOSPC error: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/fcntl.2.html + // But depending on the OS version, it might actually return it. + // Since we don't want to have unstable tests, it's better to not run it on macOS at all. + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] [Theory] [InlineData(FileMode.Create)] [InlineData(FileMode.CreateNew)] @@ -127,7 +126,7 @@ public void WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllD Assert.Contains(filePath, ex.Message); Assert.Contains(AllocationSize.ToString(), ex.Message); - Assert.False(File.Exists(filePath)); // ensure it was NOT created + Assert.False(File.Exists(filePath)); // ensure it was NOT created (provided OOTB by Windows, emulated on Unix) } [Theory] @@ -142,7 +141,7 @@ public void WhenFileIsTruncatedWithoutAllocationSizeSpecifiedTheAllocationSizeIs using (var fs = new FileStream(filePath, FileMode.Truncate, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) { - Assert.Equal(0, GetAllocationSize(fs)); + Assert.Equal(0, GetActualAllocationSize(fs)); Assert.Equal(0, fs.Length); Assert.Equal(0, fs.Position); } @@ -159,9 +158,9 @@ public void WhenFileIsTruncatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet using (var fs = new FileStream(filePath, FileMode.Truncate, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) { - Assert.True(GetAllocationSize(fs) >= allocationSize, $"Provided {allocationSize}, actual: {GetAllocationSize(fs)}"); - Assert.True(GetAllocationSize(fs) < initialSize); // less than initial file size (file got truncated) - + Assert.True(GetActualAllocationSize(fs) >= allocationSize, $"Provided {allocationSize}, actual: {GetActualAllocationSize(fs)}"); + // less than initial file size (file got truncated) + Assert.True(GetActualAllocationSize(fs) < initialSize, $"initialSize {initialSize}, actual: {GetActualAllocationSize(fs)}"); Assert.Equal(GetExpectedFileLength(allocationSize), fs.Length); Assert.Equal(0, fs.Position); } 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 be5ea5c06366ad..45bad638a71cee 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 @@ -18,7 +18,7 @@ - + @@ -50,13 +50,13 @@ - + - + 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 d1eaf1f11f3cbb..8ae1e5e9c39707 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -143,7 +143,7 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. The default value is Read. /// A positive value greater than 0 indicating the buffer size. The default buffer size is 4096. /// A bitwise combination of the enumeration values that specifies additional file options. The default value is None which means synchronous IO. - /// The initial allocation size in bytes for the file. A nonzero value has no effect unless the file is being created, overwritten, or superseded. + /// The initial allocation size in bytes for the file. A value has no effect unless it's positive and a regular file is being created, overwritten, or replaced. /// is . /// is an empty string (""), contains only white space, or contains one or more invalid characters. /// -or- @@ -157,7 +157,7 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share /// -or- /// The stream has been closed. /// -or- - /// The disk was full. + /// The disk was full (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. 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 a82303b10d8ee2..f540162e3401bb 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 @@ -7,7 +7,6 @@ using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; -using Internal.IO; namespace System.IO.Strategies { From b2cc9f3d4c9afe783d1b2c874453723e5dc855b4 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Thu, 22 Apr 2021 19:44:52 +0200 Subject: [PATCH 19/37] handle files that are too large for the current file system --- .../src/Interop/Windows/Interop.Errors.cs | 2 + .../ctor_str_fm_fa_fs_buffer_fo_as.cs | 6 +- .../src/Resources/Strings.resx | 3 + .../System.Private.CoreLib.Shared.projitems | 3 + .../Strategies/FileStreamHelpers.Windows.cs | 64 ++++++++++++++++--- 5 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs b/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs index 2186d54dfb3282..338706ea8491bc 100644 --- a/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs +++ b/src/libraries/Common/src/Interop/Windows/Interop.Errors.cs @@ -29,6 +29,7 @@ internal static partial class Errors internal const int ERROR_FILE_EXISTS = 0x50; internal const int ERROR_INVALID_PARAMETER = 0x57; internal const int ERROR_BROKEN_PIPE = 0x6D; + internal const int ERROR_DISK_FULL = 0x70; internal const int ERROR_SEM_TIMEOUT = 0x79; internal const int ERROR_CALL_NOT_IMPLEMENTED = 0x78; internal const int ERROR_INSUFFICIENT_BUFFER = 0x7A; @@ -43,6 +44,7 @@ internal static partial class Errors internal const int ERROR_ENVVAR_NOT_FOUND = 0xCB; internal const int ERROR_FILENAME_EXCED_RANGE = 0xCE; internal const int ERROR_EXE_MACHINE_TYPE_MISMATCH = 0xD8; + internal const int ERROR_FILE_TOO_LARGE = 0xDF; internal const int ERROR_PIPE_BUSY = 0xE7; internal const int ERROR_NO_DATA = 0xE8; internal const int ERROR_PIPE_NOT_CONNECTED = 0xE9; diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs index 367cd46fa843ef..7f998a3f640e18 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs @@ -36,7 +36,7 @@ public class FileStream_ctor_str_fm_fa_fs_buffer_fo_as_negative : FileStream_cto [CollectionDefinition("NoParallelTests", DisableParallelization = true)] public partial class NoParallelTests { } - // Don't run in parallel as the WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllDetails test + // Don't run in parallel as the WhenDiskIsFullTheErrorMessageContainsAllDetails test // consumes entire available free space on the disk (only on Linux, this is how posix_fallocate works) // and if we try to run other disk-writing test in the meantime we are going to get "No space left on device" exception. [Collection("NoParallelTests")] @@ -115,7 +115,7 @@ public void WhenFileIsCreatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet(F [InlineData(FileMode.Create)] [InlineData(FileMode.CreateNew)] [InlineData(FileMode.OpenOrCreate)] - public void WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllDetails(FileMode mode) + public void WhenDiskIsFullTheErrorMessageContainsAllDetails(FileMode mode) { const long tooMuch = 1024L * 1024L * 1024L * 1024L; // 1 TB @@ -124,7 +124,7 @@ public void WhenFileStreamFailsToPreallocateDiskSpaceTheErrorMessageContainsAllD IOException ex = Assert.Throws(() => new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, tooMuch)); Assert.Contains("disk was full", ex.Message); Assert.Contains(filePath, ex.Message); - Assert.Contains(AllocationSize.ToString(), ex.Message); + Assert.Contains(tooMuch.ToString(), ex.Message); Assert.False(File.Exists(filePath)); // ensure it was NOT created (provided OOTB by Windows, emulated on Unix) } diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index dc28d0991893f0..6c93a5e9ea95a6 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -2647,6 +2647,9 @@ Failed to create '{0}' with allocation size '{1}' because the disk was full. + + Failed to create '{0}' with allocation size '{1}' because the file was too large. + BindHandle for ThreadPool failed on this handle. 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 9fd08f445a9d8d..3136838f562197 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 @@ -1347,6 +1347,9 @@ Common\Interop\Windows\NtDll\Interop.NtCreateFile.cs + + Common\Interop\Windows\Kernel32\Interop.DeleteFile.cs + Common\Interop\Windows\Kernel32\Interop.CriticalSection.cs 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 c5e2b589612e06..b29464771d4248 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 @@ -27,7 +27,9 @@ internal static class TaskSourceCodes internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32; } - private const uint ERROR_STATUS_DISK_FULL = 0xC000007F; + private const uint NT_ERROR_STATUS_DISK_FULL = 0xC000007F; + private const uint NT_ERROR_STATUS_FILE_TOO_LARGE = 0xC0000904; + private const uint NT_STATUS_INVALID_PARAMETER = 0xC000000D; private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) { @@ -69,18 +71,23 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode { Debug.Assert(path != null); + uint ntStatus = 0; if (allocationSize > 0 && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append) { string prefixedAbsolutePath = PathInternal.IsExtended(path) ? path : @"\??\" + Path.GetFullPath(path); // we might consider getting rid of this managed allocation - (uint ntStatus, IntPtr fileHandle) = Interop.NtDll.CreateFile(prefixedAbsolutePath, mode, access, share, options, allocationSize); + (ntStatus, IntPtr fileHandle) = Interop.NtDll.CreateFile(prefixedAbsolutePath, mode, access, share, options, allocationSize); if (ntStatus == 0) { return ValidateFileHandle(new SafeFileHandle(fileHandle, ownsHandle: true), path, (options & FileOptions.Asynchronous) != 0); } - else if (ntStatus == ERROR_STATUS_DISK_FULL) + else if (ntStatus == NT_ERROR_STATUS_DISK_FULL) { throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, path, allocationSize)); } + else if (ntStatus == NT_ERROR_STATUS_FILE_TOO_LARGE) + { + throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, path, allocationSize)); + } // NtCreateFile has failed for some other reason than a full disk. // Instead of implementing the mapping for every NS Status value (there are plenty of them: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55) @@ -110,7 +117,34 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode // (note that this is the effective default on CreateFile2) flagsAndAttributes |= (Interop.Kernel32.SecurityOptions.SECURITY_SQOS_PRESENT | Interop.Kernel32.SecurityOptions.SECURITY_ANONYMOUS); - return ValidateFileHandle(Interop.Kernel32.CreateFile(path, fAccess, share, &secAttrs, mode, flagsAndAttributes, IntPtr.Zero), path, (options & FileOptions.Asynchronous) != 0); + SafeFileHandle safeFileHandle = ValidateFileHandle( + Interop.Kernel32.CreateFile(path, fAccess, share, &secAttrs, mode, flagsAndAttributes, IntPtr.Zero), path, (options & FileOptions.Asynchronous) != 0); + + if (ntStatus == NT_STATUS_INVALID_PARAMETER) + { + // It seems that 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. + // Since Linux reports EFBIG for such cases, we are using the following workaround to get the right exception. + // TrySetFileLength uses SetFileInformationByHandle which fails with a clear error if the file is too big. + if (!TrySetFileLength(safeFileHandle, path, allocationSize, out int errorCode)) + { + // Since we have failed to extend the file, we mimic the NtCreateFile behaviour + // which does not create a file if there is not enough space on the disk. + // So we close the handle, remove the file and then throw an exception. + safeFileHandle.Dispose(); + Interop.Kernel32.DeleteFile(path); + + if (errorCode == Interop.Errors.ERROR_FILE_TOO_LARGE // this is what we would expect in such a case + || errorCode == Interop.Errors.ERROR_DISK_FULL) // but this is what we get (verified with Windows 10.0.18363.1500) + { + throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, path, allocationSize)); + } + + throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); + } + } + + return safeFileHandle; } } @@ -339,6 +373,19 @@ internal static void GetFileTypeSpecificInformation(SafeFileHandle handle, out b } internal static unsafe void SetFileLength(SafeFileHandle handle, string? path, long length) + { + if (!TrySetFileLength(handle, path, length, out int errorCode)) + { + if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) + { + throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_FileLengthTooBig); + } + + throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); + } + } + + private static unsafe bool TrySetFileLength(SafeFileHandle handle, string? path, long length, out int errorCode) { var eofInfo = new Interop.Kernel32.FILE_END_OF_FILE_INFO { @@ -351,11 +398,12 @@ internal static unsafe void SetFileLength(SafeFileHandle handle, string? path, l &eofInfo, (uint)sizeof(Interop.Kernel32.FILE_END_OF_FILE_INFO))) { - int errorCode = Marshal.GetLastWin32Error(); - if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) - throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_FileLengthTooBig); - throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); + errorCode = Marshal.GetLastWin32Error(); + return false; } + + errorCode = 0; + return true; } // __ConsoleStream also uses this code. From ed5808fbb13329510b12ff96290f5d3e5bc03dad Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 23 Apr 2021 12:07:44 +0200 Subject: [PATCH 20/37] add a test for "file was too large" exception --- .../ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs index 8475f31aa48ae4..cba7265f2953a3 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs @@ -1,6 +1,8 @@ // 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; +using System.Text; using Xunit; namespace System.IO.Tests @@ -21,5 +23,63 @@ private unsafe long GetActualAllocationSize(FileStream fileStream) return info.AllocationSize; } + + [PlatformSpecific(TestPlatforms.Windows)] + [ConditionalTheory(nameof(IsFat32))] + [InlineData(FileMode.Create)] + [InlineData(FileMode.CreateNew)] + [InlineData(FileMode.OpenOrCreate)] + public void WhenFileIsTooLargeTheErrorMessageContainsAllDetails(FileMode mode) + { + const long tooMuch = uint.MaxValue + 1L; // more than FAT32 max size + + string filePath = GetPathToNonExistingFile(); + Assert.StartsWith(Path.GetTempPath(), filePath); // this is what IsFat32 method relies on + + IOException ex = Assert.Throws(() => new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, tooMuch)); + Assert.Contains("file was too large", ex.Message); + Assert.Contains(filePath, ex.Message); + Assert.Contains(tooMuch.ToString(), ex.Message); + + Assert.False(File.Exists(filePath)); // ensure it was NOT created + } + + public static bool IsFat32 + { + get + { + string testDirectory = Path.GetTempPath(); // logic taken from FileCleanupTestBase, can't call the property here as it's not static + + var volumeNameBufffer = new StringBuilder(250); + var fileSystemNameBuffer = new StringBuilder(250); + + if (GetVolumeInformation( + Path.GetPathRoot(testDirectory), + volumeNameBufffer, + volumeNameBufffer.Capacity, + out uint _, + out uint _, + out uint _, + fileSystemNameBuffer, + fileSystemNameBuffer.Capacity + )) + { + return fileSystemNameBuffer.ToString().Equals("FAT32", StringComparison.OrdinalIgnoreCase); + } + + return false; + } + } + + [DllImport(Interop.Libraries.Kernel32, CharSet = CharSet.Auto, SetLastError = true)] + public extern static bool GetVolumeInformation( + string rootPathName, + StringBuilder volumeNameBuffer, + int volumeNameSize, + out uint volumeSerialNumber, + out uint maximumComponentLength, + out uint fileSystemFlags, + StringBuilder fileSystemNameBuffer, + int fileSystemNameSize); } } From e250d96d7ee8ce509c58839f51a0b6532cdb0139 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 23 Apr 2021 12:41:37 +0200 Subject: [PATCH 21/37] and handling for EFBIG --- ...FAllocate.cs => Interop.PosixFAllocate.cs} | 7 ++++-- .../Native/Unix/System.Native/entrypoints.c | 2 +- .../Native/Unix/System.Native/pal_io.c | 25 +++++++++++++++++-- .../Native/Unix/System.Native/pal_io.h | 4 +-- .../System.Private.CoreLib.Shared.projitems | 4 +-- .../src/System/IO/FileStream.cs | 4 ++- .../Strategies/FileStreamHelpers.Windows.cs | 2 +- .../Net5CompatFileStreamStrategy.Unix.cs | 14 ++++++++--- 8 files changed, 48 insertions(+), 14 deletions(-) rename src/libraries/Common/src/Interop/Unix/System.Native/{Interop.FAllocate.cs => Interop.PosixFAllocate.cs} (56%) diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PosixFAllocate.cs similarity index 56% rename from src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs rename to src/libraries/Common/src/Interop/Unix/System.Native/Interop.PosixFAllocate.cs index 8554f1fe66a53a..2866ed38935bdf 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.FAllocate.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.PosixFAllocate.cs @@ -8,7 +8,10 @@ internal static partial class Interop { internal static partial class Sys { - [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_FAllocate", SetLastError = false /* this is explicitly called out in the man page */)] - internal static extern int FAllocate(SafeFileHandle fd, long offset, long length); + /// + /// Returns -1 on ENOSPC, -2 on EFBIG. On success or ignorable error, 0 is returned. + /// + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_PosixFAllocate", SetLastError = false)] + internal static extern int PosixFAllocate(SafeFileHandle fd, long offset, long length); } } diff --git a/src/libraries/Native/Unix/System.Native/entrypoints.c b/src/libraries/Native/Unix/System.Native/entrypoints.c index 3ec11f58e2ebd9..718fbafae03e96 100644 --- a/src/libraries/Native/Unix/System.Native/entrypoints.c +++ b/src/libraries/Native/Unix/System.Native/entrypoints.c @@ -90,7 +90,7 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_FTruncate) DllImportEntry(SystemNative_Poll) DllImportEntry(SystemNative_PosixFAdvise) - DllImportEntry(SystemNative_FAllocate) + DllImportEntry(SystemNative_PosixFAllocate) DllImportEntry(SystemNative_Read) DllImportEntry(SystemNative_ReadLink) DllImportEntry(SystemNative_Rename) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.c b/src/libraries/Native/Unix/System.Native/pal_io.c index 65848b1827e534..af59618952c9a3 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.c +++ b/src/libraries/Native/Unix/System.Native/pal_io.c @@ -991,7 +991,7 @@ int32_t SystemNative_PosixFAdvise(intptr_t fd, int64_t offset, int64_t length, i #endif } -int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length) +int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length) { int fileDescriptor = ToFileDescriptor(fd); int32_t result; @@ -1045,7 +1045,28 @@ int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length) } #endif - return result; + // error codes can be OS-specific, so this is why this handling is done here rather than in the managed layer + switch (result) + { + case ENOSPC: // there was not enough space + return -1; + case EFBIG: // the file was too large + return -2; + case ENODEV: // not a regular file + case ESPIPE: // a pipe + // We ignore it, as FileStream contract makes it clear that allocationSize is ignored for non-regular files. + return 0; + case EINVAL: + // We control the offset and length so they are correct. + assert_msg(offset != 0, "Invalid offset value", (int)offset); + assert_msg(length >= 0, "Invalid length value", (int)length); + // But if the underlying filesystem does not support the operation, we just ignore it and treat as a hint. + return 0; + default: + assert(result != EINTR); // it can't happen here as we retry the call on EINTR + assert(result != EBADF); // it can't happen here as this method is being called after a succesfull call to open (with write permissions) before returning the SafeFileHandle to the user + return 0; + } } int32_t SystemNative_Read(intptr_t fd, void* buffer, int32_t bufferSize) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.h b/src/libraries/Native/Unix/System.Native/pal_io.h index 313aa5a6179f6d..e3d402d9be5f66 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.h +++ b/src/libraries/Native/Unix/System.Native/pal_io.h @@ -607,9 +607,9 @@ PALEXPORT int32_t SystemNative_PosixFAdvise(intptr_t fd, int64_t offset, int64_t /** * Ensures that disk space is allocated. * - * Returns 0 on success; otherwise, the error code is returned and errno is NOT set. + * Returns -1 on ENOSPC, -2 on EFBIG. On success or ignorable error, 0 is returned. */ -PALEXPORT int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t length); +PALEXPORT int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length); /** * Reads the number of bytes specified into the provided buffer from the specified, opened file descriptor. 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 3136838f562197..3a0762c2150a4d 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 @@ -1824,8 +1824,8 @@ Common\Interop\Unix\System.Native\Interop.PosixFAdvise.cs - - Common\Interop\Unix\System.Native\Interop.FAllocate.cs + + Common\Interop\Unix\System.Native\Interop.PosixFAllocate.cs Common\Interop\Unix\System.Native\Interop.Read.cs 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 8ae1e5e9c39707..d2280b5dbf8f28 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -157,7 +157,9 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share /// -or- /// The stream has been closed. /// -or- - /// The disk was full (when was provided and was pointing to a regular file). + /// 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. 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 b29464771d4248..03b3303e15732b 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 @@ -89,7 +89,7 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, path, allocationSize)); } - // NtCreateFile has failed for some other reason than a full disk. + // NtCreateFile has failed for some other reason than a full disk or too large file. // Instead of implementing the mapping for every NS Status value (there are plenty of them: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55) // or using RtlNtStatusToDosError & GetExceptionForWin32Error // the code falls back to CreateFileW that just throws the right exception. 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 c521a3ec546425..6219835c018a1f 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 @@ -108,13 +108,21 @@ private void Init(FileMode mode, FileShare share, string originalPath, FileOptio // If allocationSize has been provided for a creatable and writeable file if (allocationSize > 0 && (_access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append) { - const int ENOSPC = 28; // != Interop.Error.ENOSPC - if (Interop.Sys.FAllocate(_fileHandle, 0, allocationSize) == ENOSPC) + int fallocateResult = Interop.Sys.PosixFAllocate(_fileHandle, 0, allocationSize); + if (fallocateResult != 0) { _fileHandle.Dispose(); Interop.Sys.Unlink(_path!); // remove the file to mimic Windows behaviour (atomic operation) - throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, _path, allocationSize)); + if (fallocateResult == -1) + { + throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, _path, allocationSize)); + } + else + { + Debug.Assert(fallocateResult == -2); + throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, path, allocationSize)); + } } // ignore not supported and other failures (pipe etc) } From df155e2571e9066762e5bccd28aabb593d136d2a Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 23 Apr 2021 12:48:53 +0200 Subject: [PATCH 22/37] fix the Unix build --- .../System/IO/Strategies/Net5CompatFileStreamStrategy.Unix.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6219835c018a1f..af8b15a171d37c 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 @@ -121,7 +121,7 @@ private void Init(FileMode mode, FileShare share, string originalPath, FileOptio else { Debug.Assert(fallocateResult == -2); - throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, path, allocationSize)); + throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, _path, allocationSize)); } } // ignore not supported and other failures (pipe etc) From b6929e37f97264e42b7346b2636e78f7af0bb65a Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 23 Apr 2021 13:40:29 +0200 Subject: [PATCH 23/37] fix a wrong assert --- src/libraries/Native/Unix/System.Native/pal_io.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.c b/src/libraries/Native/Unix/System.Native/pal_io.c index af59618952c9a3..d410d5e25b524d 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.c +++ b/src/libraries/Native/Unix/System.Native/pal_io.c @@ -1058,7 +1058,7 @@ int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length) return 0; case EINVAL: // We control the offset and length so they are correct. - assert_msg(offset != 0, "Invalid offset value", (int)offset); + assert_msg(offset == 0, "Invalid offset value", (int)offset); assert_msg(length >= 0, "Invalid length value", (int)length); // But if the underlying filesystem does not support the operation, we just ignore it and treat as a hint. return 0; From 6875a9fc1619a82ecef4442454ed7731a27ed93d Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 23 Apr 2021 13:40:59 +0200 Subject: [PATCH 24/37] try to enable the new tests for Browser build --- .../ctor_str_fm_fa_fs_buffer_fo_as.Browser.cs | 16 ++++++++++++++++ .../tests/System.IO.FileSystem.Tests.csproj | 1 + 2 files changed, 17 insertions(+) create mode 100644 src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Browser.cs diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Browser.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Browser.cs new file mode 100644 index 00000000000000..7e1dd00bbe3753 --- /dev/null +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Browser.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. + +namespace System.IO.Tests +{ + public partial class FileStream_ctor_str_fm_fa_fs_buffer_fo_as : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base + { + protected override long AllocationSize => 10; + + protected override long InitialLength => 10; + + private long GetExpectedFileLength(long allocationSize) => allocationSize; + + private long GetActualAllocationSize(FileStream fileStream) => fileStream.Length; + } +} 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 45bad638a71cee..71a37bde1baa31 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 @@ -64,6 +64,7 @@ + From 0a0edad7ea500d33374ca2a5f40b7465a862f53c Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 27 Apr 2021 12:06:51 +0200 Subject: [PATCH 25/37] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com> Co-authored-by: David Cantú --- .../src/System/IO/FileStream.cs | 20 +++++++++---------- .../Strategies/FileStreamHelpers.Windows.cs | 4 +++- .../Net5CompatFileStreamStrategy.Unix.cs | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) 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 d2280b5dbf8f28..437ba1f77003db 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -135,20 +135,20 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share } /// - /// 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. /// - /// A relative or absolute path for the file that the current object will encapsulate. + /// 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. - /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. This also determines the values returned by the and properties of the object. is if specifies a disk file. - /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. The default value is Read. - /// A positive value greater than 0 indicating the buffer size. The default buffer size is 4096. - /// A bitwise combination of the enumeration values that specifies additional file options. The default value is None which means synchronous IO. + /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. This also determines the values returned by the and properties of the object. is if specifies a disk file. + /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. The default value is . + /// A positive value greater than 0 indicating the buffer size. The default buffer size is 4096. + /// A bitwise combination of the enumeration values that specifies additional file options. The default value is , which indicates synchronous IO. /// The initial allocation size in bytes for the file. A value has no effect unless it's positive and a regular file is being created, overwritten, or replaced. - /// is . + /// 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. + /// 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 zero. /// -or- /// , , or contain an invalid value. @@ -162,7 +162,7 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share /// 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. + /// 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. 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 03b3303e15732b..1a9b7ff58866ae 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 @@ -118,7 +118,9 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode 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); + Interop.Kernel32.CreateFile(path, fAccess, share, &secAttrs, mode, flagsAndAttributes, IntPtr.Zero), + path, + (options & FileOptions.Asynchronous) != 0); if (ntStatus == NT_STATUS_INVALID_PARAMETER) { 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 af8b15a171d37c..8a60d466407fd1 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 @@ -124,7 +124,7 @@ private void Init(FileMode mode, FileShare share, string originalPath, FileOptio throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, _path, allocationSize)); } } - // ignore not supported and other failures (pipe etc) + // ignore not supported and other failures (pipe, etc) } } From c46ac6628308745bdb392ad4ab206b8528be5980 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 27 Apr 2021 12:29:27 +0200 Subject: [PATCH 26/37] apply other suggestions for the code review, get rid of the managed allocaiton of the extended path --- .../Native/Unix/System.Native/pal_io.c | 5 ++- .../ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs | 18 +++++++- .../ctor_str_fm_fa_fs_buffer_fo_as.cs | 1 - .../Strategies/FileStreamHelpers.Windows.cs | 42 ++++++++++++++++++- .../System/IO/Strategies/FileStreamHelpers.cs | 3 ++ .../Net5CompatFileStreamStrategy.Unix.cs | 2 +- 6 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.c b/src/libraries/Native/Unix/System.Native/pal_io.c index d410d5e25b524d..c63eb8898cace9 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.c +++ b/src/libraries/Native/Unix/System.Native/pal_io.c @@ -993,6 +993,8 @@ int32_t SystemNative_PosixFAdvise(intptr_t fd, int64_t offset, int64_t length, i int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length) { + assert_msg(offset == 0, "Invalid offset value", (int)offset); + int fileDescriptor = ToFileDescriptor(fd); int32_t result; #if HAVE_POSIX_FALLOCATE64 // 64-bit Linux @@ -1040,7 +1042,7 @@ int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length) } else { - // align the behaviour with what posix_fallocate does + // align the behaviour with what posix_fallocate does (change reported file size) ftruncate(fileDescriptor, length); } #endif @@ -1058,7 +1060,6 @@ int32_t SystemNative_PosixFAllocate(intptr_t fd, int64_t offset, int64_t length) return 0; case EINVAL: // We control the offset and length so they are correct. - assert_msg(offset == 0, "Invalid offset value", (int)offset); assert_msg(length >= 0, "Invalid length value", (int)length); // But if the underlying filesystem does not support the operation, we just ignore it and treat as a hint. return 0; diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs index cba7265f2953a3..e4731c573bba01 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs @@ -24,7 +24,22 @@ private unsafe long GetActualAllocationSize(FileStream fileStream) return info.AllocationSize; } - [PlatformSpecific(TestPlatforms.Windows)] + [Theory] + [InlineData(@"\\?\")] + [InlineData(@"\??\")] + [InlineData("")] + public void ExtendedPathsAreSupported(string prefix) + { + const long allocationSize = 123; + + string filePath = prefix + Path.GetFullPath(GetPathToNonExistingFile()); + + using (var fs = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + { + Assert.True(GetActualAllocationSize(fs) >= allocationSize, $"Provided {allocationSize}, actual: {GetActualAllocationSize(fs)}"); + } + } + [ConditionalTheory(nameof(IsFat32))] [InlineData(FileMode.Create)] [InlineData(FileMode.CreateNew)] @@ -37,7 +52,6 @@ public void WhenFileIsTooLargeTheErrorMessageContainsAllDetails(FileMode mode) Assert.StartsWith(Path.GetTempPath(), filePath); // this is what IsFat32 method relies on IOException ex = Assert.Throws(() => new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, tooMuch)); - Assert.Contains("file was too large", ex.Message); Assert.Contains(filePath, ex.Message); Assert.Contains(tooMuch.ToString(), ex.Message); diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs index 7f998a3f640e18..f76b1a542c49f9 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs @@ -122,7 +122,6 @@ public void WhenDiskIsFullTheErrorMessageContainsAllDetails(FileMode mode) string filePath = GetPathToNonExistingFile(); IOException ex = Assert.Throws(() => new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, tooMuch)); - Assert.Contains("disk was full", ex.Message); Assert.Contains(filePath, ex.Message); Assert.Contains(tooMuch.ToString(), ex.Message); 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 1a9b7ff58866ae..0d0d0246f1d70d 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 @@ -72,10 +72,17 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode Debug.Assert(path != null); uint ntStatus = 0; - if (allocationSize > 0 && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append) + if (IsNonIgnorable(allocationSize, access, mode)) { - string prefixedAbsolutePath = PathInternal.IsExtended(path) ? path : @"\??\" + Path.GetFullPath(path); // we might consider getting rid of this managed allocation + GetPathForNtCreateFile(path, out ReadOnlySpan prefixedAbsolutePath, out char[]? rentedArray); + (ntStatus, IntPtr fileHandle) = Interop.NtDll.CreateFile(prefixedAbsolutePath, mode, access, share, options, allocationSize); + + if (rentedArray is not null) + { + ArrayPool.Shared.Return(rentedArray); + } + if (ntStatus == 0) { return ValidateFileHandle(new SafeFileHandle(fileHandle, ownsHandle: true), path, (options & FileOptions.Asynchronous) != 0); @@ -474,6 +481,37 @@ internal static unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan prefixedAbsolutePath, out char[]? rentedArray) + { + Debug.Assert(fullPath == Path.GetFullPath(fullPath)); + + const string mandatoryNtPrefix = @"\??\"; + + if (fullPath.StartsWith(mandatoryNtPrefix, StringComparison.Ordinal)) + { + prefixedAbsolutePath = fullPath; + rentedArray = null; + } + else if (fullPath.StartsWith(@"\\?\", StringComparison.Ordinal)) // NtCreateFile does not support "\\?\" prefix, only "\??\" + { + rentedArray = ArrayPool.Shared.Rent(fullPath.Length); + + fullPath.CopyTo(rentedArray); + rentedArray[1] = '?'; + + prefixedAbsolutePath = new ReadOnlySpan(rentedArray, 0, fullPath.Length); + } + else + { + rentedArray = ArrayPool.Shared.Rent(mandatoryNtPrefix.Length + fullPath.Length); + + mandatoryNtPrefix.CopyTo(rentedArray); + fullPath.CopyTo(rentedArray.AsSpan(mandatoryNtPrefix.Length)); + + prefixedAbsolutePath = new ReadOnlySpan(rentedArray, 0, mandatoryNtPrefix.Length + fullPath.Length); + } + } + internal static async Task AsyncModeCopyToAsync(SafeFileHandle handle, string? path, bool canSeek, long filePosition, Stream destination, int bufferSize, CancellationToken cancellationToken) { // For efficiency, we avoid creating a new task and associated state for each asynchronous read. 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 65e8c35929527f..bbc3ccb27fe097 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 @@ -35,5 +35,8 @@ e is IOException || e is UnauthorizedAccessException || e is NotSupportedException || (e is ArgumentException && !(e is ArgumentNullException)); + + internal static bool IsNonIgnorable(long allocationSize, FileAccess access, FileMode mode) + => allocationSize > 0 && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append; } } 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 8a60d466407fd1..f356aac4354fa5 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 @@ -106,7 +106,7 @@ private void Init(FileMode mode, FileShare share, string originalPath, FileOptio } // If allocationSize has been provided for a creatable and writeable file - if (allocationSize > 0 && (_access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append) + if (FileStreamHelpers.IsNonIgnorable(allocationSize, _access, mode)) { int fallocateResult = Interop.Sys.PosixFAllocate(_fileHandle, 0, allocationSize); if (fallocateResult != 0) From 83f119dc70a5f639d1b824173b9dfd90bff71899 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 27 Apr 2021 16:30:12 +0200 Subject: [PATCH 27/37] disable it for WASM --- .../ctor_str_fm_fa_fs_buffer_fo_as.Browser.cs | 3 +++ .../System/IO/Strategies/FileStreamHelpers.Windows.cs | 4 +--- .../src/System/IO/Strategies/FileStreamHelpers.cs | 7 +++++-- .../Strategies/Net5CompatFileStreamStrategy.Unix.cs | 11 ++++------- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Browser.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Browser.cs index 7e1dd00bbe3753..ec36cb64f3e4e0 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Browser.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Browser.cs @@ -1,8 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Xunit; + namespace System.IO.Tests { + [PlatformSpecific(~TestPlatforms.Browser)] public partial class FileStream_ctor_str_fm_fa_fs_buffer_fo_as : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base { protected override long AllocationSize => 10; 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 92a0fe8a8cedbf..15e6aa32f0a611 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 @@ -72,7 +72,7 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode Debug.Assert(path != null); uint ntStatus = 0; - if (IsNonIgnorable(allocationSize, access, mode)) + if (ShouldPreallocate(allocationSize, access, mode)) { GetPathForNtCreateFile(path, out ReadOnlySpan prefixedAbsolutePath, out char[]? rentedArray); @@ -482,8 +482,6 @@ internal static unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan prefixedAbsolutePath, out char[]? rentedArray) { - Debug.Assert(fullPath == Path.GetFullPath(fullPath)); - const string mandatoryNtPrefix = @"\??\"; if (fullPath.StartsWith(mandatoryNtPrefix, StringComparison.Ordinal)) 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 bbc3ccb27fe097..a33d6f25097b0b 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 @@ -36,7 +36,10 @@ e is UnauthorizedAccessException || e is NotSupportedException || (e is ArgumentException && !(e is ArgumentNullException)); - internal static bool IsNonIgnorable(long allocationSize, FileAccess access, FileMode mode) - => allocationSize > 0 && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append; + internal static bool ShouldPreallocate(long allocationSize, FileAccess access, FileMode mode) + => allocationSize > 0 + && (access & FileAccess.Write) != 0 + && mode != FileMode.Open && mode != FileMode.Append + && !OperatingSystem.IsBrowser(); // WASM limitation } } 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 f356aac4354fa5..a812432fc22b74 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 @@ -106,7 +106,7 @@ private void Init(FileMode mode, FileShare share, string originalPath, FileOptio } // If allocationSize has been provided for a creatable and writeable file - if (FileStreamHelpers.IsNonIgnorable(allocationSize, _access, mode)) + if (FileStreamHelpers.ShouldPreallocate(allocationSize, _access, mode)) { int fallocateResult = Interop.Sys.PosixFAllocate(_fileHandle, 0, allocationSize); if (fallocateResult != 0) @@ -118,13 +118,10 @@ private void Init(FileMode mode, FileShare share, string originalPath, FileOptio { throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, _path, allocationSize)); } - else - { - Debug.Assert(fallocateResult == -2); - throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, _path, allocationSize)); - } + + Debug.Assert(fallocateResult == -2); + throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, _path, allocationSize)); } - // ignore not supported and other failures (pipe, etc) } } From 37b9245300e53f1eaaef455d09016a9a3d7eb9ca Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 28 Apr 2021 12:19:59 +0200 Subject: [PATCH 28/37] address code review feedback: remove unused path parameter, use stackalloc for small inputs --- .../Strategies/FileStreamHelpers.Windows.cs | 119 +++++++++--------- 1 file changed, 59 insertions(+), 60 deletions(-) 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 15e6aa32f0a611..70985426ee7c26 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,6 +5,7 @@ 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; @@ -71,35 +72,9 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode { Debug.Assert(path != null); - uint ntStatus = 0; - if (ShouldPreallocate(allocationSize, access, mode)) + if (TryNtCreateFile(path, mode, access, share, options, allocationSize, out uint ntStatus, out IntPtr fileHandle)) { - GetPathForNtCreateFile(path, out ReadOnlySpan prefixedAbsolutePath, out char[]? rentedArray); - - (ntStatus, IntPtr fileHandle) = Interop.NtDll.CreateFile(prefixedAbsolutePath, mode, access, share, options, allocationSize); - - if (rentedArray is not null) - { - ArrayPool.Shared.Return(rentedArray); - } - - if (ntStatus == 0) - { - return ValidateFileHandle(new SafeFileHandle(fileHandle, ownsHandle: true), path, (options & FileOptions.Asynchronous) != 0); - } - else if (ntStatus == NT_ERROR_STATUS_DISK_FULL) - { - throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, path, allocationSize)); - } - else if (ntStatus == NT_ERROR_STATUS_FILE_TOO_LARGE) - { - throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, path, allocationSize)); - } - - // NtCreateFile has failed for some other reason than a full disk or too large file. - // Instead of implementing the mapping for every NS Status value (there are plenty of them: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55) - // or using RtlNtStatusToDosError & GetExceptionForWin32Error - // the code falls back to CreateFileW that just throws the right exception. + return ValidateFileHandle(new SafeFileHandle(fileHandle, ownsHandle: true), path, (options & FileOptions.Asynchronous) != 0); } Interop.Kernel32.SECURITY_ATTRIBUTES secAttrs = GetSecAttrs(share); @@ -135,7 +110,7 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode // that are too big for the current file system. Example: creating a 4GB+1 file on a FAT32 drive. // Since Linux reports EFBIG for such cases, we are using the following workaround to get the right exception. // TrySetFileLength uses SetFileInformationByHandle which fails with a clear error if the file is too big. - if (!TrySetFileLength(safeFileHandle, path, allocationSize, out int errorCode)) + if (!TrySetFileLength(safeFileHandle, allocationSize, out int errorCode)) { // Since we have failed to extend the file, we mimic the NtCreateFile behaviour // which does not create a file if there is not enough space on the disk. @@ -157,6 +132,59 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode } } + private static bool TryNtCreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize, + out uint ntStatus, out IntPtr fileHandle) + { + if (!ShouldPreallocate(allocationSize, access, mode)) + { + ntStatus = 0; + fileHandle = default; + return false; + } + + const string mandatoryNtPrefix = @"\??\"; + if (fullPath.StartsWith(mandatoryNtPrefix, StringComparison.Ordinal)) + { + (ntStatus, fileHandle) = Interop.NtDll.CreateFile(fullPath, mode, access, share, options, allocationSize); + } + 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, allocationSize); + vsb.Dispose(); + } + + if (ntStatus == 0) + { + return true; + } + else if (ntStatus == NT_ERROR_STATUS_DISK_FULL) + { + throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, fullPath, allocationSize)); + } + else if (ntStatus == NT_ERROR_STATUS_FILE_TOO_LARGE) + { + throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, fullPath, allocationSize)); + } + + // NtCreateFile has failed for some other reason than a full disk or too large file. + // Instead of implementing the mapping for every NS Status value (there are plenty of them: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55) + // or using RtlNtStatusToDosError & GetExceptionForWin32Error that end up throwing a different exception compared to when NtCreateFile is not involved, + // the code falls back to CreateFileW that just throws the right exception. + return false; + } + internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsync) { return handle.IsAsync ?? !IsHandleSynchronous(handle, ignoreInvalid: true) ?? defaultIsAsync; @@ -383,7 +411,7 @@ internal static void GetFileTypeSpecificInformation(SafeFileHandle handle, out b internal static unsafe void SetFileLength(SafeFileHandle handle, string? path, long length) { - if (!TrySetFileLength(handle, path, length, out int errorCode)) + if (!TrySetFileLength(handle, length, out int errorCode)) { if (errorCode == Interop.Errors.ERROR_INVALID_PARAMETER) { @@ -394,7 +422,7 @@ internal static unsafe void SetFileLength(SafeFileHandle handle, string? path, l } } - private static unsafe bool TrySetFileLength(SafeFileHandle handle, string? path, long length, out int errorCode) + private static unsafe bool TrySetFileLength(SafeFileHandle handle, long length, out int errorCode) { var eofInfo = new Interop.Kernel32.FILE_END_OF_FILE_INFO { @@ -480,35 +508,6 @@ internal static unsafe int WriteFileNative(SafeFileHandle handle, ReadOnlySpan prefixedAbsolutePath, out char[]? rentedArray) - { - const string mandatoryNtPrefix = @"\??\"; - - if (fullPath.StartsWith(mandatoryNtPrefix, StringComparison.Ordinal)) - { - prefixedAbsolutePath = fullPath; - rentedArray = null; - } - else if (fullPath.StartsWith(@"\\?\", StringComparison.Ordinal)) // NtCreateFile does not support "\\?\" prefix, only "\??\" - { - rentedArray = ArrayPool.Shared.Rent(fullPath.Length); - - fullPath.CopyTo(rentedArray); - rentedArray[1] = '?'; - - prefixedAbsolutePath = new ReadOnlySpan(rentedArray, 0, fullPath.Length); - } - else - { - rentedArray = ArrayPool.Shared.Rent(mandatoryNtPrefix.Length + fullPath.Length); - - mandatoryNtPrefix.CopyTo(rentedArray); - fullPath.CopyTo(rentedArray.AsSpan(mandatoryNtPrefix.Length)); - - prefixedAbsolutePath = new ReadOnlySpan(rentedArray, 0, mandatoryNtPrefix.Length + fullPath.Length); - } - } - internal static async Task AsyncModeCopyToAsync(SafeFileHandle handle, string? path, bool canSeek, long filePosition, Stream destination, int bufferSize, CancellationToken cancellationToken) { // For efficiency, we avoid creating a new task and associated state for each asynchronous read. From c2f995efd5a26b2501e71df173894da7d0a707ae Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Wed, 28 Apr 2021 12:56:18 +0200 Subject: [PATCH 29/37] address code review feedback: if NtCreateFile fails, we should immediately throw an exception and don't call CreateFile --- .../Windows/NtDll/Interop.NtCreateFile.cs | 6 +- .../System.Private.CoreLib.Shared.projitems | 3 + .../Strategies/FileStreamHelpers.Windows.cs | 68 ++++++------------- 3 files changed, 27 insertions(+), 50 deletions(-) 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 bdc85f848e9167..92bf7932c81d53 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs @@ -121,6 +121,10 @@ private static DesiredAccess GetDesiredAccess(FileAccess access, FileMode fileMo { result |= DesiredAccess.SYNCHRONIZE; // requried by FILE_SYNCHRONOUS_IO_NONALERT } + if ((options & FileOptions.DeleteOnClose) != 0 || fileMode == FileMode.Create) + { + result |= DesiredAccess.DELETE; // required by FILE_DELETE_ON_CLOSE and FILE_SUPERSEDE (which deletes a file if it exsits) + } return result; } @@ -153,7 +157,7 @@ private static CreateOptions GetCreateOptions(FileOptions options) } if ((options & FileOptions.DeleteOnClose) != 0) { - result |= CreateOptions.FILE_DELETE_ON_CLOSE; + result |= CreateOptions.FILE_DELETE_ON_CLOSE; // has extra handling in GetDesiredAccess } if ((options & FileOptions.Asynchronous) == 0) { 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 2b78749b294da6..7b9fe30e74713f 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 @@ -1347,6 +1347,9 @@ Common\Interop\Windows\NtDll\Interop.NtCreateFile.cs + + Common\Interop\Windows\NtDll\Interop.RtlNtStatusToDosError.cs + Common\Interop\Windows\Kernel32\Interop.DeleteFile.cs 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 70985426ee7c26..3c7ffe9bafc161 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 @@ -72,8 +72,10 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode { Debug.Assert(path != null); - if (TryNtCreateFile(path, mode, access, share, options, allocationSize, out uint ntStatus, out IntPtr fileHandle)) + if (ShouldPreallocate(allocationSize, access, mode)) { + IntPtr fileHandle = NtCreateFile(path, mode, access, share, options, allocationSize); + return ValidateFileHandle(new SafeFileHandle(fileHandle, ownsHandle: true), path, (options & FileOptions.Asynchronous) != 0); } @@ -104,43 +106,14 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode path, (options & FileOptions.Asynchronous) != 0); - if (ntStatus == NT_STATUS_INVALID_PARAMETER) - { - // It seems that 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. - // Since Linux reports EFBIG for such cases, we are using the following workaround to get the right exception. - // TrySetFileLength uses SetFileInformationByHandle which fails with a clear error if the file is too big. - if (!TrySetFileLength(safeFileHandle, allocationSize, out int errorCode)) - { - // Since we have failed to extend the file, we mimic the NtCreateFile behaviour - // which does not create a file if there is not enough space on the disk. - // So we close the handle, remove the file and then throw an exception. - safeFileHandle.Dispose(); - Interop.Kernel32.DeleteFile(path); - - if (errorCode == Interop.Errors.ERROR_FILE_TOO_LARGE // this is what we would expect in such a case - || errorCode == Interop.Errors.ERROR_DISK_FULL) // but this is what we get (verified with Windows 10.0.18363.1500) - { - throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, path, allocationSize)); - } - - throw Win32Marshal.GetExceptionForWin32Error(errorCode, path); - } - } - return safeFileHandle; } } - private static bool TryNtCreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize, - out uint ntStatus, out IntPtr fileHandle) + private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) { - if (!ShouldPreallocate(allocationSize, access, mode)) - { - ntStatus = 0; - fileHandle = default; - return false; - } + uint ntStatus; + IntPtr fileHandle; const string mandatoryNtPrefix = @"\??\"; if (fullPath.StartsWith(mandatoryNtPrefix, StringComparison.Ordinal)) @@ -165,24 +138,21 @@ private static bool TryNtCreateFile(string fullPath, FileMode mode, FileAccess a vsb.Dispose(); } - if (ntStatus == 0) + switch (ntStatus) { - return true; - } - else if (ntStatus == NT_ERROR_STATUS_DISK_FULL) - { - throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, fullPath, allocationSize)); - } - else if (ntStatus == NT_ERROR_STATUS_FILE_TOO_LARGE) - { - throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, fullPath, allocationSize)); + case 0: + return fileHandle; + case NT_ERROR_STATUS_DISK_FULL: + throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, fullPath, allocationSize)); + // 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 NT_STATUS_INVALID_PARAMETER: + case NT_ERROR_STATUS_FILE_TOO_LARGE: + throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, fullPath, allocationSize)); + default: + int error = (int)Interop.NtDll.RtlNtStatusToDosError((int)ntStatus); + throw Win32Marshal.GetExceptionForWin32Error(error, fullPath); } - - // NtCreateFile has failed for some other reason than a full disk or too large file. - // Instead of implementing the mapping for every NS Status value (there are plenty of them: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55) - // or using RtlNtStatusToDosError & GetExceptionForWin32Error that end up throwing a different exception compared to when NtCreateFile is not involved, - // the code falls back to CreateFileW that just throws the right exception. - return false; } internal static bool GetDefaultIsAsync(SafeFileHandle handle, bool defaultIsAsync) From 2b006801d06835029ecfdaf9808ec47593cb86a9 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 14 May 2021 21:05:18 +0200 Subject: [PATCH 30/37] Apply suggestions from code review Co-authored-by: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com> --- .../src/Interop/Windows/NtDll/Interop.NtCreateFile.cs | 4 ++-- .../tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) 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 92bf7932c81d53..f9cb3193196cd2 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs @@ -119,11 +119,11 @@ private static DesiredAccess GetDesiredAccess(FileAccess access, FileMode fileMo } if ((options & FileOptions.Asynchronous) == 0) { - result |= DesiredAccess.SYNCHRONIZE; // requried by FILE_SYNCHRONOUS_IO_NONALERT + result |= DesiredAccess.SYNCHRONIZE; // required by FILE_SYNCHRONOUS_IO_NONALERT } if ((options & FileOptions.DeleteOnClose) != 0 || fileMode == FileMode.Create) { - result |= DesiredAccess.DELETE; // required by FILE_DELETE_ON_CLOSE and FILE_SUPERSEDE (which deletes a file if it exsits) + result |= DesiredAccess.DELETE; // required by FILE_DELETE_ON_CLOSE and FILE_SUPERSEDE (which deletes a file if it exists) } return result; diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs index f76b1a542c49f9..a86e2ddfd60508 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs @@ -125,7 +125,13 @@ public void WhenDiskIsFullTheErrorMessageContainsAllDetails(FileMode mode) Assert.Contains(filePath, ex.Message); Assert.Contains(tooMuch.ToString(), ex.Message); - Assert.False(File.Exists(filePath)); // ensure it was NOT created (provided OOTB by Windows, emulated on Unix) + // ensure it was NOT created (provided OOTB by Windows, emulated on Unix) + bool exists = File.Exists(filePath); + if (exists) + { + File.Delete(filePath); + } + Assert.False(exists); } [Theory] @@ -149,7 +155,7 @@ public void WhenFileIsTruncatedWithoutAllocationSizeSpecifiedTheAllocationSizeIs [Fact] public void WhenFileIsTruncatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet() { - const int initialSize = 10_000; // this must be more than 4kb which seems to be minimum allocaiton size on Windows + const int initialSize = 10_000; // this must be more than 4kb which seems to be minimum allocation size on Windows const long allocationSize = 100; string filePath = GetPathToNonExistingFile(); From 4065f43214b16c2d675358542ddaca0942826554 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 14 May 2021 21:56:37 +0200 Subject: [PATCH 31/37] introduce FileStreamOptions --- .../Windows/NtDll/Interop.NtCreateFile.cs | 8 +-- ....Browser.cs => ctor_options_as.Browser.cs} | 6 +- ..._fo_as.Unix.cs => ctor_options_as.Unix.cs} | 6 +- ....Windows.cs => ctor_options_as.Windows.cs} | 14 ++-- ..._fs_buffer_fo_as.cs => ctor_options_as.cs} | 69 ++++++++++++------- .../tests/System.IO.FileSystem.Tests.csproj | 8 +-- .../System.Private.CoreLib.Shared.projitems | 1 + .../src/System/IO/FileStream.cs | 30 ++++---- .../src/System/IO/FileStreamOptions.cs | 29 ++++++++ .../AsyncWindowsFileStreamStrategy.cs | 4 +- .../IO/Strategies/FileStreamHelpers.Unix.cs | 6 +- .../Strategies/FileStreamHelpers.Windows.cs | 28 ++++---- .../System/IO/Strategies/FileStreamHelpers.cs | 8 +-- .../Net5CompatFileStreamStrategy.Unix.cs | 14 ++-- .../Net5CompatFileStreamStrategy.Windows.cs | 2 +- .../Net5CompatFileStreamStrategy.cs | 6 +- .../SyncWindowsFileStreamStrategy.cs | 4 +- .../Strategies/WindowsFileStreamStrategy.cs | 4 +- .../System.Runtime/ref/System.Runtime.cs | 11 ++- 19 files changed, 158 insertions(+), 100 deletions(-) rename src/libraries/System.IO.FileSystem/tests/FileStream/{ctor_str_fm_fa_fs_buffer_fo_as.Browser.cs => ctor_options_as.Browser.cs} (60%) rename src/libraries/System.IO.FileSystem/tests/FileStream/{ctor_str_fm_fa_fs_buffer_fo_as.Unix.cs => ctor_options_as.Unix.cs} (69%) rename src/libraries/System.IO.FileSystem/tests/FileStream/{ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs => ctor_options_as.Windows.cs} (80%) rename src/libraries/System.IO.FileSystem/tests/FileStream/{ctor_str_fm_fa_fs_buffer_fo_as.cs => ctor_options_as.cs} (67%) create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs 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 f9cb3193196cd2..15a00f0dd85f01 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs @@ -37,7 +37,7 @@ internal static unsafe (uint status, IntPtr handle) CreateFile( ObjectAttributes objectAttributes = ObjectAttributes.OBJ_CASE_INSENSITIVE, void* eaBuffer = null, uint eaLength = 0, - long* allocationSize = null) + long* preallocationSize = null) { fixed (char* c = &MemoryMarshal.GetReference(path)) { @@ -58,7 +58,7 @@ internal static unsafe (uint status, IntPtr handle) CreateFile( desiredAccess, ref attributes, out IO_STATUS_BLOCK statusBlock, - AllocationSize: allocationSize, + AllocationSize: preallocationSize, fileAttributes, shareAccess, createDisposition, @@ -70,7 +70,7 @@ internal static unsafe (uint status, IntPtr handle) CreateFile( } } - internal static unsafe (uint status, IntPtr handle) CreateFile(ReadOnlySpan path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) + internal static unsafe (uint status, IntPtr handle) CreateFile(ReadOnlySpan path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) => CreateFile( path: path, rootDirectory: IntPtr.Zero, @@ -80,7 +80,7 @@ internal static unsafe (uint status, IntPtr handle) CreateFile(ReadOnlySpan 10; + protected override long PreallocationSize => 10; protected override long InitialLength => 10; - private long GetExpectedFileLength(long allocationSize) => allocationSize; + private long GetExpectedFileLength(long preallocationSize) => preallocationSize; private long GetActualAllocationSize(FileStream fileStream) => fileStream.Length; } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Unix.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Unix.cs similarity index 69% rename from src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Unix.cs rename to src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Unix.cs index 7a663aba85ebfb..3e5ea0a6d08897 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Unix.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Unix.cs @@ -3,13 +3,13 @@ namespace System.IO.Tests { - public partial class FileStream_ctor_str_fm_fa_fs_buffer_fo_as : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base + public partial class FileStream_ctor_options_as : FileStream_ctor_options_as_base { - protected override long AllocationSize => 10; + protected override long PreallocationSize => 10; protected override long InitialLength => 10; - private long GetExpectedFileLength(long allocationSize) => allocationSize; + private long GetExpectedFileLength(long preallocationSize) => preallocationSize; private long GetActualAllocationSize(FileStream fileStream) { diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Windows.cs similarity index 80% rename from src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs rename to src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Windows.cs index e4731c573bba01..114a8881dfe334 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.Windows.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Windows.cs @@ -7,13 +7,13 @@ namespace System.IO.Tests { - public partial class FileStream_ctor_str_fm_fa_fs_buffer_fo_as : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base + public partial class FileStream_ctor_options_as : FileStream_ctor_options_as_base { - protected override long AllocationSize => 10; + protected override long PreallocationSize => 10; protected override long InitialLength => 0; // Windows modifies AllocationSize, but not EndOfFile (file length) - private long GetExpectedFileLength(long allocationSize) => 0; // Windows modifies AllocationSize, but not EndOfFile (file length) + private long GetExpectedFileLength(long preallocationSize) => 0; // Windows modifies AllocationSize, but not EndOfFile (file length) private unsafe long GetActualAllocationSize(FileStream fileStream) { @@ -30,13 +30,13 @@ private unsafe long GetActualAllocationSize(FileStream fileStream) [InlineData("")] public void ExtendedPathsAreSupported(string prefix) { - const long allocationSize = 123; + const long preallocationSize = 123; string filePath = prefix + Path.GetFullPath(GetPathToNonExistingFile()); - using (var fs = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + using (var fs = new FileStream(filePath, GetOptions(FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) { - Assert.True(GetActualAllocationSize(fs) >= allocationSize, $"Provided {allocationSize}, actual: {GetActualAllocationSize(fs)}"); + Assert.True(GetActualAllocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualAllocationSize(fs)}"); } } @@ -51,7 +51,7 @@ public void WhenFileIsTooLargeTheErrorMessageContainsAllDetails(FileMode mode) string filePath = GetPathToNonExistingFile(); Assert.StartsWith(Path.GetTempPath(), filePath); // this is what IsFat32 method relies on - IOException ex = Assert.Throws(() => new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, tooMuch)); + IOException ex = Assert.Throws(() => new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, tooMuch))); Assert.Contains(filePath, ex.Message); Assert.Contains(tooMuch.ToString(), ex.Message); diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.cs similarity index 67% rename from src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs rename to src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.cs index a86e2ddfd60508..9084e7cb66167b 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_str_fm_fa_fs_buffer_fo_as.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.cs @@ -5,28 +5,47 @@ namespace System.IO.Tests { - public abstract class FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base : FileStream_ctor_str_fm_fa_fs_buffer_fo + public abstract class FileStream_ctor_options_as_base : FileStream_ctor_str_fm_fa_fs_buffer_fo { - protected abstract long AllocationSize { get; } + protected abstract long PreallocationSize { get; } protected override FileStream CreateFileStream(string path, FileMode mode) - => new FileStream(path, mode, mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite, allocationSize: AllocationSize); + => new FileStream(path, + new FileStreamOptions + { + Mode = mode, + Access = mode == FileMode.Append ? FileAccess.Write : FileAccess.ReadWrite, + PreallocationSize = PreallocationSize + }); protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access) - => new FileStream(path, mode, access, allocationSize: AllocationSize); - - protected override FileStream CreateFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - => new FileStream(path, mode, access, share, bufferSize, options, allocationSize: AllocationSize); + => new FileStream(path, + new FileStreamOptions + { + Mode = mode, + Access = access, + PreallocationSize = PreallocationSize + }); + + protected FileStreamOptions GetOptions(FileMode mode, FileAccess access, FileShare share, FileOptions options, long preAllocationSize) + => new FileStreamOptions + { + Mode = mode, + Access = access, + Share = share, + Options = options, + PreallocationSize = preAllocationSize + }; } - public class FileStream_ctor_str_fm_fa_fs_buffer_fo_as_zero : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base + public class FileStream_ctor_options_as_zero : FileStream_ctor_options_as_base { protected override long AllocationSize => 0; // specifying 0 should have no effect protected override long InitialLength => 0; } - public class FileStream_ctor_str_fm_fa_fs_buffer_fo_as_negative : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base + public class FileStream_ctor_options_as_negative : FileStream_ctor_options_as_base { protected override long AllocationSize => -1; // specifying negative value should have no effect @@ -40,7 +59,7 @@ public partial class NoParallelTests { } // consumes entire available free space on the disk (only on Linux, this is how posix_fallocate works) // and if we try to run other disk-writing test in the meantime we are going to get "No space left on device" exception. [Collection("NoParallelTests")] - public partial class FileStream_ctor_str_fm_fa_fs_buffer_fo_as : FileStream_ctor_str_fm_fa_fs_buffer_fo_as_base + public partial class FileStream_ctor_options_as : FileStream_ctor_options_as_base { [Theory] [InlineData(FileMode.Create, 0L)] @@ -49,9 +68,9 @@ public partial class FileStream_ctor_str_fm_fa_fs_buffer_fo_as : FileStream_ctor [InlineData(FileMode.CreateNew, -1L)] [InlineData(FileMode.OpenOrCreate, 0L)] [InlineData(FileMode.OpenOrCreate, -1L)] - public void WhenFileIsCreatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet(FileMode mode, long allocationSize) + public void WhenFileIsCreatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet(FileMode mode, long preallocationSize) { - using (var fs = new FileStream(GetPathToNonExistingFile(), mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + using (var fs = new FileStream(GetPathToNonExistingFile(), GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) { Assert.Equal(0, GetActualAllocationSize(fs)); Assert.Equal(0, fs.Length); @@ -69,19 +88,19 @@ public void WhenFileIsCreatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNo [InlineData(FileMode.Append, 0L)] [InlineData(FileMode.Append, -1L)] [InlineData(FileMode.Append, 1L)] - public void WhenExistingFileIsBeingOpenedWithAllocationSizeSpecifiedTheAllocationSizeIsNotChanged(FileMode mode, long allocationSize) + public void WhenExistingFileIsBeingOpenedWithAllocationSizeSpecifiedTheAllocationSizeIsNotChanged(FileMode mode, long preallocationSize) { const int initialSize = 1; string filePath = GetPathToNonExistingFile(); File.WriteAllBytes(filePath, new byte[initialSize]); long initialAllocationSize; - using (var fs = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None)) // allocationSize NOT provided + using (var fs = new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, 0))) // preallocationSize NOT provided { initialAllocationSize = GetActualAllocationSize(fs); // just read it to ensure it's not being changed } - using (var fs = new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + using (var fs = new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) { Assert.Equal(initialAllocationSize, GetActualAllocationSize(fs)); // it has NOT been changed Assert.Equal(initialSize, fs.Length); @@ -95,13 +114,13 @@ public void WhenExistingFileIsBeingOpenedWithAllocationSizeSpecifiedTheAllocatio [InlineData(FileMode.OpenOrCreate)] public void WhenFileIsCreatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet(FileMode mode) { - const long allocationSize = 123; + const long preallocationSize = 123; - using (var fs = new FileStream(GetPathToNonExistingFile(), mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + using (var fs = new FileStream(GetPathToNonExistingFile(), GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) { // OS might allocate MORE than we have requested - Assert.True(GetActualAllocationSize(fs) >= allocationSize, $"Provided {allocationSize}, actual: {GetActualAllocationSize(fs)}"); - Assert.Equal(GetExpectedFileLength(allocationSize), fs.Length); + Assert.True(GetActualAllocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualAllocationSize(fs)}"); + Assert.Equal(GetExpectedFileLength(preallocationSize), fs.Length); Assert.Equal(0, fs.Position); } } @@ -121,7 +140,7 @@ public void WhenDiskIsFullTheErrorMessageContainsAllDetails(FileMode mode) string filePath = GetPathToNonExistingFile(); - IOException ex = Assert.Throws(() => new FileStream(filePath, mode, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, tooMuch)); + IOException ex = Assert.Throws(() => new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, tooMuch))); Assert.Contains(filePath, ex.Message); Assert.Contains(tooMuch.ToString(), ex.Message); @@ -144,7 +163,7 @@ public void WhenFileIsTruncatedWithoutAllocationSizeSpecifiedTheAllocationSizeIs string filePath = GetPathToNonExistingFile(); File.WriteAllBytes(filePath, new byte[initialSize]); - using (var fs = new FileStream(filePath, FileMode.Truncate, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + using (var fs = new FileStream(filePath, GetOptions(FileMode.Truncate, FileAccess.Write, FileShare.None, FileOptions.None, allocationSize))) { Assert.Equal(0, GetActualAllocationSize(fs)); Assert.Equal(0, fs.Length); @@ -156,17 +175,17 @@ public void WhenFileIsTruncatedWithoutAllocationSizeSpecifiedTheAllocationSizeIs public void WhenFileIsTruncatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet() { const int initialSize = 10_000; // this must be more than 4kb which seems to be minimum allocation size on Windows - const long allocationSize = 100; + const long preallocationSize = 100; string filePath = GetPathToNonExistingFile(); File.WriteAllBytes(filePath, new byte[initialSize]); - using (var fs = new FileStream(filePath, FileMode.Truncate, FileAccess.Write, FileShare.None, c_DefaultBufferSize, FileOptions.None, allocationSize)) + using (var fs = new FileStream(filePath, GetOptions(FileMode.Truncate, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) { - Assert.True(GetActualAllocationSize(fs) >= allocationSize, $"Provided {allocationSize}, actual: {GetActualAllocationSize(fs)}"); + Assert.True(GetActualAllocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualAllocationSize(fs)}"); // less than initial file size (file got truncated) Assert.True(GetActualAllocationSize(fs) < initialSize, $"initialSize {initialSize}, actual: {GetActualAllocationSize(fs)}"); - Assert.Equal(GetExpectedFileLength(allocationSize), fs.Length); + Assert.Equal(GetExpectedFileLength(preallocationSize), fs.Length); Assert.Equal(0, fs.Position); } } 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 71a37bde1baa31..651abf050e5cce 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 @@ -18,7 +18,7 @@ - + @@ -50,13 +50,13 @@ - + - + @@ -64,7 +64,7 @@ - + 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 a528a00e6d7c38..dd26dcd341abb3 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 @@ -406,6 +406,7 @@ + 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 4adc7f710567e7..27305f598b5c85 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -13,7 +13,7 @@ namespace System.IO public class FileStream : Stream { internal const int DefaultBufferSize = 4096; - private const FileShare DefaultShare = FileShare.Read; + internal const FileShare DefaultShare = FileShare.Read; private const bool DefaultIsAsync = false; /// Caches whether Serialization Guard has been disabled for file writes @@ -138,12 +138,7 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share /// 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. /// /// 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. - /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. This also determines the values returned by the and properties of the object. is if specifies a disk file. - /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. The default value is . - /// A positive value greater than 0 indicating the buffer size. The default buffer size is 4096. - /// A bitwise combination of the enumeration values that specifies additional file options. The default value is , which indicates synchronous IO. - /// The initial allocation size in bytes for the file. A value has no effect unless it's positive and a regular file is being created, overwritten, or replaced. + /// 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- @@ -151,22 +146,27 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share /// refers to a non-file device, such as CON:, COM1:, LPT1:, etc. in a non-NTFS environment. /// is negative or zero. /// -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. + /// , , 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 stream has been closed. /// -or- - /// The disk was full (when was provided and was pointing to a regular file). + /// 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 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. + /// 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. + /// 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 FileStream(string path, FileMode mode, FileAccess access, FileShare share = DefaultShare, int bufferSize = DefaultBufferSize, FileOptions options = FileOptions.None, long allocationSize = 0) + public FileStream(string path, FileStreamOptions options) + : this(path, options.Mode, options.Access, options.Share, DefaultBufferSize, options.Options, options.PreallocationSize) + { + } + + private FileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { if (path == null) { @@ -228,7 +228,7 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share SerializationInfo.ThrowIfDeserializationInProgress("AllowFileWrites", ref s_cachedSerializationSwitch); } - _strategy = FileStreamHelpers.ChooseStrategy(this, path, mode, access, share, bufferSize, options, allocationSize); + _strategy = FileStreamHelpers.ChooseStrategy(this, path, mode, access, share, bufferSize, options, preallocationSize); } [Obsolete("This property has been deprecated. Please use FileStream's SafeFileHandle property instead. https://go.microsoft.com/fwlink/?linkid=14202")] diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs new file mode 100644 index 00000000000000..8fd78924cbf3c9 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.IO +{ + public sealed class FileStreamOptions + { + /// + /// One of the enumeration values that determines how to open or create the file. + /// + public FileMode Mode { get; set; } + /// + /// A bitwise combination of the enumeration values that determines how the file can be accessed by the object. This also determines the values returned by the and properties of the object. + /// + public FileAccess Access { get; set; } = FileAccess.Read; + /// + /// A bitwise combination of the enumeration values that determines how the file will be shared by processes. The default value is . + /// + public FileShare Share { get; set; } = FileStream.DefaultShare; + /// + /// A bitwise combination of the enumeration values that specifies additional file options. The default value is , which indicates synchronous IO. + /// + public FileOptions Options { get; set; } + /// + /// The initial allocation size in bytes for the file. A value has no effect unless it's positive and a regular file is being created, overwritten, or replaced. + /// + public long PreallocationSize { get; set; } + } +} 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 3fe2b6e1bca84f..6719dd9389566a 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 @@ -17,8 +17,8 @@ internal AsyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access { } - internal AsyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) - : base(path, mode, access, share, options, allocationSize) + internal AsyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) + : base(path, mode, access, share, options, preallocationSize) { } 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 f540162e3401bb..176429b41fe501 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 @@ -17,10 +17,10 @@ internal static partial class FileStreamHelpers private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) => new Net5CompatFileStreamStrategy(handle, access, bufferSize, isAsync); - private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long allocationSize) - => new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize, options, allocationSize); + 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, options, preallocationSize); - internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) + internal static SafeFileHandle OpenHandle(string path, 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); 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 32fed7165cd7b1..d71944b095a011 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 @@ -46,16 +46,16 @@ private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, File return EnableBufferingIfNeeded(strategy, bufferSize); } - private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long allocationSize) + private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { if (UseNet5CompatStrategy) { - return new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize, options, allocationSize); + return new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize, options, preallocationSize); } WindowsFileStreamStrategy strategy = (options & FileOptions.Asynchronous) != 0 - ? new AsyncWindowsFileStreamStrategy(path, mode, access, share, options, allocationSize) - : new SyncWindowsFileStreamStrategy(path, mode, access, share, options, allocationSize); + ? new AsyncWindowsFileStreamStrategy(path, mode, access, share, options, preallocationSize) + : new SyncWindowsFileStreamStrategy(path, mode, access, share, options, preallocationSize); return EnableBufferingIfNeeded(strategy, bufferSize); } @@ -63,18 +63,18 @@ private static FileStreamStrategy ChooseStrategyCore(string path, FileMode mode, internal static FileStreamStrategy EnableBufferingIfNeeded(WindowsFileStreamStrategy strategy, int bufferSize) => bufferSize == 1 ? strategy : new BufferedFileStreamStrategy(strategy, bufferSize); - internal static SafeFileHandle OpenHandle(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) - => CreateFileOpenHandle(path, mode, access, share, options, allocationSize); + 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 allocationSize) + 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(allocationSize, access, mode)) + if (ShouldPreallocate(preallocationSize, access, mode)) { - IntPtr fileHandle = NtCreateFile(path, mode, access, share, options, allocationSize); + IntPtr fileHandle = NtCreateFile(path, mode, access, share, options, preallocationSize); return ValidateFileHandle(new SafeFileHandle(fileHandle, ownsHandle: true), path, (options & FileOptions.Asynchronous) != 0); } @@ -110,7 +110,7 @@ private static unsafe SafeFileHandle CreateFileOpenHandle(string path, FileMode } } - private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) + private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) { uint ntStatus; IntPtr fileHandle; @@ -118,7 +118,7 @@ private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess ac const string mandatoryNtPrefix = @"\??\"; if (fullPath.StartsWith(mandatoryNtPrefix, StringComparison.Ordinal)) { - (ntStatus, fileHandle) = Interop.NtDll.CreateFile(fullPath, mode, access, share, options, allocationSize); + (ntStatus, fileHandle) = Interop.NtDll.CreateFile(fullPath, mode, access, share, options, preallocationSize); } else { @@ -134,7 +134,7 @@ private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess ac vsb.Append(fullPath); } - (ntStatus, fileHandle) = Interop.NtDll.CreateFile(vsb.AsSpan(), mode, access, share, options, allocationSize); + (ntStatus, fileHandle) = Interop.NtDll.CreateFile(vsb.AsSpan(), mode, access, share, options, preallocationSize); vsb.Dispose(); } @@ -143,12 +143,12 @@ private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess ac case 0: return fileHandle; case NT_ERROR_STATUS_DISK_FULL: - throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, fullPath, allocationSize)); + 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 NT_STATUS_INVALID_PARAMETER: case NT_ERROR_STATUS_FILE_TOO_LARGE: - throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, fullPath, allocationSize)); + 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); 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 a33d6f25097b0b..7dfaf7fe52dc73 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 @@ -12,8 +12,8 @@ internal static partial class FileStreamHelpers internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) => WrapIfDerivedType(fileStream, ChooseStrategyCore(handle, access, share, bufferSize, isAsync)); - internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long allocationSize) - => WrapIfDerivedType(fileStream, ChooseStrategyCore(path, mode, access, share, bufferSize, options, allocationSize)); + internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) + => WrapIfDerivedType(fileStream, ChooseStrategyCore(path, mode, access, share, bufferSize, options, preallocationSize)); private static FileStreamStrategy WrapIfDerivedType(FileStream fileStream, FileStreamStrategy strategy) => fileStream.GetType() == typeof(FileStream) @@ -36,8 +36,8 @@ e is UnauthorizedAccessException || e is NotSupportedException || (e is ArgumentException && !(e is ArgumentNullException)); - internal static bool ShouldPreallocate(long allocationSize, FileAccess access, FileMode mode) - => allocationSize > 0 + internal static bool ShouldPreallocate(long preallocationSize, FileAccess access, FileMode mode) + => preallocationSize > 0 && (access & FileAccess.Write) != 0 && mode != FileMode.Open && mode != FileMode.Append && !OperatingSystem.IsBrowser(); // WASM limitation 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 a812432fc22b74..f039c020433812 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 @@ -39,8 +39,8 @@ internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy /// 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 allocationSize) + /// passed to posix_fallocate + private void Init(FileMode mode, FileShare share, string originalPath, FileOptions options, long preallocationSize) { // 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.) @@ -105,10 +105,10 @@ private void Init(FileMode mode, FileShare share, string originalPath, FileOptio } } - // If allocationSize has been provided for a creatable and writeable file - if (FileStreamHelpers.ShouldPreallocate(allocationSize, _access, mode)) + // If preallocationSize has been provided for a creatable and writeable file + if (FileStreamHelpers.ShouldPreallocate(preallocationSize, _access, mode)) { - int fallocateResult = Interop.Sys.PosixFAllocate(_fileHandle, 0, allocationSize); + int fallocateResult = Interop.Sys.PosixFAllocate(_fileHandle, 0, preallocationSize); if (fallocateResult != 0) { _fileHandle.Dispose(); @@ -116,11 +116,11 @@ private void Init(FileMode mode, FileShare share, string originalPath, FileOptio if (fallocateResult == -1) { - throw new IOException(SR.Format(SR.IO_DiskFull_Path_AllocationSize, _path, allocationSize)); + 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, allocationSize)); + throw new IOException(SR.Format(SR.IO_FileTooLarge_Path_AllocationSize, _path, preallocationSize)); } } } 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 acb952a4e1dad7..8b51b2597fed0e 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 @@ -46,7 +46,7 @@ internal sealed partial class Net5CompatFileStreamStrategy : FileStreamStrategy 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 allocationSize) + private void Init(FileMode mode, FileShare share, string originalPath, FileOptions options, long preallocationSize) { FileStreamHelpers.ValidateFileTypeForNonExtendedPaths(_fileHandle, originalPath); 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 056c1ab443e2fc..d257df97722da8 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 @@ -78,7 +78,7 @@ internal Net5CompatFileStreamStrategy(SafeFileHandle handle, FileAccess access, _fileHandle = handle; } - internal Net5CompatFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long allocationSize) + internal Net5CompatFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { string fullPath = Path.GetFullPath(path); @@ -89,11 +89,11 @@ internal Net5CompatFileStreamStrategy(string path, FileMode mode, FileAccess acc if ((options & FileOptions.Asynchronous) != 0) _useAsyncIO = true; - _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options, allocationSize); + _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options, preallocationSize); try { - Init(mode, share, path, options, allocationSize); + Init(mode, share, path, options, preallocationSize); } 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 2f14c059dd99d1..30499b660ddf00 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 @@ -15,8 +15,8 @@ internal SyncWindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access, { } - internal SyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) - : base(path, mode, access, share, options, allocationSize) + internal SyncWindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) + : base(path, mode, access, share, options, preallocationSize) { } 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 ebb722a86fce72..a5ebdd6f5da599 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 @@ -37,7 +37,7 @@ internal WindowsFileStreamStrategy(SafeFileHandle handle, FileAccess access, Fil _fileHandle = handle; } - internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long allocationSize) + internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) { string fullPath = Path.GetFullPath(path); @@ -45,7 +45,7 @@ internal WindowsFileStreamStrategy(string path, FileMode mode, FileAccess access _access = access; _share = share; - _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options, allocationSize); + _fileHandle = FileStreamHelpers.OpenHandle(fullPath, mode, access, share, options, preallocationSize); try { diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 0e05c1490dc7da..8ad5921ce863dd 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -7299,6 +7299,15 @@ public enum FileShare Delete = 4, Inheritable = 16, } + public sealed class FileStreamOptions + { + public FileStreamOptions() { } + public System.IO.FileMode Mode { get; set; } + public System.IO.FileAccess Access { get; set; } + public System.IO.FileShare Share { get; set; } + public System.IO.FileOptions Options { get; set; } + public long PreallocationSize { get; set; } + } public partial class FileStream : System.IO.Stream { public FileStream(Microsoft.Win32.SafeHandles.SafeFileHandle handle, System.IO.FileAccess access) { } @@ -7318,7 +7327,7 @@ public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess acc public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share, int bufferSize) { } public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share, int bufferSize, bool useAsync) { } public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share, int bufferSize, System.IO.FileOptions options) { } - public FileStream(string path, System.IO.FileMode mode, System.IO.FileAccess access, System.IO.FileShare share = System.IO.FileShare.Read, int bufferSize = 4096, System.IO.FileOptions options = System.IO.FileOptions.None, long allocationSize = 0) { } + public FileStream(string path, System.IO.FileStreamOptions options) { } public override bool CanRead { get { throw null; } } public override bool CanSeek { get { throw null; } } public override bool CanWrite { get { throw null; } } From b0c8243afc8e83d8b00f8f954eed6dcbe2629e75 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 14 May 2021 23:12:54 +0200 Subject: [PATCH 32/37] address code review feedback: throw ArgumentOutOfRangeException when PreallocationSize is negative --- .../tests/FileStream/ctor_options_as.cs | 31 +++++++------------ .../src/System/IO/FileStream.cs | 6 +++- 2 files changed, 17 insertions(+), 20 deletions(-) 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 9084e7cb66167b..759581b9f781bc 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 @@ -40,14 +40,7 @@ protected FileStreamOptions GetOptions(FileMode mode, FileAccess access, FileSha public class FileStream_ctor_options_as_zero : FileStream_ctor_options_as_base { - protected override long AllocationSize => 0; // specifying 0 should have no effect - - protected override long InitialLength => 0; - } - - public class FileStream_ctor_options_as_negative : FileStream_ctor_options_as_base - { - protected override long AllocationSize => -1; // specifying negative value should have no effect + protected override long PreallocationSize => 0; // specifying 0 should have no effect protected override long InitialLength => 0; } @@ -61,13 +54,18 @@ public partial class NoParallelTests { } [Collection("NoParallelTests")] public partial class FileStream_ctor_options_as : FileStream_ctor_options_as_base { + [Fact] + public void NegativePreallocationSizeThrows() + { + string filePath = GetPathToNonExistingFile(); + ArgumentOutOfRangeException ex = Assert.Throws( + () => new FileStream(filePath, GetOptions(FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.None, -1))); + } + [Theory] [InlineData(FileMode.Create, 0L)] - [InlineData(FileMode.Create, -1L)] [InlineData(FileMode.CreateNew, 0L)] - [InlineData(FileMode.CreateNew, -1L)] [InlineData(FileMode.OpenOrCreate, 0L)] - [InlineData(FileMode.OpenOrCreate, -1L)] public void WhenFileIsCreatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet(FileMode mode, long preallocationSize) { using (var fs = new FileStream(GetPathToNonExistingFile(), GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) @@ -80,13 +78,10 @@ public void WhenFileIsCreatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNo [Theory] [InlineData(FileMode.Open, 0L)] - [InlineData(FileMode.Open, -1L)] [InlineData(FileMode.Open, 1L)] [InlineData(FileMode.OpenOrCreate, 0L)] - [InlineData(FileMode.OpenOrCreate, -1L)] [InlineData(FileMode.OpenOrCreate, 1L)] [InlineData(FileMode.Append, 0L)] - [InlineData(FileMode.Append, -1L)] [InlineData(FileMode.Append, 1L)] public void WhenExistingFileIsBeingOpenedWithAllocationSizeSpecifiedTheAllocationSizeIsNotChanged(FileMode mode, long preallocationSize) { @@ -153,17 +148,15 @@ public void WhenDiskIsFullTheErrorMessageContainsAllDetails(FileMode mode) Assert.False(exists); } - [Theory] - [InlineData(0L)] - [InlineData(-1L)] - public void WhenFileIsTruncatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet(int allocationSize) + [Fact] + public void WhenFileIsTruncatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet() { const int initialSize = 10_000; string filePath = GetPathToNonExistingFile(); File.WriteAllBytes(filePath, new byte[initialSize]); - using (var fs = new FileStream(filePath, GetOptions(FileMode.Truncate, FileAccess.Write, FileShare.None, FileOptions.None, allocationSize))) + using (var fs = new FileStream(filePath, GetOptions(FileMode.Truncate, FileAccess.Write, FileShare.None, FileOptions.None, 0))) { Assert.Equal(0, GetActualAllocationSize(fs)); Assert.Equal(0, fs.Length); 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 27305f598b5c85..ece38164dc1527 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -144,7 +144,7 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share /// -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 zero. + /// is negative or zero. /// -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. @@ -208,6 +208,10 @@ private FileStream(string path, FileMode mode, FileAccess access, FileShare shar { 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) From 2145dc10b6668aec8adea7bd623e38725644f527 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 14 May 2021 23:15:37 +0200 Subject: [PATCH 33/37] address code review feedback: move error codes --- .../src/Interop/Windows/NtDll/Interop.NtCreateFile.cs | 4 ++++ .../System/IO/Strategies/FileStreamHelpers.Windows.cs | 10 +++------- 2 files changed, 7 insertions(+), 7 deletions(-) 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 15a00f0dd85f01..c1de1b384cf998 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs @@ -10,6 +10,10 @@ 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)] 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 d71944b095a011..13dcc7cfb17e46 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 @@ -28,10 +28,6 @@ internal static class TaskSourceCodes internal const ulong ResultMask = ((ulong)uint.MaxValue) << 32; } - private const uint NT_ERROR_STATUS_DISK_FULL = 0xC000007F; - private const uint NT_ERROR_STATUS_FILE_TOO_LARGE = 0xC0000904; - private const uint NT_STATUS_INVALID_PARAMETER = 0xC000000D; - private static FileStreamStrategy ChooseStrategyCore(SafeFileHandle handle, FileAccess access, FileShare share, int bufferSize, bool isAsync) { if (UseNet5CompatStrategy) @@ -142,12 +138,12 @@ private static IntPtr NtCreateFile(string fullPath, FileMode mode, FileAccess ac { case 0: return fileHandle; - case NT_ERROR_STATUS_DISK_FULL: + 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 NT_STATUS_INVALID_PARAMETER: - case NT_ERROR_STATUS_FILE_TOO_LARGE: + 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); From 5eed718910f33d4f42da1dcd97cdb5e42d33a0aa Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Fri, 14 May 2021 23:39:13 +0200 Subject: [PATCH 34/37] SECURITY_ANONYMOUS --- .../Windows/Interop.OBJECT_ATTRIBUTES.cs | 6 +- .../Interop.SECURITY_QUALITY_OF_SERVICE.cs | 75 +++++++++++++++++++ .../Windows/NtDll/Interop.NtCreateFile.cs | 21 +++++- .../src/Microsoft.IO.Redist.csproj | 2 + .../src/System.IO.FileSystem.csproj | 4 +- .../System.Private.CoreLib.Shared.projitems | 3 + .../System.Security.Principal.Windows.csproj | 4 + 7 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 src/libraries/Common/src/Interop/Windows/Interop.SECURITY_QUALITY_OF_SERVICE.cs diff --git a/src/libraries/Common/src/Interop/Windows/Interop.OBJECT_ATTRIBUTES.cs b/src/libraries/Common/src/Interop/Windows/Interop.OBJECT_ATTRIBUTES.cs index f89fb97c9fdc6a..f0399f82153081 100644 --- a/src/libraries/Common/src/Interop/Windows/Interop.OBJECT_ATTRIBUTES.cs +++ b/src/libraries/Common/src/Interop/Windows/Interop.OBJECT_ATTRIBUTES.cs @@ -38,19 +38,19 @@ internal unsafe struct OBJECT_ATTRIBUTES /// Optional quality of service to be applied to the object. Used to indicate /// security impersonation level and context tracking mode (dynamic or static). /// - public void* SecurityQualityOfService; + public SECURITY_QUALITY_OF_SERVICE* SecurityQualityOfService; /// /// Equivalent of InitializeObjectAttributes macro with the exception that you can directly set SQOS. /// - public unsafe OBJECT_ATTRIBUTES(UNICODE_STRING* objectName, ObjectAttributes attributes, IntPtr rootDirectory) + public unsafe OBJECT_ATTRIBUTES(UNICODE_STRING* objectName, ObjectAttributes attributes, IntPtr rootDirectory, SECURITY_QUALITY_OF_SERVICE* securityQualityOfService = null) { Length = (uint)sizeof(OBJECT_ATTRIBUTES); RootDirectory = rootDirectory; ObjectName = objectName; Attributes = attributes; SecurityDescriptor = null; - SecurityQualityOfService = null; + SecurityQualityOfService = securityQualityOfService; } } diff --git a/src/libraries/Common/src/Interop/Windows/Interop.SECURITY_QUALITY_OF_SERVICE.cs b/src/libraries/Common/src/Interop/Windows/Interop.SECURITY_QUALITY_OF_SERVICE.cs new file mode 100644 index 00000000000000..28854c76d2a8fd --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/Interop.SECURITY_QUALITY_OF_SERVICE.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +internal static partial class Interop +{ + /// + /// SECURITY_QUALITY_OF_SERVICE structure. + /// Used to support client impersonation. Client specifies this to a server to allow + /// it to impersonate the client. + /// + internal unsafe struct SECURITY_QUALITY_OF_SERVICE + { + public uint Length; + public ImpersonationLevel ImpersonationLevel; + public ContextTrackingMode ContextTrackingMode; + public BOOLEAN EffectiveOnly; + + public unsafe SECURITY_QUALITY_OF_SERVICE(ImpersonationLevel impersonationLevel, ContextTrackingMode contextTrackingMode, bool effectiveOnly) + { + Length = (uint)sizeof(SECURITY_QUALITY_OF_SERVICE); + ImpersonationLevel = impersonationLevel; + ContextTrackingMode = contextTrackingMode; + EffectiveOnly = effectiveOnly ? BOOLEAN.TRUE : BOOLEAN.FALSE; + } + } + + /// + /// SECURITY_IMPERSONATION_LEVEL enumeration values. + /// [SECURITY_IMPERSONATION_LEVEL] + /// + public enum ImpersonationLevel : uint + { + /// + /// The server process cannot obtain identification information about the client and cannot impersonate the client. + /// [SecurityAnonymous] + /// + Anonymous, + + /// + /// The server process can obtain identification information about the client, but cannot impersonate the client. + /// [SecurityIdentification] + /// + Identification, + + /// + /// The server process can impersonate the client's security context on it's local system. + /// [SecurityImpersonation] + /// + Impersonation, + + /// + /// The server process can impersonate the client's security context on remote systems. + /// [SecurityDelegation] + /// + Delegation + } + + /// + /// SECURITY_CONTEXT_TRACKING_MODE + /// + public enum ContextTrackingMode : byte + { + /// + /// The server is given a snapshot of the client's security context. + /// [SECURITY_STATIC_TRACKING] + /// + Static = 0x00, + + /// + /// The server is continually updated with changes. + /// [SECURITY_DYNAMIC_TRACKING] + /// + Dynamic = 0x01 + } +} 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 c1de1b384cf998..9ce82c7f4ad453 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtCreateFile.cs @@ -41,7 +41,8 @@ internal static unsafe (uint status, IntPtr handle) CreateFile( ObjectAttributes objectAttributes = ObjectAttributes.OBJ_CASE_INSENSITIVE, void* eaBuffer = null, uint eaLength = 0, - long* preallocationSize = null) + long* preallocationSize = null, + SECURITY_QUALITY_OF_SERVICE* securityQualityOfService = null) { fixed (char* c = &MemoryMarshal.GetReference(path)) { @@ -55,7 +56,8 @@ internal static unsafe (uint status, IntPtr handle) CreateFile( OBJECT_ATTRIBUTES attributes = new OBJECT_ATTRIBUTES( &name, objectAttributes, - rootDirectory); + rootDirectory, + securityQualityOfService); uint status = NtCreateFile( out IntPtr handle, @@ -75,7 +77,16 @@ internal static unsafe (uint status, IntPtr handle) CreateFile( } internal static unsafe (uint status, IntPtr handle) CreateFile(ReadOnlySpan path, FileMode mode, FileAccess access, FileShare share, FileOptions options, long preallocationSize) - => CreateFile( + { + // For mitigating local elevation of privilege attack through named pipes + // make sure we always call NtCreateFile with SECURITY_ANONYMOUS so that the + // named pipe server can't impersonate a high privileged client security context + SECURITY_QUALITY_OF_SERVICE securityQualityOfService = new SECURITY_QUALITY_OF_SERVICE( + ImpersonationLevel.Anonymous, // SECURITY_ANONYMOUS + ContextTrackingMode.Static, + effectiveOnly: false); + + return CreateFile( path: path, rootDirectory: IntPtr.Zero, createDisposition: GetCreateDisposition(mode), @@ -84,7 +95,9 @@ internal static unsafe (uint status, IntPtr handle) CreateFile(ReadOnlySpan + + true true @@ -105,6 +105,8 @@ Link="Common\Interop\Windows\Interop.Libraries.cs" /> + Common\Interop\Windows\Interop.UNICODE_STRING.cs + + Common\Interop\Windows\Interop.SECURITY_QUALITY_OF_SERVICE.cs + Common\Interop\Windows\Interop.OBJECT_ATTRIBUTES.cs diff --git a/src/libraries/System.Security.Principal.Windows/src/System.Security.Principal.Windows.csproj b/src/libraries/System.Security.Principal.Windows/src/System.Security.Principal.Windows.csproj index 56fcfb62c407ee..4c9b0a3aaf9a0a 100644 --- a/src/libraries/System.Security.Principal.Windows/src/System.Security.Principal.Windows.csproj +++ b/src/libraries/System.Security.Principal.Windows/src/System.Security.Principal.Windows.csproj @@ -31,6 +31,10 @@ + + Date: Mon, 17 May 2021 21:57:32 +0200 Subject: [PATCH 35/37] address code review feedback --- .../Interop.SECURITY_QUALITY_OF_SERVICE.cs | 2 +- .../src/Microsoft.IO.Redist.csproj | 2 +- .../FileStream/ctor_options_as.Browser.cs | 2 +- .../tests/FileStream/ctor_options_as.Unix.cs | 2 +- .../FileStream/ctor_options_as.Windows.cs | 4 +-- .../tests/FileStream/ctor_options_as.cs | 26 +++++++++---------- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/libraries/Common/src/Interop/Windows/Interop.SECURITY_QUALITY_OF_SERVICE.cs b/src/libraries/Common/src/Interop/Windows/Interop.SECURITY_QUALITY_OF_SERVICE.cs index 28854c76d2a8fd..3b63485ef987e3 100644 --- a/src/libraries/Common/src/Interop/Windows/Interop.SECURITY_QUALITY_OF_SERVICE.cs +++ b/src/libraries/Common/src/Interop/Windows/Interop.SECURITY_QUALITY_OF_SERVICE.cs @@ -4,7 +4,7 @@ internal static partial class Interop { /// - /// SECURITY_QUALITY_OF_SERVICE structure. + /// SECURITY_QUALITY_OF_SERVICE structure. /// Used to support client impersonation. Client specifies this to a server to allow /// it to impersonate the client. /// diff --git a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj index 1135ce9a19be29..94b9552748a987 100644 --- a/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj +++ b/src/libraries/Microsoft.IO.Redist/src/Microsoft.IO.Redist.csproj @@ -137,7 +137,7 @@ + Link="Common\Interop\Windows\Interop.SECURITY_QUALITY_OF_SERVICE.cs" /> preallocationSize; - private long GetActualAllocationSize(FileStream fileStream) => fileStream.Length; + private long GetActualPreallocationSize(FileStream fileStream) => fileStream.Length; } } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Unix.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Unix.cs index 3e5ea0a6d08897..12e8f1641bac00 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Unix.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Unix.cs @@ -11,7 +11,7 @@ public partial class FileStream_ctor_options_as : FileStream_ctor_options_as_bas private long GetExpectedFileLength(long preallocationSize) => preallocationSize; - private long GetActualAllocationSize(FileStream fileStream) + private long GetActualPreallocationSize(FileStream fileStream) { // On Unix posix_fallocate modifies file length and we are using fstat to get it for verificaiton Interop.Sys.FStat(fileStream.SafeFileHandle, out Interop.Sys.FileStatus fileStatus); diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Windows.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Windows.cs index 114a8881dfe334..bde8cd5e4692a3 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Windows.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ctor_options_as.Windows.cs @@ -15,7 +15,7 @@ public partial class FileStream_ctor_options_as : FileStream_ctor_options_as_bas private long GetExpectedFileLength(long preallocationSize) => 0; // Windows modifies AllocationSize, but not EndOfFile (file length) - private unsafe long GetActualAllocationSize(FileStream fileStream) + private unsafe long GetActualPreallocationSize(FileStream fileStream) { Interop.Kernel32.FILE_STANDARD_INFO info; @@ -36,7 +36,7 @@ public void ExtendedPathsAreSupported(string prefix) using (var fs = new FileStream(filePath, GetOptions(FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) { - Assert.True(GetActualAllocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualAllocationSize(fs)}"); + Assert.True(GetActualPreallocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualPreallocationSize(fs)}"); } } 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 759581b9f781bc..d98090b37f67e4 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 @@ -66,11 +66,11 @@ public void NegativePreallocationSizeThrows() [InlineData(FileMode.Create, 0L)] [InlineData(FileMode.CreateNew, 0L)] [InlineData(FileMode.OpenOrCreate, 0L)] - public void WhenFileIsCreatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet(FileMode mode, long preallocationSize) + public void WhenFileIsCreatedWithoutPreallocationSizeSpecifiedThePreallocationSizeIsNotSet(FileMode mode, long preallocationSize) { using (var fs = new FileStream(GetPathToNonExistingFile(), GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) { - Assert.Equal(0, GetActualAllocationSize(fs)); + Assert.Equal(0, GetActualPreallocationSize(fs)); Assert.Equal(0, fs.Length); Assert.Equal(0, fs.Position); } @@ -83,21 +83,21 @@ public void WhenFileIsCreatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNo [InlineData(FileMode.OpenOrCreate, 1L)] [InlineData(FileMode.Append, 0L)] [InlineData(FileMode.Append, 1L)] - public void WhenExistingFileIsBeingOpenedWithAllocationSizeSpecifiedTheAllocationSizeIsNotChanged(FileMode mode, long preallocationSize) + public void WhenExistingFileIsBeingOpenedWithPreallocationSizeSpecifiedThePreallocationSizeIsNotChanged(FileMode mode, long preallocationSize) { const int initialSize = 1; string filePath = GetPathToNonExistingFile(); File.WriteAllBytes(filePath, new byte[initialSize]); - long initialAllocationSize; + long initialPreallocationSize; using (var fs = new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, 0))) // preallocationSize NOT provided { - initialAllocationSize = GetActualAllocationSize(fs); // just read it to ensure it's not being changed + initialPreallocationSize = GetActualPreallocationSize(fs); // just read it to ensure it's not being changed } using (var fs = new FileStream(filePath, GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) { - Assert.Equal(initialAllocationSize, GetActualAllocationSize(fs)); // it has NOT been changed + Assert.Equal(initialPreallocationSize, GetActualPreallocationSize(fs)); // it has NOT been changed Assert.Equal(initialSize, fs.Length); Assert.Equal(mode == FileMode.Append ? initialSize : 0, fs.Position); } @@ -107,14 +107,14 @@ public void WhenExistingFileIsBeingOpenedWithAllocationSizeSpecifiedTheAllocatio [InlineData(FileMode.Create)] [InlineData(FileMode.CreateNew)] [InlineData(FileMode.OpenOrCreate)] - public void WhenFileIsCreatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet(FileMode mode) + public void WhenFileIsCreatedWithPreallocationSizeSpecifiedThePreallocationSizeIsSet(FileMode mode) { const long preallocationSize = 123; using (var fs = new FileStream(GetPathToNonExistingFile(), GetOptions(mode, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) { // OS might allocate MORE than we have requested - Assert.True(GetActualAllocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualAllocationSize(fs)}"); + Assert.True(GetActualPreallocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualPreallocationSize(fs)}"); Assert.Equal(GetExpectedFileLength(preallocationSize), fs.Length); Assert.Equal(0, fs.Position); } @@ -149,7 +149,7 @@ public void WhenDiskIsFullTheErrorMessageContainsAllDetails(FileMode mode) } [Fact] - public void WhenFileIsTruncatedWithoutAllocationSizeSpecifiedTheAllocationSizeIsNotSet() + public void WhenFileIsTruncatedWithoutPreallocationSizeSpecifiedThePreallocationSizeIsNotSet() { const int initialSize = 10_000; @@ -158,14 +158,14 @@ public void WhenFileIsTruncatedWithoutAllocationSizeSpecifiedTheAllocationSizeIs using (var fs = new FileStream(filePath, GetOptions(FileMode.Truncate, FileAccess.Write, FileShare.None, FileOptions.None, 0))) { - Assert.Equal(0, GetActualAllocationSize(fs)); + Assert.Equal(0, GetActualPreallocationSize(fs)); Assert.Equal(0, fs.Length); Assert.Equal(0, fs.Position); } } [Fact] - public void WhenFileIsTruncatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet() + public void WhenFileIsTruncatedWithPreallocationSizeSpecifiedThePreallocationSizeIsSet() { const int initialSize = 10_000; // this must be more than 4kb which seems to be minimum allocation size on Windows const long preallocationSize = 100; @@ -175,9 +175,9 @@ public void WhenFileIsTruncatedWithAllocationSizeSpecifiedTheAllocationSizeIsSet using (var fs = new FileStream(filePath, GetOptions(FileMode.Truncate, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize))) { - Assert.True(GetActualAllocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualAllocationSize(fs)}"); + Assert.True(GetActualPreallocationSize(fs) >= preallocationSize, $"Provided {preallocationSize}, actual: {GetActualPreallocationSize(fs)}"); // less than initial file size (file got truncated) - Assert.True(GetActualAllocationSize(fs) < initialSize, $"initialSize {initialSize}, actual: {GetActualAllocationSize(fs)}"); + Assert.True(GetActualPreallocationSize(fs) < initialSize, $"initialSize {initialSize}, actual: {GetActualPreallocationSize(fs)}"); Assert.Equal(GetExpectedFileLength(preallocationSize), fs.Length); Assert.Equal(0, fs.Position); } From 972807864e2de634f6055d2108b15d41e3af78c8 Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 18 May 2021 08:01:36 +0200 Subject: [PATCH 36/37] address code review feedback and clarify which values are supported and which ones throw --- .../System.Private.CoreLib/src/System/IO/FileStream.cs | 2 +- .../System.Private.CoreLib/src/System/IO/FileStreamOptions.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 76e7d863895d97..26e90f4467d0b9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs @@ -149,7 +149,7 @@ public FileStream(string path, FileMode mode, FileAccess access, FileShare share /// -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 zero. + /// 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. diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs index 8fd78924cbf3c9..f7562041473824 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs @@ -22,7 +22,9 @@ public sealed class FileStreamOptions /// public FileOptions Options { get; set; } /// - /// The initial allocation size in bytes for the file. A value has no effect unless it's positive and a regular file is being created, overwritten, or replaced. + /// 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. + /// When the value is negative, the constructor throws an . + /// In other cases (including the default 0 value), it's ignored. /// public long PreallocationSize { get; set; } } From 8648f65c29466716fcc46443060641e6ac204dfc Mon Sep 17 00:00:00 2001 From: Adam Sitnik Date: Tue, 18 May 2021 08:29:26 +0200 Subject: [PATCH 37/37] fix XML docs --- .../System.Private.CoreLib/src/System/IO/FileStreamOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs index f7562041473824..c7dccfd2161092 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStreamOptions.cs @@ -23,7 +23,7 @@ public sealed class FileStreamOptions public FileOptions Options { get; set; } /// /// 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. - /// When the value is negative, the constructor throws an . + /// When the value is negative, the constructor throws an . /// In other cases (including the default 0 value), it's ignored. /// public long PreallocationSize { get; set; }