Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support unseekable filestream when ReadAllBytes[Async] #58434

52 changes: 52 additions & 0 deletions src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// 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;

namespace System.IO.Tests
{
Expand Down Expand Up @@ -172,5 +174,55 @@ 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<byte[]> readTask = Task.Run(() => File.ReadAllBytes(pipePath), cts.Token);
cts.CancelAfter(TimeSpan.FromSeconds(10));

await writingServerTask;
byte[] readBytes = await readTask;
Assert.Equal<byte>(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.Linux)]
public async Task ReadAllBytes_NonSeekableFileStream_InLinux()
adamsitnik marked this conversation as resolved.
Show resolved Hide resolved
{
var path = "/dev/tty";
adamsitnik marked this conversation as resolved.
Show resolved Hide resolved
var contentBytes = new byte[] { 1, 2, 3 };

using (var cts = new CancellationTokenSource())
{
Task writingTask = File.WriteAllBytesAsync(path, contentBytes, cts.Token);
Task<byte[]> readTask = Task.Run(() => File.ReadAllBytes(path), cts.Token);
cts.CancelAfter(TimeSpan.FromMilliseconds(500));

await writingTask;
byte[] readBytes = await readTask;
Assert.Equal<byte>(contentBytes, readBytes);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using System.IO.Pipes;

namespace System.IO.Tests
{
Expand Down Expand Up @@ -186,5 +187,55 @@ 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()
{
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<byte[]> readTask = File.ReadAllBytesAsync(pipePath, cts.Token);
cts.CancelAfter(TimeSpan.FromSeconds(10));

await writingServerTask;
byte[] readBytes = await readTask;
Assert.Equal<byte>(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.Linux)]
public async Task ReadAllBytesAsync_NonSeekableFileStream_InLinux()
{
var path = "/dev/tty";
var contentBytes = new byte[] { 1, 2, 3 };

using (var cts = new CancellationTokenSource())
{
Task writingTask = File.WriteAllBytesAsync(path, contentBytes, cts.Token);
Task<byte[]> readTask = File.ReadAllBytesAsync(path, cts.Token);
cts.CancelAfter(TimeSpan.FromMilliseconds(500));

await writingTask;
byte[] readBytes = await readTask;
Assert.Equal<byte>(contentBytes, readBytes);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
16 changes: 9 additions & 7 deletions src/libraries/System.Private.CoreLib/src/System/IO/File.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,15 +333,17 @@ 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)
{
#if !MS_IO_REDIST
// Some file systems (e.g. procfs on Linux) return 0 for length even when there's content.
#if MS_IO_REDIST
return Array.Empty<byte>();
#else
// 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);
#endif
Expand Down Expand Up @@ -729,8 +731,8 @@ private static async Task<string> 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);
#if !MS_IO_REDIST
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ private static unsafe int ReadSyncUsingAsyncHandle(SafeFileHandle handle, Span<b

try
{
overlapped = GetNativeOverlappedForAsyncHandle(handle.ThreadPoolBinding!, fileOffset, resetEvent);
overlapped = GetNativeOverlappedForAsyncHandle(handle, fileOffset, resetEvent);

fixed (byte* pinned = &MemoryMarshal.GetReference(buffer))
{
Expand Down Expand Up @@ -171,7 +171,7 @@ private static unsafe void WriteSyncUsingAsyncHandle(SafeFileHandle handle, Read

try
{
overlapped = GetNativeOverlappedForAsyncHandle(handle.ThreadPoolBinding!, fileOffset, resetEvent);
overlapped = GetNativeOverlappedForAsyncHandle(handle, fileOffset, resetEvent);

fixed (byte* pinned = &MemoryMarshal.GetReference(buffer))
{
Expand Down Expand Up @@ -681,15 +681,17 @@ private static async ValueTask WriteGatherAtOffsetMultipleSyscallsAsync(SafeFile
}
}

private static unsafe NativeOverlapped* GetNativeOverlappedForAsyncHandle(ThreadPoolBoundHandle threadPoolBinding, long fileOffset, CallbackResetEvent resetEvent)
private static unsafe NativeOverlapped* GetNativeOverlappedForAsyncHandle(SafeFileHandle handle, long fileOffset, CallbackResetEvent resetEvent)
{
// After SafeFileHandle is bound to ThreadPool, we need to use ThreadPoolBinding
// to allocate a native overlapped and provide a valid callback.
NativeOverlapped* result = threadPoolBinding.AllocateNativeOverlapped(s_callback, resetEvent, null);
NativeOverlapped* result = handle.ThreadPoolBinding!.AllocateNativeOverlapped(s_callback, resetEvent, null);

// For pipes the offsets are ignored by the OS
result->OffsetLow = 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.
Expand Down