From c2f287c945a17e9a49fad57d89d0936bad697e80 Mon Sep 17 00:00:00 2001 From: LateApexEarlySpeed <72254037+lateapexearlyspeed@users.noreply.github.com> Date: Thu, 14 Oct 2021 02:17:14 +0800 Subject: [PATCH] Support unseekable filestream when ReadAllBytes[Async] (#58434) Co-authored-by: Adam Sitnik --- .../tests/File/ReadWriteAllBytes.cs | 59 +++++++++++++++++++ .../tests/File/ReadWriteAllBytesAsync.cs | 58 ++++++++++++++++++ ...andle.OverlappedValueTaskSource.Windows.cs | 7 ++- .../src/System/IO/File.cs | 12 ++-- .../src/System/IO/RandomAccess.Windows.cs | 16 ++--- 5 files changed, 137 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytes.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytes.cs index 54c7643766335..bb94b18b3115e 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytes.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytes.cs @@ -2,8 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using System.Threading; using System.Threading.Tasks; using Xunit; +using System.IO.Pipes; +using Microsoft.DotNet.XUnitExtensions; namespace System.IO.Tests { @@ -172,5 +175,61 @@ public void ProcFs_NotEmpty(string path) { Assert.InRange(File.ReadAllBytes(path).Length, 1, int.MaxValue); } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] // DOS device paths (\\.\ and \\?\) are a Windows concept + public async Task ReadAllBytes_NonSeekableFileStream_InWindows() + { + string pipeName = FileSystemTest.GetNamedPipeServerStreamName(); + string pipePath = Path.GetFullPath($@"\\.\pipe\{pipeName}"); + + var namedPipeWriterStream = new NamedPipeServerStream(pipeName, PipeDirection.Out); + var contentBytes = new byte[] { 1, 2, 3 }; + + using (var cts = new CancellationTokenSource()) + { + Task writingServerTask = WaitConnectionAndWritePipeStreamAsync(namedPipeWriterStream, contentBytes, cts.Token); + Task readTask = Task.Run(() => File.ReadAllBytes(pipePath), cts.Token); + cts.CancelAfter(TimeSpan.FromSeconds(50)); + + await writingServerTask; + byte[] readBytes = await readTask; + Assert.Equal(contentBytes, readBytes); + } + + static async Task WaitConnectionAndWritePipeStreamAsync(NamedPipeServerStream namedPipeWriterStream, byte[] contentBytes, CancellationToken cancellationToken) + { + await using (namedPipeWriterStream) + { + await namedPipeWriterStream.WaitForConnectionAsync(cancellationToken); + await namedPipeWriterStream.WriteAsync(contentBytes, cancellationToken); + } + } + } + + [Fact] + [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)] + public async Task ReadAllBytes_NonSeekableFileStream_InUnix() + { + string fifoPath = GetTestFilePath(); + Assert.Equal(0, mkfifo(fifoPath, 438 /* 666 in octal */ )); + + var contentBytes = new byte[] { 1, 2, 3 }; + + await Task.WhenAll( + Task.Run(() => + { + byte[] readBytes = File.ReadAllBytes(fifoPath); + Assert.Equal(contentBytes, readBytes); + }), + Task.Run(() => + { + using var fs = new FileStream(fifoPath, FileMode.Open, FileAccess.Write, FileShare.Read); + foreach (byte content in contentBytes) + { + fs.WriteByte(content); + } + })); + } } } diff --git a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs index 748c01dbffb8f..be562f15ca953 100644 --- a/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs @@ -5,6 +5,8 @@ using System.Threading; using System.Threading.Tasks; using Xunit; +using System.IO.Pipes; +using Microsoft.DotNet.XUnitExtensions; namespace System.IO.Tests { @@ -186,5 +188,61 @@ public async Task ProcFs_NotEmpty(string path) { Assert.InRange((await File.ReadAllBytesAsync(path)).Length, 1, int.MaxValue); } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] // DOS device paths (\\.\ and \\?\) are a Windows concept + public async Task ReadAllBytesAsync_NonSeekableFileStream_InWindows() + { + string pipeName = FileSystemTest.GetNamedPipeServerStreamName(); + string pipePath = Path.GetFullPath($@"\\.\pipe\{pipeName}"); + + var namedPipeWriterStream = new NamedPipeServerStream(pipeName, PipeDirection.Out); + var contentBytes = new byte[] { 1, 2, 3 }; + + using (var cts = new CancellationTokenSource()) + { + Task writingServerTask = WaitConnectionAndWritePipeStreamAsync(namedPipeWriterStream, contentBytes, cts.Token); + Task readTask = File.ReadAllBytesAsync(pipePath, cts.Token); + cts.CancelAfter(TimeSpan.FromSeconds(50)); + + await writingServerTask; + byte[] readBytes = await readTask; + Assert.Equal(contentBytes, readBytes); + } + + static async Task WaitConnectionAndWritePipeStreamAsync(NamedPipeServerStream namedPipeWriterStream, byte[] contentBytes, CancellationToken cancellationToken) + { + await using (namedPipeWriterStream) + { + await namedPipeWriterStream.WaitForConnectionAsync(cancellationToken); + await namedPipeWriterStream.WriteAsync(contentBytes, cancellationToken); + } + } + } + + [Fact] + [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)] + public async Task ReadAllBytesAsync_NonSeekableFileStream_InUnix() + { + string fifoPath = GetTestFilePath(); + Assert.Equal(0, mkfifo(fifoPath, 438 /* 666 in octal */ )); + + var contentBytes = new byte[] { 1, 2, 3 }; + + await Task.WhenAll( + Task.Run(async () => + { + byte[] readBytes = await File.ReadAllBytesAsync(fifoPath); + Assert.Equal(contentBytes, readBytes); + }), + Task.Run(() => + { + using var fs = new FileStream(fifoPath, FileMode.Open, FileAccess.Write, FileShare.Read); + foreach (byte content in contentBytes) + { + fs.WriteByte(content); + } + })); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs index 9f689b161bbd0..c2eab851bd60e 100644 --- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs @@ -86,8 +86,11 @@ internal static Exception GetIOError(int errorCode, string? path) _bufferSize = memory.Length; _memoryHandle = memory.Pin(); _overlapped = _fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(_preallocatedOverlapped); - _overlapped->OffsetLow = (int)fileOffset; - _overlapped->OffsetHigh = (int)(fileOffset >> 32); + if (_fileHandle.CanSeek) + { + _overlapped->OffsetLow = (int)fileOffset; + _overlapped->OffsetHigh = (int)(fileOffset >> 32); + } return _overlapped; } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs index 02d0dfac8f9b6..4efdb1366d7a0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/File.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/File.cs @@ -326,14 +326,14 @@ public static byte[] ReadAllBytes(string path) // bufferSize == 1 used to avoid unnecessary buffer in FileStream using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1, FileOptions.SequentialScan)) { - long fileLength = fs.Length; - if (fileLength > int.MaxValue) + long fileLength = 0; + if (fs.CanSeek && (fileLength = fs.Length) > int.MaxValue) { throw new IOException(SR.IO_FileTooLong2GB); } - else if (fileLength == 0) + if (fileLength == 0) { - // Some file systems (e.g. procfs on Linux) return 0 for length even when there's content. + // Some file systems (e.g. procfs on Linux) return 0 for length even when there's content; also there is non-seekable file stream. // Thus we need to assume 0 doesn't mean empty. return ReadAllBytesUnknownLength(fs); } @@ -711,8 +711,8 @@ private static async Task InternalReadAllTextAsync(string path, Encoding bool returningInternalTask = false; try { - long fileLength = fs.Length; - if (fileLength > int.MaxValue) + long fileLength = 0L; + if (fs.CanSeek && (fileLength = fs.Length) > int.MaxValue) { var e = new IOException(SR.IO_FileTooLong2GB); ExceptionDispatchInfo.SetCurrentStackTrace(e); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs index 31adaca38fe5e..7f6aed824c847 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs @@ -73,7 +73,7 @@ private static unsafe int ReadSyncUsingAsyncHandle(SafeFileHandle handle, SpanOffsetLow = unchecked((int)fileOffset); - result->OffsetHigh = (int)(fileOffset >> 32); + if (handle.CanSeek) + { + result->OffsetLow = unchecked((int)fileOffset); + result->OffsetHigh = (int)(fileOffset >> 32); + } // From https://docs.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-getoverlappedresult: // "If the hEvent member of the OVERLAPPED structure is NULL, the system uses the state of the hFile handle to signal when the operation has been completed.