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 c665b746232878..75323b8cf8a316 100644 --- a/src/libraries/System.Memory/ref/System.Memory.cs +++ b/src/libraries/System.Memory/ref/System.Memory.cs @@ -160,6 +160,33 @@ 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 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 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) { } + 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 { 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..41b848970cf2ea 100644 --- a/src/libraries/System.Memory/src/System.Memory.csproj +++ b/src/libraries/System.Memory/src/System.Memory.csproj @@ -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..e43dac61e90c4c --- /dev/null +++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs @@ -0,0 +1,285 @@ +// 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 _absolutePosition; + 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; + _absolutePosition = 0; + _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 _absolutePosition; + } + set + { + EnsureNotDisposed(); + ArgumentOutOfRangeException.ThrowIfNegative(value); + + if (value >= _sequence.Length) + { + _position = _sequence.End; + } + else if (value >= _absolutePosition) + { + _position = _sequence.GetPosition(value - _absolutePosition, _position); + } + else + { + _position = _sequence.GetPosition(value, _sequence.Start); + } + + _absolutePosition = value; + } + } + + /// + 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 (_absolutePosition >= _sequence.Length) + { + 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); + _absolutePosition += n; + 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) + { + ValidateBufferArguments(buffer, offset, count); + + 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 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); + + /// + 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 => _absolutePosition + offset, + SeekOrigin.End => _sequence.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 >= _sequence.Length) + { + _position = _sequence.End; + } + else if (absolutePosition >= _absolutePosition) + { + _position = _sequence.GetPosition(absolutePosition - _absolutePosition, _position); + } + else + { + _position = _sequence.GetPosition(absolutePosition, _sequence.Start); + } + + _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); + + /// + protected override void Dispose(bool disposing) + { + _isDisposed = true; + _sequence = default; + 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..9f895947351415 100644 --- a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj @@ -132,6 +132,8 @@ + + @@ -287,4 +289,7 @@ + + + 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..f0b6e17ea42e68 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -0,0 +1,281 @@ +// 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 . +/// throws and returns . +/// +public sealed class ReadOnlyMemoryStream : MemoryStream +{ + 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) : base() + { + _buffer = source; + _isOpen = true; + } + + /// + public override bool CanRead => _isOpen; + + /// + public override bool CanSeek => _isOpen; + + /// + 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 + { + 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(); + + ReadOnlySpan span = _buffer.Span; + int position = _position; + + if ((uint)position < (uint)span.Length) + { + _position++; + return span[position]; + } + + 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) + { + 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); + EnsureNotClosed(); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.FromResult(Read(buffer, offset, count)); + } + + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + 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(_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 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); + + /// + 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() { } + + /// + 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); + } + + 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..89dcb24bde8704 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs @@ -0,0 +1,255 @@ +// 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; + 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. + /// + /// 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 && _pendingCount == 0 && _encoderFlushed)) + { + return 0; + } + + int totalBytesWritten = 0; + + // 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); + } + + 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); + _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; + + _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 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 (toCopy < flushBytes) + { + _pendingOffset = toCopy; + _pendingCount = flushBytes - toCopy; + } + } + } + + return totalBytesWritten; + } + + /// + 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 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() { } + + /// + 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..c5d1869cf18856 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -0,0 +1,359 @@ +// 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. +/// throws and returns . +/// +public sealed class WritableMemoryStream : MemoryStream +{ + private Memory _buffer; + private int _position; + private int _length; + private bool _isOpen; + + /// + /// 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 CanSeek => _isOpen; + + /// + 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 + { + get + { + EnsureNotClosed(); + + return _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(); + + ReadOnlySpan span = _buffer.Span; + int position = _position; + + if ((uint)position < (uint)_length) + { + _position++; + return span[position]; + } + + 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) + { + 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); + EnsureNotClosed(); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return Task.FromResult(Read(buffer, offset, count)); + } + + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + 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 (_length > _position) + { + destination.Write(((ReadOnlyMemory)_buffer).Span.Slice(_position, _length - _position)); + _position = _length; + } + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + + if (_length > _position) + { + ReadOnlyMemory content = ((ReadOnlyMemory)_buffer).Slice(_position, _length - _position); + _position = _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; + + if (_position > _length) + { + _length = _position; + } + } + + /// + 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 (buffer.Length == 0) + { + return; + } + + if (_position > _buffer.Length - buffer.Length) + { + throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + } + + buffer.CopyTo(_buffer.Span.Slice(_position)); + _position += buffer.Length; + + if (_position > _length) + { + _length = _position; + } + } + + /// + 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 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 (_length == 0) + { + return Array.Empty(); + } + + byte[] copy = GC.AllocateUninitializedArray(_length); + ((ReadOnlyMemory)_buffer).Span.Slice(0, _length).CopyTo(copy); + return copy; + } + + /// + public override void WriteTo(Stream stream) + { + ArgumentNullException.ThrowIfNull(stream); + EnsureNotClosed(); + + if (_length > 0) + { + stream.Write(((ReadOnlyMemory)_buffer).Span.Slice(0, _length)); + } + } + + /// + 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; + _buffer = default; + 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..cfcdc0d72737a4 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -11023,6 +11023,90 @@ 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 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.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 WriteByte(byte value) { } + public override void WriteTo(System.IO.Stream 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 { 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..e5d6599d126572 --- /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)); + } + + // 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 new file mode 100644 index 00000000000000..0b200e43242b76 --- /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_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() + { + 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..fd2eecc78fe43f --- /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_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); + + 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..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 @@ -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..5e875ffdc043f3 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs @@ -0,0 +1,43 @@ +// 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) => Task.FromResult(null); + + 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. + + } +} 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..69011e3a3446cd --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs @@ -0,0 +1,263 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.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]; + + Assert.Throws(() => stream.Write(data, 0, data.Length)); + } + + [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); + + Assert.Throws(() => stream.WriteByte(4)); + } + + [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_PastCapacity_ThrowsWithoutSideEffects() + { + 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 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); + + // 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() + { + 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); + } + } +}