From 0e0efa68ce100d01e22b24d5336460d8d08dc6ab Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 14 Jul 2026 11:25:41 -0500 Subject: [PATCH 1/5] Derive from Stream in ReadOnlyMemoryStream and WritableMemoryStream Instead of deriving from MemoryStream. --- .../System/IO/StreamConformanceTests.cs | 40 +++++- ...veWritableMemoryStream.ConformanceTests.cs | 19 +-- .../src/Resources/Strings.resx | 3 + .../src/System/IO/ReadOnlyMemoryStream.cs | 108 ++++++++++----- .../src/System/IO/WritableMemoryStream.cs | 125 +++++++++++++----- .../System.Runtime/ref/System.Runtime.cs | 32 +++-- .../MemoryStream/MemoryStreamTests.cs | 62 +++++++++ .../ReadOnlyMemoryStreamTests.cs | 9 +- .../WritableMemoryStreamConformanceTests.cs | 16 ++- .../WritableMemoryStreamTests.cs | 42 +++--- 10 files changed, 333 insertions(+), 123 deletions(-) diff --git a/src/libraries/Common/tests/StreamConformanceTests/System/IO/StreamConformanceTests.cs b/src/libraries/Common/tests/StreamConformanceTests/System/IO/StreamConformanceTests.cs index 9a3f3c6be36e06..9ecd2eaae77a84 100644 --- a/src/libraries/Common/tests/StreamConformanceTests/System/IO/StreamConformanceTests.cs +++ b/src/libraries/Common/tests/StreamConformanceTests/System/IO/StreamConformanceTests.cs @@ -1094,8 +1094,9 @@ public virtual async Task Write_Read_Success(ReadWriteMode mode) if (SkipOnWasi(mode)) return; const int Length = 1024; + const int Copies = 3; - using Stream? stream = await CreateReadWriteStream(); + using Stream? stream = await CreateReadWriteStream(new byte[Length * Copies]); if (stream is null) { return; @@ -1103,7 +1104,6 @@ public virtual async Task Write_Read_Success(ReadWriteMode mode) byte[] expected = GetRandomBytes(Length); - const int Copies = 3; for (int i = 0; i < Copies; i++) { await WriteAsync(mode, stream, expected, 0, expected.Length); @@ -1120,6 +1120,42 @@ public virtual async Task Write_Read_Success(ReadWriteMode mode) } } + [Theory] + [MemberData(nameof(AllReadWriteModes))] + public virtual async Task Write_GrowsLength_Success(ReadWriteMode mode) + { + if (SkipOnWasi(mode)) return; + + // Start from an empty stream. Streams that cannot produce an empty read-write instance + // (e.g. fixed-capacity streams that require initial data) return null and are skipped. + using Stream? stream = await CreateReadWriteStream(); + if (stream is null) + { + return; + } + + const int Length = 1024; + byte[] expected = GetRandomBytes(Length); + + if (stream.CanSeek) + { + Assert.Equal(0, stream.Length); + } + + await WriteAsync(mode, stream, expected, 0, expected.Length); + + if (stream.CanSeek) + { + Assert.Equal(Length, stream.Position); + Assert.Equal(Length, stream.Length); + + stream.Position = 0; + byte[] actual = new byte[Length]; + Assert.Equal(Length, await ReadAllAsync(mode, stream, actual, 0, actual.Length)); + AssertExtensions.SequenceEqual(expected, actual); + } + } + [Theory] [MemberData(nameof(AllReadWriteModes))] public virtual async Task Flush_ReadOnly_DoesntImpactReading(ReadWriteMode mode) diff --git a/src/libraries/System.Memory/tests/ReadOnlyBuffer/NativeWritableMemoryStream.ConformanceTests.cs b/src/libraries/System.Memory/tests/ReadOnlyBuffer/NativeWritableMemoryStream.ConformanceTests.cs index a49627cc936864..41983c20bd905d 100644 --- a/src/libraries/System.Memory/tests/ReadOnlyBuffer/NativeWritableMemoryStream.ConformanceTests.cs +++ b/src/libraries/System.Memory/tests/ReadOnlyBuffer/NativeWritableMemoryStream.ConformanceTests.cs @@ -10,7 +10,6 @@ namespace System.Memory.Tests public class NativeWritableMemoryStreamConformanceTests : StandaloneStreamConformanceTests { protected override bool CanSeek => true; - protected override bool CanSetLength => false; protected override bool NopFlushCompletesSynchronously => true; protected override bool CanSetLengthGreaterThanCapacity => false; @@ -20,17 +19,19 @@ public class NativeWritableMemoryStreamConformanceTests : StandaloneStreamConfor protected override Task CreateReadWriteStreamCore(byte[]? initialData) { - int dataLength = initialData?.Length ?? 0; - int length = dataLength == 0 ? 64 * 1024 : dataLength; - var manager = new System.Buffers.NativeMemoryManager(length); + // See WritableMemoryStreamConformanceTests: WritableMemoryStream is fixed-capacity, so the + // empty case returns null and the grow-from-empty conformance tests are skipped. + if (initialData is null or { Length: 0 }) + { + return Task.FromResult(null); + } + + var manager = new System.Buffers.NativeMemoryManager(initialData.Length); manager.GetSpan().Clear(); var inner = new WritableMemoryStream(manager.Memory); - if (dataLength != 0) - { - inner.Write(initialData!, 0, dataLength); - inner.Position = 0; - } + inner.Write(initialData, 0, initialData.Length); + inner.Position = 0; return Task.FromResult(new NativeMemoryOwningStream(inner, manager)); } diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index d05352ecb6fa96..08d49c98397a85 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -1952,6 +1952,9 @@ Stream length must be non-negative and less than the maximum array length {0} - origin. + + Stream position must be non-negative and less than or equal to {0}. + The length of the buffer must be less than the maximum UIntPtr value for your platform. 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 a3dd29fc9438a3..8b5f1f16842d38 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -7,49 +7,78 @@ namespace System.IO { /// - /// Provides a seekable, read-only over a . + /// Provides a seekable, read-only over a . /// /// - /// The stream cannot be written to. always returns . - /// throws and returns . + /// The stream cannot be written to. always returns . /// - public sealed class ReadOnlyMemoryStream : MemoryStream + public sealed class ReadOnlyMemoryStream : Stream { private ReadOnlyMemory _memory; + private int _position; + private bool _isOpen; + private CachedCompletedInt32Task _lastReadTask; // The last successful task returned from ReadAsync /// /// Initializes a new instance of the class over the specified . /// /// The to wrap. - public ReadOnlyMemoryStream(ReadOnlyMemory source) : base() + public ReadOnlyMemoryStream(ReadOnlyMemory source) { - _writable = false; _memory = source; - _length = source.Length; + _isOpen = true; } /// - public override int Capacity + public override bool CanRead => _isOpen; + + /// + public override bool CanSeek => _isOpen; + + /// + public override bool CanWrite => false; + + /// + public override long Length { get { EnsureNotClosed(); return _memory.Length; } - set => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); } /// - public override byte[] GetBuffer() => - throw new UnauthorizedAccessException(SR.UnauthorizedAccess_MemStreamBuffer); + public override long Position + { + get + { + EnsureNotClosed(); + return _position; + } + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + EnsureNotClosed(); + + if (value > int.MaxValue) + { + throw new ArgumentOutOfRangeException(nameof(value), SR.Format(SR.ArgumentOutOfRange_StreamPosition, int.MaxValue)); + } + + _position = (int)value; + } + } /// - public override bool TryGetBuffer(out ArraySegment buffer) + public override void Flush() { - buffer = default; - return false; } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// public override int ReadByte() { @@ -121,6 +150,34 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return new ValueTask(Read(buffer.Span)); } + /// + public override long Seek(long offset, SeekOrigin origin) + { + EnsureNotClosed(); + + int loc = origin switch + { + SeekOrigin.Begin => 0, + SeekOrigin.Current => _position, + SeekOrigin.End => _memory.Length, + _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin) + }; + + if (offset > int.MaxValue - loc) + { + throw new ArgumentOutOfRangeException(nameof(offset), SR.Format(SR.ArgumentOutOfRange_StreamPosition, int.MaxValue)); + } + + int tempPosition = unchecked(loc + (int)offset); + if (unchecked(loc + offset) < 0 || tempPosition < 0) + { + throw new IOException(SR.IO_SeekBeforeBegin); + } + + _position = tempPosition; + return _position; + } + /// public override void CopyTo(Stream destination, int bufferSize) { @@ -157,32 +214,17 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio } /// - public override byte[] ToArray() - { - EnsureNotClosed(); - if (_memory.Length == 0) - { - return Array.Empty(); - } - - byte[] copy = GC.AllocateUninitializedArray(_memory.Length); - _memory.Span.CopyTo(copy); - return copy; - } + public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); /// - public override void WriteTo(Stream stream) - { - ArgumentNullException.ThrowIfNull(stream); - EnsureNotClosed(); - - stream.Write(_memory.Span); - } + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(SR.NotSupported_UnwritableStream); /// protected override void Dispose(bool disposing) { + _isOpen = false; _memory = default; + _lastReadTask = 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 2b6284f7746f20..802c6191c7501c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -7,47 +7,77 @@ namespace System.IO { /// - /// Provides a seekable, writable over a with fixed capacity. + /// Provides a seekable, writable over a . /// - /// - /// The stream cannot expand beyond the initial memory capacity. - /// throws and returns . - /// - public sealed class WritableMemoryStream : MemoryStream + public sealed class WritableMemoryStream : Stream { private Memory _memory; + private int _position; + private int _length; + private bool _isOpen; + private CachedCompletedInt32Task _lastReadTask; // The last successful task returned from ReadAsync /// /// Initializes a new instance of the class over the specified . /// /// The to wrap. - public WritableMemoryStream(Memory buffer) : base() + public WritableMemoryStream(Memory buffer) { _memory = buffer; + _length = buffer.Length; + _isOpen = true; } /// - public override int Capacity + public override bool CanRead => _isOpen; + + /// + public override bool CanSeek => _isOpen; + + /// + public override bool CanWrite => _isOpen; + + /// + public override long Length { get { EnsureNotClosed(); - return _memory.Length; + return _length; } - set => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); } /// - public override byte[] GetBuffer() => - throw new UnauthorizedAccessException(SR.UnauthorizedAccess_MemStreamBuffer); + public override long Position + { + get + { + EnsureNotClosed(); + return _position; + } + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + EnsureNotClosed(); + + if (value > int.MaxValue) + { + throw new ArgumentOutOfRangeException(nameof(value), SR.Format(SR.ArgumentOutOfRange_StreamPosition, int.MaxValue)); + } + + _position = (int)value; + } + } /// - public override bool TryGetBuffer(out ArraySegment buffer) + public override void Flush() { - buffer = default; - return false; } + /// + public override Task FlushAsync(CancellationToken cancellationToken) => + cancellationToken.IsCancellationRequested ? Task.FromCanceled(cancellationToken) : Task.CompletedTask; + /// public override int ReadByte() { @@ -119,6 +149,34 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return new ValueTask(Read(buffer.Span)); } + /// + public override long Seek(long offset, SeekOrigin origin) + { + EnsureNotClosed(); + + int loc = origin switch + { + SeekOrigin.Begin => 0, + SeekOrigin.Current => _position, + SeekOrigin.End => _length, + _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin) + }; + + if (offset > int.MaxValue - loc) + { + throw new ArgumentOutOfRangeException(nameof(offset), SR.Format(SR.ArgumentOutOfRange_StreamPosition, int.MaxValue)); + } + + int tempPosition = unchecked(loc + (int)offset); + if (unchecked(loc + offset) < 0 || tempPosition < 0) + { + throw new IOException(SR.IO_SeekBeforeBegin); + } + + _position = tempPosition; + return _position; + } + /// public override void CopyTo(Stream destination, int bufferSize) { @@ -236,35 +294,36 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo } /// - public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); - - /// - public override byte[] ToArray() + public override void SetLength(long value) { + ArgumentOutOfRangeException.ThrowIfNegative(value); EnsureNotClosed(); - if (_length == 0) + + if (value > _memory.Length) { - return Array.Empty(); + throw new NotSupportedException(SR.IO_FixedCapacity); } - byte[] copy = GC.AllocateUninitializedArray(_length); - _memory.Span.Slice(0, _length).CopyTo(copy); - return copy; - } - - /// - public override void WriteTo(Stream stream) - { - ArgumentNullException.ThrowIfNull(stream); - EnsureNotClosed(); + int newLength = (int)value; + if (newLength > _length) + { + // Zero out the newly exposed region so it reads back as zeros. + _memory.Span.Slice(_length, newLength - _length).Clear(); + } - stream.Write(_memory.Span.Slice(0, _length)); + _length = newLength; + if (_position > newLength) + { + _position = newLength; + } } /// protected override void Dispose(bool disposing) { + _isOpen = false; _memory = default; + _lastReadTask = default; base.Dispose(disposing); } @@ -278,7 +337,7 @@ private void EnsureCapacity(int count) { if (count != 0 && _position > _memory.Length - count) { - throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + throw new NotSupportedException(SR.IO_FixedCapacity); } } } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 62fc8aefc1d5e5..a19e4a6972eec1 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -11109,45 +11109,53 @@ 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 sealed partial class ReadOnlyMemoryStream : System.IO.Stream { public ReadOnlyMemoryStream(System.ReadOnlyMemory source) { } - public override int Capacity { get { throw null; } set { } } + 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; } protected override void Dispose(bool disposing) { } - public override byte[] GetBuffer() { throw null; } + 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 byte[] ToArray() { throw null; } - public override bool TryGetBuffer(out System.ArraySegment buffer) { throw null; } - public override void WriteTo(System.IO.Stream stream) { } + 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.MemoryStream + public sealed partial class WritableMemoryStream : System.IO.Stream { public WritableMemoryStream(System.Memory buffer) { } - public override int Capacity { get { throw null; } set { } } + 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; } protected override void Dispose(bool disposing) { } - public override byte[] GetBuffer() { throw null; } + 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 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/MemoryStream/MemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/MemoryStream/MemoryStreamTests.cs index 4f7dc3484b163b..0f08589e18b9c9 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/MemoryStream/MemoryStreamTests.cs +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/MemoryStream/MemoryStreamTests.cs @@ -166,6 +166,68 @@ public void MemoryStream_CapacityBoundaryChecks() } } + [Fact] + public void UserBuffer_WriteBeyondCapacityThrows() + { + byte[] buffer = new byte[10]; + using MemoryStream stream = new MemoryStream(buffer); + + byte[] data = new byte[15]; + Assert.Throws(() => stream.Write(data, 0, data.Length)); + } + + [Fact] + public void UserBuffer_WriteUpToExactCapacitySucceeds() + { + byte[] buffer = new byte[10]; + using MemoryStream stream = new MemoryStream(buffer); + + byte[] data = new byte[10]; + 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); + + 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 UserBuffer_SetLengthBeyondCapacityThrows() + { + byte[] buffer = new byte[8]; + using MemoryStream stream = new MemoryStream(buffer); + + Assert.Throws(() => stream.SetLength(9)); + Assert.Equal(8, stream.Length); + } + + [Fact] + public void UserBuffer_WritePastShrunkenLengthExtendsAndZeroesGap() + { + byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8 }; + using MemoryStream stream = new MemoryStream(buffer); + + stream.SetLength(2); + Assert.Equal(2, stream.Length); + + stream.Position = 5; + stream.WriteByte(42); + + Assert.Equal(6, stream.Length); + Assert.Equal(6, stream.Position); + + stream.Position = 0; + byte[] readBack = new byte[6]; + Assert.Equal(6, stream.Read(readBack, 0, readBack.Length)); + Assert.Equal(new byte[] { 1, 2, 0, 0, 0, 42 }, readBack); + } + private class ReadWriteOverridingMemoryStream : MemoryStream { public bool ReadArrayInvoked, WriteArrayInvoked; 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 08ac730a05dddf..2d0ebaa5da542c 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 @@ -35,15 +35,12 @@ public void WorksWithSlicedMemory() } [Fact] - public void GetBuffer_Throws_TryGetBuffer_ReturnsFalse() + public void WriteThrowsNotSupported() { 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); + Assert.Throws(() => stream.Write(new byte[1], 0, 1)); + Assert.Throws(() => stream.SetLength(1)); } } } 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 a30dde0a9897f3..c33052ad883e7b 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 @@ -9,7 +9,6 @@ namespace System.IO.Tests public class WritableMemoryStreamConformanceTests : StandaloneStreamConformanceTests { protected override bool CanSeek => true; - protected override bool CanSetLength => false; protected override bool NopFlushCompletesSynchronously => true; protected override bool CanSetLengthGreaterThanCapacity => false; @@ -19,16 +18,19 @@ public class WritableMemoryStreamConformanceTests : StandaloneStreamConformanceT protected override Task CreateReadWriteStreamCore(byte[]? initialData) { - int length = initialData?.Length ?? 0; - if (length == 0) + // WritableMemoryStream wraps a fixed-capacity buffer and cannot be created empty and then + // grown, so - like the other fixed-capacity streams (UnmanagedMemoryStream, + // MemoryMappedViewStream) - the empty case returns null and the grow-from-empty conformance + // tests are skipped. Growing the length within the fixed capacity is covered by the unit + // tests in WritableMemoryStreamTests. + if (initialData is null or { Length: 0 }) { - return Task.FromResult( - new WritableMemoryStream(new Memory(new byte[64 * 1024]))); + return Task.FromResult(null); } - var memory = new Memory(new byte[length]); + var memory = new Memory(new byte[initialData.Length]); var stream = new WritableMemoryStream(memory); - stream.Write(initialData!, 0, length); + stream.Write(initialData, 0, initialData.Length); stream.Position = 0; return Task.FromResult(stream); } 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 f54e133fc86ce0..9d6e77f9c1cf56 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 @@ -80,34 +80,34 @@ public void SeekPastCapacitySucceeds() } [Fact] - public void WriteOverExistingDataReplacesData() + public void SetLengthBeyondCapacityThrows() { - 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); - - stream.Position = 3; - stream.Write(new byte[] { 100, 101, 102 }, 0, 3); - - stream.Position = 0; - byte[] result = new byte[10]; - int bytesRead = stream.Read(result, 0, 10); + byte[] buffer = new byte[8]; + Stream stream = new WritableMemoryStream(new Memory(buffer)); - Assert.Equal(10, bytesRead); - Assert.Equal(new byte[] { 1, 2, 3, 100, 101, 102, 7, 8, 9, 10 }, result); + Assert.Throws(() => stream.SetLength(9)); + Assert.Equal(8, stream.Length); } [Fact] - public void GetBuffer_Throws_TryGetBuffer_ReturnsFalse() + public void WritePastShrunkenLengthExtendsAndZeroesGap() { - using var stream = new WritableMemoryStream(new byte[8]); + byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8 }; + Stream stream = new WritableMemoryStream(new Memory(buffer)); + + stream.SetLength(2); + Assert.Equal(2, stream.Length); + + stream.Position = 5; + stream.WriteByte(42); - 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); + Assert.Equal(6, stream.Length); + Assert.Equal(6, stream.Position); + + stream.Position = 0; + byte[] readBack = new byte[6]; + Assert.Equal(6, stream.Read(readBack, 0, readBack.Length)); + Assert.Equal(new byte[] { 1, 2, 0, 0, 0, 42 }, readBack); } } } From 25d62ee550c3d869bba7e71eb9476287721f8703 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Tue, 14 Jul 2026 19:46:09 -0500 Subject: [PATCH 2/5] Align CreateReadWriteStreamCore with UnmanagedMemoryStream and MemoryMappedViewStream --- .../NativeWritableMemoryStream.ConformanceTests.cs | 4 ++-- .../WritableMemoryStreamConformanceTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Memory/tests/ReadOnlyBuffer/NativeWritableMemoryStream.ConformanceTests.cs b/src/libraries/System.Memory/tests/ReadOnlyBuffer/NativeWritableMemoryStream.ConformanceTests.cs index 41983c20bd905d..10e9dc0d5d2f56 100644 --- a/src/libraries/System.Memory/tests/ReadOnlyBuffer/NativeWritableMemoryStream.ConformanceTests.cs +++ b/src/libraries/System.Memory/tests/ReadOnlyBuffer/NativeWritableMemoryStream.ConformanceTests.cs @@ -20,8 +20,8 @@ public class NativeWritableMemoryStreamConformanceTests : StandaloneStreamConfor protected override Task CreateReadWriteStreamCore(byte[]? initialData) { // See WritableMemoryStreamConformanceTests: WritableMemoryStream is fixed-capacity, so the - // empty case returns null and the grow-from-empty conformance tests are skipped. - if (initialData is null or { Length: 0 }) + // null case returns null and the grow-from-empty conformance tests are skipped. + if (initialData is null) { return Task.FromResult(null); } 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 c33052ad883e7b..117cbf2ec8d9b5 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,10 +20,10 @@ public class WritableMemoryStreamConformanceTests : StandaloneStreamConformanceT { // WritableMemoryStream wraps a fixed-capacity buffer and cannot be created empty and then // grown, so - like the other fixed-capacity streams (UnmanagedMemoryStream, - // MemoryMappedViewStream) - the empty case returns null and the grow-from-empty conformance + // MemoryMappedViewStream) - the null case returns null and the grow-from-empty conformance // tests are skipped. Growing the length within the fixed capacity is covered by the unit // tests in WritableMemoryStreamTests. - if (initialData is null or { Length: 0 }) + if (initialData is null) { return Task.FromResult(null); } From 421fb0a8a712e12842fde9daef042c44832dbb5e Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 15 Jul 2026 09:03:01 -0500 Subject: [PATCH 3/5] Revert changes to MemoryStream --- .../src/System/IO/MemoryStream.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs index 7f6001364fcc85..4d3281732064a0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs @@ -20,19 +20,19 @@ namespace System.IO // a stream "view" of the data. public class MemoryStream : Stream { - private byte[] _buffer; // Either allocated internally or externally. + private byte[] _buffer; // Either allocated internally or externally. private readonly int _origin; // For user-provided arrays, start at this origin - private protected int _position; // read/write head. - private protected int _length; // Number of bytes within the memory stream - private int _capacity; // length of usable portion of buffer for stream + private int _position; // read/write head. + private int _length; // Number of bytes within the memory stream + private int _capacity; // length of usable portion of buffer for stream // Note that _capacity == _buffer.Length for non-user-provided byte[]'s - private bool _expandable; // User-provided buffers aren't expandable. - private protected bool _writable; // Can user write to this stream? + private bool _expandable; // User-provided buffers aren't expandable. + private bool _writable; // Can user write to this stream? private readonly bool _exposable; // Whether the array can be returned to the user. - private protected bool _isOpen; // Is this stream open or closed? + private bool _isOpen; // Is this stream open or closed? - private protected CachedCompletedInt32Task _lastReadTask; // The last successful task returned from ReadAsync + private CachedCompletedInt32Task _lastReadTask; // The last successful task returned from ReadAsync public MemoryStream() : this(0) From e82b98af954b70a7834f52dabff1010706879750 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 15 Jul 2026 13:33:45 -0500 Subject: [PATCH 4/5] Align comments --- .../src/System/IO/MemoryStream.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs index 4d3281732064a0..567de307121346 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs @@ -20,17 +20,17 @@ namespace System.IO // a stream "view" of the data. public class MemoryStream : Stream { - private byte[] _buffer; // Either allocated internally or externally. - private readonly int _origin; // For user-provided arrays, start at this origin - private int _position; // read/write head. - private int _length; // Number of bytes within the memory stream - private int _capacity; // length of usable portion of buffer for stream + private byte[] _buffer; // Either allocated internally or externally. + private readonly int _origin; // For user-provided arrays, start at this origin + private int _position; // read/write head. + private int _length; // Number of bytes within the memory stream + private int _capacity; // length of usable portion of buffer for stream // Note that _capacity == _buffer.Length for non-user-provided byte[]'s - private bool _expandable; // User-provided buffers aren't expandable. - private bool _writable; // Can user write to this stream? - private readonly bool _exposable; // Whether the array can be returned to the user. - private bool _isOpen; // Is this stream open or closed? + private bool _expandable; // User-provided buffers aren't expandable. + private bool _writable; // Can user write to this stream? + private readonly bool _exposable; // Whether the array can be returned to the user. + private bool _isOpen; // Is this stream open or closed? private CachedCompletedInt32Task _lastReadTask; // The last successful task returned from ReadAsync From 401200e5d71d5c71d31c469f95a02b55284fd6ea Mon Sep 17 00:00:00 2001 From: David Cantu Date: Wed, 15 Jul 2026 13:34:30 -0500 Subject: [PATCH 5/5] override all Write* methods --- .../src/System/IO/ReadOnlyMemoryStream.cs | 12 ++++++++++++ src/libraries/System.Runtime/ref/System.Runtime.cs | 4 ++++ .../ReadOnlyMemoryStreamTests.cs | 4 ++++ 3 files changed, 20 insertions(+) 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 8b5f1f16842d38..739a3391ff872c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -219,6 +219,18 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio /// 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); + /// protected override void Dispose(bool disposing) { diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index a19e4a6972eec1..2750fcf8993bdb 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -11130,6 +11130,10 @@ public override void Flush() { } 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 override void WriteByte(byte value) { } } public sealed partial class WritableMemoryStream : System.IO.Stream { 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 2d0ebaa5da542c..8db71fa92daa45 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 @@ -40,6 +40,10 @@ public void WriteThrowsNotSupported() var stream = new ReadOnlyMemoryStream(new byte[] { 1, 2, 3 }); Assert.Throws(() => stream.Write(new byte[1], 0, 1)); + Assert.Throws(() => stream.Write(new byte[1].AsSpan())); + Assert.Throws(() => stream.WriteByte(1)); + Assert.Throws(() => { _ = stream.WriteAsync(new byte[1], 0, 1); }); + Assert.Throws(() => { _ = stream.WriteAsync(new byte[1].AsMemory()); }); Assert.Throws(() => stream.SetLength(1)); } }