Skip to content

Commit c2f287c

Browse files
Support unseekable filestream when ReadAllBytes[Async] (#58434)
Co-authored-by: Adam Sitnik <[email protected]>
1 parent 49cf05c commit c2f287c

File tree

5 files changed

+137
-15
lines changed

5 files changed

+137
-15
lines changed

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

Lines changed: 59 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,61 @@ public void ProcFs_NotEmpty(string path)
172175
{
173176
Assert.InRange(File.ReadAllBytes(path).Length, 1, int.MaxValue);
174177
}
178+
179+
[Fact]
180+
[PlatformSpecific(TestPlatforms.Windows)] // DOS device paths (\\.\ and \\?\) are a Windows concept
181+
public async Task ReadAllBytes_NonSeekableFileStream_InWindows()
182+
{
183+
string pipeName = FileSystemTest.GetNamedPipeServerStreamName();
184+
string pipePath = Path.GetFullPath($@"\\.\pipe\{pipeName}");
185+
186+
var namedPipeWriterStream = new NamedPipeServerStream(pipeName, PipeDirection.Out);
187+
var contentBytes = new byte[] { 1, 2, 3 };
188+
189+
using (var cts = new CancellationTokenSource())
190+
{
191+
Task writingServerTask = WaitConnectionAndWritePipeStreamAsync(namedPipeWriterStream, contentBytes, cts.Token);
192+
Task<byte[]> readTask = Task.Run(() => File.ReadAllBytes(pipePath), cts.Token);
193+
cts.CancelAfter(TimeSpan.FromSeconds(50));
194+
195+
await writingServerTask;
196+
byte[] readBytes = await readTask;
197+
Assert.Equal<byte>(contentBytes, readBytes);
198+
}
199+
200+
static async Task WaitConnectionAndWritePipeStreamAsync(NamedPipeServerStream namedPipeWriterStream, byte[] contentBytes, CancellationToken cancellationToken)
201+
{
202+
await using (namedPipeWriterStream)
203+
{
204+
await namedPipeWriterStream.WaitForConnectionAsync(cancellationToken);
205+
await namedPipeWriterStream.WriteAsync(contentBytes, cancellationToken);
206+
}
207+
}
208+
}
209+
210+
[Fact]
211+
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser)]
212+
public async Task ReadAllBytes_NonSeekableFileStream_InUnix()
213+
{
214+
string fifoPath = GetTestFilePath();
215+
Assert.Equal(0, mkfifo(fifoPath, 438 /* 666 in octal */ ));
216+
217+
var contentBytes = new byte[] { 1, 2, 3 };
218+
219+
await Task.WhenAll(
220+
Task.Run(() =>
221+
{
222+
byte[] readBytes = File.ReadAllBytes(fifoPath);
223+
Assert.Equal<byte>(contentBytes, readBytes);
224+
}),
225+
Task.Run(() =>
226+
{
227+
using var fs = new FileStream(fifoPath, FileMode.Open, FileAccess.Write, FileShare.Read);
228+
foreach (byte content in contentBytes)
229+
{
230+
fs.WriteByte(content);
231+
}
232+
}));
233+
}
175234
}
176235
}

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
@@ -326,14 +326,14 @@ public static byte[] ReadAllBytes(string path)
326326
// bufferSize == 1 used to avoid unnecessary buffer in FileStream
327327
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 1, FileOptions.SequentialScan))
328328
{
329-
long fileLength = fs.Length;
330-
if (fileLength > int.MaxValue)
329+
long fileLength = 0;
330+
if (fs.CanSeek && (fileLength = fs.Length) > int.MaxValue)
331331
{
332332
throw new IOException(SR.IO_FileTooLong2GB);
333333
}
334-
else if (fileLength == 0)
334+
if (fileLength == 0)
335335
{
336-
// Some file systems (e.g. procfs on Linux) return 0 for length even when there's content.
336+
// Some file systems (e.g. procfs on Linux) return 0 for length even when there's content; also there is non-seekable file stream.
337337
// Thus we need to assume 0 doesn't mean empty.
338338
return ReadAllBytesUnknownLength(fs);
339339
}
@@ -711,8 +711,8 @@ private static async Task<string> InternalReadAllTextAsync(string path, Encoding
711711
bool returningInternalTask = false;
712712
try
713713
{
714-
long fileLength = fs.Length;
715-
if (fileLength > int.MaxValue)
714+
long fileLength = 0L;
715+
if (fs.CanSeek && (fileLength = fs.Length) > int.MaxValue)
716716
{
717717
var e = new IOException(SR.IO_FileTooLong2GB);
718718
ExceptionDispatchInfo.SetCurrentStackTrace(e);

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
@@ -73,7 +73,7 @@ private static unsafe int ReadSyncUsingAsyncHandle(SafeFileHandle handle, Span<b
7373

7474
try
7575
{
76-
overlapped = GetNativeOverlappedForAsyncHandle(handle.ThreadPoolBinding!, fileOffset, resetEvent);
76+
overlapped = GetNativeOverlappedForAsyncHandle(handle, fileOffset, resetEvent);
7777

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

172172
try
173173
{
174-
overlapped = GetNativeOverlappedForAsyncHandle(handle.ThreadPoolBinding!, fileOffset, resetEvent);
174+
overlapped = GetNativeOverlappedForAsyncHandle(handle, fileOffset, resetEvent);
175175

176176
fixed (byte* pinned = &MemoryMarshal.GetReference(buffer))
177177
{
@@ -681,15 +681,17 @@ private static async ValueTask WriteGatherAtOffsetMultipleSyscallsAsync(SafeFile
681681
}
682682
}
683683

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

690-
// For pipes the offsets are ignored by the OS
691-
result->OffsetLow = unchecked((int)fileOffset);
692-
result->OffsetHigh = (int)(fileOffset >> 32);
690+
if (handle.CanSeek)
691+
{
692+
result->OffsetLow = unchecked((int)fileOffset);
693+
result->OffsetHigh = (int)(fileOffset >> 32);
694+
}
693695

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