Add Stream ReadAtLeast and ReadExactly (#69272)
authorEric Erhardt <eric.erhardt@microsoft.com>
Fri, 20 May 2022 14:20:06 +0000 (09:20 -0500)
committerGitHub <noreply@github.com>
Fri, 20 May 2022 14:20:06 +0000 (09:20 -0500)
* Add Stream ReadAtLeast and ReadExactly

Adds methods to Stream to read at least a minimum amount of bytes, or a full buffer, of data from the stream.
ReadAtLeast allows for the caller to specify whether an exception should be thrown or not on the end of the stream.

Make use of the new methods where appropriate in net7.0.

Fix #16598

* Add ReadAtLeast and ReadExactly unit tests

* Add XML docs to the new APIs

* Preserve behavior in StreamReader.FillBuffer when passed 0.

* Handle ReadExactly with an empty buffer, and ReadAtLeast with minimumBytes == 0.

Both of these cases are a no-op. No exception is thrown. A read won't be issued to the underlying stream. They won't block until data is available.

26 files changed:
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs
src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs
src/libraries/System.IO.Compression/src/System/IO/Compression/ZipHelper.cs
src/libraries/System.IO/tests/BinaryReader/BinaryReaderTests.cs
src/libraries/System.IO/tests/Stream/Stream.ReadAtLeast.cs [new file with mode: 0644]
src/libraries/System.IO/tests/Stream/Stream.ReadExactly.cs [new file with mode: 0644]
src/libraries/System.IO/tests/System.IO.Tests.csproj
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocksHelper.cs
src/libraries/System.Net.Http/tests/FunctionalTests/LoopbackSocksServer.cs
src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs
src/libraries/System.Net.Security/src/System/Net/Security/ReadWriteAdapter.cs
src/libraries/System.Net.Security/src/System/Net/StreamFramer.cs
src/libraries/System.Net.WebSockets/src/System/Net/WebSockets/ManagedWebSocket.cs
src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs
src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs
src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs
src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonEncodingStreamWrapper.cs
src/libraries/System.Private.DataContractSerialization/src/System/Xml/EncodingStreamWrapper.cs
src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlBufferReader.cs
src/libraries/System.Private.Xml/src/System/Xml/Core/XmlReader.cs
src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImpl.cs
src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImplAsync.cs
src/libraries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/StreamExtensions.cs
src/libraries/System.Runtime/ref/System.Runtime.cs

index f039804..1e46719 100644 (file)
@@ -26,7 +26,7 @@ namespace System.Formats.Tar
             Span<byte> buffer = rented.AsSpan(0, TarHelpers.RecordSize); // minimumLength means the array could've been larger
             buffer.Clear(); // Rented arrays aren't clean
 
-            TarHelpers.ReadOrThrow(archiveStream, buffer);
+            archiveStream.ReadExactly(buffer);
 
             try
             {
@@ -486,10 +486,7 @@ namespace System.Formats.Tar
             }
 
             byte[] buffer = new byte[(int)_size];
-            if (archiveStream.Read(buffer.AsSpan()) != _size)
-            {
-                throw new EndOfStreamException();
-            }
+            archiveStream.ReadExactly(buffer);
 
             string dataAsString = TarHelpers.GetTrimmedUtf8String(buffer);
 
@@ -520,11 +517,7 @@ namespace System.Formats.Tar
             }
 
             byte[] buffer = new byte[(int)_size];
-
-            if (archiveStream.Read(buffer.AsSpan()) != _size)
-            {
-                throw new EndOfStreamException();
-            }
+            archiveStream.ReadExactly(buffer);
 
             string longPath = TarHelpers.GetTrimmedUtf8String(buffer);
 
index f5bf799..db80de8 100644 (file)
@@ -151,22 +151,6 @@ namespace System.Formats.Tar
         // removing the trailing null or space chars.
         internal static string GetTrimmedUtf8String(ReadOnlySpan<byte> buffer) => GetTrimmedString(buffer, Encoding.UTF8);
 
-        // Reads the specified number of bytes and stores it in the byte buffer passed by reference.
-        // Throws if end of stream is reached.
-        internal static void ReadOrThrow(Stream archiveStream, Span<byte> buffer)
-        {
-            int totalRead = 0;
-            while (totalRead < buffer.Length)
-            {
-                int bytesRead = archiveStream.Read(buffer.Slice(totalRead));
-                if (bytesRead == 0)
-                {
-                    throw new EndOfStreamException();
-                }
-                totalRead += bytesRead;
-            }
-        }
-
         // Returns true if it successfully converts the specified string to a DateTimeOffset, false otherwise.
         internal static bool TryConvertToDateTimeOffset(string value, out DateTimeOffset timestamp)
         {
index 9ebde5f..c4f46c1 100644 (file)
@@ -40,17 +40,10 @@ namespace System.IO.Compression
         /// </summary>
         internal static void ReadBytes(Stream stream, byte[] buffer, int bytesToRead)
         {
-            int bytesLeftToRead = bytesToRead;
-
-            int totalBytesRead = 0;
-
-            while (bytesLeftToRead > 0)
+            int bytesRead = stream.ReadAtLeast(buffer.AsSpan(0, bytesToRead), bytesToRead, throwOnEndOfStream: false);
+            if (bytesRead < bytesToRead)
             {
-                int bytesRead = stream.Read(buffer, totalBytesRead, bytesLeftToRead);
-                if (bytesRead == 0) throw new IOException(SR.UnexpectedEndOfStream);
-
-                totalBytesRead += bytesRead;
-                bytesLeftToRead -= bytesRead;
+                throw new IOException(SR.UnexpectedEndOfStream);
             }
         }
 
index e5d3bae..4a2dfa4 100644 (file)
@@ -437,5 +437,28 @@ namespace System.IO.Tests
                 Assert.Throws<ObjectDisposedException>(() => binaryReader.Read(new Span<char>()));
             }
         }
+
+        private class DerivedBinaryReader : BinaryReader
+        {
+            public DerivedBinaryReader(Stream input) : base(input) { }
+
+            public void CallFillBuffer0()
+            {
+                FillBuffer(0);
+            }
+        }
+
+        [Fact]
+        public void FillBuffer_Zero_Throws()
+        {
+            using Stream stream = CreateStream();
+
+            string hello = "Hello";
+            stream.Write(Encoding.ASCII.GetBytes(hello));
+            stream.Position = 0;
+
+            using var derivedReader = new DerivedBinaryReader(stream);
+            Assert.Throws<EndOfStreamException>(derivedReader.CallFillBuffer0);
+        }
     }
 }
diff --git a/src/libraries/System.IO/tests/Stream/Stream.ReadAtLeast.cs b/src/libraries/System.IO/tests/Stream/Stream.ReadAtLeast.cs
new file mode 100644 (file)
index 0000000..2ad3656
--- /dev/null
@@ -0,0 +1,263 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.IO.Tests
+{
+    public class Stream_ReadAtLeast
+    {
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task DelegatesToRead_Success(bool async)
+        {
+            bool readInvoked = false;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvoked = true;
+                    Assert.NotNull(array);
+                    Assert.Equal(0, offset);
+                    Assert.Equal(30, count);
+
+                    for (int i = 0; i < 10; i++) array[offset + i] = (byte)i;
+                    return 10;
+                });
+
+            byte[] buffer = new byte[30];
+
+            Assert.Equal(10, async ? await s.ReadAtLeastAsync(buffer, 10) : s.ReadAtLeast(buffer, 10));
+            Assert.True(readInvoked);
+            for (int i = 0; i < 10; i++) Assert.Equal(i, buffer[i]);
+            for (int i = 10; i < 30; i++) Assert.Equal(0, buffer[i]);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ReadMoreThanOnePage(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    for (int i = 0; i < 10; i++) array[offset + i] = (byte)i;
+                    return 10;
+                });
+
+            byte[] buffer = new byte[30];
+
+            Assert.Equal(20, async ? await s.ReadAtLeastAsync(buffer, 20) : s.ReadAtLeast(buffer, 20));
+            Assert.Equal(2, readInvokedCount);
+            for (int i = 0; i < 10; i++) Assert.Equal(i, buffer[i]);
+            for (int i = 10; i < 20; i++) Assert.Equal(i - 10, buffer[i]);
+            for (int i = 20; i < 30; i++) Assert.Equal(0, buffer[i]);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ReadMoreThanMinimumBytes(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return byteCount;
+                });
+
+            // first try with a buffer that doesn't fill 3 full pages
+            byte[] buffer = new byte[28];
+
+            Assert.Equal(28, async ? await s.ReadAtLeastAsync(buffer, 22) : s.ReadAtLeast(buffer, 22));
+            Assert.Equal(3, readInvokedCount);
+            for (int i = 0; i < 10; i++) Assert.Equal(i, buffer[i]);
+            for (int i = 10; i < 20; i++) Assert.Equal(i - 10, buffer[i]);
+            for (int i = 20; i < 28; i++) Assert.Equal(i - 20, buffer[i]);
+
+            // now try with a buffer that is bigger than 3 pages
+            readInvokedCount = 0;
+            buffer = new byte[32];
+
+            Assert.Equal(30, async ? await s.ReadAtLeastAsync(buffer, 22) : s.ReadAtLeast(buffer, 22));
+            Assert.Equal(3, readInvokedCount);
+            for (int i = 0; i < 10; i++) Assert.Equal(i, buffer[i]);
+            for (int i = 10; i < 20; i++) Assert.Equal(i - 10, buffer[i]);
+            for (int i = 20; i < 30; i++) Assert.Equal(i - 20, buffer[i]);
+            for (int i = 30; i < 32; i++) Assert.Equal(0, buffer[i]);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ReadAtLeastZero(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return byteCount;
+                });
+
+            byte[] buffer = new byte[20];
+
+            // ReadAtLeast minimumBytes=0 is a no-op
+            Assert.Equal(0, async ? await s.ReadAtLeastAsync(buffer, 0) : s.ReadAtLeast(buffer, 0));
+            Assert.Equal(0, readInvokedCount);
+
+            // now try with an empty buffer
+            byte[] emptyBuffer = Array.Empty<byte>();
+
+            Assert.Equal(0, async ? await s.ReadAtLeastAsync(emptyBuffer, 0) : s.ReadAtLeast(emptyBuffer, 0));
+            Assert.Equal(0, readInvokedCount);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task NegativeMinimumBytes(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return byteCount;
+                });
+
+            byte[] buffer = new byte[10];
+            if (async)
+            {
+                await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await s.ReadAtLeastAsync(buffer, -1));
+                await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await s.ReadAtLeastAsync(buffer, -10));
+            }
+            else
+            {
+                Assert.Throws<ArgumentOutOfRangeException>(() => s.ReadAtLeast(buffer, -1));
+                Assert.Throws<ArgumentOutOfRangeException>(() => s.ReadAtLeast(buffer, -10));
+            }
+            Assert.Equal(0, readInvokedCount);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task BufferSmallerThanMinimumBytes(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return byteCount;
+                });
+
+            byte[] buffer = new byte[20];
+            byte[] emptyBuffer = Array.Empty<byte>();
+            if (async)
+            {
+                await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await s.ReadAtLeastAsync(buffer, 21));
+                await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await s.ReadAtLeastAsync(emptyBuffer, 1));
+            }
+            else
+            {
+                Assert.Throws<ArgumentOutOfRangeException>(() => s.ReadAtLeast(buffer, 21));
+                Assert.Throws<ArgumentOutOfRangeException>(() => s.ReadAtLeast(emptyBuffer, 1));
+            }
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task HandleEndOfStream(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    if (readInvokedCount == 1)
+                    {
+                        int byteCount = Math.Min(count, 10);
+                        for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                        return byteCount;
+                    }
+                    else
+                    {
+                        return 0;
+                    }
+                });
+
+            byte[] buffer = new byte[20];
+            if (async)
+            {
+                await Assert.ThrowsAsync<EndOfStreamException>(async () => await s.ReadAtLeastAsync(buffer, 11));
+            }
+            else
+            {
+                Assert.Throws<EndOfStreamException>(() => s.ReadAtLeast(buffer, 11));
+            }
+            Assert.Equal(2, readInvokedCount);
+
+            readInvokedCount = 0;
+
+            Assert.Equal(10, async ? await s.ReadAtLeastAsync(buffer, 11, throwOnEndOfStream: false) : s.ReadAtLeast(buffer, 11, throwOnEndOfStream: false));
+            for (int i = 0; i < 10; i++) Assert.Equal(i, buffer[i]);
+            for (int i = 10; i < 20; i++) Assert.Equal(0, buffer[i]);
+            Assert.Equal(2, readInvokedCount);
+        }
+
+        [Fact]
+        public async Task CancellationTokenIsPassedThrough()
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readAsyncFunc: (array, offset, count, cancellationToken) =>
+                {
+                    readInvokedCount++;
+                    cancellationToken.ThrowIfCancellationRequested();
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return Task.FromResult(10);
+                });
+
+            byte[] buffer = new byte[20];
+
+            using CancellationTokenSource cts = new CancellationTokenSource();
+            CancellationToken token = cts.Token;
+            cts.Cancel();
+
+            await Assert.ThrowsAsync<OperationCanceledException>(async () => await s.ReadAtLeastAsync(buffer, 10, cancellationToken: token));
+            Assert.Equal(1, readInvokedCount);
+        }
+    }
+}
diff --git a/src/libraries/System.IO/tests/Stream/Stream.ReadExactly.cs b/src/libraries/System.IO/tests/Stream/Stream.ReadExactly.cs
new file mode 100644 (file)
index 0000000..91aa5ce
--- /dev/null
@@ -0,0 +1,297 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.IO.Tests
+{
+    public class Stream_ReadExactly
+    {
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task DelegatesToRead_Success(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return byteCount;
+                });
+
+            byte[] buffer = new byte[30];
+
+            if (async)
+            {
+                await s.ReadExactlyAsync(buffer);
+            }
+            else
+            {
+                s.ReadExactly(buffer);
+            }
+
+            Assert.Equal(3, readInvokedCount);
+            for (int i = 0; i < 10; i++) Assert.Equal(i, buffer[i]);
+            for (int i = 10; i < 20; i++) Assert.Equal(i - 10, buffer[i]);
+            for (int i = 20; i < 30; i++) Assert.Equal(i - 20, buffer[i]);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task DelegatesToRead_Success_OffsetCount(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return byteCount;
+                });
+
+            byte[] buffer = new byte[30];
+
+            if (async)
+            {
+                await s.ReadExactlyAsync(buffer, 0, 10);
+            }
+            else
+            {
+                s.ReadExactly(buffer, 0, 10);
+            }
+
+            Assert.Equal(1, readInvokedCount);
+            for (int i = 0; i < 10; i++) Assert.Equal(i, buffer[i]);
+            for (int i = 10; i < 30; i++) Assert.Equal(0, buffer[i]);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ReadPartialPageCorrectly(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return byteCount;
+                });
+
+            byte[] buffer = new byte[25];
+
+            if (async)
+            {
+                await s.ReadExactlyAsync(buffer);
+            }
+            else
+            {
+                s.ReadExactly(buffer);
+            }
+
+            Assert.Equal(3, readInvokedCount);
+            for (int i = 0; i < 10; i++) Assert.Equal(i, buffer[i]);
+            for (int i = 10; i < 20; i++) Assert.Equal(i - 10, buffer[i]);
+            for (int i = 20; i < 25; i++) Assert.Equal(i - 20, buffer[i]);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ReadPartialPageCorrectly_OffsetCount(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return byteCount;
+                });
+
+            byte[] buffer = new byte[25];
+
+            if (async)
+            {
+                await s.ReadExactlyAsync(buffer, 5, 15);
+            }
+            else
+            {
+                s.ReadExactly(buffer, 5, 15);
+            }
+
+            Assert.Equal(2, readInvokedCount);
+            for (int i = 0; i < 5; i++) Assert.Equal(0, buffer[i]);
+            for (int i = 5; i < 15; i++) Assert.Equal(i - 5, buffer[i]);
+            for (int i = 15; i < 20; i++) Assert.Equal(i - 15, buffer[i]);
+            for (int i = 20; i < 25; i++) Assert.Equal(0, buffer[i]);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ReadEmptyBuffer(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return byteCount;
+                });
+
+            byte[] emptyBuffer = Array.Empty<byte>();
+
+            // ReadExactly on an empty buffer is a no-op
+            if (async)
+            {
+                await s.ReadExactlyAsync(emptyBuffer);
+                await s.ReadExactlyAsync(emptyBuffer, 0, 0);
+            }
+            else
+            {
+                s.ReadExactly(emptyBuffer);
+                s.ReadExactly(emptyBuffer, 0, 0);
+            }
+
+            Assert.Equal(0, readInvokedCount);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task ThrowOnEndOfStream(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    if (readInvokedCount == 1)
+                    {
+                        int byteCount = Math.Min(count, 10);
+                        for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                        return byteCount;
+                    }
+                    else
+                    {
+                        return 0;
+                    }
+                });
+
+            byte[] buffer = new byte[11];
+            if (async)
+            {
+                await Assert.ThrowsAsync<EndOfStreamException>(async () => await s.ReadExactlyAsync(buffer));
+            }
+            else
+            {
+                Assert.Throws<EndOfStreamException>(() => s.ReadExactly(buffer));
+            }
+            Assert.Equal(2, readInvokedCount);
+
+            readInvokedCount = 0;
+            if (async)
+            {
+                await Assert.ThrowsAsync<EndOfStreamException>(async () => await s.ReadExactlyAsync(buffer, 0, buffer.Length));
+            }
+            else
+            {
+                Assert.Throws<EndOfStreamException>(() => s.ReadExactly(buffer, 0, buffer.Length));
+            }
+            Assert.Equal(2, readInvokedCount);
+        }
+
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public async Task OffsetCount_ArgumentChecking(bool async)
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readFunc: (array, offset, count) =>
+                {
+                    readInvokedCount++;
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return byteCount;
+                });
+
+            byte[] buffer = new byte[30];
+
+            if (async)
+            {
+                await Assert.ThrowsAsync<ArgumentNullException>(async () => await s.ReadExactlyAsync(null, 0, 1));
+                await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await s.ReadExactlyAsync(buffer, 0, -1));
+                await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await s.ReadExactlyAsync(buffer, -1, buffer.Length));
+                await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await s.ReadExactlyAsync(buffer, buffer.Length, 1));
+                await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await s.ReadExactlyAsync(buffer, 0, buffer.Length + 1));
+                await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await s.ReadExactlyAsync(buffer, buffer.Length - 1, 2));
+            }
+            else
+            {
+                Assert.Throws<ArgumentNullException>(() => s.ReadExactly(null, 0, 1));
+                Assert.Throws<ArgumentOutOfRangeException>(() => s.ReadExactly(buffer, 0, -1));
+                Assert.Throws<ArgumentOutOfRangeException>(() => s.ReadExactly(buffer, -1, buffer.Length));
+                Assert.Throws<ArgumentOutOfRangeException>(() => s.ReadExactly(buffer, buffer.Length, 1));
+                Assert.Throws<ArgumentOutOfRangeException>(() => s.ReadExactly(buffer, 0, buffer.Length + 1));
+                Assert.Throws<ArgumentOutOfRangeException>(() => s.ReadExactly(buffer, buffer.Length - 1, 2));
+            }
+
+            Assert.Equal(0, readInvokedCount);
+        }
+
+        [Fact]
+        public async Task CancellationTokenIsPassedThrough()
+        {
+            int readInvokedCount = 0;
+            var s = new DelegateStream(
+                canReadFunc: () => true,
+                readAsyncFunc: (array, offset, count, cancellationToken) =>
+                {
+                    readInvokedCount++;
+                    cancellationToken.ThrowIfCancellationRequested();
+
+                    int byteCount = Math.Min(count, 10);
+                    for (int i = 0; i < byteCount; i++) array[offset + i] = (byte)i;
+                    return Task.FromResult(10);
+                });
+
+            byte[] buffer = new byte[20];
+
+            using CancellationTokenSource cts = new CancellationTokenSource();
+            CancellationToken token = cts.Token;
+            cts.Cancel();
+
+            await Assert.ThrowsAsync<OperationCanceledException>(async () => await s.ReadExactlyAsync(buffer, cancellationToken: token));
+            await Assert.ThrowsAsync<OperationCanceledException>(async () => await s.ReadExactlyAsync(buffer, 0, buffer.Length, cancellationToken: token));
+            Assert.Equal(2, readInvokedCount);
+        }
+    }
+}
index 35bba18..c0d66ac 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <RootNamespace>System.IO</RootNamespace>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -36,6 +36,8 @@
     <Compile Include="StreamWriter\StreamWriter.DisposeAsync.cs" />
     <Compile Include="StreamWriter\StreamWriter.FlushTests.cs" />
     <Compile Include="StreamWriter\StreamWriter.WriteTests.cs" />
+    <Compile Include="Stream\Stream.ReadAtLeast.cs" />
+    <Compile Include="Stream\Stream.ReadExactly.cs" />
     <Compile Include="Stream\Stream.DisposeAsync.cs" />
     <Compile Include="Stream\Stream.ReadWriteSpan.cs" />
     <Compile Include="Stream\Stream.NullTests.cs" />
index 8e74084..d6e7337 100644 (file)
@@ -353,18 +353,13 @@ namespace System.Net.Http
 
         private static async ValueTask ReadToFillAsync(Stream stream, Memory<byte> buffer, bool async)
         {
-            while (buffer.Length != 0)
-            {
-                int bytesRead = async
-                    ? await stream.ReadAsync(buffer).ConfigureAwait(false)
-                    : stream.Read(buffer.Span);
+            int bytesRead = async
+                ? await stream.ReadAtLeastAsync(buffer, buffer.Length, throwOnEndOfStream: false).ConfigureAwait(false)
+                : stream.ReadAtLeast(buffer.Span, buffer.Length, throwOnEndOfStream: false);
 
-                if (bytesRead == 0)
-                {
-                    throw new IOException(SR.net_http_invalid_response_premature_eof);
-                }
-
-                buffer = buffer[bytesRead..];
+            if (bytesRead < buffer.Length)
+            {
+                throw new IOException(SR.net_http_invalid_response_premature_eof);
             }
         }
     }
index 781b7be..20ee246 100644 (file)
@@ -82,7 +82,7 @@ namespace System.Net.Http.Functional.Tests
         private async Task ProcessSocks4Request(Socket clientSocket, NetworkStream ns)
         {
             byte[] buffer = new byte[7];
-            await ReadToFillAsync(ns, buffer).ConfigureAwait(false);
+            await ns.ReadExactlyAsync(buffer).ConfigureAwait(false);
 
             if (buffer[0] != 1)
                 throw new Exception("Only CONNECT is supported.");
@@ -148,7 +148,7 @@ namespace System.Net.Http.Functional.Tests
                 throw new Exception("Early EOF");
 
             byte[] buffer = new byte[1024];
-            await ReadToFillAsync(ns, buffer.AsMemory(0, nMethods)).ConfigureAwait(false);
+            await ns.ReadExactlyAsync(buffer.AsMemory(0, nMethods)).ConfigureAwait(false);
 
             byte expectedAuthMethod = _username == null ? (byte)0 : (byte)2;
             if (!buffer.AsSpan(0, nMethods).Contains(expectedAuthMethod))
@@ -165,11 +165,11 @@ namespace System.Net.Http.Functional.Tests
                     throw new Exception("Bad subnegotiation version.");
 
                 int usernameLength = await ns.ReadByteAsync().ConfigureAwait(false);
-                await ReadToFillAsync(ns, buffer.AsMemory(0, usernameLength)).ConfigureAwait(false);
+                await ns.ReadExactlyAsync(buffer.AsMemory(0, usernameLength)).ConfigureAwait(false);
                 string username = Encoding.UTF8.GetString(buffer.AsSpan(0, usernameLength));
 
                 int passwordLength = await ns.ReadByteAsync().ConfigureAwait(false);
-                await ReadToFillAsync(ns, buffer.AsMemory(0, passwordLength)).ConfigureAwait(false);
+                await ns.ReadExactlyAsync(buffer.AsMemory(0, passwordLength)).ConfigureAwait(false);
                 string password = Encoding.UTF8.GetString(buffer.AsSpan(0, passwordLength));
 
                 if (username != _username || password != _password)
@@ -181,7 +181,7 @@ namespace System.Net.Http.Functional.Tests
                 await ns.WriteAsync(new byte[] { 1, 0 }).ConfigureAwait(false);
             }
 
-            await ReadToFillAsync(ns, buffer.AsMemory(0, 4)).ConfigureAwait(false);
+            await ns.ReadExactlyAsync(buffer.AsMemory(0, 4)).ConfigureAwait(false);
             if (buffer[0] != 5)
                 throw new Exception("Bad protocol version.");
             if (buffer[1] != 1)
@@ -191,18 +191,18 @@ namespace System.Net.Http.Functional.Tests
             switch (buffer[3])
             {
                 case 1:
-                    await ReadToFillAsync(ns, buffer.AsMemory(0, 4)).ConfigureAwait(false);
+                    await ns.ReadExactlyAsync(buffer.AsMemory(0, 4)).ConfigureAwait(false);
                     remoteHost = new IPAddress(buffer.AsSpan(0, 4)).ToString();
                     break;
                 case 4:
-                    await ReadToFillAsync(ns, buffer.AsMemory(0, 16)).ConfigureAwait(false);
+                    await ns.ReadExactlyAsync(buffer.AsMemory(0, 16)).ConfigureAwait(false);
                     remoteHost = new IPAddress(buffer.AsSpan(0, 16)).ToString();
                     break;
                 case 3:
                     int length = await ns.ReadByteAsync().ConfigureAwait(false);
                     if (length == -1)
                         throw new Exception("Early EOF");
-                    await ReadToFillAsync(ns, buffer.AsMemory(0, length)).ConfigureAwait(false);
+                    await ns.ReadExactlyAsync(buffer.AsMemory(0, length)).ConfigureAwait(false);
                     remoteHost = Encoding.UTF8.GetString(buffer.AsSpan(0, length));
                     break;
 
@@ -210,7 +210,7 @@ namespace System.Net.Http.Functional.Tests
                     throw new Exception("Unknown address type.");
             }
 
-            await ReadToFillAsync(ns, buffer.AsMemory(0, 2)).ConfigureAwait(false);
+            await ns.ReadExactlyAsync(buffer.AsMemory(0, 2)).ConfigureAwait(false);
             int port = (buffer[0] << 8) + buffer[1];
 
             await ns.WriteAsync(new byte[] { 5, 0, 0, 1, 0, 0, 0, 0, 0, 0 }).ConfigureAwait(false);
@@ -290,18 +290,6 @@ namespace System.Net.Http.Functional.Tests
             }
         }
 
-        private async ValueTask ReadToFillAsync(Stream stream, Memory<byte> buffer)
-        {
-            while (!buffer.IsEmpty)
-            {
-                int bytesRead = await stream.ReadAsync(buffer).ConfigureAwait(false);
-                if (bytesRead == 0)
-                    throw new Exception("Incomplete request");
-
-                buffer = buffer.Slice(bytesRead);
-            }
-        }
-
         public async ValueTask DisposeAsync()
         {
             _listener.Dispose();
index 447cf68..db3a21c 100644 (file)
@@ -424,24 +424,15 @@ namespace System.Net.Security
 
             static async ValueTask<int> ReadAllAsync(Stream stream, Memory<byte> buffer, bool allowZeroRead, CancellationToken cancellationToken)
             {
-                int read = 0;
-
-                do
+                int read = await TIOAdapter.ReadAtLeastAsync(
+                    stream, buffer, buffer.Length, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false);
+                if (read < buffer.Length)
                 {
-                    int bytes = await TIOAdapter.ReadAsync(stream, buffer, cancellationToken).ConfigureAwait(false);
-                    if (bytes == 0)
+                    if (read != 0 || !allowZeroRead)
                     {
-                        if (read != 0 || !allowZeroRead)
-                        {
-                            throw new IOException(SR.net_io_eof);
-                        }
-                        break;
+                        throw new IOException(SR.net_io_eof);
                     }
-
-                    buffer = buffer.Slice(bytes);
-                    read += bytes;
                 }
-                while (!buffer.IsEmpty);
 
                 return read;
             }
index fd3e705..10c689c 100644 (file)
@@ -10,6 +10,7 @@ namespace System.Net.Security
     internal interface IReadWriteAdapter
     {
         static abstract ValueTask<int> ReadAsync(Stream stream, Memory<byte> buffer, CancellationToken cancellationToken);
+        static abstract ValueTask<int> ReadAtLeastAsync(Stream stream, Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken);
         static abstract ValueTask WriteAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken);
         static abstract Task FlushAsync(Stream stream, CancellationToken cancellationToken);
         static abstract Task WaitAsync(TaskCompletionSource<bool> waiter);
@@ -20,6 +21,9 @@ namespace System.Net.Security
         public static ValueTask<int> ReadAsync(Stream stream, Memory<byte> buffer, CancellationToken cancellationToken) =>
             stream.ReadAsync(buffer, cancellationToken);
 
+        public static ValueTask<int> ReadAtLeastAsync(Stream stream, Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken) =>
+            stream.ReadAtLeastAsync(buffer, minimumBytes, throwOnEndOfStream, cancellationToken);
+
         public static ValueTask WriteAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
             stream.WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken);
 
@@ -33,6 +37,9 @@ namespace System.Net.Security
         public static ValueTask<int> ReadAsync(Stream stream, Memory<byte> buffer, CancellationToken cancellationToken) =>
             new ValueTask<int>(stream.Read(buffer.Span));
 
+        public static ValueTask<int> ReadAtLeastAsync(Stream stream, Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken) =>
+            new ValueTask<int>(stream.ReadAtLeast(buffer.Span, minimumBytes, throwOnEndOfStream));
+
         public static ValueTask WriteAsync(Stream stream, byte[] buffer, int offset, int count, CancellationToken cancellationToken)
         {
             stream.Write(buffer, offset, count);
index 9b97a29..3dc39ae 100644 (file)
@@ -31,24 +31,17 @@ namespace System.Net
 
             byte[] buffer = _readHeaderBuffer;
 
-            int bytesRead;
-            int offset = 0;
-            while (offset < buffer.Length)
+            int bytesRead = await TAdapter.ReadAtLeastAsync(
+                stream, buffer, buffer.Length, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false);
+            if (bytesRead < buffer.Length)
             {
-                bytesRead = await TAdapter.ReadAsync(stream, buffer.AsMemory(offset), cancellationToken).ConfigureAwait(false);
                 if (bytesRead == 0)
                 {
-                    if (offset == 0)
-                    {
-                        // m_Eof, return null
-                        _eof = true;
-                        return null;
-                    }
-
-                    throw new IOException(SR.Format(SR.net_io_readfailure, SR.net_io_connectionclosed));
+                    // m_Eof, return null
+                    _eof = true;
+                    return null;
                 }
-
-                offset += bytesRead;
+                throw new IOException(SR.Format(SR.net_io_readfailure, SR.net_io_connectionclosed));
             }
 
             _curReadHeader.CopyFrom(buffer, 0);
@@ -61,16 +54,14 @@ namespace System.Net
 
             buffer = new byte[_curReadHeader.PayloadSize];
 
-            offset = 0;
-            while (offset < buffer.Length)
+            if (buffer.Length > 0)
             {
-                bytesRead = await TAdapter.ReadAsync(stream, buffer.AsMemory(offset), cancellationToken).ConfigureAwait(false);
-                if (bytesRead == 0)
+                bytesRead = await TAdapter.ReadAtLeastAsync(
+                    stream, buffer, buffer.Length, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false);
+                if (bytesRead < buffer.Length)
                 {
                     throw new IOException(SR.Format(SR.net_io_readfailure, SR.net_io_connectionclosed));
                 }
-
-                offset += bytesRead;
             }
             return buffer;
         }
index 6f81d92..88c0115 100644 (file)
@@ -779,16 +779,18 @@ namespace System.Net.WebSockets
                                 totalBytesReceived += receiveBufferBytesToCopy;
                             }
 
-                            while (totalBytesReceived < limit)
+                            if (totalBytesReceived < limit)
                             {
-                                int numBytesRead = await _stream.ReadAsync(header.Compressed ?
-                                        _inflater!.Memory.Slice(totalBytesReceived, limit - totalBytesReceived) :
-                                        payloadBuffer.Slice(totalBytesReceived, limit - totalBytesReceived),
-                                    cancellationToken).ConfigureAwait(false);
-                                if (numBytesRead <= 0)
+                                int bytesToRead = limit - totalBytesReceived;
+                                Memory<byte> readBuffer = header.Compressed ?
+                                    _inflater!.Memory.Slice(totalBytesReceived, bytesToRead) :
+                                    payloadBuffer.Slice(totalBytesReceived, bytesToRead);
+
+                                int numBytesRead = await _stream.ReadAtLeastAsync(
+                                    readBuffer, bytesToRead, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false);
+                                if (numBytesRead < bytesToRead)
                                 {
                                     ThrowEOFUnexpected();
-                                    break;
                                 }
                                 totalBytesReceived += numBytesRead;
                             }
@@ -1359,16 +1361,17 @@ namespace System.Net.WebSockets
                 _receiveBufferOffset = 0;
 
                 // While we don't have enough data, read more.
-                while (_receiveBufferCount < minimumRequiredBytes)
+                if (_receiveBufferCount < minimumRequiredBytes)
                 {
-                    int numRead = await _stream.ReadAsync(_receiveBuffer.Slice(_receiveBufferCount), cancellationToken).ConfigureAwait(false);
-                    Debug.Assert(numRead >= 0, $"Expected non-negative bytes read, got {numRead}");
-                    if (numRead <= 0)
+                    int bytesToRead = minimumRequiredBytes - _receiveBufferCount;
+                    int numRead = await _stream.ReadAtLeastAsync(
+                        _receiveBuffer.Slice(_receiveBufferCount), bytesToRead, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false);
+                    _receiveBufferCount += numRead;
+
+                    if (numRead < bytesToRead)
                     {
                         ThrowEOFUnexpected();
-                        break;
                     }
-                    _receiveBufferCount += numRead;
                 }
             }
         }
index 6664593..b1deeb8 100644 (file)
@@ -1,17 +1,17 @@
 ï»¿<?xml version="1.0" encoding="utf-8"?>
 <root>
-  <!--
-    Microsoft ResX Schema
-
+  <!-- 
+    Microsoft ResX Schema 
+    
     Version 2.0
-
-    The primary goals of this format is to allow a simple XML format
-    that is mostly human readable. The generation and parsing of the
-    various data types are done through the TypeConverter classes
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
     associated with the data types.
-
+    
     Example:
-
+    
     ... ado.net/XML headers & schema ...
     <resheader name="resmimetype">text/microsoft-resx</resheader>
     <resheader name="version">2.0</resheader>
         <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
         <comment>This is a comment</comment>
     </data>
-
-    There are any number of "resheader" rows that contain simple
+                
+    There are any number of "resheader" rows that contain simple 
     name/value pairs.
-
-    Each data row contains a name, and value. The row also contains a
-    type or mimetype. Type corresponds to a .NET class that support
-    text/value conversion through the TypeConverter architecture.
-    Classes that don't support this are serialized and stored with the
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
     mimetype set.
-
-    The mimetype is used for serialized objects, and tells the
-    ResXResourceReader how to depersist the object. This is currently not
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
     extensible. For a given mimetype the value must be set accordingly:
-
-    Note - application/x-microsoft.net.object.binary.base64 is the format
-    that the ResXResourceWriter will generate, however the reader can
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
     read any of the formats listed below.
-
+    
     mimetype: application/x-microsoft.net.object.binary.base64
-    value   : The object must be serialized with
+    value   : The object must be serialized with 
             : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
             : and then encoded with base64 encoding.
-
+    
     mimetype: application/x-microsoft.net.object.soap.base64
-    value   : The object must be serialized with
+    value   : The object must be serialized with 
             : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
             : and then encoded with base64 encoding.
 
     mimetype: application/x-microsoft.net.object.bytearray.base64
-    value   : The object must be serialized into a byte array
+    value   : The object must be serialized into a byte array 
             : using a System.ComponentModel.TypeConverter
             : and then encoded with base64 encoding.
     -->
   <data name="InvalidOperation_ComInteropRequireComWrapperInstance" xml:space="preserve">
     <value>COM Interop requires ComWrapper instance registered for marshalling.</value>
   </data>
-</root>
+  <data name="ArgumentOutOfRange_NotGreaterThanBufferLength" xml:space="preserve">
+    <value>Must not be greater than the length of the buffer.</value>
+  </data>
+</root>
\ No newline at end of file
index 3d15742..0ce1c65 100644 (file)
@@ -483,18 +483,7 @@ namespace System.IO
             }
 
             byte[] result = new byte[count];
-            int numRead = 0;
-            do
-            {
-                int n = _stream.Read(result, numRead, count);
-                if (n == 0)
-                {
-                    break;
-                }
-
-                numRead += n;
-                count -= n;
-            } while (count > 0);
+            int numRead = _stream.ReadAtLeast(result, result.Length, throwOnEndOfStream: false);
 
             if (numRead != result.Length)
             {
@@ -521,16 +510,7 @@ namespace System.IO
             {
                 ThrowIfDisposed();
 
-                int bytesRead = 0;
-                do
-                {
-                    int n = _stream.Read(_buffer, bytesRead, numBytes - bytesRead);
-                    if (n == 0)
-                    {
-                        ThrowHelper.ThrowEndOfFileException();
-                    }
-                    bytesRead += n;
-                } while (bytesRead < numBytes);
+                _stream.ReadExactly(_buffer.AsSpan(0, numBytes));
 
                 return _buffer;
             }
@@ -547,9 +527,6 @@ namespace System.IO
                 throw new ArgumentOutOfRangeException(nameof(numBytes), SR.ArgumentOutOfRange_BinaryReaderFillBuffer);
             }
 
-            int bytesRead = 0;
-            int n;
-
             ThrowIfDisposed();
 
             // Need to find a good threshold for calling ReadByte() repeatedly
@@ -557,7 +534,7 @@ namespace System.IO
             // streams.
             if (numBytes == 1)
             {
-                n = _stream.ReadByte();
+                int n = _stream.ReadByte();
                 if (n == -1)
                 {
                     ThrowHelper.ThrowEndOfFileException();
@@ -567,15 +544,19 @@ namespace System.IO
                 return;
             }
 
-            do
+            if (numBytes > 0)
             {
-                n = _stream.Read(_buffer, bytesRead, numBytes - bytesRead);
+                _stream.ReadExactly(_buffer.AsSpan(0, numBytes));
+            }
+            else
+            {
+                // ReadExactly no-ops for empty buffers, so special case numBytes == 0 to preserve existing behavior.
+                int n = _stream.Read(_buffer, 0, 0);
                 if (n == 0)
                 {
                     ThrowHelper.ThrowEndOfFileException();
                 }
-                bytesRead += n;
-            } while (bytesRead < numBytes);
+            }
         }
 
         public int Read7BitEncodedInt()
index 2c823b4..49aa4e1 100644 (file)
@@ -332,6 +332,123 @@ namespace System.IO
             }
         }
 
+        /// <summary>
+        /// Asynchronously reads bytes from the current stream, advances the position within the stream until the <paramref name="buffer"/> is filled,
+        /// and monitors cancellation requests.
+        /// </summary>
+        /// <param name="buffer">The buffer to write the data into.</param>
+        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
+        /// <returns>A task that represents the asynchronous read operation.</returns>
+        /// <exception cref="EndOfStreamException">
+        /// The end of the stream is reached before filling the <paramref name="buffer"/>.
+        /// </exception>
+        /// <remarks>
+        /// When <paramref name="buffer"/> is empty, this read operation will be completed without waiting for available data in the stream.
+        /// </remarks>
+        public ValueTask ReadExactlyAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+        {
+            ValueTask<int> vt = ReadAtLeastAsyncCore(buffer, buffer.Length, throwOnEndOfStream: true, cancellationToken);
+
+            // transfer the ValueTask<int> to a ValueTask without allocating here.
+            return ValueTask.DangerousCreateFromTypedValueTask(vt);
+        }
+
+        /// <summary>
+        /// Asynchronously reads <paramref name="count"/> number of bytes from the current stream, advances the position within the stream,
+        /// and monitors cancellation requests.
+        /// </summary>
+        /// <param name="buffer">The buffer to write the data into.</param>
+        /// <param name="offset">The byte offset in <paramref name="buffer"/> at which to begin writing data from the stream.</param>
+        /// <param name="count">The number of bytes to be read from the current stream.</param>
+        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
+        /// <returns>A task that represents the asynchronous read operation.</returns>
+        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is <see langword="null"/>.</exception>
+        /// <exception cref="ArgumentOutOfRangeException">
+        /// <paramref name="offset"/> is outside the bounds of <paramref name="buffer"/>.
+        /// -or-
+        /// <paramref name="count"/> is negative.
+        /// -or-
+        /// The range specified by the combination of <paramref name="offset"/> and <paramref name="count"/> exceeds the
+        /// length of <paramref name="buffer"/>.
+        /// </exception>
+        /// <exception cref="EndOfStreamException">
+        /// The end of the stream is reached before reading <paramref name="count"/> number of bytes.
+        /// </exception>
+        /// <remarks>
+        /// When <paramref name="count"/> is 0 (zero), this read operation will be completed without waiting for available data in the stream.
+        /// </remarks>
+        public ValueTask ReadExactlyAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default)
+        {
+            ValidateBufferArguments(buffer, offset, count);
+
+            ValueTask<int> vt = ReadAtLeastAsyncCore(buffer.AsMemory(offset, count), count, throwOnEndOfStream: true, cancellationToken);
+
+            // transfer the ValueTask<int> to a ValueTask without allocating here.
+            return ValueTask.DangerousCreateFromTypedValueTask(vt);
+        }
+
+        /// <summary>
+        /// Asynchronously reads at least a minimum number of bytes from the current stream, advances the position within the stream by the
+        /// number of bytes read, and monitors cancellation requests.
+        /// </summary>
+        /// <param name="buffer">The region of memory to write the data into.</param>
+        /// <param name="minimumBytes">The minimum number of bytes to read into the buffer.</param>
+        /// <param name="throwOnEndOfStream">
+        /// <see langword="true"/> to throw an exception if the end of the stream is reached before reading <paramref name="minimumBytes"/> of bytes;
+        /// <see langword="false"/> to return less than <paramref name="minimumBytes"/> when the end of the stream is reached.
+        /// The default is <see langword="true"/>.
+        /// </param>
+        /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
+        /// <returns>
+        /// A task that represents the asynchronous read operation. The value of its <see cref="ValueTask{TResult}.Result"/> property contains the
+        /// total number of bytes read into the buffer. This is guaranteed to be greater than or equal to <paramref name="minimumBytes"/> when
+        /// <paramref name="throwOnEndOfStream"/> is <see langword="true"/>. This will be less than <paramref name="minimumBytes"/> when the end
+        /// of the stream is reached and <paramref name="throwOnEndOfStream"/> is <see langword="false"/>. This can be less than the number of
+        /// bytes allocated in the buffer if that many bytes are not currently available.
+        /// </returns>
+        /// <exception cref="ArgumentOutOfRangeException">
+        /// <paramref name="minimumBytes"/> is negative, or is greater than the length of <paramref name="buffer"/>.
+        /// </exception>
+        /// <exception cref="EndOfStreamException">
+        /// <paramref name="throwOnEndOfStream"/> is <see langword="true"/> and the end of the stream is reached before reading
+        /// <paramref name="minimumBytes"/> bytes of data.
+        /// </exception>
+        /// <remarks>
+        /// When <paramref name="minimumBytes"/> is 0 (zero), this read operation will be completed without waiting for available data in the stream.
+        /// </remarks>
+        public ValueTask<int> ReadAtLeastAsync(Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream = true, CancellationToken cancellationToken = default)
+        {
+            ValidateReadAtLeastArguments(buffer.Length, minimumBytes);
+
+            return ReadAtLeastAsyncCore(buffer, minimumBytes, throwOnEndOfStream, cancellationToken);
+        }
+
+        // No argument checking is done here. It is up to the caller.
+        [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))]
+        private async ValueTask<int> ReadAtLeastAsyncCore(Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream, CancellationToken cancellationToken)
+        {
+            Debug.Assert(minimumBytes <= buffer.Length);
+
+            int totalRead = 0;
+            while (totalRead < minimumBytes)
+            {
+                int read = await ReadAsync(buffer.Slice(totalRead), cancellationToken).ConfigureAwait(false);
+                if (read == 0)
+                {
+                    if (throwOnEndOfStream)
+                    {
+                        ThrowHelper.ThrowEndOfFileException();
+                    }
+
+                    return totalRead;
+                }
+
+                totalRead += read;
+            }
+
+            return totalRead;
+        }
+
 #if NATIVEAOT // TODO: https://github.com/dotnet/corert/issues/3251
         private static bool HasOverriddenBeginEndRead() => true;
 
@@ -701,6 +818,109 @@ namespace System.IO
             return r == 0 ? -1 : oneByteArray[0];
         }
 
+        /// <summary>
+        /// Reads bytes from the current stream and advances the position within the stream until the <paramref name="buffer"/> is filled.
+        /// </summary>
+        /// <param name="buffer">A region of memory. When this method returns, the contents of this region are replaced by the bytes read from the current stream.</param>
+        /// <exception cref="EndOfStreamException">
+        /// The end of the stream is reached before filling the <paramref name="buffer"/>.
+        /// </exception>
+        /// <remarks>
+        /// When <paramref name="buffer"/> is empty, this read operation will be completed without waiting for available data in the stream.
+        /// </remarks>
+        public void ReadExactly(Span<byte> buffer) =>
+            _ = ReadAtLeastCore(buffer, buffer.Length, throwOnEndOfStream: true);
+
+        /// <summary>
+        /// Reads <paramref name="count"/> number of bytes from the current stream and advances the position within the stream.
+        /// </summary>
+        /// <param name="buffer">
+        /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values
+        /// between <paramref name="offset"/> and (<paramref name="offset"/> + <paramref name="count"/> - 1) replaced
+        /// by the bytes read from the current stream.
+        /// </param>
+        /// <param name="offset">The byte offset in <paramref name="buffer"/> at which to begin storing the data read from the current stream.</param>
+        /// <param name="count">The number of bytes to be read from the current stream.</param>
+        /// <exception cref="ArgumentNullException"><paramref name="buffer"/> is <see langword="null"/>.</exception>
+        /// <exception cref="ArgumentOutOfRangeException">
+        /// <paramref name="offset"/> is outside the bounds of <paramref name="buffer"/>.
+        /// -or-
+        /// <paramref name="count"/> is negative.
+        /// -or-
+        /// The range specified by the combination of <paramref name="offset"/> and <paramref name="count"/> exceeds the
+        /// length of <paramref name="buffer"/>.
+        /// </exception>
+        /// <exception cref="EndOfStreamException">
+        /// The end of the stream is reached before reading <paramref name="count"/> number of bytes.
+        /// </exception>
+        /// <remarks>
+        /// When <paramref name="count"/> is 0 (zero), this read operation will be completed without waiting for available data in the stream.
+        /// </remarks>
+        public void ReadExactly(byte[] buffer, int offset, int count)
+        {
+            ValidateBufferArguments(buffer, offset, count);
+
+            _ = ReadAtLeastCore(buffer.AsSpan(offset, count), count, throwOnEndOfStream: true);
+        }
+
+        /// <summary>
+        /// Reads at least a minimum number of bytes from the current stream and advances the position within the stream by the number of bytes read.
+        /// </summary>
+        /// <param name="buffer">A region of memory. When this method returns, the contents of this region are replaced by the bytes read from the current stream.</param>
+        /// <param name="minimumBytes">The minimum number of bytes to read into the buffer.</param>
+        /// <param name="throwOnEndOfStream">
+        /// <see langword="true"/> to throw an exception if the end of the stream is reached before reading <paramref name="minimumBytes"/> of bytes;
+        /// <see langword="false"/> to return less than <paramref name="minimumBytes"/> when the end of the stream is reached.
+        /// The default is <see langword="true"/>.
+        /// </param>
+        /// <returns>
+        /// The total number of bytes read into the buffer. This is guaranteed to be greater than or equal to <paramref name="minimumBytes"/>
+        /// when <paramref name="throwOnEndOfStream"/> is <see langword="true"/>. This will be less than <paramref name="minimumBytes"/> when the
+        /// end of the stream is reached and <paramref name="throwOnEndOfStream"/> is <see langword="false"/>. This can be less than the number
+        /// of bytes allocated in the buffer if that many bytes are not currently available.
+        /// </returns>
+        /// <exception cref="ArgumentOutOfRangeException">
+        /// <paramref name="minimumBytes"/> is negative, or is greater than the length of <paramref name="buffer"/>.
+        /// </exception>
+        /// <exception cref="EndOfStreamException">
+        /// <paramref name="throwOnEndOfStream"/> is <see langword="true"/> and the end of the stream is reached before reading
+        /// <paramref name="minimumBytes"/> bytes of data.
+        /// </exception>
+        /// <remarks>
+        /// When <paramref name="minimumBytes"/> is 0 (zero), this read operation will be completed without waiting for available data in the stream.
+        /// </remarks>
+        public int ReadAtLeast(Span<byte> buffer, int minimumBytes, bool throwOnEndOfStream = true)
+        {
+            ValidateReadAtLeastArguments(buffer.Length, minimumBytes);
+
+            return ReadAtLeastCore(buffer, minimumBytes, throwOnEndOfStream);
+        }
+
+        // No argument checking is done here. It is up to the caller.
+        private int ReadAtLeastCore(Span<byte> buffer, int minimumBytes, bool throwOnEndOfStream)
+        {
+            Debug.Assert(minimumBytes <= buffer.Length);
+
+            int totalRead = 0;
+            while (totalRead < minimumBytes)
+            {
+                int read = Read(buffer.Slice(totalRead));
+                if (read == 0)
+                {
+                    if (throwOnEndOfStream)
+                    {
+                        ThrowHelper.ThrowEndOfFileException();
+                    }
+
+                    return totalRead;
+                }
+
+                totalRead += read;
+            }
+
+            return totalRead;
+        }
+
         public abstract void Write(byte[] buffer, int offset, int count);
 
         public virtual void Write(ReadOnlySpan<byte> buffer)
@@ -757,6 +977,19 @@ namespace System.IO
             }
         }
 
+        private static void ValidateReadAtLeastArguments(int bufferLength, int minimumBytes)
+        {
+            if (minimumBytes < 0)
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBytes, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
+            }
+
+            if (bufferLength < minimumBytes)
+            {
+                ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.minimumBytes, ExceptionResource.ArgumentOutOfRange_NotGreaterThanBufferLength);
+            }
+        }
+
         /// <summary>Validates arguments provided to the <see cref="CopyTo(Stream, int)"/> or <see cref="CopyToAsync(Stream, int, CancellationToken)"/> methods.</summary>
         /// <param name="destination">The <see cref="Stream"/> "destination" argument passed to the copy method.</param>
         /// <param name="bufferSize">The integer "bufferSize" argument passed to the copy method.</param>
index 108abf5..4f186f5 100644 (file)
@@ -398,6 +398,18 @@ namespace System.Threading.Tasks
             }
         }
 
+        /// <summary>
+        /// Transfers the <see cref="ValueTask{TResult}"/> to a <see cref="ValueTask"/> instance.
+        ///
+        /// The <see cref="ValueTask{TResult}"/> should not be used after calling this method.
+        /// </summary>
+        internal static ValueTask DangerousCreateFromTypedValueTask<TResult>(ValueTask<TResult> valueTask)
+        {
+            Debug.Assert(valueTask._obj is null or Task or IValueTaskSource, "If the ValueTask<>'s backing object is an IValueTaskSource<TResult>, it must also be IValueTaskSource.");
+
+            return new ValueTask(valueTask._obj, valueTask._token, valueTask._continueOnCapturedContext);
+        }
+
         /// <summary>Gets an awaiter for this <see cref="ValueTask"/>.</summary>
         public ValueTaskAwaiter GetAwaiter() => new ValueTaskAwaiter(in this);
 
index 3e68ff7..2e792c5 100644 (file)
@@ -527,12 +527,6 @@ namespace System
         }
 
         [DoesNotReturn]
-        internal static void ThrowArgumentOutOfRangeException_NeedPosNum(string? paramName)
-        {
-            throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_NeedPosNum);
-        }
-
-        [DoesNotReturn]
         internal static void ThrowArgumentOutOfRangeException_NeedNonNegNum(string paramName)
         {
             throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_NeedNonNegNum);
@@ -894,6 +888,8 @@ namespace System
                     return "anyOf";
                 case ExceptionArgument.overlapped:
                     return "overlapped";
+                case ExceptionArgument.minimumBytes:
+                    return "minimumBytes";
                 default:
                     Debug.Fail("The enum value is not defined, please check the ExceptionArgument Enum.");
                     return "";
@@ -1056,6 +1052,8 @@ namespace System
                     return SR.CancellationTokenSource_Disposed;
                 case ExceptionResource.Argument_AlignmentMustBePow2:
                     return SR.Argument_AlignmentMustBePow2;
+                case ExceptionResource.ArgumentOutOfRange_NotGreaterThanBufferLength:
+                    return SR.ArgumentOutOfRange_NotGreaterThanBufferLength;
                 default:
                     Debug.Fail("The enum value is not defined, please check the ExceptionResource Enum.");
                     return "";
@@ -1166,6 +1164,7 @@ namespace System
         stream,
         anyOf,
         overlapped,
+        minimumBytes,
     }
 
     //
@@ -1186,6 +1185,7 @@ namespace System
         ArgumentOutOfRange_GetCharCountOverflow,
         ArgumentOutOfRange_ListInsert,
         ArgumentOutOfRange_NeedNonNegNum,
+        ArgumentOutOfRange_NotGreaterThanBufferLength,
         ArgumentOutOfRange_SmallCapacity,
         Argument_InvalidOffLen,
         Argument_CannotExtractScalar,
index 5b6c17a..8f30f15 100644 (file)
@@ -419,16 +419,9 @@ namespace System.Runtime.Serialization.Json
             Debug.Assert(_bytes != null);
 
             count -= _byteCount;
-            while (count > 0)
+            if (count > 0)
             {
-                int read = _stream.Read(_bytes, _byteOffset + _byteCount, count);
-                if (read == 0)
-                {
-                    break;
-                }
-
-                _byteCount += read;
-                count -= read;
+                _byteCount += _stream.ReadAtLeast(_bytes.AsSpan(_byteOffset + _byteCount, count), count, throwOnEndOfStream: false);
             }
         }
 
index d6de230..e0fe24b 100644 (file)
@@ -281,14 +281,9 @@ namespace System.Xml
         private void FillBuffer(int count)
         {
             count -= _byteCount;
-            while (count > 0)
+            if (count > 0)
             {
-                int read = _stream.Read(_bytes!, _byteOffset + _byteCount, count);
-                if (read == 0)
-                    break;
-
-                _byteCount += read;
-                count -= read;
+                _byteCount += _stream.ReadAtLeast(_bytes.AsSpan(_byteOffset + _byteCount, count), count, throwOnEndOfStream: false);
             }
         }
 
index db06d82..b72baab 100644 (file)
@@ -234,14 +234,13 @@ namespace System.Xml
                 }
                 int needed = newOffsetMax - _offsetMax;
                 DiagnosticUtility.DebugAssert(needed > 0, "");
-                do
+                int read = _stream.ReadAtLeast(_buffer.AsSpan(_offsetMax, needed), needed, throwOnEndOfStream: false);
+                _offsetMax += read;
+
+                if (read < needed)
                 {
-                    int actual = _stream.Read(_buffer, _offsetMax, needed);
-                    if (actual == 0)
-                        return false;
-                    _offsetMax += actual;
-                    needed -= actual;
-                } while (needed > 0);
+                    return false;
+                }
             } while (true);
         }
 
index 9efea7b..3ed4021 100644 (file)
@@ -1714,13 +1714,8 @@ namespace System.Xml
             // allocate byte buffer
             byte[] bytes = new byte[CalcBufferSize(input)];
 
-            int byteCount = 0;
-            int read;
-            do
-            {
-                read = input.Read(bytes, byteCount, bytes.Length - byteCount);
-                byteCount += read;
-            } while (read > 0 && byteCount < 2);
+            int bytesToRead = Math.Min(bytes.Length, 2);
+            int byteCount = input.ReadAtLeast(bytes, bytesToRead, throwOnEndOfStream: false);
 
             // create text or binary XML reader depending on the stream first 2 bytes
             if (byteCount >= 2 && bytes[0] == 0xdf && bytes[1] == 0xff)
index 96eac65..6c12e3a 100644 (file)
@@ -2950,13 +2950,13 @@ namespace System.Xml
 
             // make sure we have at least 4 bytes to detect the encoding (no preamble of System.Text supported encoding is longer than 4 bytes)
             _ps.bytePos = 0;
-            while (_ps.bytesUsed < 4 && _ps.bytes.Length - _ps.bytesUsed > 0)
+            if (_ps.bytesUsed < 4 && _ps.bytes.Length - _ps.bytesUsed > 0)
             {
-                int read = stream.Read(_ps.bytes, _ps.bytesUsed, _ps.bytes.Length - _ps.bytesUsed);
-                if (read == 0)
+                int bytesToRead = Math.Min(4, _ps.bytes.Length - _ps.bytesUsed);
+                int read = stream.ReadAtLeast(_ps.bytes.AsSpan(_ps.bytesUsed), bytesToRead, throwOnEndOfStream: false);
+                if (read < bytesToRead)
                 {
                     _ps.isStreamEof = true;
-                    break;
                 }
                 _ps.bytesUsed += read;
             }
index 6257fa4..dc3e99f 100644 (file)
@@ -953,13 +953,13 @@ namespace System.Xml
 
             // make sure we have at least 4 bytes to detect the encoding (no preamble of System.Text supported encoding is longer than 4 bytes)
             _ps.bytePos = 0;
-            while (_ps.bytesUsed < 4 && _ps.bytes.Length - _ps.bytesUsed > 0)
+            if (_ps.bytesUsed < 4 && _ps.bytes.Length - _ps.bytesUsed > 0)
             {
-                int read = await stream.ReadAsync(_ps.bytes.AsMemory(_ps.bytesUsed)).ConfigureAwait(false);
-                if (read == 0)
+                int bytesToRead = Math.Min(4, _ps.bytes.Length - _ps.bytesUsed);
+                int read = await stream.ReadAtLeastAsync(_ps.bytes.AsMemory(_ps.bytesUsed), bytesToRead, throwOnEndOfStream: false).ConfigureAwait(false);
+                if (read < bytesToRead)
                 {
                     _ps.isStreamEof = true;
-                    break;
                 }
                 _ps.bytesUsed += read;
             }
index e3fd02e..bb52925 100644 (file)
@@ -78,6 +78,9 @@ namespace System.Reflection.Internal
 
 #if NETCOREAPP
         internal static int TryReadAll(this Stream stream, Span<byte> buffer)
+#if NET7_0_OR_GREATER
+            => stream.ReadAtLeast(buffer, buffer.Length, throwOnEndOfStream: false);
+#else
         {
             int totalBytesRead = 0;
             while (totalBytesRead < buffer.Length)
@@ -94,6 +97,7 @@ namespace System.Reflection.Internal
             return totalBytesRead;
         }
 #endif
+#endif
 
         /// <summary>
         /// Resolve image size as either the given user-specified size or distance from current position to end-of-stream.
index dc0468b..f00ebe0 100644 (file)
@@ -9493,7 +9493,13 @@ namespace System.IO
         public System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count) { throw null; }
         public virtual System.Threading.Tasks.Task<int> ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; }
         public virtual System.Threading.Tasks.ValueTask<int> ReadAsync(System.Memory<byte> buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+        public int ReadAtLeast(System.Span<byte> buffer, int minimumBytes, bool throwOnEndOfStream = true) { throw null; }
+        public System.Threading.Tasks.ValueTask<int> ReadAtLeastAsync(System.Memory<byte> buffer, int minimumBytes, bool throwOnEndOfStream = true, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
         public virtual int ReadByte() { throw null; }
+        public void ReadExactly(byte[] buffer, int offset, int count) { }
+        public void ReadExactly(System.Span<byte> buffer) { }
+        public System.Threading.Tasks.ValueTask ReadExactlyAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
+        public System.Threading.Tasks.ValueTask ReadExactlyAsync(System.Memory<byte> buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
         public abstract long Seek(long offset, System.IO.SeekOrigin origin);
         public abstract void SetLength(long value);
         public static System.IO.Stream Synchronized(System.IO.Stream stream) { throw null; }