diff --git a/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs b/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs index 8d676da9b542e2..268224ab9b9f75 100644 --- a/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs +++ b/src/libraries/System.IO.FileSystem/tests/FileStream/ReadAsync.cs @@ -11,7 +11,7 @@ namespace System.IO.Tests { public abstract class FileStream_AsyncReads : FileSystemTest { - protected abstract Task ReadAsync(FileStream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default); + protected abstract Task ReadAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken = default); [Fact] public async Task EmptyFileReadAsyncSucceedSynchronously() @@ -132,17 +132,72 @@ public async Task IncompleteReadCantSetPositionBeyondEndOfFile(FileShare fileSha Assert.Equal(fileSize, fs.Position); } } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(false, true)] + public async Task BypassingCacheInvalidatesCachedData(bool fsIsAsync, bool asyncReads) + { + const int BufferSize = 4096; + const int FileSize = BufferSize * 4; + string filePath = GetTestFilePath(); + byte[] content = RandomNumberGenerator.GetBytes(FileSize); + File.WriteAllBytes(filePath, content); + + await Test(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, BufferSize, fsIsAsync)); + await Test(new BufferedStream(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 0, fsIsAsync), BufferSize)); + + async Task Test(Stream stream) + { + try + { + // 1. Populates the private stream buffer, leaves bufferSize - 1 bytes available for next read. + await ReadAndAssertAsync(stream, 1); + // 2. Consumes all available data from the buffer, reads another bufferSize-many bytes from the disk and copies the 1 missing byte. + await ReadAndAssertAsync(stream, BufferSize); + // 3. Seek back by the number of bytes consumed from the buffer, all buffered data is now available for next read. + stream.Position -= 1; + // 4. Consume all buffered data. + await ReadAndAssertAsync(stream, BufferSize); + // 5. Bypass the cache (all buffered data has been consumed and we need bufferSize-many bytes). + // The cache should get invalidated now!! + await ReadAndAssertAsync(stream,BufferSize); + // 6. Seek back by just a few bytes. + stream.Position -= 9; + // 7. Perform a read, which should not use outdated buffered data. + await ReadAndAssertAsync(stream,BufferSize); + } + finally + { + await stream.DisposeAsync(); + } + } + + async Task ReadAndAssertAsync(Stream stream, int size) + { + var initialPosition = stream.Position; + var buffer = new byte[size]; + + var count = asyncReads + ? await ReadAsync(stream, buffer, 0, size) + : stream.Read(buffer); + + Assert.Equal(content.Skip((int)initialPosition).Take(count), buffer.Take(count)); + } + } } public class FileStream_ReadAsync_AsyncReads : FileStream_AsyncReads { - protected override Task ReadAsync(FileStream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + protected override Task ReadAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) => stream.ReadAsync(buffer, offset, count, cancellationToken); } public class FileStream_BeginEndRead_AsyncReads : FileStream_AsyncReads { - protected override Task ReadAsync(FileStream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + protected override Task ReadAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) => Task.Factory.FromAsync( (callback, state) => stream.BeginRead(buffer, offset, count, callback, state), iar => stream.EndRead(iar), diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs index 2da113fe16f314..63d3621d001fcf 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs @@ -326,6 +326,8 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken { if (_readLen == _readPos && buffer.Length >= _bufferSize) { + // invalidate the buffered data, otherwise certain Seek operation followed by a ReadAsync could try to re-use data from _buffer + _readPos = _readLen = 0; // hot path #1: the read buffer is empty and buffering would not be beneficial // To find out why we are bypassing cache here, please see WriteAsync comments. return _strategy.ReadAsync(buffer, cancellationToken);