diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Fcntl.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Fcntl.cs index 231621f2fa3074..af953da05f05e7 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Fcntl.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Fcntl.cs @@ -27,6 +27,9 @@ internal static partial class Fcntl [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_FcntlGetFD", SetLastError=true)] internal static extern int GetFD(IntPtr fd); + + [DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetIsAppend")] + internal static extern bool GetIsAppend(SafeHandle fd); } } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenFlags.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenFlags.cs index ce1b5aa6562f2a..308aa4b4c1580e 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenFlags.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenFlags.cs @@ -21,6 +21,7 @@ internal enum OpenFlags O_EXCL = 0x0040, O_TRUNC = 0x0080, O_SYNC = 0x0100, + O_APPEND = 0x0200, } } } 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 402443b05e944e..a7676b6fb95352 100644 --- a/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtQueryInformationFile.cs +++ b/src/libraries/Common/src/Interop/Windows/NtDll/Interop.NtQueryInformationFile.cs @@ -16,6 +16,7 @@ internal static extern unsafe int NtQueryInformationFile( uint Length, uint FileInformationClass); + internal const uint FileAccessInformation = 8; internal const uint FileModeInformation = 16; internal const int STATUS_INVALID_HANDLE = unchecked((int)0xC0000008); diff --git a/src/libraries/Native/Unix/System.Native/entrypoints.c b/src/libraries/Native/Unix/System.Native/entrypoints.c index c8b678ec08340b..0075e6ccbd5002 100644 --- a/src/libraries/Native/Unix/System.Native/entrypoints.c +++ b/src/libraries/Native/Unix/System.Native/entrypoints.c @@ -72,6 +72,7 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_FcntlSetPipeSz) DllImportEntry(SystemNative_FcntlSetIsNonBlocking) DllImportEntry(SystemNative_FcntlGetIsNonBlocking) + DllImportEntry(SystemNative_GetIsAppend) DllImportEntry(SystemNative_MkDir) DllImportEntry(SystemNative_ChMod) DllImportEntry(SystemNative_FChMod) diff --git a/src/libraries/Native/Unix/System.Native/pal_io.c b/src/libraries/Native/Unix/System.Native/pal_io.c index 51d841ffc55bdb..330fd5a09f78b2 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.c +++ b/src/libraries/Native/Unix/System.Native/pal_io.c @@ -249,7 +249,7 @@ static int32_t ConvertOpenFlags(int32_t flags) return -1; } - if (flags & ~(PAL_O_ACCESS_MODE_MASK | PAL_O_CLOEXEC | PAL_O_CREAT | PAL_O_EXCL | PAL_O_TRUNC | PAL_O_SYNC)) + if (flags & ~(PAL_O_ACCESS_MODE_MASK | PAL_O_CLOEXEC | PAL_O_CREAT | PAL_O_EXCL | PAL_O_TRUNC | PAL_O_SYNC | PAL_O_APPEND)) { assert_msg(false, "Unknown Open flag", (int)flags); return -1; @@ -267,6 +267,8 @@ static int32_t ConvertOpenFlags(int32_t flags) ret |= O_TRUNC; if (flags & PAL_O_SYNC) ret |= O_SYNC; + if (flags & PAL_O_APPEND) + ret |= O_APPEND; assert(ret != -1); return ret; @@ -646,6 +648,11 @@ int32_t SystemNative_FcntlGetIsNonBlocking(intptr_t fd, int32_t* isNonBlocking) return 0; } +int32_t SystemNative_GetIsAppend(intptr_t fd) +{ + return fcntl(ToFileDescriptor(fd), F_GETFL) & O_APPEND; +} + int32_t SystemNative_MkDir(const char* path, int32_t mode) { int32_t result; diff --git a/src/libraries/Native/Unix/System.Native/pal_io.h b/src/libraries/Native/Unix/System.Native/pal_io.h index 3f0db054d4368b..2641816987ba5a 100644 --- a/src/libraries/Native/Unix/System.Native/pal_io.h +++ b/src/libraries/Native/Unix/System.Native/pal_io.h @@ -154,6 +154,7 @@ enum PAL_O_EXCL = 0x0040, // When combined with CREAT, fails if file already exists PAL_O_TRUNC = 0x0080, // Truncate file to length 0 if it already exists PAL_O_SYNC = 0x0100, // Block writes call will block until physically written + PAL_O_APPEND = 0x0200, // atomic append to the end of file }; /** @@ -468,6 +469,11 @@ PALEXPORT int32_t SystemNative_FcntlSetIsNonBlocking(intptr_t fd, int32_t isNonB */ PALEXPORT int32_t SystemNative_FcntlGetIsNonBlocking(intptr_t fd, int32_t* isNonBlocking); +/** + * Gets whether or not a file was opened with 0_APPEND. Returns false on failure. +*/ +PALEXPORT int32_t SystemNative_GetIsAppend(intptr_t fd); + /** * Create a directory. Implemented as a shim to mkdir(2). * diff --git a/src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs b/src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs index a8b3619fb51cf7..157ca2b52feeec 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/OpenHandle.cs @@ -30,13 +30,6 @@ public override void NegativePreallocationSizeThrows() () => File.OpenHandle("validPath", FileMode.CreateNew, FileAccess.Write, FileShare.None, FileOptions.None, preallocationSize: -1)); } - [ActiveIssue("https://github.com/dotnet/runtime/issues/53432")] - [Theory, MemberData(nameof(StreamSpecifiers))] - public override void FileModeAppendExisting(string streamSpecifier) - { - _ = streamSpecifier; // to keep the xUnit analyser happy - } - [Theory] [InlineData(FileOptions.None)] [InlineData(FileOptions.Asynchronous)] diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/Position.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/Position.cs index 32817dcee26674..bb80f63d2ab15a 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/Position.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/Position.cs @@ -28,6 +28,7 @@ public void SetPositionAppendModify() Assert.Equal(length + 1, fs.Position); fs.Write(TestBuffer); + length = fs.Length; fs.Position = length + 1; Assert.Equal(length + 1, fs.Position); } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/Seek.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/Seek.cs index 963a1fddff57d4..99ca139ac64965 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/Seek.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/Seek.cs @@ -34,7 +34,7 @@ public void SeekAppendModifyThrows() Assert.Equal(length, fs.Position); fs.Write(TestBuffer); - Assert.Equal(length, fs.Seek(length, SeekOrigin.Begin)); + Assert.Equal(fs.Position, fs.Seek(fs.Length, SeekOrigin.Begin)); } } } diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/SetLength.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/SetLength.cs index 3a3f547d070e90..90f62fb269d4ca 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/SetLength.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/SetLength.cs @@ -27,8 +27,10 @@ public void SetLengthAppendModifyThrows() fs.Write(TestBuffer); Assert.Equal(length + TestBuffer.Length, fs.Length); - fs.SetLength(length); - Assert.Equal(length, fs.Length); + if (PlatformDetection.IsNet5CompatFileStreamDisabled) + { + Assert.Throws(() => fs.SetLength(fs.Length - 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 47e3b492373161..ac08a74bea3df8 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 @@ -223,14 +223,13 @@ public virtual void FileModeAppend(string streamSpecifier) fs.Write(Encoding.ASCII.GetBytes("abcde")); Assert.Equal(5, fs.Length); Assert.Equal(5, fs.Position); - Assert.Equal(1, fs.Seek(1, SeekOrigin.Begin)); fs.Write(Encoding.ASCII.GetBytes("xyz")); - Assert.Equal(4, fs.Position); - Assert.Equal(5, fs.Length); + Assert.Equal(8, fs.Position); + Assert.Equal(8, fs.Length); } - Assert.Equal("axyze", File.ReadAllText(fileName)); + Assert.Equal("abcdexyz", File.ReadAllText(fileName)); } [Theory, MemberData(nameof(StreamSpecifiers))] @@ -262,12 +261,11 @@ public virtual void FileModeAppendExisting(string streamSpecifier) fs.Write(Encoding.ASCII.GetBytes("abcde")); Assert.Equal(position + 5, fs.Position); - Assert.Equal(position, fs.Seek(position, SeekOrigin.Begin)); - Assert.Equal(position + 1, fs.Seek(1, SeekOrigin.Current)); fs.Write(Encoding.ASCII.GetBytes("xyz")); + Assert.Equal(position + 8, fs.Position); } - Assert.Equal(initialContents + "axyze", File.ReadAllText(fileName)); + Assert.Equal(initialContents + "abcdexyz", File.ReadAllText(fileName)); } } } 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 745ff60368b2ef..69c59685e7b8f7 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 @@ -9,6 +9,8 @@ + + diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs index ee1def85b9e024..4983fd72ffbfbb 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs @@ -47,6 +47,7 @@ private static SafeFileHandle Open(string path, Interop.Sys.OpenFlags flags, int Debug.Assert(path != null); SafeFileHandle handle = Interop.Sys.Open(path, flags, mode); handle._path = path; + handle._isAppend = (flags & Interop.Sys.OpenFlags.O_APPEND) != 0 ? NullableBool.True : NullableBool.False; if (handle.IsInvalid) { @@ -216,7 +217,6 @@ private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mo } break; - case FileMode.Append: // Append is the same as OpenOrCreate, except that we'll also separately jump to the end later case FileMode.OpenOrCreate: flags |= Interop.Sys.OpenFlags.O_CREAT; break; @@ -232,6 +232,14 @@ private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mo case FileMode.CreateNew: flags |= (Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL); break; + + case FileMode.Append: + flags |= Interop.Sys.OpenFlags.O_CREAT; + if (!FileStreamHelpers.UseNet5CompatStrategy) // we used to seek to the end of file in .NET 5 + { + flags |= Interop.Sys.OpenFlags.O_APPEND; + } + break; } // Translate FileAccess. All possible values map cleanly to corresponding values for open. @@ -389,11 +397,18 @@ private bool GetCanSeek() return canSeek == NullableBool.True; } - private enum NullableBool + private bool GetIsAppend() { - Undefined = 0, - False = -1, - True = 1 + Debug.Assert(!IsClosed); + Debug.Assert(!IsInvalid); + + NullableBool isAppend = _isAppend; + if (isAppend == NullableBool.Undefined) + { + _isAppend = isAppend = Interop.Sys.Fcntl.GetIsAppend(this) ? NullableBool.True : NullableBool.False; + } + + return isAppend == NullableBool.True; } } } diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs index a7c4751a094786..a1a5069958cc88 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs @@ -69,10 +69,19 @@ private static unsafe SafeFileHandle CreateFile(string fullPath, FileMode mode, // the security attributes class. Don't leave this bit set. share &= ~FileShare.Inheritable; - // Must use a valid Win32 constant here... - if (mode == FileMode.Append) + if (!FileStreamHelpers.UseNet5CompatStrategy) { - mode = FileMode.OpenOrCreate; + if (mode == FileMode.Append) + { + fAccess |= (int)Interop.NtDll.DesiredAccess.FILE_APPEND_DATA; + } + else + { + // FILE_GENERIC_WRITE consists of FILE_APPEND_DATA amongst many other flags + // we don't want to recognize all FILE_GENERIC_WRITE handles as opened for APPEND + // otherwise we would not be able to recognize them in GetIsAppend + fAccess &= ~(int)Interop.NtDll.DesiredAccess.FILE_APPEND_DATA; + } } int flagsAndAttributes = (int)options; @@ -83,7 +92,8 @@ private static unsafe SafeFileHandle CreateFile(string fullPath, FileMode mode, // (note that this is the effective default on CreateFile2) flagsAndAttributes |= (Interop.Kernel32.SecurityOptions.SECURITY_SQOS_PRESENT | Interop.Kernel32.SecurityOptions.SECURITY_ANONYMOUS); - SafeFileHandle fileHandle = Interop.Kernel32.CreateFile(fullPath, fAccess, share, &secAttrs, mode, flagsAndAttributes, IntPtr.Zero); + FileMode windowsMode = mode == FileMode.Append ? FileMode.OpenOrCreate : mode; // Must use a valid Win32 constant here.. + SafeFileHandle fileHandle = Interop.Kernel32.CreateFile(fullPath, fAccess, share, &secAttrs, windowsMode, flagsAndAttributes, IntPtr.Zero); if (fileHandle.IsInvalid) { // Return a meaningful exception with the full path. @@ -103,6 +113,7 @@ private static unsafe SafeFileHandle CreateFile(string fullPath, FileMode mode, fileHandle._path = fullPath; fileHandle._fileOptions = options; + fileHandle._isAppend = mode == FileMode.Append ? NullableBool.True : NullableBool.False; return fileHandle; } @@ -252,5 +263,33 @@ internal int GetFileType() return fileType; } + + private unsafe bool GetIsAppend() + { + NullableBool isAppend = _isAppend; + if (isAppend == NullableBool.Undefined) + { + Interop.NtDll.DesiredAccess access; + int ntStatus = Interop.NtDll.NtQueryInformationFile( + FileHandle: this, + IoStatusBlock: out _, + FileInformation: &access, + Length: sizeof(Interop.NtDll.DesiredAccess), + FileInformationClass: Interop.NtDll.FileModeInformation); + + if (ntStatus != Interop.StatusOptions.STATUS_SUCCESS) // we have failed, so we assume it's false + { + _isAppend = isAppend = NullableBool.False; + } + else + { + // FILE_GENERIC_WRITE consists of FILE_APPEND_DATA amongst many other flags + // we don't want to recognize all FILE_GENERIC_WRITE handles as opened for APPEND + _isAppend = isAppend = (access & Interop.NtDll.DesiredAccess.FILE_APPEND_DATA) != 0 && (access & Interop.NtDll.DesiredAccess.FILE_GENERIC_WRITE) == 0 ? NullableBool.True : NullableBool.False; + } + } + + return isAppend == NullableBool.True; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs index 76dc55b74dba71..a429a37dc2d543 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs @@ -7,6 +7,7 @@ namespace Microsoft.Win32.SafeHandles { public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid { + private volatile NullableBool _isAppend = NullableBool.Undefined; private string? _path; /// @@ -20,5 +21,14 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand } internal string? Path => _path; + + internal bool IsAppend => GetIsAppend(); + + private enum NullableBool + { + Undefined = 0, + False = -1, + True = 1 + } } } 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 5a1593f032c69a..4fe97dabf85b95 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 @@ -457,6 +457,7 @@ + @@ -1939,6 +1940,9 @@ Common\Interop\Unix\System.Native\Interop.UnixFileSystemTypes.cs + + Common\Interop\Unix\System.Native\Interop.Fcntl.cs + Common\Interop\Unix\System.Native\Interop.FLock.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs index 992dcc54f5be58..fdf6e84ce6b9d8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs @@ -90,10 +90,12 @@ internal static unsafe void WriteAtOffset(SafeFileHandle handle, ReadOnlySpan _fileHandle.CanSeek; + + public override bool CanRead => false; // this is how FileMode.Append works + + public override bool CanWrite => !_fileHandle.IsClosed; + + public override long Length => RandomAccess.GetFileLength(_fileHandle); + + public override long Position + { + get => Length; + set => Seek(value, SeekOrigin.Begin); + } + + internal override bool IsAsync => _fileHandle.IsAsync; + + internal override string Name => _fileHandle.Path ?? SR.IO_UnknownFileName; + + internal override bool IsClosed => _fileHandle.IsClosed; + + internal override SafeFileHandle SafeFileHandle => _fileHandle; + + public override ValueTask DisposeAsync() + { + if (_fileHandle != null && !_fileHandle.IsClosed) + { + _fileHandle.ThreadPoolBinding?.Dispose(); + _fileHandle.Dispose(); + } + + return ValueTask.CompletedTask; + } + + internal override void DisposeInternal(bool disposing) => Dispose(disposing); + + protected override void Dispose(bool disposing) + { + if (disposing && _fileHandle != null && !_fileHandle.IsClosed) + { + _fileHandle.ThreadPoolBinding?.Dispose(); + _fileHandle.Dispose(); + } + } + + public override void Flush() { } // no buffering = nothing to flush + + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; // no buffering = nothing to flush + + internal override void Flush(bool flushToDisk) + { + if (flushToDisk && CanWrite) + { + FileStreamHelpers.FlushToDisk(_fileHandle); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin < SeekOrigin.Begin || origin > SeekOrigin.End) + { + throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)); + } + else if (!CanSeek) + { + if (_fileHandle.IsClosed) + { + ThrowHelper.ThrowObjectDisposedException_FileClosed(); + } + + ThrowHelper.ThrowNotSupportedException_UnseekableStream(); + } + + long length = Length; + long newLength = origin == SeekOrigin.Begin ? offset : length + offset; + + if (newLength < 0) + { + // keep throwing the same exception we did when seek was causing actual offset change + FileStreamHelpers.ThrowInvalidArgument(_fileHandle); + } + + // Prevent users from overwriting data in a file that was opened in append mode. + if (newLength < length) + { + throw new IOException(SR.IO_SeekAppendOverwrite); + } + + FileStreamHelpers.SetFileLength(_fileHandle, newLength); + + return newLength; + } + + internal override void Lock(long position, long length) => FileStreamHelpers.Lock(_fileHandle, CanWrite, position, length); + + internal override void Unlock(long position, long length) => FileStreamHelpers.Unlock(_fileHandle, position, length); + + public override void SetLength(long value) + { + if (value < Length) + { + throw new IOException(SR.IO_SetLengthAppendTruncate); + } + + FileStreamHelpers.SetFileLength(_fileHandle, value); + } + + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => throw new NotSupportedException(SR.NotSupported_UnreadableStream); + + public override int EndRead(IAsyncResult asyncResult) + => throw new NotSupportedException(SR.NotSupported_UnreadableStream); + + public override int ReadByte() + => throw new NotSupportedException(SR.NotSupported_UnreadableStream); + + public override int Read(byte[] buffer, int offset, int count) + => throw new NotSupportedException(SR.NotSupported_UnreadableStream); + + public override int Read(Span buffer) + => throw new NotSupportedException(SR.NotSupported_UnreadableStream); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => throw new NotSupportedException(SR.NotSupported_UnreadableStream); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + => throw new NotSupportedException(SR.NotSupported_UnreadableStream); + + public override unsafe void WriteByte(byte value) + => Write(new ReadOnlySpan(&value, 1)); + + public override void Write(byte[] buffer, int offset, int count) + => Write(new ReadOnlySpan(buffer, offset, count)); + + public override void Write(ReadOnlySpan buffer) + => RandomAccess.WriteAtOffset(_fileHandle, buffer, WriteOffset); + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => TaskToApm.Begin(WriteAsync(buffer, offset, count), callback, state); + + public override void EndWrite(IAsyncResult asyncResult) + => TaskToApm.End(asyncResult); + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken).AsTask(); + + public override ValueTask WriteAsync(ReadOnlyMemory source, CancellationToken cancellationToken) + => RandomAccess.WriteAtOffsetAsync(_fileHandle, source, WriteOffset, cancellationToken); + } +} 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 d86c70a621ca1a..c593c61cc2317b 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 @@ -18,18 +18,20 @@ internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, SafeFil // The .NET 5 Compat strategy does not support bufferSize == 0. // To minimize the risk of introducing bugs to it, we just pass 1 to disable the buffering. - FileStreamStrategy strategy = UseNet5CompatStrategy ? - new Net5CompatFileStreamStrategy(handle, access, bufferSize == 0 ? 1 : bufferSize, isAsync) : - EnableBufferingIfNeeded(ChooseStrategyCore(handle, access, isAsync), bufferSize); + FileStreamStrategy strategy = UseNet5CompatStrategy + ? new Net5CompatFileStreamStrategy(handle, access, bufferSize == 0 ? 1 : bufferSize, isAsync) + : EnableBufferingIfNeeded(handle.IsAppend ? new AppendFileStreamStrategy(handle, access) : ChooseStrategyCore(handle, access, isAsync), bufferSize); return WrapIfDerivedType(fileStream, strategy); } internal static FileStreamStrategy ChooseStrategy(FileStream fileStream, string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options, long preallocationSize) { - FileStreamStrategy strategy = UseNet5CompatStrategy ? - new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize == 0 ? 1 : bufferSize, options, preallocationSize) : - EnableBufferingIfNeeded(ChooseStrategyCore(path, mode, access, share, options, preallocationSize), bufferSize); + FileStreamStrategy strategy = UseNet5CompatStrategy + ? new Net5CompatFileStreamStrategy(path, mode, access, share, bufferSize == 0 ? 1 : bufferSize, options, preallocationSize) + : EnableBufferingIfNeeded(mode == FileMode.Append + ? new AppendFileStreamStrategy(path, mode, access, share, options, preallocationSize) + : ChooseStrategyCore(path, mode, access, share, options, preallocationSize), bufferSize); return WrapIfDerivedType(fileStream, strategy); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs index 0386dfa78d5631..2be3c3d4f1b0b7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/OSFileStreamStrategy.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; -using System.Threading.Tasks.Sources; using Microsoft.Win32.SafeHandles; namespace System.IO.Strategies @@ -17,7 +16,6 @@ internal abstract class OSFileStreamStrategy : FileStreamStrategy protected long _filePosition; private long _length = -1; // negative means that hasn't been fetched. - private long _appendStart; // When appending, prevent overwriting file. private bool _lengthCanBeCached; // SafeFileHandle hasn't been exposed, file has been opened for reading and not shared for writing. internal OSFileStreamStrategy(SafeFileHandle handle, FileAccess access) @@ -48,26 +46,6 @@ internal OSFileStreamStrategy(string path, FileMode mode, FileAccess access, Fil _lengthCanBeCached = (share & FileShare.Write) == 0 && (access & FileAccess.Write) == 0; _fileHandle = SafeFileHandle.Open(fullPath, mode, access, share, options, preallocationSize); - - try - { - if (mode == FileMode.Append && CanSeek) - { - _appendStart = _filePosition = Length; - } - else - { - _appendStart = -1; - } - } - catch - { - // If anything goes wrong while setting up the stream, make sure we deterministically dispose - // of the opened handle. - _fileHandle.Dispose(); - _fileHandle = null!; - throw; - } } internal override bool IsAsync => _fileHandle.IsAsync; @@ -78,7 +56,7 @@ internal OSFileStreamStrategy(string path, FileMode mode, FileAccess access, Fil public sealed override bool CanWrite => !_fileHandle.IsClosed && (_access & FileAccess.Write) != 0; - public unsafe sealed override long Length + public sealed override long Length { get { @@ -197,13 +175,6 @@ public sealed override long Seek(long offset, SeekOrigin origin) FileStreamHelpers.ThrowInvalidArgument(_fileHandle); } - // Prevent users from overwriting data in a file that was opened in append mode. - if (_appendStart != -1 && pos < _appendStart) - { - _filePosition = oldPos; - throw new IOException(SR.IO_SeekAppendOverwrite); - } - return pos; } @@ -213,13 +184,10 @@ public sealed override long Seek(long offset, SeekOrigin origin) public sealed override void SetLength(long value) { - if (_appendStart != -1 && value < _appendStart) - throw new IOException(SR.IO_SetLengthAppendTruncate); - SetLengthCore(value); } - protected unsafe void SetLengthCore(long value) + protected void SetLengthCore(long value) { Debug.Assert(value >= 0, "value >= 0");