Skip to content

Commit 278057b

Browse files
Support unseekable filestream when ReadAllBytes[Async] (#58434) (#61410)
Co-authored-by: Adam Sitnik <adam.sitnik@gmail.com> Co-authored-by: LateApexEarlySpeed <72254037+lateapexearlyspeed@users.noreply.github.com>
1 parent 8881fcf commit 278057b

File tree

5 files changed

+106
-15
lines changed

5 files changed

+106
-15
lines changed

src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytes.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Text;
5+
using System.Threading;
56
using System.Threading.Tasks;
67
using Xunit;
8+
using System.IO.Pipes;
9+
using Microsoft.DotNet.XUnitExtensions;
710

811
namespace System.IO.Tests
912
{
@@ -172,5 +175,30 @@ public void ProcFs_NotEmpty(string path)
172175
{
173176
Assert.InRange(File.ReadAllBytes(path).Length, 1, int.MaxValue);
174177
}
178+
179+
[Fact]
180+
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)]
181+
public async Task ReadAllBytes_NonSeekableFileStream_InUnix()
182+
{
183+
string fifoPath = GetTestFilePath();
184+
Assert.Equal(0, mkfifo(fifoPath, 438 /* 666 in octal */ ));
185+
186+
var contentBytes = new byte[] { 1, 2, 3 };
187+
188+
await Task.WhenAll(
189+
Task.Run(() =>
190+
{
191+
byte[] readBytes = File.ReadAllBytes(fifoPath);
192+
Assert.Equal<byte>(contentBytes, readBytes);
193+
}),
194+
Task.Run(() =>
195+
{
196+
using var fs = new FileStream(fifoPath, FileMode.Open, FileAccess.Write, FileShare.Read);
197+
foreach (byte content in contentBytes)
198+
{
199+
fs.WriteByte(content);
200+
}
201+
}));
202+
}
175203
}
176204
}

src/libraries/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Threading;
66
using System.Threading.Tasks;
77
using Xunit;
8+
using System.IO.Pipes;
9+
using Microsoft.DotNet.XUnitExtensions;
810

911
namespace System.IO.Tests
1012
{
@@ -186,5 +188,61 @@ public async Task ProcFs_NotEmpty(string path)
186188
{
187189
Assert.InRange((await File.ReadAllBytesAsync(path)).Length, 1, int.MaxValue);
188190
}
191+
192+
[Fact]
193+
[PlatformSpecific(TestPlatforms.Windows)] // DOS device paths (\\.\ and \\?\) are a Windows concept
194+
public async Task ReadAllBytesAsync_NonSeekableFileStream_InWindows()
195+
{
196+
string pipeName = FileSystemTest.GetNamedPipeServerStreamName();
197+
string pipePath = Path.GetFullPath($@"\\.\pipe\{pipeName}");
198+
199+
var namedPipeWriterStream = new NamedPipeServerStream(pipeName, PipeDirection.Out);
200+
var contentBytes = new byte[] { 1, 2, 3 };
201+
202+
using (var cts = new CancellationTokenSource())
203+
{
204+
Task writingServerTask = WaitConnectionAndWritePipeStreamAsync(namedPipeWriterStream, contentBytes, cts.Token);
205+
Task<byte[]> readTask = File.ReadAllBytesAsync(pipePath, cts.Token);
206+
cts.CancelAfter(TimeSpan.FromSeconds(50));
207+
208+
await writingServerTask;
209+
byte[] readBytes = await readTask;
210+
Assert.Equal<byte>(contentBytes, readBytes);
211+
}
212+
213+
static async Task WaitConnectionAndWritePipeStreamAsync(NamedPipeServerStream namedPipeWriterStream, byte[] contentBytes, CancellationToken cancellationToken)
214+
{
215+
await using (namedPipeWriterStream)
216+
{
217+
await namedPipeWriterStream.WaitForConnectionAsync(cancellationToken);
218+
await namedPipeWriterStream.WriteAsync(contentBytes, cancellationToken);
219+
}
220+
}
221+
}
222+
223+
[Fact]
224+
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)]
225+
public async Task ReadAllBytesAsync_NonSeekableFileStream_InUnix()
226+
{
227+
string fifoPath = GetTestFilePath();
228+
Assert.Equal(0, mkfifo(fifoPath, 438 /* 666 in octal */ ));
229+
230+
var contentBytes = new byte[] { 1, 2, 3 };
231+
232+
await Task.WhenAll(
233+
Task.Run(async () =>
234+
{
235+
byte[] readBytes = await File.ReadAllBytesAsync(fifoPath);
236+
Assert.Equal<byte>(contentBytes, readBytes);
237+
}),
238+
Task.Run(() =>
239+
{
240+
using var fs = new FileStream(fifoPath, FileMode.Open, FileAccess.Write, FileShare.Read);
241+
foreach (byte content in contentBytes)
242+
{
243+
fs.WriteByte(content);
244+
}
245+
}));
246+
}
189247
}
190248
}

src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.OverlappedValueTaskSource.Windows.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ internal static Exception GetIOError(int errorCode, string? path)
8686
_bufferSize = memory.Length;
8787
_memoryHandle = memory.Pin();
8888
_overlapped = _fileHandle.ThreadPoolBinding!.AllocateNativeOverlapped(_preallocatedOverlapped);
89-
_overlapped->OffsetLow = (int)fileOffset;
90-
_overlapped->OffsetHigh = (int)(fileOffset >> 32);
89+
if (_fileHandle.CanSeek)
90+
{
91+
_overlapped->OffsetLow = (int)fileOffset;
92+
_overlapped->OffsetHigh = (int)(fileOffset >> 32);
93+
}
9194
return _overlapped;
9295
}
9396

src/libraries/System.Private.CoreLib/src/System/IO/File.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -333,15 +333,15 @@ public static byte[] ReadAllBytes(string path)
333333
// bufferSize == 1 used to avoid unnecessary buffer in FileStream
334334
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1, FileOptions.SequentialScan))
335335
{
336-
long fileLength = fs.Length;
337-
if (fileLength > int.MaxValue)
336+
long fileLength = 0;
337+
if (fs.CanSeek && (fileLength = fs.Length) > int.MaxValue)
338338
{
339339
throw new IOException(SR.IO_FileTooLong2GB);
340340
}
341-
else if (fileLength == 0)
341+
if (fileLength == 0)
342342
{
343343
#if !MS_IO_REDIST
344-
// Some file systems (e.g. procfs on Linux) return 0 for length even when there's content.
344+
// Some file systems (e.g. procfs on Linux) return 0 for length even when there's content; also there is non-seekable file stream.
345345
// Thus we need to assume 0 doesn't mean empty.
346346
return ReadAllBytesUnknownLength(fs);
347347
#endif
@@ -729,8 +729,8 @@ private static async Task<string> InternalReadAllTextAsync(string path, Encoding
729729
bool returningInternalTask = false;
730730
try
731731
{
732-
long fileLength = fs.Length;
733-
if (fileLength > int.MaxValue)
732+
long fileLength = 0L;
733+
if (fs.CanSeek && (fileLength = fs.Length) > int.MaxValue)
734734
{
735735
var e = new IOException(SR.IO_FileTooLong2GB);
736736
#if !MS_IO_REDIST

src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Windows.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ private static unsafe int ReadSyncUsingAsyncHandle(SafeFileHandle handle, Span<b
7474

7575
try
7676
{
77-
overlapped = GetNativeOverlappedForAsyncHandle(handle.ThreadPoolBinding!, fileOffset, resetEvent);
77+
overlapped = GetNativeOverlappedForAsyncHandle(handle, fileOffset, resetEvent);
7878

7979
fixed (byte* pinned = &MemoryMarshal.GetReference(buffer))
8080
{
@@ -172,7 +172,7 @@ private static unsafe void WriteSyncUsingAsyncHandle(SafeFileHandle handle, Read
172172

173173
try
174174
{
175-
overlapped = GetNativeOverlappedForAsyncHandle(handle.ThreadPoolBinding!, fileOffset, resetEvent);
175+
overlapped = GetNativeOverlappedForAsyncHandle(handle, fileOffset, resetEvent);
176176

177177
fixed (byte* pinned = &MemoryMarshal.GetReference(buffer))
178178
{
@@ -698,15 +698,17 @@ private static async ValueTask WriteGatherAtOffsetMultipleSyscallsAsync(SafeFile
698698
}
699699
}
700700

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

707-
// For pipes the offsets are ignored by the OS
708-
result->OffsetLow = unchecked((int)fileOffset);
709-
result->OffsetHigh = (int)(fileOffset >> 32);
707+
if (handle.CanSeek)
708+
{
709+
result->OffsetLow = unchecked((int)fileOffset);
710+
result->OffsetHigh = (int)(fileOffset >> 32);
711+
}
710712

711713
// From https://docs.microsoft.com/en-us/windows/win32/api/ioapiset/nf-ioapiset-getoverlappedresult:
712714
// "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.

0 commit comments

Comments
 (0)