From 1f1bfe16f4a44917f874c9dc890642fb3cf8f2f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Wed, 8 Apr 2026 13:17:26 -0700 Subject: [PATCH 01/11] First API proposal implementation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System.Memory/ref/System.Memory.cs | 18 + .../System.Memory/src/Resources/Strings.resx | 9 + .../System.Memory/src/System.Memory.csproj | 3 +- .../System/Buffers/ReadOnlySequenceStream.cs | 203 +++++++++++ ...ReadOnlySequenceStream.ConformanceTests.cs | 46 +++ .../ReadOnlySequenceStreamTests.cs | 252 ++++++++++++++ .../tests/System.Memory.Tests.csproj | 7 +- .../src/Resources/Strings.resx | 3 + .../System.Private.CoreLib.Shared.projitems | 5 +- .../src/System/IO/ReadOnlyMemoryStream.cs | 205 +++++++++++ .../src/System/IO/StringStream.cs | 162 +++++++++ .../src/System/IO/WritableMemoryStream.cs | 237 +++++++++++++ .../Serialization/Json/JsonXmlDataContract.cs | 2 +- .../Xml/Resolvers/XmlPreloadedResolver.cs | 2 +- .../System.Runtime/ref/System.Runtime.cs | 56 ++- .../ReadOnlyMemoryStreamConformanceTests.cs | 39 +++ .../ReadOnlyMemoryStreamTests.cs | 312 +++++++++++++++++ .../StringStreamConformanceTests.cs | 72 ++++ .../StringStream/StringStreamTests_Memory.cs | 297 ++++++++++++++++ .../StringStream/StringStreamTests_String.cs | 223 ++++++++++++ .../System.IO.Tests/System.IO.Tests.csproj | 9 +- .../WritableMemoryStreamConformanceTests.cs | 72 ++++ .../WritableMemoryStreamTests.cs | 323 ++++++++++++++++++ 23 files changed, 2550 insertions(+), 7 deletions(-) create mode 100644 src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs create mode 100644 src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs create mode 100644 src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs create mode 100644 src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs create mode 100644 src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs create mode 100644 src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs create mode 100644 src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs create mode 100644 src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs create mode 100644 src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs create mode 100644 src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index c665b746232878..4a9c2b70c6deb4 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -160,6 +160,24 @@ public void Rewind(long count) { } public bool TryReadExact(int count, out System.Buffers.ReadOnlySequence sequence) { throw null; } } } +namespace System.Buffers +{ + public sealed partial class ReadOnlySequenceStream : System.IO.Stream + { + public ReadOnlySequenceStream(System.Buffers.ReadOnlySequence sequence) { } + public override bool CanRead { get { throw null; } } + public override bool CanSeek { get { throw null; } } + public override bool CanWrite { get { throw null; } } + public override long Length { get { throw null; } } + public override long Position { get { throw null; } set { } } + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) { throw null; } + public override int Read(System.Span buffer) { throw null; } + public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; } + public override void SetLength(long value) { } + public override void Write(byte[] buffer, int offset, int count) { } + } +} namespace System.Runtime.InteropServices { public static partial class SequenceMarshal diff --git a/src/libraries/System.Memory/src/Resources/Strings.resx b/src/libraries/System.Memory/src/Resources/Strings.resx index 8576ac8e8642cc..90489cb74c5898 100644 --- a/src/libraries/System.Memory/src/Resources/Strings.resx +++ b/src/libraries/System.Memory/src/Resources/Strings.resx @@ -147,4 +147,13 @@ Cannot allocate a buffer of size {0}. + + Stream does not support writing. + + + An attempt was made to move the position before the beginning of the stream. + + + Invalid seek origin. + \ No newline at end of file diff --git a/src/libraries/System.Memory/src/System.Memory.csproj b/src/libraries/System.Memory/src/System.Memory.csproj index b0f0a14bca73a3..93bc936176aa7f 100644 --- a/src/libraries/System.Memory/src/System.Memory.csproj +++ b/src/libraries/System.Memory/src/System.Memory.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent) @@ -28,6 +28,7 @@ + diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs new file mode 100644 index 00000000000000..3e6423880dfd41 --- /dev/null +++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs @@ -0,0 +1,203 @@ +// 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.IO; +using System.Threading.Tasks; + +namespace System.Buffers +{ + /// + /// Provides a seekable, read-only implementation over a of bytes. + /// + /// + /// This type is not thread-safe. Synchronize access if the stream is used concurrently. + /// The underlying sequence should not be modified while the stream is in use. + /// Seeking beyond the end of the stream is supported; subsequent reads will return zero bytes. + /// + // Seekable Stream from ReadOnlySequence + public sealed class ReadOnlySequenceStream : Stream + { + private ReadOnlySequence _sequence; + private SequencePosition _position; + private long _positionPastEnd; // -1 if within bounds, or the actual position if past end + private bool _isDisposed; + + /// + /// Initializes a new instance of the class over the specified . + /// + /// The to wrap. + public ReadOnlySequenceStream(ReadOnlySequence sequence) + { + _sequence = sequence; + _position = sequence.Start; + _positionPastEnd = -1; + _isDisposed = false; + } + + /// + public override bool CanRead => !_isDisposed; + + /// + public override bool CanSeek => !_isDisposed; + + /// + public override bool CanWrite => false; + + private void EnsureNotDisposed() => ObjectDisposedException.ThrowIf(_isDisposed, this); + + /// + public override long Length + { + get + { + EnsureNotDisposed(); + return _sequence.Length; + } + } + + /// + public override long Position + { + get + { + EnsureNotDisposed(); + return _positionPastEnd >= 0 ? _positionPastEnd : _sequence.Slice(_sequence.Start, _position).Length; + } + set + { + EnsureNotDisposed(); + ArgumentOutOfRangeException.ThrowIfNegative(value); + + // Allow seeking past the end + if (value >= Length) + { + _position = _sequence.End; + _positionPastEnd = value; + } + else + { + _position = _sequence.GetPosition(value, _sequence.Start); + _positionPastEnd = -1; + } + } + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + ValidateBufferArguments(buffer, offset, count); + return Read(buffer.AsSpan(offset, count)); + } + + /// + public override int Read(Span buffer) + { + EnsureNotDisposed(); + + if (_positionPastEnd >= 0) + { + return 0; + } + + ReadOnlySequence remaining = _sequence.Slice(_position); + int n = (int)Math.Min(remaining.Length, buffer.Length); + if (n <= 0) + { + return 0; + } + + remaining.Slice(0, n).CopyTo(buffer); + _position = _sequence.GetPosition(n, _position); + return n; + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBufferArguments(buffer, offset, count); + + // If cancellation was requested, bail early + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + int n = Read(buffer, offset, count); + return Task.FromResult(n); + } + + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + int bytesRead = Read(buffer.Span); + return new ValueTask(bytesRead); + } + + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + /// Sets the position within the current stream. + /// + /// A byte offset relative to the parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// The new position within the stream. + public override long Seek(long offset, SeekOrigin origin) + { + EnsureNotDisposed(); + + long absolutePosition = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => (_positionPastEnd >= 0 ? _positionPastEnd : _sequence.Slice(_sequence.Start, _position).Length) + offset, + SeekOrigin.End => Length + offset, + _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)) + }; + + // Negative positions are invalid + if (absolutePosition < 0) + { + throw new IOException(SR.IO_SeekBeforeBegin); + } + + // Update position - seeking past end is allowed + if (absolutePosition >= Length) + { + _position = _sequence.End; + _positionPastEnd = absolutePosition; + } + else + { + _position = _sequence.GetPosition(absolutePosition, _sequence.Start); + _positionPastEnd = -1; + } + + return absolutePosition; + } + + /// + public override void Flush() { } + + /// + public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + protected override void Dispose(bool disposing) + { + _isDisposed = true; + base.Dispose(disposing); + } + } +} diff --git a/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs new file mode 100644 index 00000000000000..3b5cc139be7ff6 --- /dev/null +++ b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System.IO; +using System.Buffers; +using System.IO.Tests; +using System.Threading.Tasks; + +namespace System.Memory.Tests +{ + /// + /// Conformance tests for ReadOnlySequenceStream - a read-only, seekable stream + /// wrapper around ReadOnlySequence{byte}. + /// + public class ROSequenceStreamConformanceTests : StandaloneStreamConformanceTests + { + // StreamConformanceTests flags to specify capabilities + protected override bool CanSeek => true; + // SetLength() is not supported because ReadOnlySequence{byte} is immutable. + protected override bool CanSetLength => false; + // ReadOnlySequenceStream doesn't buffer writes (it's read-only), + protected override bool NopFlushCompletesSynchronously => true; + + protected override Task CreateReadOnlyStreamCore(byte[]? initialData) + { + if (initialData == null || initialData.Length == 0) + { + // Create empty sequence for null or empty data + var emptySequence = ReadOnlySequence.Empty; + return Task.FromResult(new ReadOnlySequenceStream(emptySequence)); + } + + // ReadOnlySequence can be constructed from: + // 1. ReadOnlyMemory (single segment) + // 2. ReadOnlySequenceSegment chain (multi-segment) + var sequence = new ReadOnlySequence(initialData); // Single segment + return Task.FromResult(new ReadOnlySequenceStream(sequence)); + } + + // Immutable + protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); + + + // Immutable + protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); + } +} diff --git a/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs new file mode 100644 index 00000000000000..17abff98bfe5d5 --- /dev/null +++ b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs @@ -0,0 +1,252 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Memory.Tests +{ + /// + /// Additional specific tests for ReadOnlySequenceStream beyond conformance tests. + /// + public class ReadOnlySequenceStreamTests + { + // NOTE: Conformance tests' coverage: Ctor correctness, stream capabilities, + // Position, Length, Seek, Read, exceptions for unsupported operations. + + // Not covered in conformance tests: Stream + multi-segment sequences + // ReadOnlySequence{byte} can represent data spread across + // multiple memory segments (linked list of ReadOnlyMemory{byte}). + // This is common in network buffers and pooled memory scenarios. + [Fact] + public void Read_MultiSegmentSequence_ReturnsCorrectData() + { + // Create multi-segment sequence: [1,2,3] -> [4,5,6] -> [7,8,9] + var segment1 = new TestSegment(new byte[] { 1, 2, 3 }); + var segment2 = segment1.Append(new byte[] { 4, 5, 6 }); + var segment3 = segment2.Append(new byte[] { 7, 8, 9 }); + + var sequence = new ReadOnlySequence(segment1, 0, segment3, 3); + var stream = new ReadOnlySequenceStream(sequence); + + // Read all data + byte[] buffer = new byte[9]; + int totalRead = 0; + + while (totalRead < 9) + { + int bytesRead = stream.Read(buffer, totalRead, 9 - totalRead); + if (bytesRead == 0) break; + totalRead += bytesRead; + } + + Assert.Equal(9, totalRead); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, buffer); + } + + [Fact] + public void Seek_MultiSegmentSequence_WorksCorrectly() + { + // Create multi-segment sequence: [1,2,3] -> [4,5,6] + var segment1 = new TestSegment(new byte[] { 1, 2, 3 }); + var segment2 = segment1.Append(new byte[] { 4, 5, 6 }); + + var sequence = new ReadOnlySequence(segment1, 0, segment2, 3); + var stream = new ReadOnlySequenceStream(sequence); + + // Seek into second segment + stream.Seek(4, SeekOrigin.Begin); // Should be at byte '5' + + byte[] buffer = new byte[1]; + stream.Read(buffer, 0, 1); + + Assert.Equal(5, buffer[0]); + Assert.Equal(5, stream.Position); + } + + [Fact] + public void Seek_AcrossSegments_BothDirections() + { + // Arrange: [10,20,30] -> [40,50,60] + var segment1 = new TestSegment(new byte[] { 10, 20, 30 }); + var segment2 = segment1.Append(new byte[] { 40, 50, 60 }); + + var sequence = new ReadOnlySequence(segment1, 0, segment2, 3); + var stream = new ReadOnlySequenceStream(sequence); + + byte[] buffer = new byte[1]; + + // Act & Assert: Start at position 2 (byte 30) + stream.Position = 2; + stream.Read(buffer, 0, 1); + Assert.Equal(30, buffer[0]); + + // Seek forward into segment 2 + stream.Seek(2, SeekOrigin.Current); // Now at position 5 (byte 60) + stream.Read(buffer, 0, 1); + Assert.Equal(60, buffer[0]); + + // Seek backward into segment 1 + stream.Seek(-4, SeekOrigin.Current); // Now at position 2 (byte 30) + stream.Read(buffer, 0, 1); + Assert.Equal(30, buffer[0]); + } + + [Fact] + public void Position_MultiSegmentSequence_TracksCorrectly() + { + // Arrange: [1,2] -> [3,4] -> [5,6] + var segment1 = new TestSegment(new byte[] { 1, 2 }); + var segment2 = segment1.Append(new byte[] { 3, 4 }); + var segment3 = segment2.Append(new byte[] { 5, 6 }); + + var sequence = new ReadOnlySequence(segment1, 0, segment3, 2); + var stream = new ReadOnlySequenceStream(sequence); + + byte[] buffer = new byte[1]; + + // Act & Assert: Position advances correctly through segments + Assert.Equal(0, stream.Position); + + stream.Read(buffer, 0, 1); // Read from segment 1 + Assert.Equal(1, stream.Position); + + stream.Read(buffer, 0, 1); // Read from segment 1 + Assert.Equal(2, stream.Position); + + stream.Read(buffer, 0, 1); // Read from segment 2 (boundary cross) + Assert.Equal(3, stream.Position); + + stream.Read(buffer, 0, 1); // Read from segment 2 + Assert.Equal(4, stream.Position); + + stream.Read(buffer, 0, 1); // Read from segment 3 (boundary cross) + Assert.Equal(5, stream.Position); + + stream.Read(buffer, 0, 1); // Read from segment 3 + Assert.Equal(6, stream.Position); + } + + /// + /// Helper class for creating multi-segment ReadOnlySequence{byte} for testing. + /// + private class TestSegment : ReadOnlySequenceSegment + { + public TestSegment(byte[] data) + { + Memory = data; + } + + public TestSegment Append(byte[] data) + { + var segment = new TestSegment(data) + { + RunningIndex = RunningIndex + Memory.Length + }; + Next = segment; + return segment; + } + } + + // Basic edge cases + [Fact] + public void Read_ZeroBytes_ReturnsZero() + { + var data = new byte[] { 1, 2, 3 }; + var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); + byte[] buffer = new byte[10]; + + int bytesRead = stream.Read(buffer, 0, 0); + + Assert.Equal(0, bytesRead); + Assert.Equal(0, stream.Position); // Position shouldn't change + } + + [Fact] + public void EmptySequence_BehavesCorrectly() + { + var stream = new ReadOnlySequenceStream(ReadOnlySequence.Empty); + + Assert.Equal(0, stream.Length); + Assert.Equal(0, stream.Position); + + byte[] buffer = new byte[10]; + int bytesRead = stream.Read(buffer, 0, 10); + Assert.Equal(0, bytesRead); + + // Seek to position 0 should succeed + stream.Seek(0, SeekOrigin.Begin); + Assert.Equal(0, stream.Position); + + // Seeking beyond empty buffer is allowed + long newPosition = stream.Seek(1, SeekOrigin.Begin); + Assert.Equal(1, newPosition); + Assert.Equal(1, stream.Position); + } + + [Fact] + public async Task ReadAsync_SameResultSize_ReusesCachedTask() + { + var data = new byte[20]; + for (int i = 0; i < 20; i++) data[i] = (byte)i; + var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); + + byte[] buffer1 = new byte[5]; + byte[] buffer2 = new byte[5]; + byte[] buffer3 = new byte[5]; + + Task task1 = stream.ReadAsync(buffer1, 0, 5); + Task task2 = stream.ReadAsync(buffer2, 0, 5); + Task task3 = stream.ReadAsync(buffer3, 0, 5); + + await task1; + await task2; + await task3; + + Assert.Same(task1, task2); + Assert.Same(task2, task3); + + Assert.Equal(new byte[] { 0, 1, 2, 3, 4 }, buffer1); + Assert.Equal(new byte[] { 5, 6, 7, 8, 9 }, buffer2); + Assert.Equal(new byte[] { 10, 11, 12, 13, 14 }, buffer3); + } + + [Fact] + public async Task ReadAsync_DifferentResultSize_CreatesNewTask() + { + var data = new byte[10]; + for (int i = 0; i < 10; i++) data[i] = (byte)i; + var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); + + byte[] buffer1 = new byte[5]; + byte[] buffer2 = new byte[3]; + byte[] buffer3 = new byte[2]; + + Task task1 = stream.ReadAsync(buffer1, 0, 5); // Returns 5 + Task task2 = stream.ReadAsync(buffer2, 0, 3); // Returns 3 + Task task3 = stream.ReadAsync(buffer3, 0, 2); // Returns 2 + + await task1; + await task2; + await task3; + + Assert.NotSame(task1, task2); + Assert.NotSame(task2, task3); + } + + [Fact] + public async Task ReadAsync_ArrayBackedMemory_UsesFastPath() + { + var data = new byte[] { 10, 20, 30, 40, 50 }; + var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); + + byte[] arrayBuffer = new byte[3]; + Memory memory = arrayBuffer.AsMemory(); + int bytesRead = await stream.ReadAsync(memory); + + Assert.Equal(3, bytesRead); + Assert.Equal(new byte[] { 10, 20, 30 }, arrayBuffer); + } + } +} diff --git a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj index 2de051c81fbdba..f529355a9cdf60 100644 --- a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj @@ -1,4 +1,4 @@ - + true true @@ -132,6 +132,8 @@ + + @@ -287,4 +289,7 @@ + + + diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index cf37982fbe434e..d51cbb5e5fba60 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3068,6 +3068,9 @@ This operation is invalid on overlapping buffers. + + Stream resynchronization exceeded maximum iterations. + The operation cannot be performed when TimeProvider.LocalTimeZone is null. diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 096397e4998e80..0daf4942b3e76b 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -529,6 +529,9 @@ + + + @@ -2978,4 +2981,4 @@ - + \ No newline at end of file diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000000000..8736ab6a072657 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -0,0 +1,205 @@ +// 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; + +namespace System.IO; + +/// +/// Provides a seekable, read-only over a . +/// +/// +/// This type is not thread-safe. Synchronize access if the stream is used concurrently. +/// The stream cannot be written to. always returns . +/// +public sealed class ReadOnlyMemoryStream : Stream +{ + private ReadOnlyMemory _buffer; + private int _position; + private bool _isOpen; + + /// + /// Initializes a new instance of the class over the specified . + /// + /// The to wrap. + public ReadOnlyMemoryStream(ReadOnlyMemory source) + { + _buffer = source; + _isOpen = true; + } + + /// + public override bool CanRead => _isOpen; + + /// + public override bool CanSeek => _isOpen; + + /// + public override bool CanWrite => false; + + /// + public override long Length + { + get + { + EnsureNotClosed(); + + return _buffer.Length; + } + } + + /// + public override long Position + { + get + { + EnsureNotClosed(); + + return _position; + } + set + { + EnsureNotClosed(); + ArgumentOutOfRangeException.ThrowIfNegative(value); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue); + _position = (int)value; + } + } + + /// + public override int ReadByte() + { + EnsureNotClosed(); + + if (_position >= _buffer.Length) + return -1; + + return _buffer.Span[_position++]; + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + ValidateBufferArguments(buffer, offset, count); + + return Read(new Span(buffer, offset, count)); + } + + /// + public override int Read(Span buffer) + { + EnsureNotClosed(); + + int remaining = _buffer.Length - _position; + if (remaining <= 0 || buffer.Length == 0) + return 0; + + int bytesToRead = Math.Min(remaining, buffer.Length); + _buffer.Span.Slice(_position, bytesToRead).CopyTo(buffer); + _position += bytesToRead; + + return bytesToRead; + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBufferArguments(buffer, offset, count); + + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + return Task.FromResult(Read(buffer, offset, count)); + } + + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + return ValueTask.FromCanceled(cancellationToken); + + return new ValueTask(Read(buffer.Span)); + } + + /// + public override void CopyTo(Stream destination, int bufferSize) + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + + if (_buffer.Length > _position) + { + destination.Write(_buffer.Span.Slice(_position)); + _position = _buffer.Length; + } + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + + if (_buffer.Length > _position) + { + ReadOnlyMemory content = _buffer.Slice(_position); + _position = _buffer.Length; + + return destination.WriteAsync(content, cancellationToken).AsTask(); + } + + return Task.CompletedTask; + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + EnsureNotClosed(); + + long newPosition = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _buffer.Length + offset, + _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin) + }; + + if (newPosition < 0) + throw new IOException(SR.IO_SeekBeforeBegin); + + ArgumentOutOfRangeException.ThrowIfGreaterThan(newPosition, int.MaxValue, nameof(offset)); + + _position = (int)newPosition; + + return newPosition; + } + + /// + public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override void Flush() { } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + + /// + protected override void Dispose(bool disposing) + { + _isOpen = false; + base.Dispose(disposing); + } + + private void EnsureNotClosed() + { + ObjectDisposedException.ThrowIf(!_isOpen, this); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs new file mode 100644 index 00000000000000..887aea3f5edbde --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs @@ -0,0 +1,162 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO; + +/// +/// Provides a read-only, non-seekable that encodes a or +/// into bytes on-the-fly using a specified . +/// +/// +/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. +/// This type is not thread-safe. Synchronize access if the stream is used concurrently. +/// +public sealed class StringStream : Stream +{ + private readonly ReadOnlyMemory _text; + private readonly Encoder _encoder; + private readonly Encoding _encoding; + private int _charPosition; + private bool _disposed; + + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + /// The string to read from. + /// The encoding to use when converting the string to bytes. + /// or is . + public StringStream(string text, Encoding encoding) + { + ArgumentNullException.ThrowIfNull(text); + ArgumentNullException.ThrowIfNull(encoding); + + _text = text.AsMemory(); + _encoding = encoding; + _encoder = encoding.GetEncoder(); + } + + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + /// The character memory to read from. + /// The encoding to use when converting the characters to bytes. + /// is . + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + ArgumentNullException.ThrowIfNull(encoding); + + _text = text; + _encoding = encoding; + _encoder = encoding.GetEncoder(); + } + + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => _encoding; + + /// + public override bool CanRead => !_disposed; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => false; + + /// + public override long Length => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + + /// + public override long Position + { + get => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + set => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + ValidateBufferArguments(buffer, offset, count); + + return Read(new Span(buffer, offset, count)); + } + + /// + public override int Read(Span buffer) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (buffer.Length == 0 || _charPosition >= _text.Length) + { + return 0; + } + + ReadOnlySpan remaining = _text.Span.Slice(_charPosition); + bool flush = true; + + _encoder.Convert(remaining, buffer, flush, out int charsUsed, out int bytesUsed, out _); + _charPosition += charsUsed; + + return bytesUsed; + } + + /// + public override int ReadByte() + { + Span oneByte = stackalloc byte[1]; + int bytesRead = Read(oneByte); + + return bytesRead > 0 ? oneByte[0] : -1; + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBufferArguments(buffer, offset, count); + + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + return Task.FromResult(Read(buffer, offset, count)); + } + + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + return ValueTask.FromCanceled(cancellationToken); + + return new ValueTask(Read(buffer.Span)); + } + + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + + /// + public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override void Flush() { } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + + /// + protected override void Dispose(bool disposing) + { + _disposed = true; + base.Dispose(disposing); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs new file mode 100644 index 00000000000000..4cb9f6efbe059b --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -0,0 +1,237 @@ +// 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; + +namespace System.IO; + +/// +/// Provides a seekable, writable over a with fixed capacity. +/// +/// +/// The stream cannot expand beyond the initial memory capacity. +/// This type is not thread-safe. Synchronize access if the stream is used concurrently. +/// +public sealed class WritableMemoryStream : Stream +{ + private Memory _buffer; + private int _position; + private bool _isOpen; + + /// + /// Initializes a new instance of the class over the specified . + /// + /// The to wrap. + public WritableMemoryStream(Memory buffer) + { + _buffer = buffer; + _isOpen = true; + } + + /// + public override bool CanRead => _isOpen; + + /// + public override bool CanSeek => _isOpen; + + /// + public override bool CanWrite => _isOpen; + + /// + public override long Length + { + get + { + EnsureNotClosed(); + + return _buffer.Length; + } + } + + /// + public override long Position + { + get + { + EnsureNotClosed(); + + return _position; + } + set + { + EnsureNotClosed(); + ArgumentOutOfRangeException.ThrowIfNegative(value); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue); + _position = (int)value; + } + } + + /// + public override int ReadByte() + { + EnsureNotClosed(); + + if (_position >= _buffer.Length) + return -1; + + return _buffer.Span[_position++]; + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + ValidateBufferArguments(buffer, offset, count); + + return Read(new Span(buffer, offset, count)); + } + + /// + public override int Read(Span buffer) + { + EnsureNotClosed(); + + int remaining = _buffer.Length - _position; + if (remaining <= 0 || buffer.Length == 0) + return 0; + + int bytesToRead = Math.Min(remaining, buffer.Length); + ((ReadOnlyMemory)_buffer).Span.Slice(_position, bytesToRead).CopyTo(buffer); + _position += bytesToRead; + + return bytesToRead; + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBufferArguments(buffer, offset, count); + + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + return Task.FromResult(Read(buffer, offset, count)); + } + + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + return ValueTask.FromCanceled(cancellationToken); + + return new ValueTask(Read(buffer.Span)); + } + + /// + public override void WriteByte(byte value) + { + EnsureNotClosed(); + + if (_position >= _buffer.Length) + throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + + _buffer.Span[_position++] = value; + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + ValidateBufferArguments(buffer, offset, count); + Write(new ReadOnlySpan(buffer, offset, count)); + } + + /// + public override void Write(ReadOnlySpan buffer) + { + EnsureNotClosed(); + + if (_position > _buffer.Length - buffer.Length) + throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + + buffer.CopyTo(_buffer.Span.Slice(_position)); + _position += buffer.Length; + } + + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBufferArguments(buffer, offset, count); + + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + try + { + Write(buffer, offset, count); + + return Task.CompletedTask; + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + return ValueTask.FromCanceled(cancellationToken); + + try + { + Write(buffer.Span); + + return default; + } + catch (Exception exception) + { + return ValueTask.FromException(exception); + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + EnsureNotClosed(); + + long newPosition = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _buffer.Length + offset, + _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin) + }; + + if (newPosition < 0) + throw new IOException(SR.IO_SeekBeforeBegin); + + ArgumentOutOfRangeException.ThrowIfGreaterThan(newPosition, int.MaxValue, nameof(offset)); + + _position = (int)newPosition; + + return newPosition; + } + + /// + public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + + /// + public override void Flush() { } + + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + + /// + protected override void Dispose(bool disposing) + { + _isOpen = false; + base.Dispose(disposing); + } + + private void EnsureNotClosed() + { + ObjectDisposedException.ThrowIf(!_isOpen, this); + } +} diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs index 94dc86ace2400d..f431d1b4119dac 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs @@ -30,7 +30,7 @@ public JsonXmlDataContract(XmlDataContract traditionalXmlDataContract) DataContractSerializer dataContractSerializer = new DataContractSerializer(TraditionalDataContract.UnderlyingType, GetKnownTypesFromContext(context, context?.SerializerKnownTypeList), 1, false, false); // maxItemsInObjectGraph // ignoreExtensionDataObject // preserveObjectReferences - MemoryStream memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlContent)); + Stream memoryStream = new StringStream(xmlContent, Encoding.UTF8); object? xmlValue; XmlDictionaryReaderQuotas? quotas = ((JsonReaderDelegator)jsonReader).ReaderQuotas; if (quotas == null) diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs b/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs index 1756485ad05b12..7a834012c9ca3d 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs @@ -103,7 +103,7 @@ internal StringData(string str) internal override Stream AsStream() { - return new MemoryStream(Encoding.Unicode.GetBytes(_str)); + return new StringStream(_str, Encoding.Unicode); } internal override TextReader AsTextReader() diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 867cc434767ac1..bebef41cb1fe83 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -10904,7 +10904,7 @@ public void ReadExactly(System.Span buffer) { } public System.Threading.Tasks.ValueTask ReadExactlyAsync(System.Memory 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; } + public static Stream Synchronized(Stream stream) { throw null; } protected static void ValidateBufferArguments(byte[] buffer, int offset, int count) { } protected static void ValidateCopyToArguments(System.IO.Stream destination, int bufferSize) { } public abstract void Write(byte[] buffer, int offset, int count); @@ -11023,6 +11023,60 @@ protected override void Dispose(bool disposing) { } public override System.Threading.Tasks.Task ReadToEndAsync() { throw null; } public override System.Threading.Tasks.Task ReadToEndAsync(System.Threading.CancellationToken cancellationToken) { throw null; } } + public sealed partial class StringStream : System.IO.Stream + { + public StringStream(string text, System.Text.Encoding encoding) { } + public StringStream(System.ReadOnlyMemory text, System.Text.Encoding encoding) { } + public System.Text.Encoding Encoding { get { throw null; } } + public override bool CanRead { get { throw null; } } + public override bool CanSeek { get { throw null; } } + public override bool CanWrite { get { throw null; } } + public override long Length { get { throw null; } } + public override long Position { get { throw null; } set { } } + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) { throw null; } + public override int Read(System.Span buffer) { throw null; } + public override int ReadByte() { throw null; } + public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; } + public override void SetLength(long value) { } + public override void Write(byte[] buffer, int offset, int count) { } + } + public sealed partial class ReadOnlyMemoryStream : System.IO.Stream + { + public ReadOnlyMemoryStream(System.ReadOnlyMemory source) { } + public override bool CanRead { get { throw null; } } + public override bool CanSeek { get { throw null; } } + public override bool CanWrite { get { throw null; } } + public override long Length { get { throw null; } } + public override long Position { get { throw null; } set { } } + public override void CopyTo(System.IO.Stream destination, int bufferSize) { } + public override System.Threading.Tasks.Task CopyToAsync(System.IO.Stream destination, int bufferSize, System.Threading.CancellationToken cancellationToken) { throw null; } + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) { throw null; } + public override int Read(System.Span buffer) { throw null; } + public override int ReadByte() { throw null; } + public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; } + public override void SetLength(long value) { } + public override void Write(byte[] buffer, int offset, int count) { } + } + public sealed partial class WritableMemoryStream : System.IO.Stream + { + public WritableMemoryStream(System.Memory buffer) { } + public override bool CanRead { get { throw null; } } + public override bool CanSeek { get { throw null; } } + public override bool CanWrite { get { throw null; } } + public override long Length { get { throw null; } } + public override long Position { get { throw null; } set { } } + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) { throw null; } + public override int Read(System.Span buffer) { throw null; } + public override int ReadByte() { throw null; } + public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; } + public override void SetLength(long value) { } + public override void Write(byte[] buffer, int offset, int count) { } + public override void Write(System.ReadOnlySpan buffer) { } + public override void WriteByte(byte value) { } + } public partial class StringWriter : System.IO.TextWriter { public StringWriter() { } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs new file mode 100644 index 00000000000000..389e10f1848d00 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs @@ -0,0 +1,39 @@ +// 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.Tasks; + +namespace System.IO.Tests +{ + /// + /// Conformance tests for ReadOnlyMemoryStream - a read-only, seekable stream + /// over a ReadOnlyMemory<byte>. + /// + public class ReadOnlyMemoryStreamConformanceTests : StandaloneStreamConformanceTests + { + protected override bool CanSeek => true; + protected override bool CanSetLength => false; // Immutable stream + protected override bool NopFlushCompletesSynchronously => true; + + /// + /// Creates a read-only ReadOnlyMemoryStream with provided initial data. + /// + protected override Task CreateReadOnlyStreamCore(byte[]? initialData) + { + if (initialData == null || initialData.Length == 0) + { + // Empty data + return Task.FromResult(new ReadOnlyMemoryStream(ReadOnlyMemory.Empty)); + } + + var data = new ReadOnlyMemory(initialData); + return Task.FromResult(new ReadOnlyMemoryStream(data)); + } + + // Write only stream - no write support + protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); + + // Read only stream - no read/write support + protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs new file mode 100644 index 00000000000000..6727e800c0c76b --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs @@ -0,0 +1,312 @@ +// 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.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + /// + /// Additional specific tests for ReadOnlyMemoryStream beyond conformance tests. + /// + public class ReadOnlyMemoryStreamTests + { + [Fact] + public void Constructor_DefaultParameters_CreatesPubliclyVisibleStream() + { + byte[] buffer = new byte[100]; + Stream stream = new ReadOnlyMemoryStream(new ReadOnlyMemory(buffer)); + + Assert.True(stream.CanRead); + Assert.False(stream.CanWrite); + Assert.True(stream.CanSeek); + Assert.Equal(100, stream.Length); + Assert.Equal(0, stream.Position); + } + + // Empty ReadOnlyMemory creates valid zero-length stream. + [Fact] + public void Constructor_EmptyMemory_CreatesZeroLengthStream() + { + ReadOnlyMemory emptyMemory = ReadOnlyMemory.Empty; + Stream stream = new ReadOnlyMemoryStream(emptyMemory); + + Assert.Equal(0, stream.Length); + Assert.Equal(0, stream.Position); + Assert.True(stream.CanRead); + Assert.False(stream.CanWrite); + } + + [Fact] + public void Constructor_FromMemory_WorksCorrectly() + { + byte[] buffer = { 1, 2, 3, 4, 5 }; + Memory memory = buffer; + Stream stream = new ReadOnlyMemoryStream(memory); // Implicit conversion + + Assert.Equal(5, stream.Length); + Assert.True(stream.CanRead); + } + + // Not covered in conformance tests: ReadOnlyMemory slices stream handling + [Fact] + public void Stream_WorksWithSlicedMemory() + { + byte[] largeBuffer = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + ReadOnlyMemory slice = largeBuffer.AsMemory(3, 4); // [3, 4, 5, 6] + Stream stream = new ReadOnlyMemoryStream(slice); + + Assert.Equal(4, stream.Length); + + byte[] result = new byte[4]; + int bytesRead = stream.Read(result, 0, 4); + + Assert.Equal(4, bytesRead); + Assert.Equal(new byte[] { 3, 4, 5, 6 }, result); + } + + [Fact] + public void Position_AdvancesDuringRead() + { + byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + Stream stream = new ReadOnlyMemoryStream(buffer); + byte[] readBuffer = new byte[3]; + + Assert.Equal(0, stream.Position); + + stream.Read(readBuffer, 0, 3); + Assert.Equal(3, stream.Position); + + stream.Read(readBuffer, 0, 3); + Assert.Equal(6, stream.Position); + + stream.Read(readBuffer, 0, 3); + Assert.Equal(9, stream.Position); + } + + [Fact] + public void Seek_FromCurrent_RelativeOffset() + { + Stream stream = new ReadOnlyMemoryStream(new byte[100]); + stream.Position = 50; + + // Seek forward 10 bytes + long newPosition = stream.Seek(10, SeekOrigin.Current); + Assert.Equal(60, newPosition); + + // Seek backward 20 bytes + newPosition = stream.Seek(-20, SeekOrigin.Current); + Assert.Equal(40, newPosition); + } + + [Fact] + public void Seek_InvalidOrigin_ThrowsArgumentException() + { + Stream stream = new ReadOnlyMemoryStream(new byte[100]); + + Assert.Throws(() => stream.Seek(0, (SeekOrigin)999)); + } + + [Fact] + public void Read_ReturnsCorrectData() + { + byte[] data = { 10, 20, 30, 40, 50 }; + Stream stream = new ReadOnlyMemoryStream(data); + byte[] buffer = new byte[3]; + + int bytesRead = stream.Read(buffer, 0, 3); + + Assert.Equal(3, bytesRead); + Assert.Equal(new byte[] { 10, 20, 30 }, buffer); + Assert.Equal(3, stream.Position); + } + + [Fact] + public void Read_LargerThanAvailable_ReturnsPartialData() + { + byte[] data = { 1, 2, 3 }; + Stream stream = new ReadOnlyMemoryStream(data); + byte[] buffer = new byte[10]; + + int bytesRead = stream.Read(buffer, 0, 10); + + Assert.Equal(3, bytesRead); + Assert.Equal(new byte[] { 1, 2, 3 }, buffer[..3]); + } + + [Fact] + public void Read_AfterSeek_ReturnsCorrectData() + { + byte[] data = { 10, 20, 30, 40, 50 }; + Stream stream = new ReadOnlyMemoryStream(data); + + stream.Seek(2, SeekOrigin.Begin); + byte[] buffer = new byte[2]; + int bytesRead = stream.Read(buffer, 0, 2); + + Assert.Equal(2, bytesRead); + Assert.Equal(new byte[] { 30, 40 }, buffer); + } + + [Fact] + public void Read_DoesNotModifyUnderlyingMemory() + { + byte[] originalData = { 1, 2, 3, 4, 5 }; + byte[] dataCopy = (byte[])originalData.Clone(); + Stream stream = new ReadOnlyMemoryStream(originalData); + + byte[] buffer = new byte[5]; + stream.Read(buffer, 0, 5); + + // Original data should be unchanged + Assert.Equal(dataCopy, originalData); + } + + [Fact] + public void Write_ThrowsNotSupportedException() + { + Stream stream = new ReadOnlyMemoryStream(new ReadOnlyMemory(new byte[10])); + byte[] data = { 1, 2, 3 }; + + Assert.Throws(() => stream.Write(data, 0, 3)); + } + + [Fact] + public void SetLength_ThrowsNotSupportedException() + { + Stream stream = new ReadOnlyMemoryStream(new byte[10]); + Assert.Throws(() => stream.SetLength(20)); + } + + [Fact] + public void Dispose_SetsCanPropertiesToFalse() + { + Stream stream = new ReadOnlyMemoryStream(new byte[10]); + + stream.Dispose(); + + Assert.False(stream.CanRead); + Assert.False(stream.CanSeek); + Assert.False(stream.CanWrite); + } + + [Fact] + public void Operations_AfterDispose_ThrowObjectDisposedException() + { + byte[] buffer = new byte[10]; + Stream stream = new ReadOnlyMemoryStream(buffer); + stream.Dispose(); + + Assert.Throws(() => stream.Read(new byte[5], 0, 5)); + Assert.Throws(() => stream.ReadByte()); + Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); + Assert.Throws(() => _ = stream.Position); + Assert.Throws(() => stream.Position = 0); + Assert.Throws(() => _ = stream.Length); + } + + // Standard IDisposable pattern - Dispose() should be idempotent. + [Fact] + public void Dispose_MultipleCalls_DoesNotThrow() + { + Stream stream = new ReadOnlyMemoryStream(new byte[10]); + + stream.Dispose(); + stream.Dispose(); // Should not throw + stream.Dispose(); // Should not throw + } + + [Fact] + public void Read_NullBuffer_ThrowsArgumentNullException() + { + Stream stream = new ReadOnlyMemoryStream(new byte[10]); + + Assert.Throws(() => stream.Read(null!, 0, 5)); + } + + [Fact] + public void EmptyBuffer_BehavesCorrectly() + { + Stream stream = new ReadOnlyMemoryStream(ReadOnlyMemory.Empty); + + Assert.Equal(0, stream.Length); + Assert.Equal(0, stream.Position); + + byte[] buffer = new byte[10]; + Assert.Equal(0, stream.Read(buffer, 0, 10)); + + stream.Seek(0, SeekOrigin.Begin); + Assert.Equal(0, stream.Position); + + // Seeking beyond empty buffer is allowed + long newPosition = stream.Seek(1, SeekOrigin.Begin); + Assert.Equal(1, newPosition); + Assert.Equal(1, stream.Position); + } + + [Fact] + public async Task ReadAsync_SameResultSize_ReusesCachedTask() + { + byte[] data = new byte[20]; + for (int i = 0; i < 20; i++) data[i] = (byte)i; + Stream stream = new ReadOnlyMemoryStream(data); + + byte[] buffer1 = new byte[5]; + byte[] buffer2 = new byte[5]; + byte[] buffer3 = new byte[5]; + + Task task1 = stream.ReadAsync(buffer1, 0, 5); + Task task2 = stream.ReadAsync(buffer2, 0, 5); + Task task3 = stream.ReadAsync(buffer3, 0, 5); + + await task1; + await task2; + await task3; + + Assert.Same(task1, task2); + Assert.Same(task2, task3); + + Assert.Equal(new byte[] { 0, 1, 2, 3, 4 }, buffer1); + Assert.Equal(new byte[] { 5, 6, 7, 8, 9 }, buffer2); + Assert.Equal(new byte[] { 10, 11, 12, 13, 14 }, buffer3); + } + + [Fact] + public async Task ReadAsync_DifferentResultSize_CreatesNewTask() + { + byte[] data = new byte[10]; + for (int i = 0; i < 10; i++) data[i] = (byte)i; + Stream stream = new ReadOnlyMemoryStream(data); + + byte[] buffer1 = new byte[5]; + byte[] buffer2 = new byte[3]; + byte[] buffer3 = new byte[2]; + + Task task1 = stream.ReadAsync(buffer1, 0, 5); // Returns 5 + Task task2 = stream.ReadAsync(buffer2, 0, 3); // Returns 3 + Task task3 = stream.ReadAsync(buffer3, 0, 2); // Returns 2 + + await task1; + await task2; + await task3; + + Assert.NotSame(task1, task2); + Assert.NotSame(task2, task3); + } + + [Fact] + public async Task ReadAsync_ArrayBackedMemory_UsesFastPath() + { + byte[] data = { 10, 20, 30, 40, 50 }; + Stream stream = new ReadOnlyMemoryStream(data); + + byte[] arrayBuffer = new byte[3]; + Memory memory = arrayBuffer.AsMemory(); + + int bytesRead = await stream.ReadAsync(memory); + + Assert.Equal(3, bytesRead); + Assert.Equal(new byte[] { 10, 20, 30 }, arrayBuffer); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs new file mode 100644 index 00000000000000..45afe263b65323 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Threading.Tasks; + +namespace System.IO.Tests +{ + /// + /// Conformance tests for StringStream using the ReadOnlyMemory{char} overload. + /// + public class StringStreamConformanceTests_Memory : StandaloneStreamConformanceTests + { + protected override bool CanSeek => false; + protected override bool CanSetLength => false; + protected override bool NopFlushCompletesSynchronously => true; + + protected override Task CreateReadOnlyStreamCore(byte[]? initialData) + { + if (initialData is null || initialData.Length == 0) + { + return Task.FromResult(new StringStream(ReadOnlyMemory.Empty, Encoding.UTF8)); + } + + string sourceString = Encoding.UTF8.GetString(initialData); + + byte[] reencoded = Encoding.UTF8.GetBytes(sourceString); + if (reencoded.Length != initialData.Length || !reencoded.AsSpan().SequenceEqual(initialData)) + { + return Task.FromResult(null); + } + + return Task.FromResult(new StringStream(sourceString.AsMemory(), Encoding.UTF8)); + } + + protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); + + protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); + } + + /// + /// Conformance tests for StringStream using the string overload. + /// + public class StringStreamConformanceTests_String : StandaloneStreamConformanceTests + { + protected override bool CanSeek => false; + protected override bool CanSetLength => false; + protected override bool NopFlushCompletesSynchronously => true; + + protected override Task CreateReadOnlyStreamCore(byte[]? initialData) + { + if (initialData is null || initialData.Length == 0) + { + return Task.FromResult(new StringStream("", Encoding.UTF8)); + } + + string sourceString = Encoding.UTF8.GetString(initialData); + + byte[] reencoded = Encoding.UTF8.GetBytes(sourceString); + if (reencoded.Length != initialData.Length || !reencoded.AsSpan().SequenceEqual(initialData)) + { + return Task.FromResult(null); + } + + return Task.FromResult(new StringStream(sourceString, Encoding.UTF8)); + } + + protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); + + protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs new file mode 100644 index 00000000000000..44382ef648d7ef --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs @@ -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.Text; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + /// + /// Additional specific tests for StringStream with ReadOnlyMemory{char} beyond conformance tests. + /// + public class StringStreamTests_Memory + { + [Fact] + public void Constructor_DefaultEncoding_UsesUTF8() + { + var chars = "test".AsMemory(); + var stream = new StringStream(chars, Encoding.UTF8); + + Assert.True(stream.CanRead); + Assert.False(stream.CanSeek); + Assert.False(stream.CanWrite); + } + + [Fact] + public void Constructor_ExplicitEncoding_UsesSpecifiedEncoding() + { + var chars = "test".AsMemory(); + var stream = new StringStream(chars, Encoding.UTF32); + + Assert.True(stream.CanRead); + } + + [Fact] + public void Constructor_EmptyMemory_CreatesValidStream() + { + var emptyMemory = ReadOnlyMemory.Empty; + var stream = new StringStream(emptyMemory, Encoding.UTF8); + + Assert.True(stream.CanRead); + + byte[] buffer = new byte[10]; + int bytesRead = stream.Read(buffer, 0, 10); + Assert.Equal(0, bytesRead); + } + + [Theory] + [InlineData("ASCII text")] + [InlineData("Ñoño español")] + [InlineData("Emoji: 😀🎉")] + public async Task WorksWithDifferentEncodings(string input) + { + var encodings = new[] { Encoding.UTF8, Encoding.Unicode, Encoding.UTF32 }; + + foreach (var encoding in encodings) + { + byte[] expectedBytes = encoding.GetBytes(input); + var chars = input.AsMemory(); + var stream = new StringStream(chars, encoding); + + byte[] actualBytes = new byte[expectedBytes.Length * 2]; + int totalRead = 0; + int bytesRead; + + while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) + { + totalRead += bytesRead; + } + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + } + + [Fact] + public async Task WorksWithMemorySlice() + { + string largeString = "0123456789ABCDEFGHIJ"; + var fullMemory = largeString.AsMemory(); + var slice = fullMemory.Slice(5, 10); + + byte[] expectedBytes = Encoding.UTF8.GetBytes("56789ABCDE"); + var stream = new StringStream(slice, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length + 10]; + int totalRead = 0; + int bytesRead; + + while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) + { + totalRead += bytesRead; + } + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Fact] + public async Task WorksWithCharArray() + { + char[] charArray = { 'H', 'e', 'l', 'l', 'o' }; + var memory = new ReadOnlyMemory(charArray); + + byte[] expectedBytes = Encoding.UTF8.GetBytes("Hello"); + var stream = new StringStream(memory, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length + 10]; + int totalRead = 0; + int bytesRead; + + while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) + { + totalRead += bytesRead; + } + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Fact] + public async Task MultipleSlicesIndependent() + { + string source = "ABCDEFGHIJKLMNOP"; + var slice1 = source.AsMemory(0, 5); + var slice2 = source.AsMemory(5, 5); + var slice3 = source.AsMemory(10, 6); + + var stream1 = new StringStream(slice1, Encoding.UTF8); + var stream2 = new StringStream(slice2, Encoding.UTF8); + var stream3 = new StringStream(slice3, Encoding.UTF8); + + byte[] result1 = new byte[10]; + byte[] result2 = new byte[10]; + byte[] result3 = new byte[10]; + + int read1 = await stream1.ReadAsync(result1); + int read2 = await stream2.ReadAsync(result2); + int read3 = await stream3.ReadAsync(result3); + + Assert.Equal("ABCDE", Encoding.UTF8.GetString(result1, 0, read1)); + Assert.Equal("FGHIJ", Encoding.UTF8.GetString(result2, 0, read2)); + Assert.Equal("KLMNOP", Encoding.UTF8.GetString(result3, 0, read3)); + } + + [Fact] + public async Task HandlesSurrogatePairs() + { + string input = "😀😁😂🤣😃😄"; + var chars = input.AsMemory(); + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + var stream = new StringStream(chars, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length]; + int totalRead = 0; + int bytesRead; + + while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) + { + totalRead += bytesRead; + } + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Fact] + public async Task MultiByteCharactersAcrossChunkBoundary() + { + string input = new string('A', 1023) + "你"; + var chars = input.AsMemory(); + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + var stream = new StringStream(chars, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length]; + int totalRead = 0; + int bytesRead; + + while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) + { + totalRead += bytesRead; + } + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Fact] + public void LengthThrowsNotSupportedException() + { + var chars = "test".AsMemory(); + var stream = new StringStream(chars, Encoding.UTF8); + + Assert.Throws(() => stream.Length); + } + + [Fact] + public void PositionGetThrowsNotSupportedException() + { + var chars = "test".AsMemory(); + var stream = new StringStream(chars, Encoding.UTF8); + + Assert.Throws(() => stream.Position); + } + + [Fact] + public void PositionSetThrowsNotSupportedException() + { + var chars = "test".AsMemory(); + var stream = new StringStream(chars, Encoding.UTF8); + + Assert.Throws(() => stream.Position = 0); + } + + [Fact] + public void SeekThrowsNotSupportedException() + { + var chars = "test".AsMemory(); + var stream = new StringStream(chars, Encoding.UTF8); + + Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); + } + + [Fact] + public void WriteThrowsNotSupportedException() + { + var chars = "test".AsMemory(); + var stream = new StringStream(chars, Encoding.UTF8); + + Assert.Throws(() => stream.Write(new byte[1], 0, 1)); + } + + [Fact] + public void SetLengthThrowsNotSupportedException() + { + var chars = "test".AsMemory(); + var stream = new StringStream(chars, Encoding.UTF8); + + Assert.Throws(() => stream.SetLength(100)); + } + + [Fact] + public void CanReadFalseAfterDispose() + { + var chars = "test".AsMemory(); + var stream = new StringStream(chars, Encoding.UTF8); + + stream.Dispose(); + + Assert.False(stream.CanRead); + } + + [Fact] + public void ReadAfterDispose_ThrowsObjectDisposedException() + { + var chars = "test".AsMemory(); + var stream = new StringStream(chars, Encoding.UTF8); + stream.Dispose(); + + byte[] buffer = new byte[10]; + Assert.Throws(() => stream.Read(buffer, 0, 10)); + } + + [Fact] + public void MultipleDispose_DoesNotThrow() + { + var chars = "test".AsMemory(); + var stream = new StringStream(chars, Encoding.UTF8); + + stream.Dispose(); + stream.Dispose(); + stream.Dispose(); + } + + [Theory] + [InlineData("Hello")] + [InlineData("Unicode: 你好")] + [InlineData("Emoji: 😀")] + public async Task ProducesSameOutputAsStringOverload(string input) + { + var memoryStream = new StringStream(input.AsMemory(), Encoding.UTF8); + var stringStream = new StringStream(input, Encoding.UTF8); + + byte[] memoryResult = new byte[1000]; + byte[] stringResult = new byte[1000]; + + int memoryBytesRead = await memoryStream.ReadAsync(memoryResult); + int stringBytesRead = await stringStream.ReadAsync(stringResult); + + Assert.Equal(stringBytesRead, memoryBytesRead); + Assert.Equal( + stringResult.AsSpan(0, stringBytesRead).ToArray(), + memoryResult.AsSpan(0, memoryBytesRead).ToArray() + ); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs new file mode 100644 index 00000000000000..3ae8d1c1c34f4f --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs @@ -0,0 +1,223 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + /// + /// Additional specific tests for StringStream with string beyond conformance tests. + /// + public class StringStreamTests_String + { + [Theory] + [InlineData("Hello, World! ")] + [InlineData("Unicode: 你好世界 🌍")] + [InlineData("Multi\nLine\r\nText")] + public async Task ReadsCorrectBytesForDifferentStrings(string input) + { + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + var stream = new StringStream(input, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length + 100]; + int totalRead = 0; + int bytesRead; + while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) + { + totalRead += bytesRead; + } + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Theory] + [InlineData("ASCII text")] + [InlineData("Ñoño español")] + public async Task WorksWithDifferentEncodings(string input) + { + var encodings = new[] { Encoding.UTF8, Encoding.Unicode, Encoding.UTF32 }; + + foreach (var encoding in encodings) + { + byte[] expectedBytes = encoding.GetBytes(input); + var stream = new StringStream(input, encoding); + + byte[] actualBytes = new byte[expectedBytes.Length * 2]; + int totalRead = 0; + int bytesRead; + + while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) + { + totalRead += bytesRead; + } + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + } + + [Fact] + public void ThrowsOnNullString() + { + Assert.Throws(() => new StringStream((string)null!, Encoding.UTF8)); + } + + [Fact] + public void ThrowsOnNullEncoding() + { + Assert.Throws(() => new StringStream("test", null!)); + } + + [Fact] + public void CanReadPropertyReturnsTrue() + { + var stream = new StringStream("test", Encoding.UTF8); + Assert.True(stream.CanRead); + } + + [Fact] + public void CanSeekPropertyReturnsFalse() + { + var stream = new StringStream("test", Encoding.UTF8); + Assert.False(stream.CanSeek); + } + + [Fact] + public void CanWritePropertyReturnsFalse() + { + var stream = new StringStream("test", Encoding.UTF8); + Assert.False(stream.CanWrite); + } + + [Fact] + public void EncodingPropertyReturnsCorrectEncoding() + { + var stream = new StringStream("test", Encoding.UTF32); + Assert.Equal(Encoding.UTF32, stream.Encoding); + } + + [Fact] + public void LengthThrowsNotSupportedException() + { + var stream = new StringStream("test", Encoding.UTF8); + Assert.Throws(() => stream.Length); + } + + [Fact] + public void PositionGetThrowsNotSupportedException() + { + var stream = new StringStream("test", Encoding.UTF8); + Assert.Throws(() => stream.Position); + } + + [Fact] + public void PositionSetThrowsNotSupportedException() + { + var stream = new StringStream("test", Encoding.UTF8); + Assert.Throws(() => stream.Position = 0); + } + + [Fact] + public void SeekThrowsNotSupportedException() + { + var stream = new StringStream("test", Encoding.UTF8); + Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); + } + + [Fact] + public void WriteThrowsNotSupportedException() + { + var stream = new StringStream("test", Encoding.UTF8); + Assert.Throws(() => stream.Write(new byte[1], 0, 1)); + } + + [Fact] + public void SetLengthThrowsNotSupportedException() + { + var stream = new StringStream("test", Encoding.UTF8); + Assert.Throws(() => stream.SetLength(100)); + } + + [Fact] + public async Task HandlesChunkedReading() + { + string largeString = new string('A', 10000); + byte[] expectedBytes = Encoding.UTF8.GetBytes(largeString); + var stream = new StringStream(largeString, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length]; + int totalRead = 0; + int chunkSize = 512; + + int bytesRead; + while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead, Math.Min(chunkSize, expectedBytes.Length - totalRead)))) > 0) + { + totalRead += bytesRead; + } + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes); + } + + [Fact] + public async Task ReadsWithExactBufferSizeMatch() + { + string input = new string('A', 4096); + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + var stream = new StringStream(input, Encoding.UTF8); + + byte[] buffer = new byte[4096]; + int bytesRead = await stream.ReadAsync(buffer); + + Assert.Equal(4096, bytesRead); + Assert.Equal(expectedBytes, buffer); + } + + [Fact] + public async Task MultipleReadsEventuallyReturnZero() + { + var stream = new StringStream("small", Encoding.UTF8); + byte[] buffer = new byte[100]; + + int bytesRead = await stream.ReadAsync(buffer); + Assert.Equal(5, bytesRead); + + int finalRead = await stream.ReadAsync(buffer); + Assert.Equal(0, finalRead); + } + + [Fact] + public async Task SequentialReadAsync_WithSmallChunks_ReadsEntireStream() + { + string input = new string('A', 5000); + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + var stream = new StringStream(input, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length]; + int totalBytesRead = 0; + int chunkSize = 128; + + int bytesRead; + while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalBytesRead, Math.Min(chunkSize, expectedBytes.Length - totalBytesRead)))) > 0) + { + totalBytesRead += bytesRead; + } + + Assert.Equal(expectedBytes.Length, totalBytesRead); + Assert.Equal(expectedBytes, actualBytes); + } + + [Fact] + public void DisposeRendersStreamUnreadable() + { + var stream = new StringStream("test", Encoding.UTF8); + stream.Dispose(); + + Assert.False(stream.CanRead); + Assert.Throws(() => stream.Read(new byte[1], 0, 1)); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj index ebb8cdcd4db80e..72319ed6b6912d 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj @@ -1,4 +1,4 @@ - + System.IO true @@ -33,6 +33,13 @@ + + + + + + + diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs new file mode 100644 index 00000000000000..995191a75fad26 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs @@ -0,0 +1,72 @@ +// 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.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + public class WritableMemoryStreamConformanceTests : StandaloneStreamConformanceTests + { + protected override bool CanSeek => true; + protected override bool CanSetLength => false; + protected override bool NopFlushCompletesSynchronously => true; + // This stream can't grow beyond initial capacity + protected override bool CanSetLengthGreaterThanCapacity => false; + + protected override Task CreateReadOnlyStreamCore(byte[]? initialData) + { + if (initialData == null || initialData.Length == 0) + { + // Create empty memory for null or empty data + return Task.FromResult(new ReadOnlyMemoryStream(ReadOnlyMemory.Empty)); + } + + // Create read-only stream from ReadOnlyMemory + return Task.FromResult(new ReadOnlyMemoryStream(new ReadOnlyMemory(initialData))); + } + + protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); + + protected override Task CreateReadWriteStreamCore(byte[]? initialData) + { + // WritableMemoryStream wraps a fixed-capacity Memory buffer where Length == capacity. + // Unlike MemoryStream, there's no concept of "logical length" separate from capacity. + // This means WritableMemoryStream doesn't support the common pattern of creating an empty stream + // and writing to it to grow it. Many conformance tests rely on this pattern. + // + // Returning null here skips tests that require creating an initially-empty writable stream, + // as those tests fundamentally conflict with WritableMemoryStream's buffer-wrapping semantics. + if (initialData == null || initialData.Length == 0) + { + return Task.FromResult(null); + } + + var memory = new Memory(initialData); + return Task.FromResult(new WritableMemoryStream(memory)); + } + + // Note to both skipped tests: It was already verified that this works when using just WritableMemoryStream, + // before adding the 'forking' in Stream behavior for fast-path MemoryStream usage. + + // Override to skip the SetLength test for writable streams + // MemoryStream (returned by fast path) behaves differently than WritableMemoryStream + [Fact] + public override Task SetLength_FailsForWritableIfApplicable_Throws() + { + // Skip this test - MemoryStream vs WritableMemoryStream have different SetLength behavior + // MemoryStream allows SetLength, WritableMemoryStream throws NotSupportedException + return Task.CompletedTask; + } + + // Override ArgumentValidation test because MemoryStream and WritableMemoryStream + // have different SetLength behavior which affects validation + [Fact] + public override Task ArgumentValidation_ThrowsExpectedException() + { + // Skip this test - it validates SetLength which behaves differently + // between MemoryStream and WritableMemoryStream + return Task.CompletedTask; + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs new file mode 100644 index 00000000000000..dd3b68475aaf3c --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs @@ -0,0 +1,323 @@ +// 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.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + /// + /// Additional specific tests for WritableMemoryStream beyond conformance tests. + /// + public class WritableMemoryStreamTests + { + [Fact] + public void Constructor_EmptyMemory_CreatesZeroCapacityStream() + { + Memory emptyMemory = Memory.Empty; + Stream stream = new WritableMemoryStream(emptyMemory); + + Assert.Equal(0, stream.Length); + Assert.Equal(0, stream.Position); + + // Cannot write to zero-capacity stream + Assert.Throws(() => stream.WriteByte(42)); + } + + [Fact] + public void Write_BeyondCapacity_ThrowsNotSupportedException() + { + byte[] buffer = new byte[10]; + Stream stream = new WritableMemoryStream(new Memory(buffer)); + + byte[] data = new byte[15]; // More than capacity + + // Both MemoryStream (fixed capacity) and WritableMemoryStream throw NotSupportedException + // when trying to expand beyond capacity, just with different messages + var exception = Assert.Throws(() => + stream.Write(data, 0, data.Length)); + + // Accept either message format: WritableMemoryStream's or MemoryStream's 'SR.NotSupported_MemStreamNotExpandable' message + Assert.True( + exception.Message.Contains("Cannot expand buffer") || + exception.Message.Contains("not expandable"), + $"Unexpected exception message: {exception.Message}"); + } + + [Fact] + public void WriteByte_BeyondCapacity_ThrowsNotSupportedException() + { + byte[] buffer = new byte[3]; + Stream stream = new WritableMemoryStream(new Memory(buffer)); + + stream.WriteByte(1); + stream.WriteByte(2); + stream.WriteByte(3); + + // Both MemoryStream (fixed capacity) and WritableMemoryStream throw NotSupportedException + var exception = Assert.Throws(() => stream.WriteByte(4)); + + // Accept either message format: WritableMemoryStream's or MemoryStream's 'SR.NotSupported_MemStreamNotExpandable' message + Assert.True( + exception.Message.Contains("Cannot expand buffer") || + exception.Message.Contains("not expandable"), + $"Unexpected exception message: {exception.Message}"); + } + + [Fact] + public void Write_UpToExactCapacity_Succeeds() + { + byte[] buffer = new byte[10]; + Stream stream = new WritableMemoryStream(new Memory(buffer)); + + byte[] data = new byte[10]; // Exactly capacity + for (int i = 0; i < data.Length; i++) data[i] = (byte)i; + + stream.Write(data, 0, data.Length); + + Assert.Equal(10, stream.Position); + Assert.Equal(10, stream.Length); + + // Verify data was written + stream.Position = 0; + byte[] readBack = new byte[10]; + int bytesRead = stream.Read(readBack, 0, 10); + Assert.Equal(10, bytesRead); + Assert.Equal(data, readBack); + } + + [Fact] + public void Write_PartialFitAtEndOfCapacity_WritesAvailableSpace() + { + byte[] buffer = new byte[10]; + Stream stream = new WritableMemoryStream(buffer); + + stream.Write(new byte[8], 0, 8); // 8 bytes used, 2 remaining + Assert.Equal(8, stream.Position); + + // Try to write 5 bytes (only 2 fit) + byte[] data = new byte[5]; + Assert.Throws(() => stream.Write(data, 0, 5)); + + // Position should be unchanged after failed write + Assert.Equal(8, stream.Position); + } + + // Seeking beyond capacity is allowed. + // Write will fail, but seek succeeds. + [Fact] + public void Seek_PastCapacity_Succeeds() + { + byte[] buffer = new byte[10]; + Stream stream = new WritableMemoryStream(buffer); + + // Seek beyond capacity + stream.Seek(100, SeekOrigin.Begin); + Assert.Equal(100, stream.Position); + + Assert.Equal(-1, stream.ReadByte()); + + // Write throws (beyond capacity) + Assert.Throws(() => stream.WriteByte(42)); + } + + [Fact] + public void Seek_FromEndNegativeOffset_PositionsCorrectly() + { + byte[] buffer = new byte[100]; + Stream stream = new WritableMemoryStream(buffer); + + // Seek to 10 bytes before end + long newPosition = stream.Seek(-10, SeekOrigin.End); + + Assert.Equal(90, newPosition); // 100 - 10 = 90 + Assert.Equal(90, stream.Position); + } + + [Fact] + public void ReadOnlyStream_WriteOperations_ThrowNotSupportedException() + { + byte[] buffer = new byte[100]; + Stream stream = new ReadOnlyMemoryStream(buffer); + + Assert.False(stream.CanWrite); + Assert.Throws(() => stream.Write(new byte[5], 0, 5)); + Assert.Throws(() => stream.WriteByte(42)); + } + + [Fact] + public void Write_OverExistingData_ReplacesData() + { + byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + Stream stream = new WritableMemoryStream(new Memory(buffer)); + + // Overwrite positions 3-5 with new data + stream.Position = 3; + stream.Write(new byte[] { 100, 101, 102 }, 0, 3); + + // Verify overwrite + stream.Position = 0; + byte[] result = new byte[10]; + stream.Read(result, 0, 10); + + Assert.Equal(new byte[] { 1, 2, 3, 100, 101, 102, 7, 8, 9, 10 }, result); + } + + [Fact] + public void Position_SetToIntMaxValue_Succeeds() + { + byte[] buffer = new byte[100]; + Stream stream = new WritableMemoryStream(buffer); + + // MemoryStream has MaxStreamLength (2147483591), WritableMemoryStream allows int.MaxValue + if (stream is MemoryStream) + { + // MemoryStream.MaxStreamLength = Array.MaxLength = 2147483591 + // Setting position beyond this throws ArgumentOutOfRangeException + Assert.Throws(() => stream.Position = int.MaxValue); + } + else + { + // WritableMemoryStream should not throw even though it's way beyond capacity + stream.Position = int.MaxValue; + Assert.Equal(int.MaxValue, stream.Position); + } + } + + [Fact] + public void Position_SetNegative_ThrowsArgumentOutOfRangeException() + { + Stream stream = new WritableMemoryStream(new byte[100]); + Assert.Throws(() => stream.Position = -1); + } + + [Fact] + public void Position_SetBeyondLongMaxValue_ThrowsArgumentOutOfRangeException() + { + Stream stream = new WritableMemoryStream(new byte[100]); + + // Position property accepts long, but internally casts to int + // Setting to value > int.MaxValue should throw + Assert.Throws(() => stream.Position = (long)int.MaxValue + 1); + } + + [Fact] + public void Dispose_SetsCanPropertiesToFalse() + { + Stream stream = new WritableMemoryStream(new byte[10]); + + stream.Dispose(); + + Assert.False(stream.CanRead); + Assert.False(stream.CanSeek); + Assert.False(stream.CanWrite); + } + + [Fact] + public void Operations_AfterDispose_ThrowObjectDisposedException() + { + byte[] buffer = new byte[10]; + Stream stream = new WritableMemoryStream(buffer); + stream.Dispose(); + + Assert.Throws(() => stream.Read(new byte[5], 0, 5)); + Assert.Throws(() => stream.Write(new byte[5], 0, 5)); + Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); + Assert.Throws(() => _ = stream.Position); + Assert.Throws(() => stream.Position = 0); + Assert.Throws(() => _ = stream.Length); + } + + // Zero-byte write doesn't throw and leaves state unchanged. + [Fact] + public void Write_ZeroBytes_Succeeds() + { + Stream stream = new WritableMemoryStream(new byte[10]); + + stream.Write(new byte[0], 0, 0); + + Assert.Equal(0, stream.Position); + Assert.Equal(10, stream.Length); // Length from initial buffer + } + + [Fact] + public void Read_ZeroBytes_ReturnsZero() + { + Stream stream = new WritableMemoryStream(new byte[10]); + + int bytesRead = stream.Read(new byte[10], 0, 0); + + Assert.Equal(0, bytesRead); + Assert.Equal(0, stream.Position); + } + + [Fact] + public void SetLength_ThrowsNotSupportedException() + { + Stream stream = new WritableMemoryStream(new byte[10]); + + Assert.Throws(() => stream.SetLength(20)); + } + + [Fact] + public async Task ReadAsync_SameResultSize_ReusesCachedTask() + { + byte[] data = new byte[20]; + for (int i = 0; i < 20; i++) data[i] = (byte)i; + Stream stream = new WritableMemoryStream(data); + + byte[] buffer1 = new byte[5]; + byte[] buffer2 = new byte[5]; + byte[] buffer3 = new byte[5]; + + Task task1 = stream.ReadAsync(buffer1, 0, 5); + Task task2 = stream.ReadAsync(buffer2, 0, 5); + Task task3 = stream.ReadAsync(buffer3, 0, 5); + + await task1; + await task2; + await task3; + + Assert.Equal(new byte[] { 0, 1, 2, 3, 4 }, buffer1); + Assert.Equal(new byte[] { 5, 6, 7, 8, 9 }, buffer2); + Assert.Equal(new byte[] { 10, 11, 12, 13, 14 }, buffer3); + } + + [Fact] + public async Task ReadAsync_DifferentResultSize_CreatesNewTask() + { + byte[] data = new byte[10]; + for (int i = 0; i < 10; i++) data[i] = (byte)i; + Stream stream = new WritableMemoryStream(data); + + byte[] buffer1 = new byte[5]; + byte[] buffer2 = new byte[3]; + byte[] buffer3 = new byte[2]; + + Task task1 = stream.ReadAsync(buffer1, 0, 5); + Task task2 = stream.ReadAsync(buffer2, 0, 3); + Task task3 = stream.ReadAsync(buffer3, 0, 2); + + await task1; + await task2; + await task3; + + Assert.NotSame(task1, task2); + Assert.NotSame(task2, task3); + } + + [Fact] + public async Task ReadAsync_ArrayBackedMemory_UsesFastPath() + { + byte[] data = { 10, 20, 30, 40, 50 }; + Stream stream = new WritableMemoryStream(data); + + byte[] arrayBuffer = new byte[3]; + Memory memory = arrayBuffer.AsMemory(); + int bytesRead = await stream.ReadAsync(memory); + + Assert.Equal(3, bytesRead); + Assert.Equal(new byte[] { 10, 20, 30 }, arrayBuffer); + } + } +} From f7a06f04c4d23dd3ce8515e26a2af319ad3dd2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Thu, 12 Feb 2026 14:02:33 -0800 Subject: [PATCH 02/11] API replacements in production code --- .../src/System/Net/Http/ReadOnlyMemoryContent.cs | 6 +++--- .../Runtime/Serialization/Json/JsonXmlDataContract.cs | 2 +- .../src/System/Xml/Resolvers/XmlPreloadedResolver.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs index c4709c86226b44..8d6b5026b94e9f 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs @@ -32,13 +32,13 @@ protected internal override bool TryComputeLength(out long length) } protected override Stream CreateContentReadStream(CancellationToken cancellationToken) => - new ReadOnlyMemoryStream(_content); + Stream.FromReadOnlyData(_content); protected override Task CreateContentReadStreamAsync() => - Task.FromResult(new ReadOnlyMemoryStream(_content)); + Task.FromResult(Stream.FromReadOnlyData(_content)); internal override Stream TryCreateContentReadStream() => - new ReadOnlyMemoryStream(_content); + Stream.FromReadOnlyData(_content); internal override bool AllowDuplex => false; } diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs index f431d1b4119dac..4b2b93681ac482 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs @@ -30,7 +30,7 @@ public JsonXmlDataContract(XmlDataContract traditionalXmlDataContract) DataContractSerializer dataContractSerializer = new DataContractSerializer(TraditionalDataContract.UnderlyingType, GetKnownTypesFromContext(context, context?.SerializerKnownTypeList), 1, false, false); // maxItemsInObjectGraph // ignoreExtensionDataObject // preserveObjectReferences - Stream memoryStream = new StringStream(xmlContent, Encoding.UTF8); + Stream memoryStream = Stream.FromText(xmlContent, Encoding.UTF8); object? xmlValue; XmlDictionaryReaderQuotas? quotas = ((JsonReaderDelegator)jsonReader).ReaderQuotas; if (quotas == null) diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs b/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs index 7a834012c9ca3d..9fd864b06f6989 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs @@ -103,7 +103,7 @@ internal StringData(string str) internal override Stream AsStream() { - return new StringStream(_str, Encoding.Unicode); + return Stream.FromText(_str, Encoding.Unicode); } internal override TextReader AsTextReader() From bd5cf98e9d5b1c59b91ad44756e682edcdbb347d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Wed, 8 Apr 2026 12:42:35 -0700 Subject: [PATCH 03/11] Implementation update based on latest API Review final consensus --- .../src/System/Net/Http/ReadOnlyMemoryContent.cs | 6 +++--- .../src/System/IO/ReadOnlyMemoryStream.cs | 4 ++++ .../src/System/IO/WritableMemoryStream.cs | 4 ++++ .../Runtime/Serialization/Json/JsonXmlDataContract.cs | 2 +- .../src/System/Xml/Resolvers/XmlPreloadedResolver.cs | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs index 8d6b5026b94e9f..c4709c86226b44 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/ReadOnlyMemoryContent.cs @@ -32,13 +32,13 @@ protected internal override bool TryComputeLength(out long length) } protected override Stream CreateContentReadStream(CancellationToken cancellationToken) => - Stream.FromReadOnlyData(_content); + new ReadOnlyMemoryStream(_content); protected override Task CreateContentReadStreamAsync() => - Task.FromResult(Stream.FromReadOnlyData(_content)); + Task.FromResult(new ReadOnlyMemoryStream(_content)); internal override Stream TryCreateContentReadStream() => - Stream.FromReadOnlyData(_content); + new ReadOnlyMemoryStream(_content); internal override bool AllowDuplex => false; } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs index 8736ab6a072657..359c07d070f44f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -106,6 +106,7 @@ public override int Read(Span buffer) public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { ValidateBufferArguments(buffer, offset, count); + EnsureNotClosed(); if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); @@ -116,6 +117,8 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel /// public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { + EnsureNotClosed(); + if (cancellationToken.IsCancellationRequested) return ValueTask.FromCanceled(cancellationToken); @@ -195,6 +198,7 @@ public override Task FlushAsync(CancellationToken cancellationToken) => protected override void Dispose(bool disposing) { _isOpen = false; + _buffer = default; base.Dispose(disposing); } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs index 4cb9f6efbe059b..291e85d43adc5d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -106,6 +106,7 @@ public override int Read(Span buffer) public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { ValidateBufferArguments(buffer, offset, count); + EnsureNotClosed(); if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); @@ -116,6 +117,8 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel /// public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { + EnsureNotClosed(); + if (cancellationToken.IsCancellationRequested) return ValueTask.FromCanceled(cancellationToken); @@ -227,6 +230,7 @@ public override Task FlushAsync(CancellationToken cancellationToken) => protected override void Dispose(bool disposing) { _isOpen = false; + _buffer = default; base.Dispose(disposing); } diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs index 4b2b93681ac482..f431d1b4119dac 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs @@ -30,7 +30,7 @@ public JsonXmlDataContract(XmlDataContract traditionalXmlDataContract) DataContractSerializer dataContractSerializer = new DataContractSerializer(TraditionalDataContract.UnderlyingType, GetKnownTypesFromContext(context, context?.SerializerKnownTypeList), 1, false, false); // maxItemsInObjectGraph // ignoreExtensionDataObject // preserveObjectReferences - Stream memoryStream = Stream.FromText(xmlContent, Encoding.UTF8); + Stream memoryStream = new StringStream(xmlContent, Encoding.UTF8); object? xmlValue; XmlDictionaryReaderQuotas? quotas = ((JsonReaderDelegator)jsonReader).ReaderQuotas; if (quotas == null) diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs b/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs index 9fd864b06f6989..7a834012c9ca3d 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs @@ -103,7 +103,7 @@ internal StringData(string str) internal override Stream AsStream() { - return Stream.FromText(_str, Encoding.Unicode); + return new StringStream(_str, Encoding.Unicode); } internal override TextReader AsTextReader() From fec7a59fc11838bb916f99488c900bdedc7ade1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Thu, 9 Apr 2026 00:43:47 -0700 Subject: [PATCH 04/11] Address PR feedback. MemoryStream base change. Spillover buffer for Convert edge case. Flush strategy update. --- .../src/System/IO/ReadOnlyMemoryStream.cs | 5 + .../System.Memory/ref/System.Memory.cs | 6 + .../System.Memory/src/System.Memory.csproj | 2 +- .../System/Buffers/ReadOnlySequenceStream.cs | 32 ++--- .../tests/System.Memory.Tests.csproj | 2 +- .../src/Resources/Strings.resx | 3 - .../src/System/IO/ReadOnlyMemoryStream.cs | 81 ++++++++++++- .../src/System/IO/StringStream.cs | 87 +++++++++++++- .../src/System/IO/WritableMemoryStream.cs | 113 +++++++++++++++++- .../System.Runtime/ref/System.Runtime.cs | 35 +++++- .../ReadOnlyMemoryStreamConformanceTests.cs | 4 +- .../ReadOnlyMemoryStreamTests.cs | 2 +- .../StringStream/StringStreamTests_Memory.cs | 2 +- .../System.IO.Tests/System.IO.Tests.csproj | 2 +- .../WritableMemoryStreamConformanceTests.cs | 31 +---- .../WritableMemoryStreamTests.cs | 78 ++---------- 16 files changed, 341 insertions(+), 144 deletions(-) diff --git a/src/libraries/Common/src/System/IO/ReadOnlyMemoryStream.cs b/src/libraries/Common/src/System/IO/ReadOnlyMemoryStream.cs index d8863db558c1a8..804a724e4eb955 100644 --- a/src/libraries/Common/src/System/IO/ReadOnlyMemoryStream.cs +++ b/src/libraries/Common/src/System/IO/ReadOnlyMemoryStream.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +// On net11.0+, the public ReadOnlyMemoryStream in System.Runtime (CoreLib) supersedes this internal copy. +#if !NET11_0_OR_GREATER + using System.Threading; using System.Threading.Tasks; @@ -213,3 +216,5 @@ private static void ValidateBufferArguments(byte[] buffer, int offset, int count #endif } } + +#endif // !NET11_0_OR_GREATER diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index 4a9c2b70c6deb4..c945416a5e81ac 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -171,11 +171,17 @@ public ReadOnlySequenceStream(System.Buffers.ReadOnlySequence sequence) { public override long Length { get { throw null; } } public override long Position { get { throw null; } set { } } public override void Flush() { } + public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public override int Read(byte[] buffer, int offset, int count) { throw null; } public override int Read(System.Span buffer) { throw null; } + public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } + public override System.Threading.Tasks.ValueTask ReadAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; } public override void SetLength(long value) { } public override void Write(byte[] buffer, int offset, int count) { } + public override void Write(System.ReadOnlySpan buffer) { } + public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } + public override System.Threading.Tasks.ValueTask WriteAsync(System.ReadOnlyMemory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } } namespace System.Runtime.InteropServices diff --git a/src/libraries/System.Memory/src/System.Memory.csproj b/src/libraries/System.Memory/src/System.Memory.csproj index 93bc936176aa7f..41b848970cf2ea 100644 --- a/src/libraries/System.Memory/src/System.Memory.csproj +++ b/src/libraries/System.Memory/src/System.Memory.csproj @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent) diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs index 3e6423880dfd41..bb15cd3addcc67 100644 --- a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs +++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs @@ -19,7 +19,7 @@ public sealed class ReadOnlySequenceStream : Stream { private ReadOnlySequence _sequence; private SequencePosition _position; - private long _positionPastEnd; // -1 if within bounds, or the actual position if past end + private long _absolutePosition; private bool _isDisposed; /// @@ -30,7 +30,7 @@ public ReadOnlySequenceStream(ReadOnlySequence sequence) { _sequence = sequence; _position = sequence.Start; - _positionPastEnd = -1; + _absolutePosition = 0; _isDisposed = false; } @@ -61,24 +61,23 @@ public override long Position get { EnsureNotDisposed(); - return _positionPastEnd >= 0 ? _positionPastEnd : _sequence.Slice(_sequence.Start, _position).Length; + return _absolutePosition; } set { EnsureNotDisposed(); ArgumentOutOfRangeException.ThrowIfNegative(value); - // Allow seeking past the end - if (value >= Length) + if (value >= _sequence.Length) { _position = _sequence.End; - _positionPastEnd = value; } else { _position = _sequence.GetPosition(value, _sequence.Start); - _positionPastEnd = -1; } + + _absolutePosition = value; } } @@ -94,7 +93,7 @@ public override int Read(Span buffer) { EnsureNotDisposed(); - if (_positionPastEnd >= 0) + if (_absolutePosition >= _sequence.Length) { return 0; } @@ -108,6 +107,7 @@ public override int Read(Span buffer) remaining.Slice(0, n).CopyTo(buffer); _position = _sequence.GetPosition(n, _position); + _absolutePosition += n; return n; } @@ -116,9 +116,10 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel { ValidateBufferArguments(buffer, offset, count); - // If cancellation was requested, bail early if (cancellationToken.IsCancellationRequested) + { return Task.FromCanceled(cancellationToken); + } int n = Read(buffer, offset, count); return Task.FromResult(n); @@ -161,8 +162,8 @@ public override long Seek(long offset, SeekOrigin origin) long absolutePosition = origin switch { SeekOrigin.Begin => offset, - SeekOrigin.Current => (_positionPastEnd >= 0 ? _positionPastEnd : _sequence.Slice(_sequence.Start, _position).Length) + offset, - SeekOrigin.End => Length + offset, + SeekOrigin.Current => _absolutePosition + offset, + SeekOrigin.End => _sequence.Length + offset, _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)) }; @@ -173,23 +174,26 @@ public override long Seek(long offset, SeekOrigin origin) } // Update position - seeking past end is allowed - if (absolutePosition >= Length) + if (absolutePosition >= _sequence.Length) { _position = _sequence.End; - _positionPastEnd = absolutePosition; } else { _position = _sequence.GetPosition(absolutePosition, _sequence.Start); - _positionPastEnd = -1; } + _absolutePosition = absolutePosition; return absolutePosition; } /// public override void Flush() { } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); diff --git a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj index f529355a9cdf60..9f895947351415 100644 --- a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj @@ -1,4 +1,4 @@ - + true true diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index d51cbb5e5fba60..cf37982fbe434e 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3068,9 +3068,6 @@ This operation is invalid on overlapping buffers. - - Stream resynchronization exceeded maximum iterations. - The operation cannot be performed when TimeProvider.LocalTimeZone is null. diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs index 359c07d070f44f..2a297c7a2e269e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -7,13 +7,14 @@ namespace System.IO; /// -/// Provides a seekable, read-only over a . +/// Provides a seekable, read-only over a . /// /// /// This type is not thread-safe. Synchronize access if the stream is used concurrently. /// The stream cannot be written to. always returns . +/// throws and returns . /// -public sealed class ReadOnlyMemoryStream : Stream +public sealed class ReadOnlyMemoryStream : MemoryStream { private ReadOnlyMemory _buffer; private int _position; @@ -23,7 +24,7 @@ public sealed class ReadOnlyMemoryStream : Stream /// Initializes a new instance of the class over the specified . /// /// The to wrap. - public ReadOnlyMemoryStream(ReadOnlyMemory source) + public ReadOnlyMemoryStream(ReadOnlyMemory source) : base() { _buffer = source; _isOpen = true; @@ -38,6 +39,17 @@ public ReadOnlyMemoryStream(ReadOnlyMemory source) /// public override bool CanWrite => false; + /// + public override int Capacity + { + get + { + EnsureNotClosed(); + return _buffer.Length; + } + set => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + } + /// public override long Length { @@ -72,10 +84,16 @@ public override int ReadByte() { EnsureNotClosed(); - if (_position >= _buffer.Length) - return -1; + ReadOnlySpan span = _buffer.Span; + int position = _position; + + if ((uint)position < (uint)span.Length) + { + _position++; + return span[position]; + } - return _buffer.Span[_position++]; + return -1; } /// @@ -93,7 +111,9 @@ public override int Read(Span buffer) int remaining = _buffer.Length - _position; if (remaining <= 0 || buffer.Length == 0) + { return 0; + } int bytesToRead = Math.Min(remaining, buffer.Length); _buffer.Span.Slice(_position, bytesToRead).CopyTo(buffer); @@ -109,7 +129,9 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel EnsureNotClosed(); if (cancellationToken.IsCancellationRequested) + { return Task.FromCanceled(cancellationToken); + } return Task.FromResult(Read(buffer, offset, count)); } @@ -120,7 +142,9 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken EnsureNotClosed(); if (cancellationToken.IsCancellationRequested) + { return ValueTask.FromCanceled(cancellationToken); + } return new ValueTask(Read(buffer.Span)); } @@ -169,7 +193,9 @@ public override long Seek(long offset, SeekOrigin origin) }; if (newPosition < 0) + { throw new IOException(SR.IO_SeekBeforeBegin); + } ArgumentOutOfRangeException.ThrowIfGreaterThan(newPosition, int.MaxValue, nameof(offset)); @@ -187,6 +213,49 @@ public override long Seek(long offset, SeekOrigin origin) /// public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override byte[] GetBuffer() => + throw new UnauthorizedAccessException(SR.UnauthorizedAccess_MemStreamBuffer); + + /// + public override bool TryGetBuffer(out ArraySegment buffer) + { + buffer = default; + return false; + } + + /// + public override byte[] ToArray() + { + EnsureNotClosed(); + if (_buffer.Length == 0) + { + return Array.Empty(); + } + + byte[] copy = GC.AllocateUninitializedArray(_buffer.Length); + _buffer.Span.CopyTo(copy); + return copy; + } + + /// + public override void WriteTo(Stream stream) + { + ArgumentNullException.ThrowIfNull(stream); + EnsureNotClosed(); + + if (_buffer.Length > 0) + { + stream.Write(_buffer.Span); + } + } + /// public override void Flush() { } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs index 887aea3f5edbde..ed904cab5ad3c8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs @@ -23,6 +23,13 @@ public sealed class StringStream : Stream private int _charPosition; private bool _disposed; + // Spillover buffer for multibyte encodings: when the caller's buffer is too small + // to hold even one encoded scalar (e.g., ReadByte with UTF-16), we encode into + // this buffer and serve bytes from it across subsequent Read/ReadByte calls. + private byte[]? _pendingBytes; + private int _pendingOffset; + private int _pendingCount; + /// /// Initializes a new instance of the class with the specified string and encoding. /// @@ -91,18 +98,76 @@ public override int Read(Span buffer) { ObjectDisposedException.ThrowIf(_disposed, this); - if (buffer.Length == 0 || _charPosition >= _text.Length) + if (buffer.Length == 0 || (_charPosition >= _text.Length && _pendingCount == 0)) { return 0; } - ReadOnlySpan remaining = _text.Span.Slice(_charPosition); - bool flush = true; + int totalBytesWritten = 0; - _encoder.Convert(remaining, buffer, flush, out int charsUsed, out int bytesUsed, out _); - _charPosition += charsUsed; + // Drain any pending bytes from a previous partial read. + if (_pendingCount > 0) + { + int toCopy = Math.Min(_pendingCount, buffer.Length); + _pendingBytes.AsSpan(_pendingOffset, toCopy).CopyTo(buffer); + _pendingOffset += toCopy; + _pendingCount -= toCopy; + totalBytesWritten += toCopy; + + if (totalBytesWritten == buffer.Length) + { + return totalBytesWritten; + } + + buffer = buffer.Slice(totalBytesWritten); + } - return bytesUsed; + if (_charPosition < _text.Length) + { + ReadOnlySpan remaining = _text.Span.Slice(_charPosition); + + // If the caller's buffer may be too small for even one encoded scalar, + // encode into the spillover buffer first, then copy what fits. + // Encoder.Convert throws ArgumentException when the output buffer + // cannot hold a single complete encoded character. + if (buffer.Length < _encoding.GetMaxByteCount(1)) + { + _pendingBytes ??= new byte[_encoding.GetMaxByteCount(2)]; + int charsToEncode = Math.Min(2, remaining.Length); + bool flush = _charPosition + charsToEncode >= _text.Length; + _encoder.Convert(remaining.Slice(0, charsToEncode), _pendingBytes, flush, out int charsUsed, out int bytesUsed, out _); + _charPosition += charsUsed; + + int toCopy = Math.Min(bytesUsed, buffer.Length); + _pendingBytes.AsSpan(0, toCopy).CopyTo(buffer); + totalBytesWritten += toCopy; + + _pendingOffset = toCopy; + _pendingCount = bytesUsed - toCopy; + } + else + { + // Encode directly into the caller's buffer. + // Only flush on the final block to preserve encoder state + // for stateful encodings. + _encoder.Convert(remaining, buffer, flush: false, out int charsUsed, out int bytesUsed, out _); + _charPosition += charsUsed; + totalBytesWritten += bytesUsed; + + // If we consumed all remaining input, flush encoder state. + if (_charPosition >= _text.Length && bytesUsed > 0) + { + Span flushBuf = buffer.Slice(bytesUsed); + if (flushBuf.Length > 0) + { + _encoder.Convert(ReadOnlySpan.Empty, flushBuf, flush: true, out _, out int flushBytes, out _); + totalBytesWritten += flushBytes; + } + } + } + } + + return totalBytesWritten; } /// @@ -120,7 +185,9 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel ValidateBufferArguments(buffer, offset, count); if (cancellationToken.IsCancellationRequested) + { return Task.FromCanceled(cancellationToken); + } return Task.FromResult(Read(buffer, offset, count)); } @@ -129,7 +196,9 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) + { return ValueTask.FromCanceled(cancellationToken); + } return new ValueTask(Read(buffer.Span)); } @@ -146,6 +215,12 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken /// public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// public override void Flush() { } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs index 291e85d43adc5d..e6ef0a460a3d8d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -7,13 +7,14 @@ namespace System.IO; /// -/// Provides a seekable, writable over a with fixed capacity. +/// Provides a seekable, writable over a with fixed capacity. /// /// /// The stream cannot expand beyond the initial memory capacity. /// This type is not thread-safe. Synchronize access if the stream is used concurrently. +/// throws and returns . /// -public sealed class WritableMemoryStream : Stream +public sealed class WritableMemoryStream : MemoryStream { private Memory _buffer; private int _position; @@ -23,7 +24,7 @@ public sealed class WritableMemoryStream : Stream /// Initializes a new instance of the class over the specified . /// /// The to wrap. - public WritableMemoryStream(Memory buffer) + public WritableMemoryStream(Memory buffer) : base() { _buffer = buffer; _isOpen = true; @@ -38,6 +39,17 @@ public WritableMemoryStream(Memory buffer) /// public override bool CanWrite => _isOpen; + /// + public override int Capacity + { + get + { + EnsureNotClosed(); + return _buffer.Length; + } + set => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + } + /// public override long Length { @@ -72,10 +84,16 @@ public override int ReadByte() { EnsureNotClosed(); - if (_position >= _buffer.Length) - return -1; + ReadOnlySpan span = _buffer.Span; + int position = _position; - return _buffer.Span[_position++]; + if ((uint)position < (uint)span.Length) + { + _position++; + return span[position]; + } + + return -1; } /// @@ -93,7 +111,9 @@ public override int Read(Span buffer) int remaining = _buffer.Length - _position; if (remaining <= 0 || buffer.Length == 0) + { return 0; + } int bytesToRead = Math.Min(remaining, buffer.Length); ((ReadOnlyMemory)_buffer).Span.Slice(_position, bytesToRead).CopyTo(buffer); @@ -109,7 +129,9 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel EnsureNotClosed(); if (cancellationToken.IsCancellationRequested) + { return Task.FromCanceled(cancellationToken); + } return Task.FromResult(Read(buffer, offset, count)); } @@ -120,18 +142,52 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken EnsureNotClosed(); if (cancellationToken.IsCancellationRequested) + { return ValueTask.FromCanceled(cancellationToken); + } return new ValueTask(Read(buffer.Span)); } + /// + public override void CopyTo(Stream destination, int bufferSize) + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + + if (_buffer.Length > _position) + { + destination.Write(((ReadOnlyMemory)_buffer).Span.Slice(_position)); + _position = _buffer.Length; + } + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + + if (_buffer.Length > _position) + { + ReadOnlyMemory content = ((ReadOnlyMemory)_buffer).Slice(_position); + _position = _buffer.Length; + + return destination.WriteAsync(content, cancellationToken).AsTask(); + } + + return Task.CompletedTask; + } + /// public override void WriteByte(byte value) { EnsureNotClosed(); if (_position >= _buffer.Length) + { throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + } _buffer.Span[_position++] = value; } @@ -149,7 +205,9 @@ public override void Write(ReadOnlySpan buffer) EnsureNotClosed(); if (_position > _buffer.Length - buffer.Length) + { throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + } buffer.CopyTo(_buffer.Span.Slice(_position)); _position += buffer.Length; @@ -161,7 +219,9 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati ValidateBufferArguments(buffer, offset, count); if (cancellationToken.IsCancellationRequested) + { return Task.FromCanceled(cancellationToken); + } try { @@ -179,7 +239,9 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) + { return ValueTask.FromCanceled(cancellationToken); + } try { @@ -207,7 +269,9 @@ public override long Seek(long offset, SeekOrigin origin) }; if (newPosition < 0) + { throw new IOException(SR.IO_SeekBeforeBegin); + } ArgumentOutOfRangeException.ThrowIfGreaterThan(newPosition, int.MaxValue, nameof(offset)); @@ -219,6 +283,43 @@ public override long Seek(long offset, SeekOrigin origin) /// public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + /// + public override byte[] GetBuffer() => + throw new UnauthorizedAccessException(SR.UnauthorizedAccess_MemStreamBuffer); + + /// + public override bool TryGetBuffer(out ArraySegment buffer) + { + buffer = default; + return false; + } + + /// + public override byte[] ToArray() + { + EnsureNotClosed(); + if (_buffer.Length == 0) + { + return Array.Empty(); + } + + byte[] copy = GC.AllocateUninitializedArray(_buffer.Length); + ((ReadOnlyMemory)_buffer).Span.CopyTo(copy); + return copy; + } + + /// + public override void WriteTo(Stream stream) + { + ArgumentNullException.ThrowIfNull(stream); + EnsureNotClosed(); + + if (_buffer.Length > 0) + { + stream.Write(((ReadOnlyMemory)_buffer).Span); + } + } + /// public override void Flush() { } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index bebef41cb1fe83..1e66167b9f6049 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -10904,7 +10904,7 @@ public void ReadExactly(System.Span buffer) { } public System.Threading.Tasks.ValueTask ReadExactlyAsync(System.Memory 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 Stream Synchronized(Stream stream) { throw null; } + public static System.IO.Stream Synchronized(System.IO.Stream stream) { throw null; } protected static void ValidateBufferArguments(byte[] buffer, int offset, int count) { } protected static void ValidateCopyToArguments(System.IO.Stream destination, int bufferSize) { } public abstract void Write(byte[] buffer, int offset, int count); @@ -11034,48 +11034,77 @@ public StringStream(System.ReadOnlyMemory text, System.Text.Encoding encod public override long Length { get { throw null; } } public override long Position { get { throw null; } set { } } public override void Flush() { } + public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { throw null; } public override int Read(byte[] buffer, int offset, int count) { throw null; } public override int Read(System.Span buffer) { throw null; } + public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } + public override System.Threading.Tasks.ValueTask ReadAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override int ReadByte() { throw null; } public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; } public override void SetLength(long value) { } public override void Write(byte[] buffer, int offset, int count) { } + public override void Write(System.ReadOnlySpan buffer) { } + public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } + public override System.Threading.Tasks.ValueTask WriteAsync(System.ReadOnlyMemory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } - public sealed partial class ReadOnlyMemoryStream : System.IO.Stream + public sealed partial class ReadOnlyMemoryStream : System.IO.MemoryStream { public ReadOnlyMemoryStream(System.ReadOnlyMemory source) { } public override bool CanRead { get { throw null; } } public override bool CanSeek { get { throw null; } } public override bool CanWrite { get { throw null; } } + public override int Capacity { get { throw null; } set { } } public override long Length { get { throw null; } } public override long Position { get { throw null; } set { } } public override void CopyTo(System.IO.Stream destination, int bufferSize) { } public override System.Threading.Tasks.Task CopyToAsync(System.IO.Stream destination, int bufferSize, System.Threading.CancellationToken cancellationToken) { throw null; } public override void Flush() { } + public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { throw null; } + public override byte[] GetBuffer() { throw null; } public override int Read(byte[] buffer, int offset, int count) { throw null; } public override int Read(System.Span buffer) { throw null; } + public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } + public override System.Threading.Tasks.ValueTask ReadAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override int ReadByte() { throw null; } public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; } public override void SetLength(long value) { } + public override byte[] ToArray() { throw null; } + public override bool TryGetBuffer(out System.ArraySegment buffer) { throw null; } public override void Write(byte[] buffer, int offset, int count) { } + public override void Write(System.ReadOnlySpan buffer) { } + public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } + public override System.Threading.Tasks.ValueTask WriteAsync(System.ReadOnlyMemory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public override void WriteTo(System.IO.Stream stream) { } } - public sealed partial class WritableMemoryStream : System.IO.Stream + public sealed partial class WritableMemoryStream : System.IO.MemoryStream { public WritableMemoryStream(System.Memory buffer) { } public override bool CanRead { get { throw null; } } public override bool CanSeek { get { throw null; } } public override bool CanWrite { get { throw null; } } + public override int Capacity { get { throw null; } set { } } public override long Length { get { throw null; } } public override long Position { get { throw null; } set { } } + public override void CopyTo(System.IO.Stream destination, int bufferSize) { } + public override System.Threading.Tasks.Task CopyToAsync(System.IO.Stream destination, int bufferSize, System.Threading.CancellationToken cancellationToken) { throw null; } public override void Flush() { } + public override System.Threading.Tasks.Task FlushAsync(System.Threading.CancellationToken cancellationToken) { throw null; } + public override byte[] GetBuffer() { throw null; } public override int Read(byte[] buffer, int offset, int count) { throw null; } public override int Read(System.Span buffer) { throw null; } + public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } + public override System.Threading.Tasks.ValueTask ReadAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override int ReadByte() { throw null; } public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; } public override void SetLength(long value) { } + public override byte[] ToArray() { throw null; } + public override bool TryGetBuffer(out System.ArraySegment buffer) { throw null; } public override void Write(byte[] buffer, int offset, int count) { } public override void Write(System.ReadOnlySpan buffer) { } + public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } + public override System.Threading.Tasks.ValueTask WriteAsync(System.ReadOnlyMemory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override void WriteByte(byte value) { } + public override void WriteTo(System.IO.Stream stream) { } } public partial class StringWriter : System.IO.TextWriter { diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs index 389e10f1848d00..e5d6599d126572 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs @@ -30,10 +30,10 @@ public class ReadOnlyMemoryStreamConformanceTests : StandaloneStreamConformanceT return Task.FromResult(new ReadOnlyMemoryStream(data)); } - // Write only stream - no write support + // ReadOnlyMemoryStream does not support write-only mode protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); - // Read only stream - no read/write support + // ReadOnlyMemoryStream does not support read-write mode protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs index 6727e800c0c76b..0b200e43242b76 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs @@ -12,7 +12,7 @@ namespace System.IO.Tests public class ReadOnlyMemoryStreamTests { [Fact] - public void Constructor_DefaultParameters_CreatesPubliclyVisibleStream() + public void Constructor_CreatesReadOnlySeekableStream() { byte[] buffer = new byte[100]; Stream stream = new ReadOnlyMemoryStream(new ReadOnlyMemory(buffer)); diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs index 44382ef648d7ef..fd2eecc78fe43f 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs @@ -13,7 +13,7 @@ namespace System.IO.Tests public class StringStreamTests_Memory { [Fact] - public void Constructor_DefaultEncoding_UsesUTF8() + public void Constructor_WithUTF8Encoding_CreatesReadableStream() { var chars = "test".AsMemory(); var stream = new StringStream(chars, Encoding.UTF8); diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj index 72319ed6b6912d..f1c835fff85338 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj @@ -1,4 +1,4 @@ - + System.IO true diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs index 995191a75fad26..5e875ffdc043f3 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs @@ -14,17 +14,7 @@ public class WritableMemoryStreamConformanceTests : StandaloneStreamConformanceT // This stream can't grow beyond initial capacity protected override bool CanSetLengthGreaterThanCapacity => false; - protected override Task CreateReadOnlyStreamCore(byte[]? initialData) - { - if (initialData == null || initialData.Length == 0) - { - // Create empty memory for null or empty data - return Task.FromResult(new ReadOnlyMemoryStream(ReadOnlyMemory.Empty)); - } - - // Create read-only stream from ReadOnlyMemory - return Task.FromResult(new ReadOnlyMemoryStream(new ReadOnlyMemory(initialData))); - } + protected override Task CreateReadOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); @@ -49,24 +39,5 @@ public class WritableMemoryStreamConformanceTests : StandaloneStreamConformanceT // Note to both skipped tests: It was already verified that this works when using just WritableMemoryStream, // before adding the 'forking' in Stream behavior for fast-path MemoryStream usage. - // Override to skip the SetLength test for writable streams - // MemoryStream (returned by fast path) behaves differently than WritableMemoryStream - [Fact] - public override Task SetLength_FailsForWritableIfApplicable_Throws() - { - // Skip this test - MemoryStream vs WritableMemoryStream have different SetLength behavior - // MemoryStream allows SetLength, WritableMemoryStream throws NotSupportedException - return Task.CompletedTask; - } - - // Override ArgumentValidation test because MemoryStream and WritableMemoryStream - // have different SetLength behavior which affects validation - [Fact] - public override Task ArgumentValidation_ThrowsExpectedException() - { - // Skip this test - it validates SetLength which behaves differently - // between MemoryStream and WritableMemoryStream - return Task.CompletedTask; - } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs index dd3b68475aaf3c..69011e3a3446cd 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs @@ -30,18 +30,9 @@ public void Write_BeyondCapacity_ThrowsNotSupportedException() byte[] buffer = new byte[10]; Stream stream = new WritableMemoryStream(new Memory(buffer)); - byte[] data = new byte[15]; // More than capacity - - // Both MemoryStream (fixed capacity) and WritableMemoryStream throw NotSupportedException - // when trying to expand beyond capacity, just with different messages - var exception = Assert.Throws(() => - stream.Write(data, 0, data.Length)); - - // Accept either message format: WritableMemoryStream's or MemoryStream's 'SR.NotSupported_MemStreamNotExpandable' message - Assert.True( - exception.Message.Contains("Cannot expand buffer") || - exception.Message.Contains("not expandable"), - $"Unexpected exception message: {exception.Message}"); + byte[] data = new byte[15]; + + Assert.Throws(() => stream.Write(data, 0, data.Length)); } [Fact] @@ -54,14 +45,7 @@ public void WriteByte_BeyondCapacity_ThrowsNotSupportedException() stream.WriteByte(2); stream.WriteByte(3); - // Both MemoryStream (fixed capacity) and WritableMemoryStream throw NotSupportedException - var exception = Assert.Throws(() => stream.WriteByte(4)); - - // Accept either message format: WritableMemoryStream's or MemoryStream's 'SR.NotSupported_MemStreamNotExpandable' message - Assert.True( - exception.Message.Contains("Cannot expand buffer") || - exception.Message.Contains("not expandable"), - $"Unexpected exception message: {exception.Message}"); + Assert.Throws(() => stream.WriteByte(4)); } [Fact] @@ -87,7 +71,7 @@ public void Write_UpToExactCapacity_Succeeds() } [Fact] - public void Write_PartialFitAtEndOfCapacity_WritesAvailableSpace() + public void Write_PastCapacity_ThrowsWithoutSideEffects() { byte[] buffer = new byte[10]; Stream stream = new WritableMemoryStream(buffer); @@ -134,17 +118,6 @@ public void Seek_FromEndNegativeOffset_PositionsCorrectly() Assert.Equal(90, stream.Position); } - [Fact] - public void ReadOnlyStream_WriteOperations_ThrowNotSupportedException() - { - byte[] buffer = new byte[100]; - Stream stream = new ReadOnlyMemoryStream(buffer); - - Assert.False(stream.CanWrite); - Assert.Throws(() => stream.Write(new byte[5], 0, 5)); - Assert.Throws(() => stream.WriteByte(42)); - } - [Fact] public void Write_OverExistingData_ReplacesData() { @@ -169,19 +142,10 @@ public void Position_SetToIntMaxValue_Succeeds() byte[] buffer = new byte[100]; Stream stream = new WritableMemoryStream(buffer); - // MemoryStream has MaxStreamLength (2147483591), WritableMemoryStream allows int.MaxValue - if (stream is MemoryStream) - { - // MemoryStream.MaxStreamLength = Array.MaxLength = 2147483591 - // Setting position beyond this throws ArgumentOutOfRangeException - Assert.Throws(() => stream.Position = int.MaxValue); - } - else - { - // WritableMemoryStream should not throw even though it's way beyond capacity - stream.Position = int.MaxValue; - Assert.Equal(int.MaxValue, stream.Position); - } + // WritableMemoryStream allows Position up to int.MaxValue even though it's beyond capacity. + // Our override permits this — reads return -1, writes throw. + stream.Position = int.MaxValue; + Assert.Equal(int.MaxValue, stream.Position); } [Fact] @@ -259,30 +223,6 @@ public void SetLength_ThrowsNotSupportedException() Assert.Throws(() => stream.SetLength(20)); } - [Fact] - public async Task ReadAsync_SameResultSize_ReusesCachedTask() - { - byte[] data = new byte[20]; - for (int i = 0; i < 20; i++) data[i] = (byte)i; - Stream stream = new WritableMemoryStream(data); - - byte[] buffer1 = new byte[5]; - byte[] buffer2 = new byte[5]; - byte[] buffer3 = new byte[5]; - - Task task1 = stream.ReadAsync(buffer1, 0, 5); - Task task2 = stream.ReadAsync(buffer2, 0, 5); - Task task3 = stream.ReadAsync(buffer3, 0, 5); - - await task1; - await task2; - await task3; - - Assert.Equal(new byte[] { 0, 1, 2, 3, 4 }, buffer1); - Assert.Equal(new byte[] { 5, 6, 7, 8, 9 }, buffer2); - Assert.Equal(new byte[] { 10, 11, 12, 13, 14 }, buffer3); - } - [Fact] public async Task ReadAsync_DifferentResultSize_CreatesNewTask() { From 341ad5751259e7755dddafa240c2c20feb1fb7e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Tue, 14 Apr 2026 12:25:16 -0700 Subject: [PATCH 05/11] Address PR feedback --- .../System.Memory/ref/System.Memory.cs | 3 + .../System/Buffers/ReadOnlySequenceStream.cs | 78 +++++++++++++++++++ .../src/System/IO/ReadOnlyMemoryStream.cs | 3 + .../src/System/IO/StringStream.cs | 40 +++++++--- .../src/System/IO/WritableMemoryStream.cs | 45 +++++++---- .../System.Runtime/ref/System.Runtime.cs | 1 + 6 files changed, 145 insertions(+), 25 deletions(-) diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index c945416a5e81ac..75323b8cf8a316 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -176,6 +176,9 @@ public override void Flush() { } public override int Read(System.Span buffer) { throw null; } public override System.Threading.Tasks.Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } public override System.Threading.Tasks.ValueTask ReadAsync(System.Memory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public override int ReadByte() { throw null; } + public override void CopyTo(System.IO.Stream destination, int bufferSize) { } + public override System.Threading.Tasks.Task CopyToAsync(System.IO.Stream destination, int bufferSize, System.Threading.CancellationToken cancellationToken) { throw null; } public override long Seek(long offset, System.IO.SeekOrigin origin) { throw null; } public override void SetLength(long value) { } public override void Write(byte[] buffer, int offset, int count) { } diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs index bb15cd3addcc67..e43dac61e90c4c 100644 --- a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs +++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs @@ -72,6 +72,10 @@ public override long Position { _position = _sequence.End; } + else if (value >= _absolutePosition) + { + _position = _sequence.GetPosition(value - _absolutePosition, _position); + } else { _position = _sequence.GetPosition(value, _sequence.Start); @@ -111,6 +115,23 @@ public override int Read(Span buffer) return n; } + /// + public override int ReadByte() + { + EnsureNotDisposed(); + + if (_absolutePosition >= _sequence.Length) + { + return -1; + } + + ReadOnlySequence remaining = _sequence.Slice(_position); + byte value = remaining.FirstSpan[0]; + _position = _sequence.GetPosition(1, _position); + _absolutePosition++; + return value; + } + /// public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { @@ -137,6 +158,58 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return new ValueTask(bytesRead); } + /// + public override void CopyTo(Stream destination, int bufferSize) + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotDisposed(); + + if (_absolutePosition >= _sequence.Length) + { + return; + } + + ReadOnlySequence remaining = _sequence.Slice(_position); + foreach (ReadOnlyMemory segment in remaining) + { + destination.Write(segment.Span); + } + + _position = _sequence.End; + _absolutePosition = _sequence.Length; + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotDisposed(); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + if (_absolutePosition >= _sequence.Length) + { + return Task.CompletedTask; + } + + return CopyToAsyncCore(destination, cancellationToken); + } + + private async Task CopyToAsyncCore(Stream destination, CancellationToken cancellationToken) + { + ReadOnlySequence remaining = _sequence.Slice(_position); + foreach (ReadOnlyMemory segment in remaining) + { + await destination.WriteAsync(segment, cancellationToken).ConfigureAwait(false); + } + + _position = _sequence.End; + _absolutePosition = _sequence.Length; + } + /// public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); @@ -178,6 +251,10 @@ public override long Seek(long offset, SeekOrigin origin) { _position = _sequence.End; } + else if (absolutePosition >= _absolutePosition) + { + _position = _sequence.GetPosition(absolutePosition - _absolutePosition, _position); + } else { _position = _sequence.GetPosition(absolutePosition, _sequence.Start); @@ -201,6 +278,7 @@ public override Task FlushAsync(CancellationToken cancellationToken) => protected override void Dispose(bool disposing) { _isDisposed = true; + _sequence = default; base.Dispose(disposing); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs index 2a297c7a2e269e..f0b6e17ea42e68 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -213,6 +213,9 @@ public override long Seek(long offset, SeekOrigin origin) /// public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override void WriteByte(byte value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs index ed904cab5ad3c8..89dcb24bde8704 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs @@ -22,10 +22,12 @@ public sealed class StringStream : Stream private readonly Encoding _encoding; private int _charPosition; private bool _disposed; + private bool _encoderFlushed; // Spillover buffer for multibyte encodings: when the caller's buffer is too small // to hold even one encoded scalar (e.g., ReadByte with UTF-16), we encode into // this buffer and serve bytes from it across subsequent Read/ReadByte calls. + // Also used to hold final encoder flush bytes when the caller's buffer had no room. private byte[]? _pendingBytes; private int _pendingOffset; private int _pendingCount; @@ -98,7 +100,7 @@ public override int Read(Span buffer) { ObjectDisposedException.ThrowIf(_disposed, this); - if (buffer.Length == 0 || (_charPosition >= _text.Length && _pendingCount == 0)) + if (buffer.Length == 0 || (_charPosition >= _text.Length && _pendingCount == 0 && _encoderFlushed)) { return 0; } @@ -134,8 +136,7 @@ public override int Read(Span buffer) { _pendingBytes ??= new byte[_encoding.GetMaxByteCount(2)]; int charsToEncode = Math.Min(2, remaining.Length); - bool flush = _charPosition + charsToEncode >= _text.Length; - _encoder.Convert(remaining.Slice(0, charsToEncode), _pendingBytes, flush, out int charsUsed, out int bytesUsed, out _); + _encoder.Convert(remaining.Slice(0, charsToEncode), _pendingBytes, flush: false, out int charsUsed, out int bytesUsed, out _); _charPosition += charsUsed; int toCopy = Math.Min(bytesUsed, buffer.Length); @@ -153,16 +154,33 @@ public override int Read(Span buffer) _encoder.Convert(remaining, buffer, flush: false, out int charsUsed, out int bytesUsed, out _); _charPosition += charsUsed; totalBytesWritten += bytesUsed; + } + } + + // If all input chars are consumed but the encoder hasn't been flushed, + // flush any remaining encoder state (e.g., stateful encoding reset sequences). + // Always flush into _pendingBytes (which is guaranteed large enough) to + // avoid ArgumentException if the caller's remaining buffer is too small. + if (_charPosition >= _text.Length && !_encoderFlushed && _pendingCount == 0) + { + _pendingBytes ??= new byte[_encoding.GetMaxByteCount(2)]; + _encoder.Convert(ReadOnlySpan.Empty, _pendingBytes, flush: true, out _, out int flushBytes, out _); + _encoderFlushed = true; + + if (flushBytes > 0) + { + Span flushTarget = buffer.Slice(totalBytesWritten); + int toCopy = Math.Min(flushBytes, flushTarget.Length); + if (toCopy > 0) + { + _pendingBytes.AsSpan(0, toCopy).CopyTo(flushTarget); + totalBytesWritten += toCopy; + } - // If we consumed all remaining input, flush encoder state. - if (_charPosition >= _text.Length && bytesUsed > 0) + if (toCopy < flushBytes) { - Span flushBuf = buffer.Slice(bytesUsed); - if (flushBuf.Length > 0) - { - _encoder.Convert(ReadOnlySpan.Empty, flushBuf, flush: true, out _, out int flushBytes, out _); - totalBytesWritten += flushBytes; - } + _pendingOffset = toCopy; + _pendingCount = flushBytes - toCopy; } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs index e6ef0a460a3d8d..c5d1869cf18856 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -18,6 +18,7 @@ public sealed class WritableMemoryStream : MemoryStream { private Memory _buffer; private int _position; + private int _length; private bool _isOpen; /// @@ -27,6 +28,7 @@ public sealed class WritableMemoryStream : MemoryStream public WritableMemoryStream(Memory buffer) : base() { _buffer = buffer; + _length = 0; _isOpen = true; } @@ -57,7 +59,7 @@ public override long Length { EnsureNotClosed(); - return _buffer.Length; + return _length; } } @@ -87,7 +89,7 @@ public override int ReadByte() ReadOnlySpan span = _buffer.Span; int position = _position; - if ((uint)position < (uint)span.Length) + if ((uint)position < (uint)_length) { _position++; return span[position]; @@ -109,7 +111,7 @@ public override int Read(Span buffer) { EnsureNotClosed(); - int remaining = _buffer.Length - _position; + int remaining = _length - _position; if (remaining <= 0 || buffer.Length == 0) { return 0; @@ -155,10 +157,10 @@ public override void CopyTo(Stream destination, int bufferSize) ValidateCopyToArguments(destination, bufferSize); EnsureNotClosed(); - if (_buffer.Length > _position) + if (_length > _position) { - destination.Write(((ReadOnlyMemory)_buffer).Span.Slice(_position)); - _position = _buffer.Length; + destination.Write(((ReadOnlyMemory)_buffer).Span.Slice(_position, _length - _position)); + _position = _length; } } @@ -168,10 +170,10 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio ValidateCopyToArguments(destination, bufferSize); EnsureNotClosed(); - if (_buffer.Length > _position) + if (_length > _position) { - ReadOnlyMemory content = ((ReadOnlyMemory)_buffer).Slice(_position); - _position = _buffer.Length; + ReadOnlyMemory content = ((ReadOnlyMemory)_buffer).Slice(_position, _length - _position); + _position = _length; return destination.WriteAsync(content, cancellationToken).AsTask(); } @@ -190,6 +192,11 @@ public override void WriteByte(byte value) } _buffer.Span[_position++] = value; + + if (_position > _length) + { + _length = _position; + } } /// @@ -204,6 +211,11 @@ public override void Write(ReadOnlySpan buffer) { EnsureNotClosed(); + if (buffer.Length == 0) + { + return; + } + if (_position > _buffer.Length - buffer.Length) { throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); @@ -211,6 +223,11 @@ public override void Write(ReadOnlySpan buffer) buffer.CopyTo(_buffer.Span.Slice(_position)); _position += buffer.Length; + + if (_position > _length) + { + _length = _position; + } } /// @@ -298,13 +315,13 @@ public override bool TryGetBuffer(out ArraySegment buffer) public override byte[] ToArray() { EnsureNotClosed(); - if (_buffer.Length == 0) + if (_length == 0) { return Array.Empty(); } - byte[] copy = GC.AllocateUninitializedArray(_buffer.Length); - ((ReadOnlyMemory)_buffer).Span.CopyTo(copy); + byte[] copy = GC.AllocateUninitializedArray(_length); + ((ReadOnlyMemory)_buffer).Span.Slice(0, _length).CopyTo(copy); return copy; } @@ -314,9 +331,9 @@ public override void WriteTo(Stream stream) ArgumentNullException.ThrowIfNull(stream); EnsureNotClosed(); - if (_buffer.Length > 0) + if (_length > 0) { - stream.Write(((ReadOnlyMemory)_buffer).Span); + stream.Write(((ReadOnlyMemory)_buffer).Span.Slice(0, _length)); } } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 1e66167b9f6049..cfcdc0d72737a4 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -11074,6 +11074,7 @@ public override void Write(byte[] buffer, int offset, int count) { } public override void Write(System.ReadOnlySpan buffer) { } public override System.Threading.Tasks.Task WriteAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) { throw null; } public override System.Threading.Tasks.ValueTask WriteAsync(System.ReadOnlyMemory buffer, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public override void WriteByte(byte value) { } public override void WriteTo(System.IO.Stream stream) { } } public sealed partial class WritableMemoryStream : System.IO.MemoryStream From 111aa7707bf8c1ecc0dca7bf28da51bdbca46e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Fri, 29 May 2026 08:28:40 -0700 Subject: [PATCH 06/11] Address PR feedback: fix StringStream flush offset, WritableMemoryStream length/seek semantics, ReadOnlySequenceStream safety, and consolidate tests --- .../System.Memory/ref/System.Memory.cs | 2 +- .../System/Buffers/ReadOnlySequenceStream.cs | 23 ++---- .../src/System/IO/StringStream.cs | 11 +-- .../src/System/IO/WritableMemoryStream.cs | 2 +- .../Serialization/Json/JsonXmlDataContract.cs | 6 +- .../ReadOnlyMemoryStreamConformanceTests.cs | 4 +- .../StringStream/StringStreamTests_Memory.cs | 70 ++----------------- .../StringStream/StringStreamTests_String.cs | 47 +------------ .../WritableMemoryStreamConformanceTests.cs | 24 +++---- .../WritableMemoryStreamTests.cs | 24 ++++--- 10 files changed, 54 insertions(+), 159 deletions(-) diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index 48454b352586fc..86078321d5c729 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -164,7 +164,7 @@ namespace System.Buffers { public sealed partial class ReadOnlySequenceStream : System.IO.Stream { - public ReadOnlySequenceStream(System.Buffers.ReadOnlySequence sequence) { } + public ReadOnlySequenceStream(System.Buffers.ReadOnlySequence source) { } public override bool CanRead { get { throw null; } } public override bool CanSeek { get { throw null; } } public override bool CanWrite { get { throw null; } } diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs index e43dac61e90c4c..c786a694e51932 100644 --- a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs +++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.IO; @@ -14,7 +14,6 @@ namespace System.Buffers /// The underlying sequence should not be modified while the stream is in use. /// Seeking beyond the end of the stream is supported; subsequent reads will return zero bytes. /// - // Seekable Stream from ReadOnlySequence public sealed class ReadOnlySequenceStream : Stream { private ReadOnlySequence _sequence; @@ -25,11 +24,11 @@ public sealed class ReadOnlySequenceStream : Stream /// /// Initializes a new instance of the class over the specified . /// - /// The to wrap. - public ReadOnlySequenceStream(ReadOnlySequence sequence) + /// The to wrap. + public ReadOnlySequenceStream(ReadOnlySequence source) { - _sequence = sequence; - _position = sequence.Start; + _sequence = source; + _position = source.Start; _absolutePosition = 0; _isDisposed = false; } @@ -120,16 +119,8 @@ public override int ReadByte() { EnsureNotDisposed(); - if (_absolutePosition >= _sequence.Length) - { - return -1; - } - - ReadOnlySequence remaining = _sequence.Slice(_position); - byte value = remaining.FirstSpan[0]; - _position = _sequence.GetPosition(1, _position); - _absolutePosition++; - return value; + byte b = 0; + return Read(new Span(ref b)) > 0 ? b : -1; } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs index 89dcb24bde8704..11e2e7bb3c132f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs @@ -106,6 +106,7 @@ public override int Read(Span buffer) } int totalBytesWritten = 0; + int bufferBytesWritten = 0; // Drain any pending bytes from a previous partial read. if (_pendingCount > 0) @@ -142,6 +143,7 @@ public override int Read(Span buffer) int toCopy = Math.Min(bytesUsed, buffer.Length); _pendingBytes.AsSpan(0, toCopy).CopyTo(buffer); totalBytesWritten += toCopy; + bufferBytesWritten += toCopy; _pendingOffset = toCopy; _pendingCount = bytesUsed - toCopy; @@ -154,6 +156,7 @@ public override int Read(Span buffer) _encoder.Convert(remaining, buffer, flush: false, out int charsUsed, out int bytesUsed, out _); _charPosition += charsUsed; totalBytesWritten += bytesUsed; + bufferBytesWritten += bytesUsed; } } @@ -169,7 +172,7 @@ public override int Read(Span buffer) if (flushBytes > 0) { - Span flushTarget = buffer.Slice(totalBytesWritten); + Span flushTarget = buffer.Slice(bufferBytesWritten); int toCopy = Math.Min(flushBytes, flushTarget.Length); if (toCopy > 0) { @@ -191,10 +194,8 @@ public override int Read(Span buffer) /// public override int ReadByte() { - Span oneByte = stackalloc byte[1]; - int bytesRead = Read(oneByte); - - return bytesRead > 0 ? oneByte[0] : -1; + byte b = 0; + return Read(new Span(ref b)) > 0 ? b : -1; } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs index c5d1869cf18856..81690d7838078b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -281,7 +281,7 @@ public override long Seek(long offset, SeekOrigin origin) { SeekOrigin.Begin => offset, SeekOrigin.Current => _position + offset, - SeekOrigin.End => _buffer.Length + offset, + SeekOrigin.End => _length + offset, _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin) }; diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs index f431d1b4119dac..03623be82a277a 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs @@ -30,16 +30,16 @@ public JsonXmlDataContract(XmlDataContract traditionalXmlDataContract) DataContractSerializer dataContractSerializer = new DataContractSerializer(TraditionalDataContract.UnderlyingType, GetKnownTypesFromContext(context, context?.SerializerKnownTypeList), 1, false, false); // maxItemsInObjectGraph // ignoreExtensionDataObject // preserveObjectReferences - Stream memoryStream = new StringStream(xmlContent, Encoding.UTF8); + Stream stream = new StringStream(xmlContent, Encoding.UTF8); object? xmlValue; XmlDictionaryReaderQuotas? quotas = ((JsonReaderDelegator)jsonReader).ReaderQuotas; if (quotas == null) { - xmlValue = dataContractSerializer.ReadObject(memoryStream); + xmlValue = dataContractSerializer.ReadObject(stream); } else { - xmlValue = dataContractSerializer.ReadObject(XmlDictionaryReader.CreateTextReader(memoryStream, quotas)); + xmlValue = dataContractSerializer.ReadObject(XmlDictionaryReader.CreateTextReader(stream, quotas)); } context?.AddNewObject(xmlValue); return xmlValue; diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs index e5d6599d126572..bfa4a24b72980e 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs @@ -30,10 +30,10 @@ public class ReadOnlyMemoryStreamConformanceTests : StandaloneStreamConformanceT return Task.FromResult(new ReadOnlyMemoryStream(data)); } - // ReadOnlyMemoryStream does not support write-only mode + // Read-only stream does not support write-only mode protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); - // ReadOnlyMemoryStream does not support read-write mode + // Read-only stream does not support read-write mode protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs index fd2eecc78fe43f..77b63866024859 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs @@ -186,88 +186,28 @@ public async Task MultiByteCharactersAcrossChunkBoundary() } [Fact] - public void LengthThrowsNotSupportedException() + public void UnsupportedOperations_ThrowNotSupportedException() { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); + var stream = new StringStream("test".AsMemory(), Encoding.UTF8); Assert.Throws(() => stream.Length); - } - - [Fact] - public void PositionGetThrowsNotSupportedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - Assert.Throws(() => stream.Position); - } - - [Fact] - public void PositionSetThrowsNotSupportedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - Assert.Throws(() => stream.Position = 0); - } - - [Fact] - public void SeekThrowsNotSupportedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); - } - - [Fact] - public void WriteThrowsNotSupportedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - Assert.Throws(() => stream.Write(new byte[1], 0, 1)); - } - - [Fact] - public void SetLengthThrowsNotSupportedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - Assert.Throws(() => stream.SetLength(100)); } [Fact] - public void CanReadFalseAfterDispose() + public void Dispose_RendersStreamUnreadableAndIsIdempotent() { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); + var stream = new StringStream("test".AsMemory(), Encoding.UTF8); stream.Dispose(); Assert.False(stream.CanRead); - } + Assert.Throws(() => stream.Read(new byte[10], 0, 10)); - [Fact] - public void ReadAfterDispose_ThrowsObjectDisposedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - stream.Dispose(); - - byte[] buffer = new byte[10]; - Assert.Throws(() => stream.Read(buffer, 0, 10)); - } - - [Fact] - public void MultipleDispose_DoesNotThrow() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - - stream.Dispose(); stream.Dispose(); stream.Dispose(); } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs index 3ae8d1c1c34f4f..818baf9c2f6e09 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs @@ -72,23 +72,11 @@ public void ThrowsOnNullEncoding() } [Fact] - public void CanReadPropertyReturnsTrue() + public void StreamCapabilities_ReturnsExpectedValues() { var stream = new StringStream("test", Encoding.UTF8); Assert.True(stream.CanRead); - } - - [Fact] - public void CanSeekPropertyReturnsFalse() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.False(stream.CanSeek); - } - - [Fact] - public void CanWritePropertyReturnsFalse() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.False(stream.CanWrite); } @@ -100,44 +88,15 @@ public void EncodingPropertyReturnsCorrectEncoding() } [Fact] - public void LengthThrowsNotSupportedException() + public void UnsupportedOperations_ThrowNotSupportedException() { var stream = new StringStream("test", Encoding.UTF8); - Assert.Throws(() => stream.Length); - } - [Fact] - public void PositionGetThrowsNotSupportedException() - { - var stream = new StringStream("test", Encoding.UTF8); + Assert.Throws(() => stream.Length); Assert.Throws(() => stream.Position); - } - - [Fact] - public void PositionSetThrowsNotSupportedException() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.Throws(() => stream.Position = 0); - } - - [Fact] - public void SeekThrowsNotSupportedException() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); - } - - [Fact] - public void WriteThrowsNotSupportedException() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.Throws(() => stream.Write(new byte[1], 0, 1)); - } - - [Fact] - public void SetLengthThrowsNotSupportedException() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.Throws(() => stream.SetLength(100)); } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs index 5e875ffdc043f3..36bd788815d75d 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs @@ -20,24 +20,20 @@ public class WritableMemoryStreamConformanceTests : StandaloneStreamConformanceT protected override Task CreateReadWriteStreamCore(byte[]? initialData) { - // WritableMemoryStream wraps a fixed-capacity Memory buffer where Length == capacity. - // Unlike MemoryStream, there's no concept of "logical length" separate from capacity. - // This means WritableMemoryStream doesn't support the common pattern of creating an empty stream - // and writing to it to grow it. Many conformance tests rely on this pattern. - // - // Returning null here skips tests that require creating an initially-empty writable stream, - // as those tests fundamentally conflict with WritableMemoryStream's buffer-wrapping semantics. - if (initialData == null || initialData.Length == 0) + // WritableMemoryStream wraps a fixed-capacity Memory buffer. + // Length starts at 0 and grows as data is written, but the buffer cannot expand. + // Returning null for empty data skips conformance tests that rely on + // creating an initially-empty stream and growing it via writes. + if (initialData is null || initialData.Length == 0) { return Task.FromResult(null); } - var memory = new Memory(initialData); - return Task.FromResult(new WritableMemoryStream(memory)); + var memory = new Memory(new byte[initialData.Length]); + var stream = new WritableMemoryStream(memory); + stream.Write(initialData, 0, initialData.Length); + stream.Position = 0; + return Task.FromResult(stream); } - - // Note to both skipped tests: It was already verified that this works when using just WritableMemoryStream, - // before adding the 'forking' in Stream behavior for fast-path MemoryStream usage. - } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs index 69011e3a3446cd..6bd4d6b90ea083 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs @@ -110,6 +110,7 @@ public void Seek_FromEndNegativeOffset_PositionsCorrectly() { byte[] buffer = new byte[100]; Stream stream = new WritableMemoryStream(buffer); + stream.Write(new byte[100]); // Seek to 10 bytes before end long newPosition = stream.Seek(-10, SeekOrigin.End); @@ -121,18 +122,19 @@ public void Seek_FromEndNegativeOffset_PositionsCorrectly() [Fact] public void Write_OverExistingData_ReplacesData() { - byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; - Stream stream = new WritableMemoryStream(new Memory(buffer)); + byte[] initialData = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + byte[] backing = new byte[10]; + Stream stream = new WritableMemoryStream(new Memory(backing)); + stream.Write(initialData, 0, initialData.Length); - // Overwrite positions 3-5 with new data stream.Position = 3; stream.Write(new byte[] { 100, 101, 102 }, 0, 3); - // Verify overwrite stream.Position = 0; byte[] result = new byte[10]; - stream.Read(result, 0, 10); + int bytesRead = stream.Read(result, 0, 10); + Assert.Equal(10, bytesRead); Assert.Equal(new byte[] { 1, 2, 3, 100, 101, 102, 7, 8, 9, 10 }, result); } @@ -201,7 +203,7 @@ public void Write_ZeroBytes_Succeeds() stream.Write(new byte[0], 0, 0); Assert.Equal(0, stream.Position); - Assert.Equal(10, stream.Length); // Length from initial buffer + Assert.Equal(0, stream.Length); } [Fact] @@ -228,7 +230,10 @@ public async Task ReadAsync_DifferentResultSize_CreatesNewTask() { byte[] data = new byte[10]; for (int i = 0; i < 10; i++) data[i] = (byte)i; - Stream stream = new WritableMemoryStream(data); + byte[] backing = new byte[10]; + Stream stream = new WritableMemoryStream(backing); + stream.Write(data, 0, data.Length); + stream.Position = 0; byte[] buffer1 = new byte[5]; byte[] buffer2 = new byte[3]; @@ -250,7 +255,10 @@ public async Task ReadAsync_DifferentResultSize_CreatesNewTask() public async Task ReadAsync_ArrayBackedMemory_UsesFastPath() { byte[] data = { 10, 20, 30, 40, 50 }; - Stream stream = new WritableMemoryStream(data); + byte[] backing = new byte[5]; + Stream stream = new WritableMemoryStream(backing); + stream.Write(data, 0, data.Length); + stream.Position = 0; byte[] arrayBuffer = new byte[3]; Memory memory = arrayBuffer.AsMemory(); From 0946bfc5cae15f99a15c161be9c6e5ab0644f76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Tue, 16 Jun 2026 12:32:51 -0700 Subject: [PATCH 07/11] Address PR feedback: remove redundant code, improve test coverage and structure --- .../System/Buffers/ReadOnlySequenceStream.cs | 6 +- ...ReadOnlySequenceStream.ConformanceTests.cs | 50 +-- .../ReadOnlySequenceStreamTests.cs | 169 +--------- .../src/System.Net.Http.csproj | 2 - .../src/System/IO/WritableMemoryStream.cs | 37 +-- .../Serialization/Json/JsonXmlDataContract.cs | 6 +- .../ReadOnlyMemoryStreamConformanceTests.cs | 17 +- .../ReadOnlyMemoryStreamTests.cs | 236 ++------------ .../StringStreamConformanceTests.cs | 103 ++++-- .../StringStream/StringStreamTestBase.cs | 134 ++++++++ .../StringStream/StringStreamTests_Memory.cs | 292 +++++------------- .../StringStream/StringStreamTests_String.cs | 202 +++--------- .../System.IO.Tests/System.IO.Tests.csproj | 1 + .../WritableMemoryStreamConformanceTests.cs | 21 +- .../WritableMemoryStreamTests.cs | 182 +++-------- 15 files changed, 476 insertions(+), 982 deletions(-) create mode 100644 src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTestBase.cs diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs index e43dac61e90c4c..c065ee68e67564 100644 --- a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs +++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs @@ -154,8 +154,8 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return ValueTask.FromCanceled(cancellationToken); } - int bytesRead = Read(buffer.Span); - return new ValueTask(bytesRead); + int n = Read(buffer.Span); + return new ValueTask(n); } /// @@ -240,13 +240,11 @@ public override long Seek(long offset, SeekOrigin origin) _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)) }; - // Negative positions are invalid if (absolutePosition < 0) { throw new IOException(SR.IO_SeekBeforeBegin); } - // Update position - seeking past end is allowed if (absolutePosition >= _sequence.Length) { _position = _sequence.End; diff --git a/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs index 3b5cc139be7ff6..0d6f40f83723f5 100644 --- a/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs +++ b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs @@ -7,40 +7,50 @@ namespace System.Memory.Tests { - /// - /// Conformance tests for ReadOnlySequenceStream - a read-only, seekable stream - /// wrapper around ReadOnlySequence{byte}. - /// public class ROSequenceStreamConformanceTests : StandaloneStreamConformanceTests { - // StreamConformanceTests flags to specify capabilities protected override bool CanSeek => true; - // SetLength() is not supported because ReadOnlySequence{byte} is immutable. protected override bool CanSetLength => false; - // ReadOnlySequenceStream doesn't buffer writes (it's read-only), protected override bool NopFlushCompletesSynchronously => true; protected override Task CreateReadOnlyStreamCore(byte[]? initialData) { - if (initialData == null || initialData.Length == 0) + if (initialData is null || initialData.Length == 0) { - // Create empty sequence for null or empty data - var emptySequence = ReadOnlySequence.Empty; - return Task.FromResult(new ReadOnlySequenceStream(emptySequence)); + return Task.FromResult( + new ReadOnlySequenceStream(ReadOnlySequence.Empty)); } - // ReadOnlySequence can be constructed from: - // 1. ReadOnlyMemory (single segment) - // 2. ReadOnlySequenceSegment chain (multi-segment) - var sequence = new ReadOnlySequence(initialData); // Single segment - return Task.FromResult(new ReadOnlySequenceStream(sequence)); + return Task.FromResult( + new ReadOnlySequenceStream(CreateSequence(initialData))); } - // Immutable - protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); + protected virtual ReadOnlySequence CreateSequence(byte[] data) + => new ReadOnlySequence(data); + + protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) + => Task.FromResult(null); + protected override Task CreateReadWriteStreamCore(byte[]? initialData) + => Task.FromResult(null); + } + + /// + /// Runs the full conformance suite against multi-segment sequences (3 segments). + /// + public class ROSequenceStreamMultiSegmentConformanceTests : ROSequenceStreamConformanceTests + { + protected override ReadOnlySequence CreateSequence(byte[] data) + => ReadOnlySequenceFactory.SplitInThree.CreateWithContent(data); + } - // Immutable - protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); + /// + /// Runs the full conformance suite against maximally-fragmented sequences + /// (one byte per segment with empty segments interspersed). + /// + public class ROSequenceStreamSegmentPerItemConformanceTests : ROSequenceStreamConformanceTests + { + protected override ReadOnlySequence CreateSequence(byte[] data) + => ReadOnlySequenceFactory.SegmentPerItemFactory.CreateWithContent(data); } } diff --git a/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs index 17abff98bfe5d5..7cfffadf10d033 100644 --- a/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs +++ b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs @@ -7,164 +7,23 @@ namespace System.Memory.Tests { - /// - /// Additional specific tests for ReadOnlySequenceStream beyond conformance tests. - /// public class ReadOnlySequenceStreamTests { - // NOTE: Conformance tests' coverage: Ctor correctness, stream capabilities, - // Position, Length, Seek, Read, exceptions for unsupported operations. - - // Not covered in conformance tests: Stream + multi-segment sequences - // ReadOnlySequence{byte} can represent data spread across - // multiple memory segments (linked list of ReadOnlyMemory{byte}). - // This is common in network buffers and pooled memory scenarios. - [Fact] - public void Read_MultiSegmentSequence_ReturnsCorrectData() - { - // Create multi-segment sequence: [1,2,3] -> [4,5,6] -> [7,8,9] - var segment1 = new TestSegment(new byte[] { 1, 2, 3 }); - var segment2 = segment1.Append(new byte[] { 4, 5, 6 }); - var segment3 = segment2.Append(new byte[] { 7, 8, 9 }); - - var sequence = new ReadOnlySequence(segment1, 0, segment3, 3); - var stream = new ReadOnlySequenceStream(sequence); - - // Read all data - byte[] buffer = new byte[9]; - int totalRead = 0; - - while (totalRead < 9) - { - int bytesRead = stream.Read(buffer, totalRead, 9 - totalRead); - if (bytesRead == 0) break; - totalRead += bytesRead; - } - - Assert.Equal(9, totalRead); - Assert.Equal(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, buffer); - } - - [Fact] - public void Seek_MultiSegmentSequence_WorksCorrectly() - { - // Create multi-segment sequence: [1,2,3] -> [4,5,6] - var segment1 = new TestSegment(new byte[] { 1, 2, 3 }); - var segment2 = segment1.Append(new byte[] { 4, 5, 6 }); - - var sequence = new ReadOnlySequence(segment1, 0, segment2, 3); - var stream = new ReadOnlySequenceStream(sequence); - - // Seek into second segment - stream.Seek(4, SeekOrigin.Begin); // Should be at byte '5' - - byte[] buffer = new byte[1]; - stream.Read(buffer, 0, 1); - - Assert.Equal(5, buffer[0]); - Assert.Equal(5, stream.Position); - } - - [Fact] - public void Seek_AcrossSegments_BothDirections() - { - // Arrange: [10,20,30] -> [40,50,60] - var segment1 = new TestSegment(new byte[] { 10, 20, 30 }); - var segment2 = segment1.Append(new byte[] { 40, 50, 60 }); - - var sequence = new ReadOnlySequence(segment1, 0, segment2, 3); - var stream = new ReadOnlySequenceStream(sequence); - - byte[] buffer = new byte[1]; - - // Act & Assert: Start at position 2 (byte 30) - stream.Position = 2; - stream.Read(buffer, 0, 1); - Assert.Equal(30, buffer[0]); - - // Seek forward into segment 2 - stream.Seek(2, SeekOrigin.Current); // Now at position 5 (byte 60) - stream.Read(buffer, 0, 1); - Assert.Equal(60, buffer[0]); - - // Seek backward into segment 1 - stream.Seek(-4, SeekOrigin.Current); // Now at position 2 (byte 30) - stream.Read(buffer, 0, 1); - Assert.Equal(30, buffer[0]); - } - [Fact] - public void Position_MultiSegmentSequence_TracksCorrectly() + public void ReadZeroBytesReturnsZero() { - // Arrange: [1,2] -> [3,4] -> [5,6] - var segment1 = new TestSegment(new byte[] { 1, 2 }); - var segment2 = segment1.Append(new byte[] { 3, 4 }); - var segment3 = segment2.Append(new byte[] { 5, 6 }); - - var sequence = new ReadOnlySequence(segment1, 0, segment3, 2); - var stream = new ReadOnlySequenceStream(sequence); - - byte[] buffer = new byte[1]; - - // Act & Assert: Position advances correctly through segments - Assert.Equal(0, stream.Position); - - stream.Read(buffer, 0, 1); // Read from segment 1 - Assert.Equal(1, stream.Position); - - stream.Read(buffer, 0, 1); // Read from segment 1 - Assert.Equal(2, stream.Position); - - stream.Read(buffer, 0, 1); // Read from segment 2 (boundary cross) - Assert.Equal(3, stream.Position); - - stream.Read(buffer, 0, 1); // Read from segment 2 - Assert.Equal(4, stream.Position); - - stream.Read(buffer, 0, 1); // Read from segment 3 (boundary cross) - Assert.Equal(5, stream.Position); - - stream.Read(buffer, 0, 1); // Read from segment 3 - Assert.Equal(6, stream.Position); - } - - /// - /// Helper class for creating multi-segment ReadOnlySequence{byte} for testing. - /// - private class TestSegment : ReadOnlySequenceSegment - { - public TestSegment(byte[] data) - { - Memory = data; - } - - public TestSegment Append(byte[] data) - { - var segment = new TestSegment(data) - { - RunningIndex = RunningIndex + Memory.Length - }; - Next = segment; - return segment; - } - } - - // Basic edge cases - [Fact] - public void Read_ZeroBytes_ReturnsZero() - { - var data = new byte[] { 1, 2, 3 }; + byte[] data = [1, 2, 3]; var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); byte[] buffer = new byte[10]; int bytesRead = stream.Read(buffer, 0, 0); Assert.Equal(0, bytesRead); - Assert.Equal(0, stream.Position); // Position shouldn't change + Assert.Equal(0, stream.Position); } [Fact] - public void EmptySequence_BehavesCorrectly() + public void SeekingBeyondEmptyBufferIsAllowed() { var stream = new ReadOnlySequenceStream(ReadOnlySequence.Empty); @@ -175,20 +34,18 @@ public void EmptySequence_BehavesCorrectly() int bytesRead = stream.Read(buffer, 0, 10); Assert.Equal(0, bytesRead); - // Seek to position 0 should succeed stream.Seek(0, SeekOrigin.Begin); Assert.Equal(0, stream.Position); - // Seeking beyond empty buffer is allowed long newPosition = stream.Seek(1, SeekOrigin.Begin); Assert.Equal(1, newPosition); Assert.Equal(1, stream.Position); } [Fact] - public async Task ReadAsync_SameResultSize_ReusesCachedTask() + public async Task ReadAsyncSameResultSizeReusesCachedTask() { - var data = new byte[20]; + byte[] data = new byte[20]; for (int i = 0; i < 20; i++) data[i] = (byte)i; var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); @@ -213,9 +70,9 @@ public async Task ReadAsync_SameResultSize_ReusesCachedTask() } [Fact] - public async Task ReadAsync_DifferentResultSize_CreatesNewTask() + public async Task ReadAsyncDifferentResultSizeCreatesNewTask() { - var data = new byte[10]; + byte[] data = new byte[10]; for (int i = 0; i < 10; i++) data[i] = (byte)i; var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); @@ -223,9 +80,9 @@ public async Task ReadAsync_DifferentResultSize_CreatesNewTask() byte[] buffer2 = new byte[3]; byte[] buffer3 = new byte[2]; - Task task1 = stream.ReadAsync(buffer1, 0, 5); // Returns 5 - Task task2 = stream.ReadAsync(buffer2, 0, 3); // Returns 3 - Task task3 = stream.ReadAsync(buffer3, 0, 2); // Returns 2 + Task task1 = stream.ReadAsync(buffer1, 0, 5); + Task task2 = stream.ReadAsync(buffer2, 0, 3); + Task task3 = stream.ReadAsync(buffer3, 0, 2); await task1; await task2; @@ -236,9 +93,9 @@ public async Task ReadAsync_DifferentResultSize_CreatesNewTask() } [Fact] - public async Task ReadAsync_ArrayBackedMemory_UsesFastPath() + public async Task ReadAsyncArrayBackedMemoryUsesFastPath() { - var data = new byte[] { 10, 20, 30, 40, 50 }; + byte[] data = [10, 20, 30, 40, 50]; var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); byte[] arrayBuffer = new byte[3]; diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index 06c5430d2db5c7..268572d39133c5 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -128,8 +128,6 @@ Link="Common\DisableRuntimeMarshalling.cs" /> - buffer) } int bytesToRead = Math.Min(remaining, buffer.Length); - ((ReadOnlyMemory)_buffer).Span.Slice(_position, bytesToRead).CopyTo(buffer); + _buffer.Span.Slice(_position, bytesToRead).CopyTo(buffer); _position += bytesToRead; return bytesToRead; @@ -159,7 +159,7 @@ public override void CopyTo(Stream destination, int bufferSize) if (_length > _position) { - destination.Write(((ReadOnlyMemory)_buffer).Span.Slice(_position, _length - _position)); + destination.Write(_buffer.Span.Slice(_position, _length - _position)); _position = _length; } } @@ -172,7 +172,7 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio if (_length > _position) { - ReadOnlyMemory content = ((ReadOnlyMemory)_buffer).Slice(_position, _length - _position); + ReadOnlyMemory content = _buffer.Slice(_position, _length - _position); _position = _length; return destination.WriteAsync(content, cancellationToken).AsTask(); @@ -234,42 +234,29 @@ public override void Write(ReadOnlySpan buffer) public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { ValidateBufferArguments(buffer, offset, count); + EnsureNotClosed(); if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } - try - { - Write(buffer, offset, count); - - return Task.CompletedTask; - } - catch (Exception exception) - { - return Task.FromException(exception); - } + Write(buffer, offset, count); + return Task.CompletedTask; } /// public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { + EnsureNotClosed(); + if (cancellationToken.IsCancellationRequested) { return ValueTask.FromCanceled(cancellationToken); } - try - { - Write(buffer.Span); - - return default; - } - catch (Exception exception) - { - return ValueTask.FromException(exception); - } + Write(buffer.Span); + return default; } /// @@ -321,7 +308,7 @@ public override byte[] ToArray() } byte[] copy = GC.AllocateUninitializedArray(_length); - ((ReadOnlyMemory)_buffer).Span.Slice(0, _length).CopyTo(copy); + _buffer.Span.Slice(0, _length).CopyTo(copy); return copy; } @@ -333,7 +320,7 @@ public override void WriteTo(Stream stream) if (_length > 0) { - stream.Write(((ReadOnlyMemory)_buffer).Span.Slice(0, _length)); + stream.Write(_buffer.Span.Slice(0, _length)); } } diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs index f431d1b4119dac..a66f8da44cfcac 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs @@ -30,16 +30,16 @@ public JsonXmlDataContract(XmlDataContract traditionalXmlDataContract) DataContractSerializer dataContractSerializer = new DataContractSerializer(TraditionalDataContract.UnderlyingType, GetKnownTypesFromContext(context, context?.SerializerKnownTypeList), 1, false, false); // maxItemsInObjectGraph // ignoreExtensionDataObject // preserveObjectReferences - Stream memoryStream = new StringStream(xmlContent, Encoding.UTF8); + StringStream stream = new(xmlContent, Encoding.UTF8); object? xmlValue; XmlDictionaryReaderQuotas? quotas = ((JsonReaderDelegator)jsonReader).ReaderQuotas; if (quotas == null) { - xmlValue = dataContractSerializer.ReadObject(memoryStream); + xmlValue = dataContractSerializer.ReadObject(stream); } else { - xmlValue = dataContractSerializer.ReadObject(XmlDictionaryReader.CreateTextReader(memoryStream, quotas)); + xmlValue = dataContractSerializer.ReadObject(XmlDictionaryReader.CreateTextReader(stream, quotas)); } context?.AddNewObject(xmlValue); return xmlValue; diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs index e5d6599d126572..c8a0929de319a7 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs @@ -5,35 +5,24 @@ namespace System.IO.Tests { - /// - /// Conformance tests for ReadOnlyMemoryStream - a read-only, seekable stream - /// over a ReadOnlyMemory<byte>. - /// public class ReadOnlyMemoryStreamConformanceTests : StandaloneStreamConformanceTests { protected override bool CanSeek => true; - protected override bool CanSetLength => false; // Immutable stream + protected override bool CanSetLength => false; protected override bool NopFlushCompletesSynchronously => true; - /// - /// Creates a read-only ReadOnlyMemoryStream with provided initial data. - /// protected override Task CreateReadOnlyStreamCore(byte[]? initialData) { - if (initialData == null || initialData.Length == 0) + if (initialData is null || initialData.Length == 0) { - // Empty data return Task.FromResult(new ReadOnlyMemoryStream(ReadOnlyMemory.Empty)); } - var data = new ReadOnlyMemory(initialData); - return Task.FromResult(new ReadOnlyMemoryStream(data)); + return Task.FromResult(new ReadOnlyMemoryStream(new ReadOnlyMemory(initialData))); } - // ReadOnlyMemoryStream does not support write-only mode protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); - // ReadOnlyMemoryStream does not support read-write mode protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs index 0b200e43242b76..2bb55aa98bafc1 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs @@ -1,59 +1,30 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Threading.Tasks; using Xunit; namespace System.IO.Tests { - /// - /// Additional specific tests for ReadOnlyMemoryStream beyond conformance tests. - /// public class ReadOnlyMemoryStreamTests { [Fact] - public void Constructor_CreatesReadOnlySeekableStream() - { - byte[] buffer = new byte[100]; - Stream stream = new ReadOnlyMemoryStream(new ReadOnlyMemory(buffer)); - - Assert.True(stream.CanRead); - Assert.False(stream.CanWrite); - Assert.True(stream.CanSeek); - Assert.Equal(100, stream.Length); - Assert.Equal(0, stream.Position); - } - - // Empty ReadOnlyMemory creates valid zero-length stream. - [Fact] - public void Constructor_EmptyMemory_CreatesZeroLengthStream() - { - ReadOnlyMemory emptyMemory = ReadOnlyMemory.Empty; - Stream stream = new ReadOnlyMemoryStream(emptyMemory); - - Assert.Equal(0, stream.Length); - Assert.Equal(0, stream.Position); - Assert.True(stream.CanRead); - Assert.False(stream.CanWrite); - } - - [Fact] - public void Constructor_FromMemory_WorksCorrectly() + public void ConstructorFromMemoryImplicitConversion() { byte[] buffer = { 1, 2, 3, 4, 5 }; Memory memory = buffer; - Stream stream = new ReadOnlyMemoryStream(memory); // Implicit conversion + Stream stream = new ReadOnlyMemoryStream(memory); Assert.Equal(5, stream.Length); Assert.True(stream.CanRead); } - // Not covered in conformance tests: ReadOnlyMemory slices stream handling [Fact] - public void Stream_WorksWithSlicedMemory() + public void WorksWithSlicedMemory() { byte[] largeBuffer = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - ReadOnlyMemory slice = largeBuffer.AsMemory(3, 4); // [3, 4, 5, 6] + ReadOnlyMemory slice = largeBuffer.AsMemory(3, 4); Stream stream = new ReadOnlyMemoryStream(slice); Assert.Equal(4, stream.Length); @@ -66,90 +37,7 @@ public void Stream_WorksWithSlicedMemory() } [Fact] - public void Position_AdvancesDuringRead() - { - byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; - Stream stream = new ReadOnlyMemoryStream(buffer); - byte[] readBuffer = new byte[3]; - - Assert.Equal(0, stream.Position); - - stream.Read(readBuffer, 0, 3); - Assert.Equal(3, stream.Position); - - stream.Read(readBuffer, 0, 3); - Assert.Equal(6, stream.Position); - - stream.Read(readBuffer, 0, 3); - Assert.Equal(9, stream.Position); - } - - [Fact] - public void Seek_FromCurrent_RelativeOffset() - { - Stream stream = new ReadOnlyMemoryStream(new byte[100]); - stream.Position = 50; - - // Seek forward 10 bytes - long newPosition = stream.Seek(10, SeekOrigin.Current); - Assert.Equal(60, newPosition); - - // Seek backward 20 bytes - newPosition = stream.Seek(-20, SeekOrigin.Current); - Assert.Equal(40, newPosition); - } - - [Fact] - public void Seek_InvalidOrigin_ThrowsArgumentException() - { - Stream stream = new ReadOnlyMemoryStream(new byte[100]); - - Assert.Throws(() => stream.Seek(0, (SeekOrigin)999)); - } - - [Fact] - public void Read_ReturnsCorrectData() - { - byte[] data = { 10, 20, 30, 40, 50 }; - Stream stream = new ReadOnlyMemoryStream(data); - byte[] buffer = new byte[3]; - - int bytesRead = stream.Read(buffer, 0, 3); - - Assert.Equal(3, bytesRead); - Assert.Equal(new byte[] { 10, 20, 30 }, buffer); - Assert.Equal(3, stream.Position); - } - - [Fact] - public void Read_LargerThanAvailable_ReturnsPartialData() - { - byte[] data = { 1, 2, 3 }; - Stream stream = new ReadOnlyMemoryStream(data); - byte[] buffer = new byte[10]; - - int bytesRead = stream.Read(buffer, 0, 10); - - Assert.Equal(3, bytesRead); - Assert.Equal(new byte[] { 1, 2, 3 }, buffer[..3]); - } - - [Fact] - public void Read_AfterSeek_ReturnsCorrectData() - { - byte[] data = { 10, 20, 30, 40, 50 }; - Stream stream = new ReadOnlyMemoryStream(data); - - stream.Seek(2, SeekOrigin.Begin); - byte[] buffer = new byte[2]; - int bytesRead = stream.Read(buffer, 0, 2); - - Assert.Equal(2, bytesRead); - Assert.Equal(new byte[] { 30, 40 }, buffer); - } - - [Fact] - public void Read_DoesNotModifyUnderlyingMemory() + public void ReadDoesNotModifyUnderlyingMemory() { byte[] originalData = { 1, 2, 3, 4, 5 }; byte[] dataCopy = (byte[])originalData.Clone(); @@ -158,94 +46,11 @@ public void Read_DoesNotModifyUnderlyingMemory() byte[] buffer = new byte[5]; stream.Read(buffer, 0, 5); - // Original data should be unchanged Assert.Equal(dataCopy, originalData); } [Fact] - public void Write_ThrowsNotSupportedException() - { - Stream stream = new ReadOnlyMemoryStream(new ReadOnlyMemory(new byte[10])); - byte[] data = { 1, 2, 3 }; - - Assert.Throws(() => stream.Write(data, 0, 3)); - } - - [Fact] - public void SetLength_ThrowsNotSupportedException() - { - Stream stream = new ReadOnlyMemoryStream(new byte[10]); - Assert.Throws(() => stream.SetLength(20)); - } - - [Fact] - public void Dispose_SetsCanPropertiesToFalse() - { - Stream stream = new ReadOnlyMemoryStream(new byte[10]); - - stream.Dispose(); - - Assert.False(stream.CanRead); - Assert.False(stream.CanSeek); - Assert.False(stream.CanWrite); - } - - [Fact] - public void Operations_AfterDispose_ThrowObjectDisposedException() - { - byte[] buffer = new byte[10]; - Stream stream = new ReadOnlyMemoryStream(buffer); - stream.Dispose(); - - Assert.Throws(() => stream.Read(new byte[5], 0, 5)); - Assert.Throws(() => stream.ReadByte()); - Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); - Assert.Throws(() => _ = stream.Position); - Assert.Throws(() => stream.Position = 0); - Assert.Throws(() => _ = stream.Length); - } - - // Standard IDisposable pattern - Dispose() should be idempotent. - [Fact] - public void Dispose_MultipleCalls_DoesNotThrow() - { - Stream stream = new ReadOnlyMemoryStream(new byte[10]); - - stream.Dispose(); - stream.Dispose(); // Should not throw - stream.Dispose(); // Should not throw - } - - [Fact] - public void Read_NullBuffer_ThrowsArgumentNullException() - { - Stream stream = new ReadOnlyMemoryStream(new byte[10]); - - Assert.Throws(() => stream.Read(null!, 0, 5)); - } - - [Fact] - public void EmptyBuffer_BehavesCorrectly() - { - Stream stream = new ReadOnlyMemoryStream(ReadOnlyMemory.Empty); - - Assert.Equal(0, stream.Length); - Assert.Equal(0, stream.Position); - - byte[] buffer = new byte[10]; - Assert.Equal(0, stream.Read(buffer, 0, 10)); - - stream.Seek(0, SeekOrigin.Begin); - Assert.Equal(0, stream.Position); - - // Seeking beyond empty buffer is allowed - long newPosition = stream.Seek(1, SeekOrigin.Begin); - Assert.Equal(1, newPosition); - Assert.Equal(1, stream.Position); - } - - [Fact] - public async Task ReadAsync_SameResultSize_ReusesCachedTask() + public async Task ReadAsyncSameResultSizeReusesCachedTask() { byte[] data = new byte[20]; for (int i = 0; i < 20; i++) data[i] = (byte)i; @@ -272,7 +77,7 @@ public async Task ReadAsync_SameResultSize_ReusesCachedTask() } [Fact] - public async Task ReadAsync_DifferentResultSize_CreatesNewTask() + public async Task ReadAsyncDifferentResultSizeCreatesNewTask() { byte[] data = new byte[10]; for (int i = 0; i < 10; i++) data[i] = (byte)i; @@ -282,9 +87,9 @@ public async Task ReadAsync_DifferentResultSize_CreatesNewTask() byte[] buffer2 = new byte[3]; byte[] buffer3 = new byte[2]; - Task task1 = stream.ReadAsync(buffer1, 0, 5); // Returns 5 - Task task2 = stream.ReadAsync(buffer2, 0, 3); // Returns 3 - Task task3 = stream.ReadAsync(buffer3, 0, 2); // Returns 2 + Task task1 = stream.ReadAsync(buffer1, 0, 5); + Task task2 = stream.ReadAsync(buffer2, 0, 3); + Task task3 = stream.ReadAsync(buffer3, 0, 2); await task1; await task2; @@ -295,7 +100,7 @@ public async Task ReadAsync_DifferentResultSize_CreatesNewTask() } [Fact] - public async Task ReadAsync_ArrayBackedMemory_UsesFastPath() + public async Task ReadAsyncArrayBackedMemoryUsesFastPath() { byte[] data = { 10, 20, 30, 40, 50 }; Stream stream = new ReadOnlyMemoryStream(data); @@ -308,5 +113,22 @@ public async Task ReadAsync_ArrayBackedMemory_UsesFastPath() Assert.Equal(3, bytesRead); Assert.Equal(new byte[] { 10, 20, 30 }, arrayBuffer); } + + [Fact] + public void ReadFromUnmanagedMemory() + { + byte[] expected = [1, 2, 3, 4, 5]; + + using var manager = new NativeMemoryManager(expected.Length); + expected.CopyTo(manager.GetSpan()); + + using var stream = new ReadOnlyMemoryStream(manager.Memory); + + byte[] result = new byte[expected.Length]; + int bytesRead = stream.Read(result); + + Assert.Equal(expected.Length, bytesRead); + Assert.Equal(expected, result); + } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs index 45afe263b65323..8d89332bd15a0c 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs @@ -6,9 +6,6 @@ namespace System.IO.Tests { - /// - /// Conformance tests for StringStream using the ReadOnlyMemory{char} overload. - /// public class StringStreamConformanceTests_Memory : StandaloneStreamConformanceTests { protected override bool CanSeek => false; @@ -22,15 +19,12 @@ public class StringStreamConformanceTests_Memory : StandaloneStreamConformanceTe return Task.FromResult(new StringStream(ReadOnlyMemory.Empty, Encoding.UTF8)); } - string sourceString = Encoding.UTF8.GetString(initialData); - - byte[] reencoded = Encoding.UTF8.GetBytes(sourceString); - if (reencoded.Length != initialData.Length || !reencoded.AsSpan().SequenceEqual(initialData)) - { - return Task.FromResult(null); - } + char[] chars = new char[initialData.Length]; + for (int i = 0; i < initialData.Length; i++) + chars[i] = (char)initialData[i]; - return Task.FromResult(new StringStream(sourceString.AsMemory(), Encoding.UTF8)); + return Task.FromResult( + new StringStream(chars.AsMemory(), IdentityEncoding.Instance)); } protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); @@ -38,9 +32,6 @@ public class StringStreamConformanceTests_Memory : StandaloneStreamConformanceTe protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); } - /// - /// Conformance tests for StringStream using the string overload. - /// public class StringStreamConformanceTests_String : StandaloneStreamConformanceTests { protected override bool CanSeek => false; @@ -51,22 +42,90 @@ public class StringStreamConformanceTests_String : StandaloneStreamConformanceTe { if (initialData is null || initialData.Length == 0) { - return Task.FromResult(new StringStream("", Encoding.UTF8)); + return Task.FromResult(new StringStream("", IdentityEncoding.Instance)); } - string sourceString = Encoding.UTF8.GetString(initialData); + char[] chars = new char[initialData.Length]; + for (int i = 0; i < initialData.Length; i++) + chars[i] = (char)initialData[i]; + + return Task.FromResult( + new StringStream(new string(chars), IdentityEncoding.Instance)); + } + + protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); - byte[] reencoded = Encoding.UTF8.GetBytes(sourceString); - if (reencoded.Length != initialData.Length || !reencoded.AsSpan().SequenceEqual(initialData)) + protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); + } + + /// + /// Maps each char to/from a single byte (1:1), allowing conformance tests to + /// exercise StringStream with arbitrary byte data without UTF-8 round-trip issues. + /// Provides a custom Encoder because the base Encoder.Convert throws + /// "Conversion buffer overflow" when called with 0 chars (encoder flush path). + /// + internal sealed class IdentityEncoding : Encoding + { + public static IdentityEncoding Instance { get; } = new(); + + public override int GetByteCount(char[] chars, int index, int count) => count; + + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) + { + for (int i = 0; i < charCount; i++) { - return Task.FromResult(null); + bytes[byteIndex + i] = (byte)chars[charIndex + i]; } - return Task.FromResult(new StringStream(sourceString, Encoding.UTF8)); + return charCount; } - protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); + public override int GetCharCount(byte[] bytes, int index, int count) => count; - protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + { + for (int i = 0; i < byteCount; i++) + { + chars[charIndex + i] = (char)bytes[byteIndex + i]; + } + + return byteCount; + } + + public override int GetMaxByteCount(int charCount) => charCount; + + public override int GetMaxCharCount(int byteCount) => byteCount; + + public override byte[] GetPreamble() => Array.Empty(); + + public override Encoder GetEncoder() => new IdentityEncoder(); + + private sealed class IdentityEncoder : Encoder + { + public override int GetByteCount(char[] chars, int index, int count, bool flush) => count; + + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex, bool flush) + { + for (int i = 0; i < charCount; i++) + { + bytes[byteIndex + i] = (byte)chars[charIndex + i]; + } + + return charCount; + } + + public override void Convert(ReadOnlySpan chars, Span bytes, bool flush, out int charsUsed, out int bytesUsed, out bool completed) + { + int count = Math.Min(chars.Length, bytes.Length); + for (int i = 0; i < count; i++) + { + bytes[i] = (byte)chars[i]; + } + + charsUsed = count; + bytesUsed = count; + completed = chars.Length <= bytes.Length; + } + } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTestBase.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTestBase.cs new file mode 100644 index 00000000000000..36a2b0a562445a --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTestBase.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Xunit; + +namespace System.IO.Tests +{ + public abstract class StringStreamTestBase + { + protected abstract Stream CreateStream(string input, Encoding encoding); + protected abstract int ReadFromStream(Stream stream, byte[] buffer, int offset, int count); + + private int ReadToEnd(Stream stream, byte[] buffer) + { + int totalRead = 0; + int bytesRead; + while ((bytesRead = ReadFromStream(stream, buffer, totalRead, buffer.Length - totalRead)) > 0) + { + totalRead += bytesRead; + } + + return totalRead; + } + + [Theory] + [InlineData("Hello, World! ")] + [InlineData("Unicode: 你好世界 🌍")] + [InlineData("Multi\nLine\r\nText")] + public void ReadsCorrectBytesForDifferentStrings(string input) + { + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + using Stream stream = CreateStream(input, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length + 100]; + int totalRead = ReadToEnd(stream, actualBytes); + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Theory] + [InlineData("ASCII text")] + [InlineData("Ñoño español")] + public void WorksWithDifferentEncodings(string input) + { + Encoding[] encodings = [Encoding.UTF8, Encoding.Unicode, Encoding.UTF32]; + + foreach (Encoding encoding in encodings) + { + byte[] expectedBytes = encoding.GetBytes(input); + using Stream stream = CreateStream(input, encoding); + + byte[] actualBytes = new byte[expectedBytes.Length * 2]; + int totalRead = ReadToEnd(stream, actualBytes); + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + } + + [Fact] + public void HandlesEmptyInput() + { + using Stream stream = CreateStream("", Encoding.UTF8); + byte[] buffer = new byte[10]; + int bytesRead = ReadFromStream(stream, buffer, 0, 10); + Assert.Equal(0, bytesRead); + } + + [Fact] + public void HandlesSurrogatePairs() + { + string input = "😀😁😂🤣😃😄"; + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + using Stream stream = CreateStream(input, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length + 100]; + int totalRead = ReadToEnd(stream, actualBytes); + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Fact] + public void MultiByteCharactersAcrossChunkBoundary() + { + string input = new string('A', 1023) + "你"; + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + using Stream stream = CreateStream(input, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length + 100]; + int totalRead = ReadToEnd(stream, actualBytes); + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Fact] + public void HandlesChunkedReading() + { + string input = new string('A', 10000); + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + using Stream stream = CreateStream(input, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length]; + int totalRead = 0; + int chunkSize = 512; + + int bytesRead; + while ((bytesRead = ReadFromStream(stream, actualBytes, totalRead, + Math.Min(chunkSize, expectedBytes.Length - totalRead))) > 0) + { + totalRead += bytesRead; + } + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes); + } + + [Fact] + public void MultipleReadsEventuallyReturnZero() + { + using Stream stream = CreateStream("small", Encoding.UTF8); + byte[] buffer = new byte[100]; + + int totalRead = ReadToEnd(stream, buffer); + Assert.Equal(5, totalRead); + + int finalRead = ReadFromStream(stream, buffer, 0, buffer.Length); + Assert.Equal(0, finalRead); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs index fd2eecc78fe43f..65cc717f79f7d3 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs @@ -7,90 +7,67 @@ namespace System.IO.Tests { - /// - /// Additional specific tests for StringStream with ReadOnlyMemory{char} beyond conformance tests. - /// - public class StringStreamTests_Memory + public class StringStreamTests_Memory_Read : StringStreamTestBase { - [Fact] - public void Constructor_WithUTF8Encoding_CreatesReadableStream() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - - Assert.True(stream.CanRead); - Assert.False(stream.CanSeek); - Assert.False(stream.CanWrite); - } - - [Fact] - public void Constructor_ExplicitEncoding_UsesSpecifiedEncoding() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF32); + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input.AsMemory(), encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.Read(buffer, offset, count); + } - Assert.True(stream.CanRead); - } + public class StringStreamTests_Memory_ReadSpan : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input.AsMemory(), encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.Read(buffer.AsSpan(offset, count)); + } - [Fact] - public void Constructor_EmptyMemory_CreatesValidStream() + public class StringStreamTests_Memory_ReadByte : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input.AsMemory(), encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) { - var emptyMemory = ReadOnlyMemory.Empty; - var stream = new StringStream(emptyMemory, Encoding.UTF8); - - Assert.True(stream.CanRead); - - byte[] buffer = new byte[10]; - int bytesRead = stream.Read(buffer, 0, 10); - Assert.Equal(0, bytesRead); + int b = stream.ReadByte(); + if (b == -1) return 0; + buffer[offset] = (byte)b; + return 1; } + } - [Theory] - [InlineData("ASCII text")] - [InlineData("Ñoño español")] - [InlineData("Emoji: 😀🎉")] - public async Task WorksWithDifferentEncodings(string input) - { - var encodings = new[] { Encoding.UTF8, Encoding.Unicode, Encoding.UTF32 }; - - foreach (var encoding in encodings) - { - byte[] expectedBytes = encoding.GetBytes(input); - var chars = input.AsMemory(); - var stream = new StringStream(chars, encoding); - - byte[] actualBytes = new byte[expectedBytes.Length * 2]; - int totalRead = 0; - int bytesRead; - - while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) - { - totalRead += bytesRead; - } + public class StringStreamTests_Memory_ReadAsyncMemory : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input.AsMemory(), encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.ReadAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult(); + } - Assert.Equal(expectedBytes.Length, totalRead); - Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); - } - } + public class StringStreamTests_Memory_ReadAsyncArray : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input.AsMemory(), encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); + } + public class StringStreamTests_Memory_Misc + { [Fact] public async Task WorksWithMemorySlice() { - string largeString = "0123456789ABCDEFGHIJ"; - var fullMemory = largeString.AsMemory(); - var slice = fullMemory.Slice(5, 10); + string source = "0123456789ABCDEFGHIJ"; + ReadOnlyMemory slice = source.AsMemory(5, 10); byte[] expectedBytes = Encoding.UTF8.GetBytes("56789ABCDE"); - var stream = new StringStream(slice, Encoding.UTF8); + using var stream = new StringStream(slice, Encoding.UTF8); byte[] actualBytes = new byte[expectedBytes.Length + 10]; int totalRead = 0; int bytesRead; - while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) - { totalRead += bytesRead; - } Assert.Equal(expectedBytes.Length, totalRead); Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); @@ -99,199 +76,88 @@ public async Task WorksWithMemorySlice() [Fact] public async Task WorksWithCharArray() { - char[] charArray = { 'H', 'e', 'l', 'l', 'o' }; + char[] charArray = ['H', 'e', 'l', 'l', 'o']; var memory = new ReadOnlyMemory(charArray); byte[] expectedBytes = Encoding.UTF8.GetBytes("Hello"); - var stream = new StringStream(memory, Encoding.UTF8); + using var stream = new StringStream(memory, Encoding.UTF8); byte[] actualBytes = new byte[expectedBytes.Length + 10]; int totalRead = 0; int bytesRead; - - while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) - { - totalRead += bytesRead; - } - - Assert.Equal(expectedBytes.Length, totalRead); - Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); - } - - [Fact] - public async Task MultipleSlicesIndependent() - { - string source = "ABCDEFGHIJKLMNOP"; - var slice1 = source.AsMemory(0, 5); - var slice2 = source.AsMemory(5, 5); - var slice3 = source.AsMemory(10, 6); - - var stream1 = new StringStream(slice1, Encoding.UTF8); - var stream2 = new StringStream(slice2, Encoding.UTF8); - var stream3 = new StringStream(slice3, Encoding.UTF8); - - byte[] result1 = new byte[10]; - byte[] result2 = new byte[10]; - byte[] result3 = new byte[10]; - - int read1 = await stream1.ReadAsync(result1); - int read2 = await stream2.ReadAsync(result2); - int read3 = await stream3.ReadAsync(result3); - - Assert.Equal("ABCDE", Encoding.UTF8.GetString(result1, 0, read1)); - Assert.Equal("FGHIJ", Encoding.UTF8.GetString(result2, 0, read2)); - Assert.Equal("KLMNOP", Encoding.UTF8.GetString(result3, 0, read3)); - } - - [Fact] - public async Task HandlesSurrogatePairs() - { - string input = "😀😁😂🤣😃😄"; - var chars = input.AsMemory(); - byte[] expectedBytes = Encoding.UTF8.GetBytes(input); - var stream = new StringStream(chars, Encoding.UTF8); - - byte[] actualBytes = new byte[expectedBytes.Length]; - int totalRead = 0; - int bytesRead; - while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) - { totalRead += bytesRead; - } Assert.Equal(expectedBytes.Length, totalRead); Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); } - [Fact] - public async Task MultiByteCharactersAcrossChunkBoundary() + [Theory] + [InlineData("Hello")] + [InlineData("Unicode: 你好")] + [InlineData("Emoji: 😀")] + public async Task ProducesSameOutputAsStringOverload(string input) { - string input = new string('A', 1023) + "你"; - var chars = input.AsMemory(); - byte[] expectedBytes = Encoding.UTF8.GetBytes(input); - var stream = new StringStream(chars, Encoding.UTF8); + using var memoryStream = new StringStream(input.AsMemory(), Encoding.UTF8); + using var stringStream = new StringStream(input, Encoding.UTF8); - byte[] actualBytes = new byte[expectedBytes.Length]; - int totalRead = 0; - int bytesRead; + byte[] memoryResult = new byte[1000]; + byte[] stringResult = new byte[1000]; - while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) - { - totalRead += bytesRead; - } + int memoryBytesRead = await memoryStream.ReadAsync(memoryResult); + int stringBytesRead = await stringStream.ReadAsync(stringResult); - Assert.Equal(expectedBytes.Length, totalRead); - Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + Assert.Equal(stringBytesRead, memoryBytesRead); + Assert.Equal( + stringResult.AsSpan(0, stringBytesRead).ToArray(), + memoryResult.AsSpan(0, memoryBytesRead).ToArray()); } [Fact] - public void LengthThrowsNotSupportedException() + public void UnsupportedOperationsThrow() { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); + var stream = new StringStream("test".AsMemory(), Encoding.UTF8); Assert.Throws(() => stream.Length); - } - - [Fact] - public void PositionGetThrowsNotSupportedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - Assert.Throws(() => stream.Position); - } - - [Fact] - public void PositionSetThrowsNotSupportedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - Assert.Throws(() => stream.Position = 0); - } - - [Fact] - public void SeekThrowsNotSupportedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); - } - - [Fact] - public void WriteThrowsNotSupportedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - Assert.Throws(() => stream.Write(new byte[1], 0, 1)); - } - - [Fact] - public void SetLengthThrowsNotSupportedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - Assert.Throws(() => stream.SetLength(100)); } [Fact] - public void CanReadFalseAfterDispose() + public void DisposeIsIdempotent() { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); + var stream = new StringStream("test".AsMemory(), Encoding.UTF8); stream.Dispose(); - Assert.False(stream.CanRead); - } + Assert.Throws(() => stream.Read(new byte[10], 0, 10)); - [Fact] - public void ReadAfterDispose_ThrowsObjectDisposedException() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - stream.Dispose(); - - byte[] buffer = new byte[10]; - Assert.Throws(() => stream.Read(buffer, 0, 10)); - } - - [Fact] - public void MultipleDispose_DoesNotThrow() - { - var chars = "test".AsMemory(); - var stream = new StringStream(chars, Encoding.UTF8); - - stream.Dispose(); stream.Dispose(); stream.Dispose(); } - [Theory] - [InlineData("Hello")] - [InlineData("Unicode: 你好")] - [InlineData("Emoji: 😀")] - public async Task ProducesSameOutputAsStringOverload(string input) + [Fact] + public void TruncatedSurrogatePairProducesReplacementChar() { - var memoryStream = new StringStream(input.AsMemory(), Encoding.UTF8); - var stringStream = new StringStream(input, Encoding.UTF8); + // "🌍" is U+1F30D = surrogate pair (0xD83C, 0xDF0D) + // Slice after the high surrogate to create an unpaired surrogate + string emoji = "A\U0001F30D"; + ReadOnlyMemory truncated = emoji.AsMemory(0, 2); // 'A' + high surrogate only - byte[] memoryResult = new byte[1000]; - byte[] stringResult = new byte[1000]; + using var stream = new StringStream(truncated, Encoding.UTF8); + byte[] buffer = new byte[64]; + int totalRead = 0; + int bytesRead; - int memoryBytesRead = await memoryStream.ReadAsync(memoryResult); - int stringBytesRead = await stringStream.ReadAsync(stringResult); + while ((bytesRead = stream.Read(buffer, totalRead, buffer.Length - totalRead)) > 0) + totalRead += bytesRead; - Assert.Equal(stringBytesRead, memoryBytesRead); - Assert.Equal( - stringResult.AsSpan(0, stringBytesRead).ToArray(), - memoryResult.AsSpan(0, memoryBytesRead).ToArray() - ); + // Encoder should produce U+FFFD replacement character for the unpaired surrogate + byte[] expected = Encoding.UTF8.GetBytes("A\uFFFD"); + Assert.Equal(expected, buffer.AsSpan(0, totalRead).ToArray()); } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs index 3ae8d1c1c34f4f..42017c38383db7 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs @@ -2,63 +2,57 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; -using System.Threading.Tasks; using Xunit; namespace System.IO.Tests { - /// - /// Additional specific tests for StringStream with string beyond conformance tests. - /// - public class StringStreamTests_String + public class StringStreamTests_String_Read : StringStreamTestBase { - [Theory] - [InlineData("Hello, World! ")] - [InlineData("Unicode: 你好世界 🌍")] - [InlineData("Multi\nLine\r\nText")] - public async Task ReadsCorrectBytesForDifferentStrings(string input) - { - byte[] expectedBytes = Encoding.UTF8.GetBytes(input); - var stream = new StringStream(input, Encoding.UTF8); - - byte[] actualBytes = new byte[expectedBytes.Length + 100]; - int totalRead = 0; - int bytesRead; - while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) - { - totalRead += bytesRead; - } + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input, encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.Read(buffer, offset, count); + } - Assert.Equal(expectedBytes.Length, totalRead); - Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); - } + public class StringStreamTests_String_ReadSpan : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input, encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.Read(buffer.AsSpan(offset, count)); + } - [Theory] - [InlineData("ASCII text")] - [InlineData("Ñoño español")] - public async Task WorksWithDifferentEncodings(string input) + public class StringStreamTests_String_ReadByte : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input, encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) { - var encodings = new[] { Encoding.UTF8, Encoding.Unicode, Encoding.UTF32 }; - - foreach (var encoding in encodings) - { - byte[] expectedBytes = encoding.GetBytes(input); - var stream = new StringStream(input, encoding); - - byte[] actualBytes = new byte[expectedBytes.Length * 2]; - int totalRead = 0; - int bytesRead; + int b = stream.ReadByte(); + if (b == -1) return 0; + buffer[offset] = (byte)b; + return 1; + } + } - while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) - { - totalRead += bytesRead; - } + public class StringStreamTests_String_ReadAsyncMemory : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input, encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.ReadAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult(); + } - Assert.Equal(expectedBytes.Length, totalRead); - Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); - } - } + public class StringStreamTests_String_ReadAsyncArray : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input, encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); + } + public class StringStreamTests_String_Misc + { [Fact] public void ThrowsOnNullString() { @@ -72,23 +66,11 @@ public void ThrowsOnNullEncoding() } [Fact] - public void CanReadPropertyReturnsTrue() + public void StreamCapabilities() { var stream = new StringStream("test", Encoding.UTF8); Assert.True(stream.CanRead); - } - - [Fact] - public void CanSeekPropertyReturnsFalse() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.False(stream.CanSeek); - } - - [Fact] - public void CanWritePropertyReturnsFalse() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.False(stream.CanWrite); } @@ -100,116 +82,18 @@ public void EncodingPropertyReturnsCorrectEncoding() } [Fact] - public void LengthThrowsNotSupportedException() + public void UnsupportedOperationsThrow() { var stream = new StringStream("test", Encoding.UTF8); - Assert.Throws(() => stream.Length); - } - [Fact] - public void PositionGetThrowsNotSupportedException() - { - var stream = new StringStream("test", Encoding.UTF8); + Assert.Throws(() => stream.Length); Assert.Throws(() => stream.Position); - } - - [Fact] - public void PositionSetThrowsNotSupportedException() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.Throws(() => stream.Position = 0); - } - - [Fact] - public void SeekThrowsNotSupportedException() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); - } - - [Fact] - public void WriteThrowsNotSupportedException() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.Throws(() => stream.Write(new byte[1], 0, 1)); - } - - [Fact] - public void SetLengthThrowsNotSupportedException() - { - var stream = new StringStream("test", Encoding.UTF8); Assert.Throws(() => stream.SetLength(100)); } - [Fact] - public async Task HandlesChunkedReading() - { - string largeString = new string('A', 10000); - byte[] expectedBytes = Encoding.UTF8.GetBytes(largeString); - var stream = new StringStream(largeString, Encoding.UTF8); - - byte[] actualBytes = new byte[expectedBytes.Length]; - int totalRead = 0; - int chunkSize = 512; - - int bytesRead; - while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead, Math.Min(chunkSize, expectedBytes.Length - totalRead)))) > 0) - { - totalRead += bytesRead; - } - - Assert.Equal(expectedBytes.Length, totalRead); - Assert.Equal(expectedBytes, actualBytes); - } - - [Fact] - public async Task ReadsWithExactBufferSizeMatch() - { - string input = new string('A', 4096); - byte[] expectedBytes = Encoding.UTF8.GetBytes(input); - var stream = new StringStream(input, Encoding.UTF8); - - byte[] buffer = new byte[4096]; - int bytesRead = await stream.ReadAsync(buffer); - - Assert.Equal(4096, bytesRead); - Assert.Equal(expectedBytes, buffer); - } - - [Fact] - public async Task MultipleReadsEventuallyReturnZero() - { - var stream = new StringStream("small", Encoding.UTF8); - byte[] buffer = new byte[100]; - - int bytesRead = await stream.ReadAsync(buffer); - Assert.Equal(5, bytesRead); - - int finalRead = await stream.ReadAsync(buffer); - Assert.Equal(0, finalRead); - } - - [Fact] - public async Task SequentialReadAsync_WithSmallChunks_ReadsEntireStream() - { - string input = new string('A', 5000); - byte[] expectedBytes = Encoding.UTF8.GetBytes(input); - var stream = new StringStream(input, Encoding.UTF8); - - byte[] actualBytes = new byte[expectedBytes.Length]; - int totalBytesRead = 0; - int chunkSize = 128; - - int bytesRead; - while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalBytesRead, Math.Min(chunkSize, expectedBytes.Length - totalBytesRead)))) > 0) - { - totalBytesRead += bytesRead; - } - - Assert.Equal(expectedBytes.Length, totalBytesRead); - Assert.Equal(expectedBytes, actualBytes); - } - [Fact] public void DisposeRendersStreamUnreadable() { diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj index f1c835fff85338..d61f4473c9c7f0 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj @@ -38,6 +38,7 @@ + diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs index 5e875ffdc043f3..d1a742bb7fd942 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs @@ -11,7 +11,6 @@ public class WritableMemoryStreamConformanceTests : StandaloneStreamConformanceT protected override bool CanSeek => true; protected override bool CanSetLength => false; protected override bool NopFlushCompletesSynchronously => true; - // This stream can't grow beyond initial capacity protected override bool CanSetLengthGreaterThanCapacity => false; protected override Task CreateReadOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); @@ -20,24 +19,16 @@ public class WritableMemoryStreamConformanceTests : StandaloneStreamConformanceT protected override Task CreateReadWriteStreamCore(byte[]? initialData) { - // WritableMemoryStream wraps a fixed-capacity Memory buffer where Length == capacity. - // Unlike MemoryStream, there's no concept of "logical length" separate from capacity. - // This means WritableMemoryStream doesn't support the common pattern of creating an empty stream - // and writing to it to grow it. Many conformance tests rely on this pattern. - // - // Returning null here skips tests that require creating an initially-empty writable stream, - // as those tests fundamentally conflict with WritableMemoryStream's buffer-wrapping semantics. - if (initialData == null || initialData.Length == 0) + if (initialData is null || initialData.Length == 0) { return Task.FromResult(null); } - var memory = new Memory(initialData); - return Task.FromResult(new WritableMemoryStream(memory)); + var memory = new Memory(new byte[initialData.Length]); + var stream = new WritableMemoryStream(memory); + stream.Write(initialData, 0, initialData.Length); + stream.Position = 0; + return Task.FromResult(stream); } - - // Note to both skipped tests: It was already verified that this works when using just WritableMemoryStream, - // before adding the 'forking' in Stream behavior for fast-path MemoryStream usage. - } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs index 69011e3a3446cd..de3a0d33f9593c 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs @@ -1,42 +1,26 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Threading.Tasks; using Xunit; namespace System.IO.Tests { - /// - /// Additional specific tests for WritableMemoryStream beyond conformance tests. - /// public class WritableMemoryStreamTests { [Fact] - public void Constructor_EmptyMemory_CreatesZeroCapacityStream() - { - Memory emptyMemory = Memory.Empty; - Stream stream = new WritableMemoryStream(emptyMemory); - - Assert.Equal(0, stream.Length); - Assert.Equal(0, stream.Position); - - // Cannot write to zero-capacity stream - Assert.Throws(() => stream.WriteByte(42)); - } - - [Fact] - public void Write_BeyondCapacity_ThrowsNotSupportedException() + public void WriteBeyondCapacityThrows() { byte[] buffer = new byte[10]; Stream stream = new WritableMemoryStream(new Memory(buffer)); byte[] data = new byte[15]; - Assert.Throws(() => stream.Write(data, 0, data.Length)); } [Fact] - public void WriteByte_BeyondCapacity_ThrowsNotSupportedException() + public void WriteByteBeyondCapacityThrows() { byte[] buffer = new byte[3]; Stream stream = new WritableMemoryStream(new Memory(buffer)); @@ -49,12 +33,12 @@ public void WriteByte_BeyondCapacity_ThrowsNotSupportedException() } [Fact] - public void Write_UpToExactCapacity_Succeeds() + public void WriteUpToExactCapacitySucceeds() { byte[] buffer = new byte[10]; Stream stream = new WritableMemoryStream(new Memory(buffer)); - byte[] data = new byte[10]; // Exactly capacity + byte[] data = new byte[10]; for (int i = 0; i < data.Length; i++) data[i] = (byte)i; stream.Write(data, 0, data.Length); @@ -62,7 +46,6 @@ public void Write_UpToExactCapacity_Succeeds() Assert.Equal(10, stream.Position); Assert.Equal(10, stream.Length); - // Verify data was written stream.Position = 0; byte[] readBack = new byte[10]; int bytesRead = stream.Read(readBack, 0, 10); @@ -71,164 +54,61 @@ public void Write_UpToExactCapacity_Succeeds() } [Fact] - public void Write_PastCapacity_ThrowsWithoutSideEffects() + public void WritePastCapacityThrowsWithoutSideEffects() { byte[] buffer = new byte[10]; Stream stream = new WritableMemoryStream(buffer); - stream.Write(new byte[8], 0, 8); // 8 bytes used, 2 remaining + stream.Write(new byte[8], 0, 8); Assert.Equal(8, stream.Position); - // Try to write 5 bytes (only 2 fit) byte[] data = new byte[5]; Assert.Throws(() => stream.Write(data, 0, 5)); - // Position should be unchanged after failed write Assert.Equal(8, stream.Position); } - // Seeking beyond capacity is allowed. - // Write will fail, but seek succeeds. [Fact] - public void Seek_PastCapacity_Succeeds() + public void SeekPastCapacitySucceeds() { byte[] buffer = new byte[10]; Stream stream = new WritableMemoryStream(buffer); - // Seek beyond capacity stream.Seek(100, SeekOrigin.Begin); Assert.Equal(100, stream.Position); Assert.Equal(-1, stream.ReadByte()); - - // Write throws (beyond capacity) Assert.Throws(() => stream.WriteByte(42)); } [Fact] - public void Seek_FromEndNegativeOffset_PositionsCorrectly() + public void WriteOverExistingDataReplacesData() { - byte[] buffer = new byte[100]; - Stream stream = new WritableMemoryStream(buffer); - - // Seek to 10 bytes before end - long newPosition = stream.Seek(-10, SeekOrigin.End); + byte[] initialData = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + byte[] backing = new byte[10]; + Stream stream = new WritableMemoryStream(new Memory(backing)); + stream.Write(initialData, 0, initialData.Length); - Assert.Equal(90, newPosition); // 100 - 10 = 90 - Assert.Equal(90, stream.Position); - } - - [Fact] - public void Write_OverExistingData_ReplacesData() - { - byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; - Stream stream = new WritableMemoryStream(new Memory(buffer)); - - // Overwrite positions 3-5 with new data stream.Position = 3; stream.Write(new byte[] { 100, 101, 102 }, 0, 3); - // Verify overwrite stream.Position = 0; byte[] result = new byte[10]; - stream.Read(result, 0, 10); + int bytesRead = stream.Read(result, 0, 10); + Assert.Equal(10, bytesRead); Assert.Equal(new byte[] { 1, 2, 3, 100, 101, 102, 7, 8, 9, 10 }, result); } [Fact] - public void Position_SetToIntMaxValue_Succeeds() - { - byte[] buffer = new byte[100]; - Stream stream = new WritableMemoryStream(buffer); - - // WritableMemoryStream allows Position up to int.MaxValue even though it's beyond capacity. - // Our override permits this — reads return -1, writes throw. - stream.Position = int.MaxValue; - Assert.Equal(int.MaxValue, stream.Position); - } - - [Fact] - public void Position_SetNegative_ThrowsArgumentOutOfRangeException() - { - Stream stream = new WritableMemoryStream(new byte[100]); - Assert.Throws(() => stream.Position = -1); - } - - [Fact] - public void Position_SetBeyondLongMaxValue_ThrowsArgumentOutOfRangeException() - { - Stream stream = new WritableMemoryStream(new byte[100]); - - // Position property accepts long, but internally casts to int - // Setting to value > int.MaxValue should throw - Assert.Throws(() => stream.Position = (long)int.MaxValue + 1); - } - - [Fact] - public void Dispose_SetsCanPropertiesToFalse() - { - Stream stream = new WritableMemoryStream(new byte[10]); - - stream.Dispose(); - - Assert.False(stream.CanRead); - Assert.False(stream.CanSeek); - Assert.False(stream.CanWrite); - } - - [Fact] - public void Operations_AfterDispose_ThrowObjectDisposedException() - { - byte[] buffer = new byte[10]; - Stream stream = new WritableMemoryStream(buffer); - stream.Dispose(); - - Assert.Throws(() => stream.Read(new byte[5], 0, 5)); - Assert.Throws(() => stream.Write(new byte[5], 0, 5)); - Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); - Assert.Throws(() => _ = stream.Position); - Assert.Throws(() => stream.Position = 0); - Assert.Throws(() => _ = stream.Length); - } - - // Zero-byte write doesn't throw and leaves state unchanged. - [Fact] - public void Write_ZeroBytes_Succeeds() - { - Stream stream = new WritableMemoryStream(new byte[10]); - - stream.Write(new byte[0], 0, 0); - - Assert.Equal(0, stream.Position); - Assert.Equal(10, stream.Length); // Length from initial buffer - } - - [Fact] - public void Read_ZeroBytes_ReturnsZero() - { - Stream stream = new WritableMemoryStream(new byte[10]); - - int bytesRead = stream.Read(new byte[10], 0, 0); - - Assert.Equal(0, bytesRead); - Assert.Equal(0, stream.Position); - } - - [Fact] - public void SetLength_ThrowsNotSupportedException() - { - Stream stream = new WritableMemoryStream(new byte[10]); - - Assert.Throws(() => stream.SetLength(20)); - } - - [Fact] - public async Task ReadAsync_DifferentResultSize_CreatesNewTask() + public async Task ReadAsyncDifferentResultSizeCreatesNewTask() { byte[] data = new byte[10]; for (int i = 0; i < 10; i++) data[i] = (byte)i; - Stream stream = new WritableMemoryStream(data); + byte[] backing = new byte[10]; + Stream stream = new WritableMemoryStream(backing); + stream.Write(data, 0, data.Length); + stream.Position = 0; byte[] buffer1 = new byte[5]; byte[] buffer2 = new byte[3]; @@ -247,10 +127,13 @@ public async Task ReadAsync_DifferentResultSize_CreatesNewTask() } [Fact] - public async Task ReadAsync_ArrayBackedMemory_UsesFastPath() + public async Task ReadAsyncArrayBackedMemoryUsesFastPath() { byte[] data = { 10, 20, 30, 40, 50 }; - Stream stream = new WritableMemoryStream(data); + byte[] backing = new byte[5]; + Stream stream = new WritableMemoryStream(backing); + stream.Write(data, 0, data.Length); + stream.Position = 0; byte[] arrayBuffer = new byte[3]; Memory memory = arrayBuffer.AsMemory(); @@ -259,5 +142,20 @@ public async Task ReadAsync_ArrayBackedMemory_UsesFastPath() Assert.Equal(3, bytesRead); Assert.Equal(new byte[] { 10, 20, 30 }, arrayBuffer); } + + [Fact] + public void WriteToUnmanagedMemory() + { + byte[] data = [10, 20, 30, 40, 50]; + + using var manager = new NativeMemoryManager(data.Length); + manager.GetSpan().Clear(); + + using var stream = new WritableMemoryStream(manager.Memory); + stream.Write(data); + + Assert.Equal(data, manager.GetSpan().ToArray()); + Assert.Equal(data.Length, stream.Position); + } } } From 8e4a44158fcbdb0b9cad5e64229c52e1a1016817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Tue, 16 Jun 2026 23:21:25 -0700 Subject: [PATCH 08/11] Address PR feedback: gap-zeroing, disposal checks, cancellation guards, and conformance coverage --- .../System/Buffers/ReadOnlySequenceStream.cs | 3 +++ .../src/System/IO/ReadOnlyMemoryStream.cs | 10 ++++++---- .../src/System/IO/WritableMemoryStream.cs | 20 +++++++++++++++---- .../WritableMemoryStreamConformanceTests.cs | 8 +++++++- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs index a107d82283a7ff..f57ec919cc1852 100644 --- a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs +++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs @@ -127,6 +127,7 @@ public override int ReadByte() public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { ValidateBufferArguments(buffer, offset, count); + EnsureNotDisposed(); if (cancellationToken.IsCancellationRequested) { @@ -140,6 +141,8 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel /// public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) { + EnsureNotDisposed(); + if (cancellationToken.IsCancellationRequested) { return ValueTask.FromCanceled(cancellationToken); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs index f0b6e17ea42e68..2087ae8f26ff1e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -168,6 +168,11 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio ValidateCopyToArguments(destination, bufferSize); EnsureNotClosed(); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + if (_buffer.Length > _position) { ReadOnlyMemory content = _buffer.Slice(_position); @@ -253,10 +258,7 @@ public override void WriteTo(Stream stream) ArgumentNullException.ThrowIfNull(stream); EnsureNotClosed(); - if (_buffer.Length > 0) - { - stream.Write(_buffer.Span); - } + stream.Write(_buffer.Span); } /// diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs index ac6f0c5c68199f..ad15c4e75fb7ff 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -170,6 +170,11 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio ValidateCopyToArguments(destination, bufferSize); EnsureNotClosed(); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + if (_length > _position) { ReadOnlyMemory content = _buffer.Slice(_position, _length - _position); @@ -191,6 +196,11 @@ public override void WriteByte(byte value) throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); } + if (_position > _length) + { + _buffer.Span.Slice(_length, _position - _length).Clear(); + } + _buffer.Span[_position++] = value; if (_position > _length) @@ -221,6 +231,11 @@ public override void Write(ReadOnlySpan buffer) throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); } + if (_position > _length) + { + _buffer.Span.Slice(_length, _position - _length).Clear(); + } + buffer.CopyTo(_buffer.Span.Slice(_position)); _position += buffer.Length; @@ -318,10 +333,7 @@ public override void WriteTo(Stream stream) ArgumentNullException.ThrowIfNull(stream); EnsureNotClosed(); - if (_length > 0) - { - stream.Write(_buffer.Span.Slice(0, _length)); - } + stream.Write(_buffer.Span.Slice(0, _length)); } /// diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs index d1a742bb7fd942..1125961cf0bb63 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs @@ -19,11 +19,17 @@ public class WritableMemoryStreamConformanceTests : StandaloneStreamConformanceT protected override Task CreateReadWriteStreamCore(byte[]? initialData) { - if (initialData is null || initialData.Length == 0) + if (initialData is null) { return Task.FromResult(null); } + if (initialData.Length == 0) + { + return Task.FromResult( + new WritableMemoryStream(new Memory(new byte[1024]))); + } + var memory = new Memory(new byte[initialData.Length]); var stream = new WritableMemoryStream(memory); stream.Write(initialData, 0, initialData.Length); From 70f9f8bee5a12aea3c3ce490df0aee278876846e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Thu, 18 Jun 2026 01:36:45 -0700 Subject: [PATCH 09/11] Cache ReadAsync Task in ReadOnly/Writable MemoryStream wrappers --- .../Tasks/CachedCompletedInt32Task.cs | 0 .../System.Memory/src/System.Memory.csproj | 2 + .../System/Buffers/ReadOnlySequenceStream.cs | 17 ++-- .../ReadOnlySequenceStreamTests.cs | 78 ------------------ .../System.Private.CoreLib.Shared.projitems | 3 +- .../src/System/IO/ReadOnlyMemoryStream.cs | 17 +++- .../src/System/IO/WritableMemoryStream.cs | 17 +++- .../Xml/Resolvers/XmlPreloadedResolver.cs | 2 +- .../ReadOnlyMemoryStreamTests.cs | 79 ------------------- .../WritableMemoryStreamTests.cs | 44 ----------- 10 files changed, 41 insertions(+), 218 deletions(-) rename src/libraries/{System.Private.CoreLib => Common}/src/System/Threading/Tasks/CachedCompletedInt32Task.cs (100%) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/CachedCompletedInt32Task.cs b/src/libraries/Common/src/System/Threading/Tasks/CachedCompletedInt32Task.cs similarity index 100% rename from src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/CachedCompletedInt32Task.cs rename to src/libraries/Common/src/System/Threading/Tasks/CachedCompletedInt32Task.cs diff --git a/src/libraries/System.Memory/src/System.Memory.csproj b/src/libraries/System.Memory/src/System.Memory.csproj index 41b848970cf2ea..b377b90f62ae90 100644 --- a/src/libraries/System.Memory/src/System.Memory.csproj +++ b/src/libraries/System.Memory/src/System.Memory.csproj @@ -41,6 +41,8 @@ + diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs index f57ec919cc1852..f52cdd4fc42aec 100644 --- a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs +++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs @@ -1,25 +1,20 @@ // 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.IO; +using System.Threading; using System.Threading.Tasks; namespace System.Buffers { - /// - /// Provides a seekable, read-only implementation over a of bytes. - /// - /// - /// This type is not thread-safe. Synchronize access if the stream is used concurrently. - /// The underlying sequence should not be modified while the stream is in use. - /// Seeking beyond the end of the stream is supported; subsequent reads will return zero bytes. - /// + public sealed class ReadOnlySequenceStream : Stream { private ReadOnlySequence _sequence; private SequencePosition _position; private long _absolutePosition; private bool _isDisposed; + private CachedCompletedInt32Task _lastReadTask; /// /// Initializes a new instance of the class over the specified . @@ -135,7 +130,7 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel } int n = Read(buffer, offset, count); - return Task.FromResult(n); + return _lastReadTask.GetTask(n); } /// @@ -231,7 +226,7 @@ public override long Seek(long offset, SeekOrigin origin) SeekOrigin.Begin => offset, SeekOrigin.Current => _absolutePosition + offset, SeekOrigin.End => _sequence.Length + offset, - _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)) + _ => throw new ArgumentOutOfRangeException(nameof(origin)) }; if (absolutePosition < 0) diff --git a/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs index 7cfffadf10d033..85a3b3015aea23 100644 --- a/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs +++ b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs @@ -2,26 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; using System.IO; -using System.Threading.Tasks; using Xunit; namespace System.Memory.Tests { public class ReadOnlySequenceStreamTests { - [Fact] - public void ReadZeroBytesReturnsZero() - { - byte[] data = [1, 2, 3]; - var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); - byte[] buffer = new byte[10]; - - int bytesRead = stream.Read(buffer, 0, 0); - - Assert.Equal(0, bytesRead); - Assert.Equal(0, stream.Position); - } - [Fact] public void SeekingBeyondEmptyBufferIsAllowed() { @@ -41,69 +27,5 @@ public void SeekingBeyondEmptyBufferIsAllowed() Assert.Equal(1, newPosition); Assert.Equal(1, stream.Position); } - - [Fact] - public async Task ReadAsyncSameResultSizeReusesCachedTask() - { - byte[] data = new byte[20]; - for (int i = 0; i < 20; i++) data[i] = (byte)i; - var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); - - byte[] buffer1 = new byte[5]; - byte[] buffer2 = new byte[5]; - byte[] buffer3 = new byte[5]; - - Task task1 = stream.ReadAsync(buffer1, 0, 5); - Task task2 = stream.ReadAsync(buffer2, 0, 5); - Task task3 = stream.ReadAsync(buffer3, 0, 5); - - await task1; - await task2; - await task3; - - Assert.Same(task1, task2); - Assert.Same(task2, task3); - - Assert.Equal(new byte[] { 0, 1, 2, 3, 4 }, buffer1); - Assert.Equal(new byte[] { 5, 6, 7, 8, 9 }, buffer2); - Assert.Equal(new byte[] { 10, 11, 12, 13, 14 }, buffer3); - } - - [Fact] - public async Task ReadAsyncDifferentResultSizeCreatesNewTask() - { - byte[] data = new byte[10]; - for (int i = 0; i < 10; i++) data[i] = (byte)i; - var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); - - byte[] buffer1 = new byte[5]; - byte[] buffer2 = new byte[3]; - byte[] buffer3 = new byte[2]; - - Task task1 = stream.ReadAsync(buffer1, 0, 5); - Task task2 = stream.ReadAsync(buffer2, 0, 3); - Task task3 = stream.ReadAsync(buffer3, 0, 2); - - await task1; - await task2; - await task3; - - Assert.NotSame(task1, task2); - Assert.NotSame(task2, task3); - } - - [Fact] - public async Task ReadAsyncArrayBackedMemoryUsesFastPath() - { - byte[] data = [10, 20, 30, 40, 50]; - var stream = new ReadOnlySequenceStream(new ReadOnlySequence(data)); - - byte[] arrayBuffer = new byte[3]; - Memory memory = arrayBuffer.AsMemory(); - int bytesRead = await stream.ReadAsync(memory); - - Assert.Equal(3, bytesRead); - Assert.Equal(new byte[] { 10, 20, 30 }, arrayBuffer); - } } } diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 705a3ed0d78b4d..e31085aa1fc9af 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1310,7 +1310,8 @@ - + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs index 2087ae8f26ff1e..e2c0c42e43aa13 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -19,6 +19,7 @@ public sealed class ReadOnlyMemoryStream : MemoryStream private ReadOnlyMemory _buffer; private int _position; private bool _isOpen; + private CachedCompletedInt32Task _lastReadTask; /// /// Initializes a new instance of the class over the specified . @@ -133,7 +134,19 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel return Task.FromCanceled(cancellationToken); } - return Task.FromResult(Read(buffer, offset, count)); + try + { + int n = Read(buffer, offset, count); + return _lastReadTask.GetTask(n); + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception exception) + { + return Task.FromException(exception); + } } /// @@ -194,7 +207,7 @@ public override long Seek(long offset, SeekOrigin origin) SeekOrigin.Begin => offset, SeekOrigin.Current => _position + offset, SeekOrigin.End => _buffer.Length + offset, - _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin) + _ => throw new ArgumentOutOfRangeException(nameof(origin)) }; if (newPosition < 0) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs index ad15c4e75fb7ff..896daa13db0b13 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -20,6 +20,7 @@ public sealed class WritableMemoryStream : MemoryStream private int _position; private int _length; private bool _isOpen; + private CachedCompletedInt32Task _lastReadTask; /// /// Initializes a new instance of the class over the specified . @@ -135,7 +136,19 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel return Task.FromCanceled(cancellationToken); } - return Task.FromResult(Read(buffer, offset, count)); + try + { + int n = Read(buffer, offset, count); + return _lastReadTask.GetTask(n); + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception exception) + { + return Task.FromException(exception); + } } /// @@ -284,7 +297,7 @@ public override long Seek(long offset, SeekOrigin origin) SeekOrigin.Begin => offset, SeekOrigin.Current => _position + offset, SeekOrigin.End => _length + offset, - _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin) + _ => throw new ArgumentOutOfRangeException(nameof(origin)) }; if (newPosition < 0) diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs b/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs index 7a834012c9ca3d..1756485ad05b12 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Resolvers/XmlPreloadedResolver.cs @@ -103,7 +103,7 @@ internal StringData(string str) internal override Stream AsStream() { - return new StringStream(_str, Encoding.Unicode); + return new MemoryStream(Encoding.Unicode.GetBytes(_str)); } internal override TextReader AsTextReader() diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs index 2bb55aa98bafc1..b8950a0d475550 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Threading.Tasks; using Xunit; namespace System.IO.Tests @@ -36,84 +35,6 @@ public void WorksWithSlicedMemory() Assert.Equal(new byte[] { 3, 4, 5, 6 }, result); } - [Fact] - public void ReadDoesNotModifyUnderlyingMemory() - { - byte[] originalData = { 1, 2, 3, 4, 5 }; - byte[] dataCopy = (byte[])originalData.Clone(); - Stream stream = new ReadOnlyMemoryStream(originalData); - - byte[] buffer = new byte[5]; - stream.Read(buffer, 0, 5); - - Assert.Equal(dataCopy, originalData); - } - - [Fact] - public async Task ReadAsyncSameResultSizeReusesCachedTask() - { - byte[] data = new byte[20]; - for (int i = 0; i < 20; i++) data[i] = (byte)i; - Stream stream = new ReadOnlyMemoryStream(data); - - byte[] buffer1 = new byte[5]; - byte[] buffer2 = new byte[5]; - byte[] buffer3 = new byte[5]; - - Task task1 = stream.ReadAsync(buffer1, 0, 5); - Task task2 = stream.ReadAsync(buffer2, 0, 5); - Task task3 = stream.ReadAsync(buffer3, 0, 5); - - await task1; - await task2; - await task3; - - Assert.Same(task1, task2); - Assert.Same(task2, task3); - - Assert.Equal(new byte[] { 0, 1, 2, 3, 4 }, buffer1); - Assert.Equal(new byte[] { 5, 6, 7, 8, 9 }, buffer2); - Assert.Equal(new byte[] { 10, 11, 12, 13, 14 }, buffer3); - } - - [Fact] - public async Task ReadAsyncDifferentResultSizeCreatesNewTask() - { - byte[] data = new byte[10]; - for (int i = 0; i < 10; i++) data[i] = (byte)i; - Stream stream = new ReadOnlyMemoryStream(data); - - byte[] buffer1 = new byte[5]; - byte[] buffer2 = new byte[3]; - byte[] buffer3 = new byte[2]; - - Task task1 = stream.ReadAsync(buffer1, 0, 5); - Task task2 = stream.ReadAsync(buffer2, 0, 3); - Task task3 = stream.ReadAsync(buffer3, 0, 2); - - await task1; - await task2; - await task3; - - Assert.NotSame(task1, task2); - Assert.NotSame(task2, task3); - } - - [Fact] - public async Task ReadAsyncArrayBackedMemoryUsesFastPath() - { - byte[] data = { 10, 20, 30, 40, 50 }; - Stream stream = new ReadOnlyMemoryStream(data); - - byte[] arrayBuffer = new byte[3]; - Memory memory = arrayBuffer.AsMemory(); - - int bytesRead = await stream.ReadAsync(memory); - - Assert.Equal(3, bytesRead); - Assert.Equal(new byte[] { 10, 20, 30 }, arrayBuffer); - } - [Fact] public void ReadFromUnmanagedMemory() { diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs index de3a0d33f9593c..e24cd0d0e2bc6f 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; -using System.Threading.Tasks; using Xunit; namespace System.IO.Tests @@ -100,49 +99,6 @@ public void WriteOverExistingDataReplacesData() Assert.Equal(new byte[] { 1, 2, 3, 100, 101, 102, 7, 8, 9, 10 }, result); } - [Fact] - public async Task ReadAsyncDifferentResultSizeCreatesNewTask() - { - byte[] data = new byte[10]; - for (int i = 0; i < 10; i++) data[i] = (byte)i; - byte[] backing = new byte[10]; - Stream stream = new WritableMemoryStream(backing); - stream.Write(data, 0, data.Length); - stream.Position = 0; - - byte[] buffer1 = new byte[5]; - byte[] buffer2 = new byte[3]; - byte[] buffer3 = new byte[2]; - - Task task1 = stream.ReadAsync(buffer1, 0, 5); - Task task2 = stream.ReadAsync(buffer2, 0, 3); - Task task3 = stream.ReadAsync(buffer3, 0, 2); - - await task1; - await task2; - await task3; - - Assert.NotSame(task1, task2); - Assert.NotSame(task2, task3); - } - - [Fact] - public async Task ReadAsyncArrayBackedMemoryUsesFastPath() - { - byte[] data = { 10, 20, 30, 40, 50 }; - byte[] backing = new byte[5]; - Stream stream = new WritableMemoryStream(backing); - stream.Write(data, 0, data.Length); - stream.Position = 0; - - byte[] arrayBuffer = new byte[3]; - Memory memory = arrayBuffer.AsMemory(); - int bytesRead = await stream.ReadAsync(memory); - - Assert.Equal(3, bytesRead); - Assert.Equal(new byte[] { 10, 20, 30 }, arrayBuffer); - } - [Fact] public void WriteToUnmanagedMemory() { From 242dbd56e87098b405e9c1897b0c4edc3635f674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Fri, 19 Jun 2026 14:26:22 -0700 Subject: [PATCH 10/11] Address feedback: perf fast paths, CopyTo overrides, sync-exception bridging, Seek hardening, and test dedup --- .../System/Buffers/ReadOnlySequenceStream.cs | 37 +- .../src/System/IO/ReadOnlyMemoryStream.cs | 437 +++++++------- .../src/System/IO/StringStream.cs | 490 +++++++++------ .../src/System/IO/WritableMemoryStream.cs | 571 +++++++++--------- .../StringStream/StringStreamTestBase.cs | 21 + .../StringStream/StringStreamTests_Memory.cs | 26 - .../StringStream/StringStreamTests_String.cs | 26 +- 7 files changed, 874 insertions(+), 734 deletions(-) diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs index f52cdd4fc42aec..a6b6f4c119b3aa 100644 --- a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs +++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs @@ -129,8 +129,19 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel return Task.FromCanceled(cancellationToken); } - int n = Read(buffer, offset, count); - return _lastReadTask.GetTask(n); + try + { + int n = Read(buffer, offset, count); + return _lastReadTask.GetTask(n); + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } } /// @@ -143,8 +154,19 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return ValueTask.FromCanceled(cancellationToken); } - int n = Read(buffer.Span); - return new ValueTask(n); + try + { + int n = Read(buffer.Span); + return new ValueTask(n); + } + catch (OperationCanceledException oce) + { + return ValueTask.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } } /// @@ -211,12 +233,7 @@ private async Task CopyToAsyncCore(Stream destination, CancellationToken cancell /// public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - /// Sets the position within the current stream. - /// - /// A byte offset relative to the parameter. - /// A value of type indicating the reference point used to obtain the new position. - /// The new position within the stream. + /// public override long Seek(long offset, SeekOrigin origin) { EnsureNotDisposed(); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs index e2c0c42e43aa13..75f9cac67f7368 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -4,293 +4,296 @@ using System.Threading; using System.Threading.Tasks; -namespace System.IO; - -/// -/// Provides a seekable, read-only over a . -/// -/// -/// This type is not thread-safe. Synchronize access if the stream is used concurrently. -/// The stream cannot be written to. always returns . -/// throws and returns . -/// -public sealed class ReadOnlyMemoryStream : MemoryStream +namespace System.IO { - private ReadOnlyMemory _buffer; - private int _position; - private bool _isOpen; - private CachedCompletedInt32Task _lastReadTask; - /// - /// Initializes a new instance of the class over the specified . + /// Provides a seekable, read-only over a . /// - /// The to wrap. - public ReadOnlyMemoryStream(ReadOnlyMemory source) : base() + /// + /// This type is not thread-safe. Synchronize access if the stream is used concurrently. + /// The stream cannot be written to. always returns . + /// throws and returns . + /// + public sealed class ReadOnlyMemoryStream : MemoryStream { - _buffer = source; - _isOpen = true; - } + private ReadOnlyMemory _buffer; + private int _position; + private bool _isOpen; + private CachedCompletedInt32Task _lastReadTask; + + /// + /// Initializes a new instance of the class over the specified . + /// + /// The to wrap. + public ReadOnlyMemoryStream(ReadOnlyMemory source) : base() + { + _buffer = source; + _isOpen = true; + } - /// - public override bool CanRead => _isOpen; + /// + public override bool CanRead => _isOpen; - /// - public override bool CanSeek => _isOpen; + /// + public override bool CanSeek => _isOpen; - /// - public override bool CanWrite => false; + /// + public override bool CanWrite => false; - /// - public override int Capacity - { - get + /// + public override int Capacity { - EnsureNotClosed(); - return _buffer.Length; + get + { + EnsureNotClosed(); + return _buffer.Length; + } + set => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); } - set => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); - } - /// - public override long Length - { - get + /// + public override long Length { - EnsureNotClosed(); + get + { + EnsureNotClosed(); - return _buffer.Length; + return _buffer.Length; + } } - } - /// - public override long Position - { - get + /// + public override long Position { - EnsureNotClosed(); - - return _position; + get + { + EnsureNotClosed(); + + return _position; + } + set + { + EnsureNotClosed(); + ArgumentOutOfRangeException.ThrowIfNegative(value); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue); + _position = (int)value; + } } - set + + /// + public override int ReadByte() { EnsureNotClosed(); - ArgumentOutOfRangeException.ThrowIfNegative(value); - ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue); - _position = (int)value; - } - } - /// - public override int ReadByte() - { - EnsureNotClosed(); + ReadOnlySpan span = _buffer.Span; + int position = _position; - ReadOnlySpan span = _buffer.Span; - int position = _position; + if ((uint)position < (uint)span.Length) + { + _position++; + return span[position]; + } - if ((uint)position < (uint)span.Length) - { - _position++; - return span[position]; + return -1; } - return -1; - } - - /// - public override int Read(byte[] buffer, int offset, int count) - { - ValidateBufferArguments(buffer, offset, count); - - return Read(new Span(buffer, offset, count)); - } - - /// - public override int Read(Span buffer) - { - EnsureNotClosed(); - - int remaining = _buffer.Length - _position; - if (remaining <= 0 || buffer.Length == 0) + /// + public override int Read(byte[] buffer, int offset, int count) { - return 0; + ValidateBufferArguments(buffer, offset, count); + + return Read(new Span(buffer, offset, count)); } - int bytesToRead = Math.Min(remaining, buffer.Length); - _buffer.Span.Slice(_position, bytesToRead).CopyTo(buffer); - _position += bytesToRead; + /// + public override int Read(Span buffer) + { + EnsureNotClosed(); - return bytesToRead; - } + int remaining = _buffer.Length - _position; + if (remaining <= 0 || buffer.Length == 0) + { + return 0; + } - /// - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ValidateBufferArguments(buffer, offset, count); - EnsureNotClosed(); + int bytesToRead = Math.Min(remaining, buffer.Length); + _buffer.Span.Slice(_position, bytesToRead).CopyTo(buffer); + _position += bytesToRead; - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); + return bytesToRead; } - try - { - int n = Read(buffer, offset, count); - return _lastReadTask.GetTask(n); - } - catch (OperationCanceledException oce) - { - return Task.FromCanceled(oce.CancellationToken); - } - catch (Exception exception) + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - return Task.FromException(exception); - } - } - - /// - public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - EnsureNotClosed(); + ValidateBufferArguments(buffer, offset, count); + EnsureNotClosed(); - if (cancellationToken.IsCancellationRequested) - { - return ValueTask.FromCanceled(cancellationToken); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + int n = Read(buffer, offset, count); + return _lastReadTask.GetTask(n); + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception exception) + { + return Task.FromException(exception); + } } - return new ValueTask(Read(buffer.Span)); - } + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + EnsureNotClosed(); - /// - public override void CopyTo(Stream destination, int bufferSize) - { - ValidateCopyToArguments(destination, bufferSize); - EnsureNotClosed(); + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } - if (_buffer.Length > _position) - { - destination.Write(_buffer.Span.Slice(_position)); - _position = _buffer.Length; + return new ValueTask(Read(buffer.Span)); } - } - - /// - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - ValidateCopyToArguments(destination, bufferSize); - EnsureNotClosed(); - if (cancellationToken.IsCancellationRequested) + /// + public override void CopyTo(Stream destination, int bufferSize) { - return Task.FromCanceled(cancellationToken); + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + + if (_buffer.Length > _position) + { + destination.Write(_buffer.Span.Slice(_position)); + _position = _buffer.Length; + } } - if (_buffer.Length > _position) + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { - ReadOnlyMemory content = _buffer.Slice(_position); - _position = _buffer.Length; + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); - return destination.WriteAsync(content, cancellationToken).AsTask(); - } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } - return Task.CompletedTask; - } + if (_buffer.Length > _position) + { + ReadOnlyMemory content = _buffer.Slice(_position); + _position = _buffer.Length; - /// - public override long Seek(long offset, SeekOrigin origin) - { - EnsureNotClosed(); + return destination.WriteAsync(content, cancellationToken).AsTask(); + } - long newPosition = origin switch - { - SeekOrigin.Begin => offset, - SeekOrigin.Current => _position + offset, - SeekOrigin.End => _buffer.Length + offset, - _ => throw new ArgumentOutOfRangeException(nameof(origin)) - }; + return Task.CompletedTask; + } - if (newPosition < 0) + /// + public override long Seek(long offset, SeekOrigin origin) { - throw new IOException(SR.IO_SeekBeforeBegin); - } + EnsureNotClosed(); - ArgumentOutOfRangeException.ThrowIfGreaterThan(newPosition, int.MaxValue, nameof(offset)); + long newPosition = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _buffer.Length + offset, + _ => throw new ArgumentOutOfRangeException(nameof(origin)) + }; - _position = (int)newPosition; + // Order matches MemoryStream.SeekCore / Common/src/System/IO/ReadOnlyMemoryStream.cs: + // overflow check first, then seek-before-begin. + ArgumentOutOfRangeException.ThrowIfGreaterThan(newPosition, int.MaxValue, nameof(offset)); - return newPosition; - } + if (newPosition < 0) + { + throw new IOException(SR.IO_SeekBeforeBegin); + } - /// - public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + _position = (int)newPosition; - /// - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + return newPosition; + } - /// - public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - public override void WriteByte(byte value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override void WriteByte(byte value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - public override byte[] GetBuffer() => - throw new UnauthorizedAccessException(SR.UnauthorizedAccess_MemStreamBuffer); + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - public override bool TryGetBuffer(out ArraySegment buffer) - { - buffer = default; - return false; - } + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - public override byte[] ToArray() - { - EnsureNotClosed(); - if (_buffer.Length == 0) + /// + public override byte[] GetBuffer() => + throw new UnauthorizedAccessException(SR.UnauthorizedAccess_MemStreamBuffer); + + /// + public override bool TryGetBuffer(out ArraySegment buffer) { - return Array.Empty(); + buffer = default; + return false; } - byte[] copy = GC.AllocateUninitializedArray(_buffer.Length); - _buffer.Span.CopyTo(copy); - return copy; - } + /// + public override byte[] ToArray() + { + EnsureNotClosed(); + if (_buffer.Length == 0) + { + return Array.Empty(); + } + + byte[] copy = GC.AllocateUninitializedArray(_buffer.Length); + _buffer.Span.CopyTo(copy); + return copy; + } - /// - public override void WriteTo(Stream stream) - { - ArgumentNullException.ThrowIfNull(stream); - EnsureNotClosed(); + /// + public override void WriteTo(Stream stream) + { + ArgumentNullException.ThrowIfNull(stream); + EnsureNotClosed(); - stream.Write(_buffer.Span); - } + stream.Write(_buffer.Span); + } - /// - public override void Flush() { } + /// + public override void Flush() { } - /// - public override Task FlushAsync(CancellationToken cancellationToken) => - cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; - /// - protected override void Dispose(bool disposing) - { - _isOpen = false; - _buffer = default; - base.Dispose(disposing); - } + /// + protected override void Dispose(bool disposing) + { + _isOpen = false; + _buffer = default; + base.Dispose(disposing); + } - private void EnsureNotClosed() - { - ObjectDisposedException.ThrowIf(!_isOpen, this); + private void EnsureNotClosed() + { + ObjectDisposedException.ThrowIf(!_isOpen, this); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs index 11e2e7bb3c132f..093ca473b337e5 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs @@ -1,256 +1,364 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Text; using System.Threading; using System.Threading.Tasks; -namespace System.IO; - -/// -/// Provides a read-only, non-seekable that encodes a or -/// into bytes on-the-fly using a specified . -/// -/// -/// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. -/// This type is not thread-safe. Synchronize access if the stream is used concurrently. -/// -public sealed class StringStream : Stream +namespace System.IO { - private readonly ReadOnlyMemory _text; - private readonly Encoder _encoder; - private readonly Encoding _encoding; - private int _charPosition; - private bool _disposed; - private bool _encoderFlushed; - - // Spillover buffer for multibyte encodings: when the caller's buffer is too small - // to hold even one encoded scalar (e.g., ReadByte with UTF-16), we encode into - // this buffer and serve bytes from it across subsequent Read/ReadByte calls. - // Also used to hold final encoder flush bytes when the caller's buffer had no room. - private byte[]? _pendingBytes; - private int _pendingOffset; - private int _pendingCount; - - /// - /// Initializes a new instance of the class with the specified string and encoding. - /// - /// The string to read from. - /// The encoding to use when converting the string to bytes. - /// or is . - public StringStream(string text, Encoding encoding) - { - ArgumentNullException.ThrowIfNull(text); - ArgumentNullException.ThrowIfNull(encoding); - - _text = text.AsMemory(); - _encoding = encoding; - _encoder = encoding.GetEncoder(); - } - /// - /// Initializes a new instance of the class with the specified character memory and encoding. + /// Provides a read-only, non-seekable that encodes a or + /// into bytes on-the-fly using a specified . /// - /// The character memory to read from. - /// The encoding to use when converting the characters to bytes. - /// is . - public StringStream(ReadOnlyMemory text, Encoding encoding) + /// + /// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. + /// This type is not thread-safe. Synchronize access if the stream is used concurrently. + /// + public sealed class StringStream : Stream { - ArgumentNullException.ThrowIfNull(encoding); - - _text = text; - _encoding = encoding; - _encoder = encoding.GetEncoder(); - } - - /// - /// Gets the encoding used by this stream. - /// - public Encoding Encoding => _encoding; + private readonly ReadOnlyMemory _text; + // Lazily created on the encoder slow path. The single-shot fast path in Read + // uses stateless Encoding.GetBytes and never touches this field. + private Encoder? _encoder; + private readonly Encoding _encoding; + private readonly int _maxBytesPerChar; + private int _charPosition; + private bool _disposed; + private bool _encoderFlushed; + + // Spillover buffer for multibyte encodings: when the caller's buffer is too small + // to hold even one encoded scalar (e.g., ReadByte with UTF-16), we encode into + // this buffer and serve bytes from it across subsequent Read/ReadByte calls. + // Also used to hold final encoder flush bytes when the caller's buffer had no room. + private byte[]? _pendingBytes; + private int _pendingOffset; + private int _pendingCount; + + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + /// The string to read from. + /// The encoding to use when converting the string to bytes. + /// or is . + public StringStream(string text, Encoding encoding) + { + ArgumentNullException.ThrowIfNull(text); + ArgumentNullException.ThrowIfNull(encoding); - /// - public override bool CanRead => !_disposed; + _text = text.AsMemory(); + _encoding = encoding; + _maxBytesPerChar = encoding.GetMaxByteCount(1); + } - /// - public override bool CanSeek => false; + /// + /// Initializes a new instance of the class with the specified character memory and encoding. + /// + /// The character memory to read from. + /// The encoding to use when converting the characters to bytes. + /// is . + public StringStream(ReadOnlyMemory text, Encoding encoding) + { + ArgumentNullException.ThrowIfNull(encoding); - /// - public override bool CanWrite => false; + _text = text; + _encoding = encoding; + _maxBytesPerChar = encoding.GetMaxByteCount(1); + } - /// - public override long Length => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + /// + /// Gets the encoding used by this stream. + /// + public Encoding Encoding => _encoding; - /// - public override long Position - { - get => throw new NotSupportedException(SR.NotSupported_UnseekableStream); - set => throw new NotSupportedException(SR.NotSupported_UnseekableStream); - } + /// + public override bool CanRead => !_disposed; - /// - public override int Read(byte[] buffer, int offset, int count) - { - ValidateBufferArguments(buffer, offset, count); + /// + public override bool CanSeek => false; - return Read(new Span(buffer, offset, count)); - } + /// + public override bool CanWrite => false; - /// - public override int Read(Span buffer) - { - ObjectDisposedException.ThrowIf(_disposed, this); + /// + public override long Length => throw new NotSupportedException(SR.NotSupported_UnseekableStream); - if (buffer.Length == 0 || (_charPosition >= _text.Length && _pendingCount == 0 && _encoderFlushed)) + /// + public override long Position { - return 0; + get => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + set => throw new NotSupportedException(SR.NotSupported_UnseekableStream); } - int totalBytesWritten = 0; - int bufferBytesWritten = 0; - - // Drain any pending bytes from a previous partial read. - if (_pendingCount > 0) + /// + public override int Read(byte[] buffer, int offset, int count) { - int toCopy = Math.Min(_pendingCount, buffer.Length); - _pendingBytes.AsSpan(_pendingOffset, toCopy).CopyTo(buffer); - _pendingOffset += toCopy; - _pendingCount -= toCopy; - totalBytesWritten += toCopy; - - if (totalBytesWritten == buffer.Length) - { - return totalBytesWritten; - } + ValidateBufferArguments(buffer, offset, count); - buffer = buffer.Slice(totalBytesWritten); + return Read(new Span(buffer, offset, count)); } - if (_charPosition < _text.Length) + /// + // All Read overloads funnel here; ObjectDisposedException guards every path (TranscodingStream pattern). + public override int Read(Span buffer) { - ReadOnlySpan remaining = _text.Span.Slice(_charPosition); + ObjectDisposedException.ThrowIf(_disposed, this); - // If the caller's buffer may be too small for even one encoded scalar, - // encode into the spillover buffer first, then copy what fits. - // Encoder.Convert throws ArgumentException when the output buffer - // cannot hold a single complete encoded character. - if (buffer.Length < _encoding.GetMaxByteCount(1)) + if (buffer.Length == 0 || (_charPosition >= _text.Length && _pendingCount == 0 && _encoderFlushed)) { - _pendingBytes ??= new byte[_encoding.GetMaxByteCount(2)]; - int charsToEncode = Math.Min(2, remaining.Length); - _encoder.Convert(remaining.Slice(0, charsToEncode), _pendingBytes, flush: false, out int charsUsed, out int bytesUsed, out _); - _charPosition += charsUsed; - - int toCopy = Math.Min(bytesUsed, buffer.Length); - _pendingBytes.AsSpan(0, toCopy).CopyTo(buffer); - totalBytesWritten += toCopy; - bufferBytesWritten += toCopy; - - _pendingOffset = toCopy; - _pendingCount = bytesUsed - toCopy; + return 0; } - else + + // Fast path: nothing emitted yet and the caller's buffer is guaranteed + // large enough to hold the entire encoded payload in a single shot. + // Encoding.GetBytes is stateless and emits any reset/shift sequences + // required by stateful encodings for a complete conversion, so we can + // mark the encoder as flushed without ever allocating an Encoder. + if (_charPosition == 0 && _pendingCount == 0 && + buffer.Length >= _encoding.GetMaxByteCount(_text.Length)) { - // Encode directly into the caller's buffer. - // Only flush on the final block to preserve encoder state - // for stateful encodings. - _encoder.Convert(remaining, buffer, flush: false, out int charsUsed, out int bytesUsed, out _); - _charPosition += charsUsed; - totalBytesWritten += bytesUsed; - bufferBytesWritten += bytesUsed; + int written = _encoding.GetBytes(_text.Span, buffer); + _charPosition = _text.Length; + _encoderFlushed = true; + return written; } - } - // If all input chars are consumed but the encoder hasn't been flushed, - // flush any remaining encoder state (e.g., stateful encoding reset sequences). - // Always flush into _pendingBytes (which is guaranteed large enough) to - // avoid ArgumentException if the caller's remaining buffer is too small. - if (_charPosition >= _text.Length && !_encoderFlushed && _pendingCount == 0) - { - _pendingBytes ??= new byte[_encoding.GetMaxByteCount(2)]; - _encoder.Convert(ReadOnlySpan.Empty, _pendingBytes, flush: true, out _, out int flushBytes, out _); - _encoderFlushed = true; + int totalBytesWritten = 0; + int bufferBytesWritten = 0; - if (flushBytes > 0) + // Drain any pending bytes from a previous partial read. + if (_pendingCount > 0) { - Span flushTarget = buffer.Slice(bufferBytesWritten); - int toCopy = Math.Min(flushBytes, flushTarget.Length); - if (toCopy > 0) + int toCopy = Math.Min(_pendingCount, buffer.Length); + _pendingBytes.AsSpan(_pendingOffset, toCopy).CopyTo(buffer); + _pendingOffset += toCopy; + _pendingCount -= toCopy; + totalBytesWritten += toCopy; + + if (totalBytesWritten == buffer.Length) { - _pendingBytes.AsSpan(0, toCopy).CopyTo(flushTarget); - totalBytesWritten += toCopy; + return totalBytesWritten; } - if (toCopy < flushBytes) + buffer = buffer.Slice(totalBytesWritten); + } + + if (_charPosition < _text.Length) + { + ReadOnlySpan remaining = _text.Span.Slice(_charPosition); + + // If the caller's buffer may be too small for even one encoded scalar, + // encode into the spillover buffer first, then copy what fits. + // Encoder.Convert throws ArgumentException when the output buffer + // cannot hold a single complete encoded character. + if (buffer.Length < _maxBytesPerChar) { + // Instance field — ArrayPool not appropriate. Contents are + // always overwritten by Encoder.Convert before being read out, + // so we can skip the JIT-emitted zero-init (mirrors TranscodingStream). + _pendingBytes ??= GC.AllocateUninitializedArray(_encoding.GetMaxByteCount(2)); + int charsToEncode = Math.Min(2, remaining.Length); + GetEncoder().Convert(remaining.Slice(0, charsToEncode), _pendingBytes, flush: false, out int charsUsed, out int bytesUsed, out _); + _charPosition += charsUsed; + + int toCopy = Math.Min(bytesUsed, buffer.Length); + _pendingBytes.AsSpan(0, toCopy).CopyTo(buffer); + totalBytesWritten += toCopy; + bufferBytesWritten += toCopy; + _pendingOffset = toCopy; - _pendingCount = flushBytes - toCopy; + _pendingCount = bytesUsed - toCopy; + } + else + { + // Encode directly into the caller's buffer. + // Only flush on the final block to preserve encoder state + // for stateful encodings. + GetEncoder().Convert(remaining, buffer, flush: false, out int charsUsed, out int bytesUsed, out _); + _charPosition += charsUsed; + totalBytesWritten += bytesUsed; + bufferBytesWritten += bytesUsed; } } - } - return totalBytesWritten; - } + // If all input chars are consumed but the encoder hasn't been flushed, + // flush any remaining encoder state (e.g., stateful encoding reset sequences). + // Always flush into _pendingBytes (which is guaranteed large enough) to + // avoid ArgumentException if the caller's remaining buffer is too small. + if (_charPosition >= _text.Length && !_encoderFlushed && _pendingCount == 0) + { + _pendingBytes ??= GC.AllocateUninitializedArray(_encoding.GetMaxByteCount(2)); + GetEncoder().Convert(ReadOnlySpan.Empty, _pendingBytes, flush: true, out _, out int flushBytes, out _); + _encoderFlushed = true; - /// - public override int ReadByte() - { - byte b = 0; - return Read(new Span(ref b)) > 0 ? b : -1; - } + if (flushBytes > 0) + { + Span flushTarget = buffer.Slice(bufferBytesWritten); + int toCopy = Math.Min(flushBytes, flushTarget.Length); + if (toCopy > 0) + { + _pendingBytes.AsSpan(0, toCopy).CopyTo(flushTarget); + totalBytesWritten += toCopy; + } + + if (toCopy < flushBytes) + { + _pendingOffset = toCopy; + _pendingCount = flushBytes - toCopy; + } + } + } - /// - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ValidateBufferArguments(buffer, offset, count); + return totalBytesWritten; + } - if (cancellationToken.IsCancellationRequested) + /// + public override int ReadByte() { - return Task.FromCanceled(cancellationToken); + byte b = 0; + return Read(new Span(ref b)) > 0 ? b : -1; } - return Task.FromResult(Read(buffer, offset, count)); - } + private Encoder GetEncoder() => _encoder ??= _encoding.GetEncoder(); - /// - public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - if (cancellationToken.IsCancellationRequested) + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - return ValueTask.FromCanceled(cancellationToken); + ValidateBufferArguments(buffer, offset, count); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } } - return new ValueTask(Read(buffer.Span)); - } + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } - /// - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(SR.NotSupported_UnseekableStream); + try + { + return new ValueTask(Read(buffer.Span)); + } + catch (OperationCanceledException oce) + { + return ValueTask.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } + } - /// - public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(SR.NotSupported_UnseekableStream); - /// - public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - public override void Flush() { } + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); - /// - public override Task FlushAsync(CancellationToken cancellationToken) => - cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override void Flush() { } - /// - protected override void Dispose(bool disposing) - { - _disposed = true; - base.Dispose(disposing); + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + + /// + public override void CopyTo(Stream destination, int bufferSize) + { + ValidateCopyToArguments(destination, bufferSize); + ObjectDisposedException.ThrowIf(_disposed, this); + + // Size the rented buffer to the remaining encoded payload (capped by bufferSize). + // The base Stream.CopyTo falls back to an 80 KB buffer because our Length throws; + // sizing here avoids that waste and lets the single-shot Read fast path consume + // the entire input in one call when the rented buffer is large enough. + int maxBytes = _encoding.GetMaxByteCount(_text.Length - _charPosition); + int rentSize = Math.Max(1, Math.Min(maxBytes, bufferSize)); + byte[] buffer = ArrayPool.Shared.Rent(rentSize); + try + { + int n; + while ((n = Read(buffer, 0, buffer.Length)) != 0) + { + destination.Write(buffer, 0, n); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + ValidateCopyToArguments(destination, bufferSize); + ObjectDisposedException.ThrowIf(_disposed, this); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + int maxBytes = _encoding.GetMaxByteCount(_text.Length - _charPosition); + int rentSize = Math.Max(1, Math.Min(maxBytes, bufferSize)); + return CopyToAsyncCore(destination, rentSize, cancellationToken); + } + + private async Task CopyToAsyncCore(Stream destination, int rentSize, CancellationToken cancellationToken) + { + byte[] buffer = ArrayPool.Shared.Rent(rentSize); + try + { + int n; + // Read is synchronous and CPU-bound; no underlying IO to await. + while ((n = Read(buffer, 0, buffer.Length)) != 0) + { + await destination.WriteAsync(buffer.AsMemory(0, n), cancellationToken).ConfigureAwait(false); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + protected override void Dispose(bool disposing) + { + _disposed = true; + base.Dispose(disposing); + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs index 896daa13db0b13..7130e943488d66 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -4,368 +4,393 @@ using System.Threading; using System.Threading.Tasks; -namespace System.IO; - -/// -/// Provides a seekable, writable over a with fixed capacity. -/// -/// -/// The stream cannot expand beyond the initial memory capacity. -/// This type is not thread-safe. Synchronize access if the stream is used concurrently. -/// throws and returns . -/// -public sealed class WritableMemoryStream : MemoryStream +namespace System.IO { - private Memory _buffer; - private int _position; - private int _length; - private bool _isOpen; - private CachedCompletedInt32Task _lastReadTask; - /// - /// Initializes a new instance of the class over the specified . + /// Provides a seekable, writable over a with fixed capacity. /// - /// The to wrap. - public WritableMemoryStream(Memory buffer) : base() + /// + /// The stream cannot expand beyond the initial memory capacity. + /// This type is not thread-safe. Synchronize access if the stream is used concurrently. + /// throws and returns . + /// + public sealed class WritableMemoryStream : MemoryStream { - _buffer = buffer; - _length = 0; - _isOpen = true; - } + private Memory _buffer; + private int _position; + private int _length; + private bool _isOpen; + private CachedCompletedInt32Task _lastReadTask; + + /// + /// Initializes a new instance of the class over the specified . + /// + /// The to wrap. + public WritableMemoryStream(Memory buffer) : base() + { + _buffer = buffer; + _length = 0; + _isOpen = true; + } - /// - public override bool CanRead => _isOpen; + /// + public override bool CanRead => _isOpen; - /// - public override bool CanSeek => _isOpen; + /// + public override bool CanSeek => _isOpen; - /// - public override bool CanWrite => _isOpen; + /// + public override bool CanWrite => _isOpen; - /// - public override int Capacity - { - get + /// + public override int Capacity { - EnsureNotClosed(); - return _buffer.Length; + get + { + EnsureNotClosed(); + return _buffer.Length; + } + set => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); } - set => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); - } - /// - public override long Length - { - get + /// + public override long Length { - EnsureNotClosed(); + get + { + EnsureNotClosed(); - return _length; + return _length; + } } - } - /// - public override long Position - { - get + /// + public override long Position { - EnsureNotClosed(); - - return _position; + get + { + EnsureNotClosed(); + + return _position; + } + set + { + EnsureNotClosed(); + ArgumentOutOfRangeException.ThrowIfNegative(value); + ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue); + _position = (int)value; + } } - set + + /// + public override int ReadByte() { EnsureNotClosed(); - ArgumentOutOfRangeException.ThrowIfNegative(value); - ArgumentOutOfRangeException.ThrowIfGreaterThan(value, int.MaxValue); - _position = (int)value; - } - } - /// - public override int ReadByte() - { - EnsureNotClosed(); + ReadOnlySpan span = _buffer.Span; + int position = _position; - ReadOnlySpan span = _buffer.Span; - int position = _position; + if ((uint)position < (uint)_length) + { + _position++; + return span[position]; + } - if ((uint)position < (uint)_length) - { - _position++; - return span[position]; + return -1; } - return -1; - } - - /// - public override int Read(byte[] buffer, int offset, int count) - { - ValidateBufferArguments(buffer, offset, count); - - return Read(new Span(buffer, offset, count)); - } - - /// - public override int Read(Span buffer) - { - EnsureNotClosed(); - - int remaining = _length - _position; - if (remaining <= 0 || buffer.Length == 0) + /// + public override int Read(byte[] buffer, int offset, int count) { - return 0; + ValidateBufferArguments(buffer, offset, count); + + return Read(new Span(buffer, offset, count)); } - int bytesToRead = Math.Min(remaining, buffer.Length); - _buffer.Span.Slice(_position, bytesToRead).CopyTo(buffer); - _position += bytesToRead; + /// + public override int Read(Span buffer) + { + EnsureNotClosed(); - return bytesToRead; - } + int remaining = _length - _position; + if (remaining <= 0 || buffer.Length == 0) + { + return 0; + } - /// - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ValidateBufferArguments(buffer, offset, count); - EnsureNotClosed(); + int bytesToRead = Math.Min(remaining, buffer.Length); + _buffer.Span.Slice(_position, bytesToRead).CopyTo(buffer); + _position += bytesToRead; - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); + return bytesToRead; } - try - { - int n = Read(buffer, offset, count); - return _lastReadTask.GetTask(n); - } - catch (OperationCanceledException oce) - { - return Task.FromCanceled(oce.CancellationToken); - } - catch (Exception exception) + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - return Task.FromException(exception); - } - } - - /// - public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - EnsureNotClosed(); + ValidateBufferArguments(buffer, offset, count); + EnsureNotClosed(); - if (cancellationToken.IsCancellationRequested) - { - return ValueTask.FromCanceled(cancellationToken); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + int n = Read(buffer, offset, count); + return _lastReadTask.GetTask(n); + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception exception) + { + return Task.FromException(exception); + } } - return new ValueTask(Read(buffer.Span)); - } + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + EnsureNotClosed(); - /// - public override void CopyTo(Stream destination, int bufferSize) - { - ValidateCopyToArguments(destination, bufferSize); - EnsureNotClosed(); + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } - if (_length > _position) - { - destination.Write(_buffer.Span.Slice(_position, _length - _position)); - _position = _length; + return new ValueTask(Read(buffer.Span)); } - } - /// - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - ValidateCopyToArguments(destination, bufferSize); - EnsureNotClosed(); - - if (cancellationToken.IsCancellationRequested) + /// + public override void CopyTo(Stream destination, int bufferSize) { - return Task.FromCanceled(cancellationToken); + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + + if (_length > _position) + { + destination.Write(_buffer.Span.Slice(_position, _length - _position)); + _position = _length; + } } - if (_length > _position) + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) { - ReadOnlyMemory content = _buffer.Slice(_position, _length - _position); - _position = _length; + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); - return destination.WriteAsync(content, cancellationToken).AsTask(); - } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } - return Task.CompletedTask; - } + if (_length > _position) + { + ReadOnlyMemory content = _buffer.Slice(_position, _length - _position); + _position = _length; - /// - public override void WriteByte(byte value) - { - EnsureNotClosed(); + return destination.WriteAsync(content, cancellationToken).AsTask(); + } - if (_position >= _buffer.Length) - { - throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + return Task.CompletedTask; } - if (_position > _length) + /// + public override void WriteByte(byte value) { - _buffer.Span.Slice(_length, _position - _length).Clear(); - } - - _buffer.Span[_position++] = value; + EnsureNotClosed(); - if (_position > _length) - { - _length = _position; - } - } + if (_position >= _buffer.Length) + { + throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + } - /// - public override void Write(byte[] buffer, int offset, int count) - { - ValidateBufferArguments(buffer, offset, count); - Write(new ReadOnlySpan(buffer, offset, count)); - } + if (_position > _length) + { + _buffer.Span.Slice(_length, _position - _length).Clear(); + } - /// - public override void Write(ReadOnlySpan buffer) - { - EnsureNotClosed(); + _buffer.Span[_position++] = value; - if (buffer.Length == 0) - { - return; + if (_position > _length) + { + _length = _position; + } } - if (_position > _buffer.Length - buffer.Length) + /// + public override void Write(byte[] buffer, int offset, int count) { - throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + ValidateBufferArguments(buffer, offset, count); + Write(new ReadOnlySpan(buffer, offset, count)); } - if (_position > _length) + /// + public override void Write(ReadOnlySpan buffer) { - _buffer.Span.Slice(_length, _position - _length).Clear(); - } - - buffer.CopyTo(_buffer.Span.Slice(_position)); - _position += buffer.Length; + EnsureNotClosed(); - if (_position > _length) - { - _length = _position; - } - } + if (buffer.Length == 0) + { + return; + } - /// - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - ValidateBufferArguments(buffer, offset, count); - EnsureNotClosed(); + if (_position > _buffer.Length - buffer.Length) + { + throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + } - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } + if (_position > _length) + { + _buffer.Span.Slice(_length, _position - _length).Clear(); + } - Write(buffer, offset, count); - return Task.CompletedTask; - } + buffer.CopyTo(_buffer.Span.Slice(_position)); + _position += buffer.Length; - /// - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - EnsureNotClosed(); + if (_position > _length) + { + _length = _position; + } + } - if (cancellationToken.IsCancellationRequested) + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - return ValueTask.FromCanceled(cancellationToken); + ValidateBufferArguments(buffer, offset, count); + EnsureNotClosed(); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + Write(buffer, offset, count); + return Task.CompletedTask; + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } } - Write(buffer.Span); - return default; - } + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + EnsureNotClosed(); - /// - public override long Seek(long offset, SeekOrigin origin) - { - EnsureNotClosed(); + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + try + { + Write(buffer.Span); + return default; + } + catch (OperationCanceledException oce) + { + return ValueTask.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } + } - long newPosition = origin switch + /// + public override long Seek(long offset, SeekOrigin origin) { - SeekOrigin.Begin => offset, - SeekOrigin.Current => _position + offset, - SeekOrigin.End => _length + offset, - _ => throw new ArgumentOutOfRangeException(nameof(origin)) - }; + EnsureNotClosed(); - if (newPosition < 0) - { - throw new IOException(SR.IO_SeekBeforeBegin); - } + long newPosition = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _length + offset, + _ => throw new ArgumentOutOfRangeException(nameof(origin)) + }; - ArgumentOutOfRangeException.ThrowIfGreaterThan(newPosition, int.MaxValue, nameof(offset)); + // Order matches MemoryStream.SeekCore / Common/src/System/IO/ReadOnlyMemoryStream.cs: + // overflow check first, then seek-before-begin. + ArgumentOutOfRangeException.ThrowIfGreaterThan(newPosition, int.MaxValue, nameof(offset)); - _position = (int)newPosition; + if (newPosition < 0) + { + throw new IOException(SR.IO_SeekBeforeBegin); + } - return newPosition; - } + _position = (int)newPosition; - /// - public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + return newPosition; + } - /// - public override byte[] GetBuffer() => - throw new UnauthorizedAccessException(SR.UnauthorizedAccess_MemStreamBuffer); + /// + public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); - /// - public override bool TryGetBuffer(out ArraySegment buffer) - { - buffer = default; - return false; - } + /// + public override byte[] GetBuffer() => + throw new UnauthorizedAccessException(SR.UnauthorizedAccess_MemStreamBuffer); - /// - public override byte[] ToArray() - { - EnsureNotClosed(); - if (_length == 0) + /// + public override bool TryGetBuffer(out ArraySegment buffer) { - return Array.Empty(); + buffer = default; + return false; } - byte[] copy = GC.AllocateUninitializedArray(_length); - _buffer.Span.Slice(0, _length).CopyTo(copy); - return copy; - } + /// + public override byte[] ToArray() + { + EnsureNotClosed(); + if (_length == 0) + { + return Array.Empty(); + } + + byte[] copy = GC.AllocateUninitializedArray(_length); + _buffer.Span.Slice(0, _length).CopyTo(copy); + return copy; + } - /// - public override void WriteTo(Stream stream) - { - ArgumentNullException.ThrowIfNull(stream); - EnsureNotClosed(); + /// + public override void WriteTo(Stream stream) + { + ArgumentNullException.ThrowIfNull(stream); + EnsureNotClosed(); - stream.Write(_buffer.Span.Slice(0, _length)); - } + stream.Write(_buffer.Span.Slice(0, _length)); + } - /// - public override void Flush() { } + /// + public override void Flush() { } - /// - public override Task FlushAsync(CancellationToken cancellationToken) => - cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; - /// - protected override void Dispose(bool disposing) - { - _isOpen = false; - _buffer = default; - base.Dispose(disposing); - } + /// + protected override void Dispose(bool disposing) + { + _isOpen = false; + _buffer = default; + base.Dispose(disposing); + } - private void EnsureNotClosed() - { - ObjectDisposedException.ThrowIf(!_isOpen, this); + private void EnsureNotClosed() + { + ObjectDisposedException.ThrowIf(!_isOpen, this); + } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTestBase.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTestBase.cs index 36a2b0a562445a..05dee7d5f16253 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTestBase.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTestBase.cs @@ -130,5 +130,26 @@ public void MultipleReadsEventuallyReturnZero() int finalRead = ReadFromStream(stream, buffer, 0, buffer.Length); Assert.Equal(0, finalRead); } + + [Fact] + public void PendingBytesDrainAcrossSingleByteReads() + { + // '你' encodes to 3 UTF-8 bytes (0xE4 0xBD 0xA0). A 1-byte buffer forces + // the encoder spillover path; subsequent reads must drain _pendingBytes + // before encoding the next character. + string input = "你"; + byte[] expected = Encoding.UTF8.GetBytes(input); + + using Stream stream = CreateStream(input, Encoding.UTF8); + byte[] buffer = new byte[1]; + + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(1, ReadFromStream(stream, buffer, 0, 1)); + Assert.Equal(expected[i], buffer[0]); + } + + Assert.Equal(0, ReadFromStream(stream, buffer, 0, 1)); + } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs index 65cc717f79f7d3..3e30a4d8803ec1 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs @@ -113,32 +113,6 @@ public async Task ProducesSameOutputAsStringOverload(string input) memoryResult.AsSpan(0, memoryBytesRead).ToArray()); } - [Fact] - public void UnsupportedOperationsThrow() - { - var stream = new StringStream("test".AsMemory(), Encoding.UTF8); - - Assert.Throws(() => stream.Length); - Assert.Throws(() => stream.Position); - Assert.Throws(() => stream.Position = 0); - Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); - Assert.Throws(() => stream.Write(new byte[1], 0, 1)); - Assert.Throws(() => stream.SetLength(100)); - } - - [Fact] - public void DisposeIsIdempotent() - { - var stream = new StringStream("test".AsMemory(), Encoding.UTF8); - - stream.Dispose(); - Assert.False(stream.CanRead); - Assert.Throws(() => stream.Read(new byte[10], 0, 10)); - - stream.Dispose(); - stream.Dispose(); - } - [Fact] public void TruncatedSurrogatePairProducesReplacementChar() { diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs index 42017c38383db7..b123499b6132ca 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace System.IO.Tests @@ -82,26 +84,16 @@ public void EncodingPropertyReturnsCorrectEncoding() } [Fact] - public void UnsupportedOperationsThrow() + public async Task CopyToAsync_HonorsCancellation() { - var stream = new StringStream("test", Encoding.UTF8); + using var stream = new StringStream("hello", Encoding.UTF8); + using var destination = new MemoryStream(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); - Assert.Throws(() => stream.Length); - Assert.Throws(() => stream.Position); - Assert.Throws(() => stream.Position = 0); - Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); - Assert.Throws(() => stream.Write(new byte[1], 0, 1)); - Assert.Throws(() => stream.SetLength(100)); + await Assert.ThrowsAsync( + () => stream.CopyToAsync(destination, bufferSize: 81920, cts.Token)); } - [Fact] - public void DisposeRendersStreamUnreadable() - { - var stream = new StringStream("test", Encoding.UTF8); - stream.Dispose(); - - Assert.False(stream.CanRead); - Assert.Throws(() => stream.Read(new byte[1], 0, 1)); - } } } From af113289e1f82ba8c82411d6a114a448dec073dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Viviana=20Due=C3=B1as=20Chavez?= Date: Fri, 19 Jun 2026 16:58:26 -0700 Subject: [PATCH 11/11] Address review: BCL Seek exception parity, StringStream overflow guards, GetBuffer contract tests, ReadOnlySequenceStream XML doc --- .../System/Buffers/ReadOnlySequenceStream.cs | 11 +++++-- .../src/System/IO/ReadOnlyMemoryStream.cs | 2 +- .../src/System/IO/StringStream.cs | 32 ++++++++++++++----- .../src/System/IO/WritableMemoryStream.cs | 2 +- .../ReadOnlyMemoryStreamTests.cs | 12 +++++++ .../WritableMemoryStreamTests.cs | 12 +++++++ 6 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs index a6b6f4c119b3aa..59ba86a3ce1786 100644 --- a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs +++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs @@ -7,7 +7,14 @@ namespace System.Buffers { - + /// + /// Provides a seekable, read-only over a . + /// + /// + /// The underlying sequence is not copied; reads are served directly from its segments. + /// This type is not thread-safe. Synchronize access if the stream is used concurrently. + /// The stream cannot be written to. always returns . + /// public sealed class ReadOnlySequenceStream : Stream { private ReadOnlySequence _sequence; @@ -243,7 +250,7 @@ public override long Seek(long offset, SeekOrigin origin) SeekOrigin.Begin => offset, SeekOrigin.Current => _absolutePosition + offset, SeekOrigin.End => _sequence.Length + offset, - _ => throw new ArgumentOutOfRangeException(nameof(origin)) + _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)) }; if (absolutePosition < 0) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs index 75f9cac67f7368..ce4fe91bd02342 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -207,7 +207,7 @@ public override long Seek(long offset, SeekOrigin origin) SeekOrigin.Begin => offset, SeekOrigin.Current => _position + offset, SeekOrigin.End => _buffer.Length + offset, - _ => throw new ArgumentOutOfRangeException(nameof(origin)) + _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)) }; // Order matches MemoryStream.SeekCore / Common/src/System/IO/ReadOnlyMemoryStream.cs: diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs index 093ca473b337e5..61b416488706e8 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs @@ -115,7 +115,12 @@ public override int Read(Span buffer) // Encoding.GetBytes is stateless and emits any reset/shift sequences // required by stateful encodings for a complete conversion, so we can // mark the encoder as flushed without ever allocating an Encoder. + // + // Guard against Encoding.GetMaxByteCount overflowing for very large inputs + // ((_text.Length + 1) * _maxBytesPerChar must fit in int). If it would, + // fall through to the streaming path instead of throwing from Read. if (_charPosition == 0 && _pendingCount == 0 && + _text.Length <= (int.MaxValue / _maxBytesPerChar) - 1 && buffer.Length >= _encoding.GetMaxByteCount(_text.Length)) { int written = _encoding.GetBytes(_text.Span, buffer); @@ -299,12 +304,7 @@ public override void CopyTo(Stream destination, int bufferSize) ValidateCopyToArguments(destination, bufferSize); ObjectDisposedException.ThrowIf(_disposed, this); - // Size the rented buffer to the remaining encoded payload (capped by bufferSize). - // The base Stream.CopyTo falls back to an 80 KB buffer because our Length throws; - // sizing here avoids that waste and lets the single-shot Read fast path consume - // the entire input in one call when the rented buffer is large enough. - int maxBytes = _encoding.GetMaxByteCount(_text.Length - _charPosition); - int rentSize = Math.Max(1, Math.Min(maxBytes, bufferSize)); + int rentSize = ComputeCopyToRentSize(bufferSize); byte[] buffer = ArrayPool.Shared.Rent(rentSize); try { @@ -331,11 +331,27 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio return Task.FromCanceled(cancellationToken); } - int maxBytes = _encoding.GetMaxByteCount(_text.Length - _charPosition); - int rentSize = Math.Max(1, Math.Min(maxBytes, bufferSize)); + int rentSize = ComputeCopyToRentSize(bufferSize); return CopyToAsyncCore(destination, rentSize, cancellationToken); } + // Size the rented buffer to the remaining encoded payload (capped by bufferSize). + // Stream.CopyTo's default buffer is 80 KB because this stream is non-seekable; + // sizing here avoids that waste and lets the single-shot Read fast path consume + // the entire input in one call when the rented buffer is large enough. + // Falls back to bufferSize when Encoding.GetMaxByteCount would overflow. + private int ComputeCopyToRentSize(int bufferSize) + { + int remainingChars = _text.Length - _charPosition; + if (remainingChars <= (int.MaxValue / _maxBytesPerChar) - 1) + { + int maxBytes = _encoding.GetMaxByteCount(remainingChars); + return Math.Max(1, Math.Min(maxBytes, bufferSize)); + } + + return bufferSize; + } + private async Task CopyToAsyncCore(Stream destination, int rentSize, CancellationToken cancellationToken) { byte[] buffer = ArrayPool.Shared.Rent(rentSize); diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs index 7130e943488d66..d51b1e83b86be1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -319,7 +319,7 @@ public override long Seek(long offset, SeekOrigin origin) SeekOrigin.Begin => offset, SeekOrigin.Current => _position + offset, SeekOrigin.End => _length + offset, - _ => throw new ArgumentOutOfRangeException(nameof(origin)) + _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)) }; // Order matches MemoryStream.SeekCore / Common/src/System/IO/ReadOnlyMemoryStream.cs: diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs index b8950a0d475550..fa1800d9b765d0 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs @@ -51,5 +51,17 @@ public void ReadFromUnmanagedMemory() Assert.Equal(expected.Length, bytesRead); Assert.Equal(expected, result); } + + [Fact] + public void GetBuffer_Throws_TryGetBuffer_ReturnsFalse() + { + var stream = new ReadOnlyMemoryStream(new byte[] { 1, 2, 3 }); + + Assert.Throws(() => stream.GetBuffer()); + Assert.False(stream.TryGetBuffer(out ArraySegment segment)); + Assert.Null(segment.Array); + Assert.Equal(0, segment.Offset); + Assert.Equal(0, segment.Count); + } } } diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs index e24cd0d0e2bc6f..dc9770c13c9232 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs @@ -113,5 +113,17 @@ public void WriteToUnmanagedMemory() Assert.Equal(data, manager.GetSpan().ToArray()); Assert.Equal(data.Length, stream.Position); } + + [Fact] + public void GetBuffer_Throws_TryGetBuffer_ReturnsFalse() + { + using var stream = new WritableMemoryStream(new byte[8]); + + Assert.Throws(() => stream.GetBuffer()); + Assert.False(stream.TryGetBuffer(out ArraySegment segment)); + Assert.Null(segment.Array); + Assert.Equal(0, segment.Offset); + Assert.Equal(0, segment.Count); + } } }