From eebf5c1f589f302ac542d592ba4d2098f1e8973b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:35:40 +0000 Subject: [PATCH 1/4] Initial plan From 03f34ecf4742e0e0b53ec351253947f2a8ec0940 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:07:15 +0000 Subject: [PATCH 2/4] Add Memory and ReadOnlyMemory constructors to MemoryStream with delegation pattern Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> --- .../src/System/IO/MemoryStream.cs | 398 ++++++++++++++++++ .../System.Runtime/ref/System.Runtime.cs | 3 + 2 files changed, 401 insertions(+) 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 5cd6160fecafdc..5d0200fe4f4e2e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs @@ -34,6 +34,10 @@ public class MemoryStream : Stream private CachedCompletedInt32Task _lastReadTask; // The last successful task returned from ReadAsync + // When non-null, all operations are delegated to this instance. + // Only set by the ReadOnlyMemory/Memory constructors. + private readonly MemoryMemoryStream? _memoryMemoryStream; + private static int MemStreamMaxLength => Array.MaxLength; public MemoryStream() @@ -96,6 +100,33 @@ public MemoryStream(byte[] buffer, int index, int count, bool writable, bool pub _isOpen = true; } + /// Initializes a new non-writable instance of the class based on the specified . + /// The read-only memory from which to create the current stream. + public MemoryStream(ReadOnlyMemory memory) + { + _memoryMemoryStream = new MemoryMemoryStream(memory); + _buffer = []; + _isOpen = true; + } + + /// Initializes a new writable instance of the class based on the specified . + /// The memory from which to create the current stream. + public MemoryStream(Memory memory) + : this(memory, true) + { + } + + /// Initializes a new instance of the class based on the specified with the property set as specified. + /// The memory from which to create the current stream. + /// to enable writing; otherwise, . + public MemoryStream(Memory memory, bool writable) + { + _memoryMemoryStream = new MemoryMemoryStream(memory, writable); + _buffer = []; + _writable = writable; + _isOpen = true; + } + public override bool CanRead => _isOpen; public override bool CanSeek => _isOpen; @@ -123,6 +154,7 @@ protected override void Dispose(bool disposing) _expandable = false; // Don't set buffer to null - allow TryGetBuffer, GetBuffer & ToArray to work. _lastReadTask = default; + _memoryMemoryStream?.Dispose(); } } @@ -179,6 +211,8 @@ public override Task FlushAsync(CancellationToken cancellationToken) public virtual byte[] GetBuffer() { + if (_memoryMemoryStream is not null) + throw new UnauthorizedAccessException(SR.UnauthorizedAccess_MemStreamBuffer); if (!_exposable) throw new UnauthorizedAccessException(SR.UnauthorizedAccess_MemStreamBuffer); return _buffer; @@ -186,6 +220,12 @@ public virtual byte[] GetBuffer() public virtual bool TryGetBuffer(out ArraySegment buffer) { + if (_memoryMemoryStream is not null) + { + buffer = default; + return false; + } + if (!_exposable) { buffer = default; @@ -214,6 +254,9 @@ internal int InternalGetPosition() [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ReadOnlySpan InternalReadSpan(int count) { + if (_memoryMemoryStream is not null) + return _memoryMemoryStream.InternalReadSpan(count); + EnsureNotClosed(); int origPos = _position; @@ -233,6 +276,9 @@ internal ReadOnlySpan InternalReadSpan(int count) // PERF: Get actual length of bytes available for read; do sanity checks; shift position - i.e. everything except actual copying bytes internal int InternalEmulateRead(int count) { + if (_memoryMemoryStream is not null) + return _memoryMemoryStream.InternalEmulateRead(count); + EnsureNotClosed(); int n = _length - _position; @@ -254,11 +300,18 @@ public virtual int Capacity { get { + if (_memoryMemoryStream is not null) + return _memoryMemoryStream.Capacity; EnsureNotClosed(); return _capacity - _origin; } set { + if (_memoryMemoryStream is not null) + { + _memoryMemoryStream.SetCapacity(value); + return; + } // Only update the capacity if the MS is expandable and the value is different than the current capacity. // Special behavior if the MS isn't expandable: we don't throw if value is the same as the current capacity if (value < Length) @@ -294,6 +347,8 @@ public override long Length { get { + if (_memoryMemoryStream is not null) + return _memoryMemoryStream.Length; EnsureNotClosed(); return _length - _origin; } @@ -303,11 +358,18 @@ public override long Position { get { + if (_memoryMemoryStream is not null) + return _memoryMemoryStream.Position; EnsureNotClosed(); return _position - _origin; } set { + if (_memoryMemoryStream is not null) + { + _memoryMemoryStream.Position = value; + return; + } ArgumentOutOfRangeException.ThrowIfNegative(value); EnsureNotClosed(); @@ -320,6 +382,10 @@ public override long Position public override int Read(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); + + if (_memoryMemoryStream is not null) + return _memoryMemoryStream.Read(new Span(buffer, offset, count)); + EnsureNotClosed(); int n = _length - _position; @@ -353,6 +419,9 @@ public override int Read(Span buffer) return base.Read(buffer); } + if (_memoryMemoryStream is not null) + return _memoryMemoryStream.Read(buffer); + EnsureNotClosed(); int n = Math.Min(_length - _position, buffer.Length); @@ -426,6 +495,9 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken public override int ReadByte() { + if (_memoryMemoryStream is not null) + return _memoryMemoryStream.ReadByte(); + EnsureNotClosed(); if (_position >= _length) @@ -448,6 +520,13 @@ public override void CopyTo(Stream destination, int bufferSize) // Validate the arguments the same way Stream does for back-compat. ValidateCopyToArguments(destination, bufferSize); + + if (_memoryMemoryStream is not null) + { + _memoryMemoryStream.CopyTo(destination); + return; + } + EnsureNotClosed(); int originalPosition = _position; @@ -469,6 +548,10 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio // This implementation offers better performance compared to the base class version. ValidateCopyToArguments(destination, bufferSize); + + if (_memoryMemoryStream is not null) + return _memoryMemoryStream.CopyToAsync(destination, cancellationToken); + EnsureNotClosed(); // If we have been inherited into a subclass, the following implementation could be incorrect @@ -511,6 +594,9 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio public override long Seek(long offset, SeekOrigin loc) { + if (_memoryMemoryStream is not null) + return _memoryMemoryStream.Seek(offset, loc); + EnsureNotClosed(); return SeekCore(offset, loc switch @@ -547,6 +633,12 @@ private long SeekCore(long offset, int loc) // public override void SetLength(long value) { + if (_memoryMemoryStream is not null) + { + _memoryMemoryStream.SetLength(value); + return; + } + if (value < 0 || value > MemStreamMaxLength) throw new ArgumentOutOfRangeException(nameof(value), SR.Format(SR.ArgumentOutOfRange_StreamLength, Array.MaxLength)); @@ -568,6 +660,8 @@ public override void SetLength(long value) public virtual byte[] ToArray() { + if (_memoryMemoryStream is not null) + return _memoryMemoryStream.ToArray(); int count = _length - _origin; if (count == 0) return []; @@ -579,6 +673,13 @@ public virtual byte[] ToArray() public override void Write(byte[] buffer, int offset, int count) { ValidateBufferArguments(buffer, offset, count); + + if (_memoryMemoryStream is not null) + { + _memoryMemoryStream.Write(new ReadOnlySpan(buffer, offset, count)); + return; + } + EnsureNotClosed(); EnsureWriteable(); @@ -630,6 +731,12 @@ public override void Write(ReadOnlySpan buffer) return; } + if (_memoryMemoryStream is not null) + { + _memoryMemoryStream.Write(buffer); + return; + } + EnsureNotClosed(); EnsureWriteable(); @@ -716,6 +823,12 @@ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationTo public override void WriteByte(byte value) { + if (_memoryMemoryStream is not null) + { + _memoryMemoryStream.WriteByte(value); + return; + } + EnsureNotClosed(); EnsureWriteable(); @@ -743,11 +856,296 @@ public override void WriteByte(byte value) // Writes this MemoryStream to another stream. public virtual void WriteTo(Stream stream) { + if (_memoryMemoryStream is not null) + { + _memoryMemoryStream.WriteTo(stream); + return; + } + ArgumentNullException.ThrowIfNull(stream); EnsureNotClosed(); stream.Write(_buffer, _origin, _length - _origin); } + + private sealed class MemoryMemoryStream + { + private readonly ReadOnlyMemory _memory; + private readonly Memory _writableMemory; + private int _position; + private int _length; + private bool _writable; + private bool _isOpen; + + public MemoryMemoryStream(ReadOnlyMemory memory) + { + _memory = memory; + _length = memory.Length; + _isOpen = true; + } + + public MemoryMemoryStream(Memory memory, bool writable) + { + _memory = memory; + if (writable) + { + _writableMemory = memory; + } + _writable = writable; + _length = memory.Length; + _isOpen = true; + } + + public int Capacity + { + get + { + EnsureNotClosed(); + return _memory.Length; + } + } + + public void SetCapacity(int value) + { + if (value < _length) + throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_SmallCapacity); + EnsureNotClosed(); + if (value != _memory.Length) + throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + } + + public long Length + { + get + { + EnsureNotClosed(); + return _length; + } + } + + public long Position + { + get + { + EnsureNotClosed(); + return _position; + } + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value); + EnsureNotClosed(); + if (value > MemStreamMaxLength) + throw new ArgumentOutOfRangeException(nameof(value), SR.Format(SR.ArgumentOutOfRange_StreamLength, Array.MaxLength)); + _position = (int)value; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureNotClosed() + { + if (!_isOpen) + ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureWriteable() + { + if (!_writable) + ThrowHelper.ThrowNotSupportedException_UnwritableStream(); + } + + public int Read(Span buffer) + { + EnsureNotClosed(); + + int n = Math.Min(_length - _position, buffer.Length); + if (n <= 0) + return 0; + + _memory.Span.Slice(_position, n).CopyTo(buffer); + _position += n; + return n; + } + + public int ReadByte() + { + EnsureNotClosed(); + + if (_position >= _length) + return -1; + + return _memory.Span[_position++]; + } + + public void Write(ReadOnlySpan buffer) + { + EnsureNotClosed(); + EnsureWriteable(); + + int i = _position + buffer.Length; + if (i < 0) + throw new IOException(SR.IO_StreamTooLong); + + if (i > _memory.Length) + throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + + if (i > _length) + { + if (_position > _length) + { + _writableMemory.Span.Slice(_length, _position - _length).Clear(); + } + _length = i; + } + + buffer.CopyTo(_writableMemory.Span.Slice(_position)); + _position = i; + } + + public void WriteByte(byte value) + { + EnsureNotClosed(); + EnsureWriteable(); + + if (_position >= _length) + { + int newLength = _position + 1; + if (newLength > _memory.Length) + throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + + if (_position > _length) + { + _writableMemory.Span.Slice(_length, _position - _length).Clear(); + } + _length = newLength; + } + _writableMemory.Span[_position++] = value; + } + + public long Seek(long offset, SeekOrigin loc) + { + EnsureNotClosed(); + + long tempPosition = loc switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _length + offset, + _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin) + }; + + if (tempPosition < 0) + throw new IOException(SR.IO_SeekBeforeBegin); + if (tempPosition > MemStreamMaxLength) + throw new ArgumentOutOfRangeException(nameof(offset), SR.Format(SR.ArgumentOutOfRange_StreamLength, Array.MaxLength)); + + _position = (int)tempPosition; + return _position; + } + + public void SetLength(long value) + { + if (value < 0 || value > MemStreamMaxLength) + throw new ArgumentOutOfRangeException(nameof(value), SR.Format(SR.ArgumentOutOfRange_StreamLength, Array.MaxLength)); + + EnsureWriteable(); + + int newLength = (int)value; + if (newLength > _memory.Length) + throw new NotSupportedException(SR.NotSupported_MemStreamNotExpandable); + + if (newLength > _length) + _writableMemory.Span.Slice(_length, newLength - _length).Clear(); + + _length = newLength; + if (_position > newLength) + _position = newLength; + } + + public byte[] ToArray() + { + if (_length == 0) + return []; + byte[] copy = GC.AllocateUninitializedArray(_length); + _memory.Span.Slice(0, _length).CopyTo(copy); + return copy; + } + + public void WriteTo(Stream stream) + { + ArgumentNullException.ThrowIfNull(stream); + EnsureNotClosed(); + stream.Write(_memory.Span.Slice(0, _length)); + } + + public void CopyTo(Stream destination) + { + EnsureNotClosed(); + + int remaining = _length - _position; + if (remaining > 0) + { + destination.Write(_memory.Span.Slice(_position, remaining)); + _position = _length; + } + } + + public Task CopyToAsync(Stream destination, CancellationToken cancellationToken) + { + EnsureNotClosed(); + + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + int pos = _position; + int n = _length - _position; + _position = _length; + + if (n == 0) + return Task.CompletedTask; + + return destination.WriteAsync(_memory.Slice(pos, n), cancellationToken).AsTask(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlySpan InternalReadSpan(int count) + { + EnsureNotClosed(); + + int origPos = _position; + int newPos = origPos + count; + + if ((uint)newPos > (uint)_length) + { + _position = _length; + ThrowHelper.ThrowEndOfFileException(); + } + + var span = _memory.Span.Slice(origPos, count); + _position = newPos; + return span; + } + + public int InternalEmulateRead(int count) + { + EnsureNotClosed(); + + int n = _length - _position; + if (n > count) + n = count; + if (n < 0) + n = 0; + + _position += n; + return n; + } + + public void Dispose() + { + _isOpen = false; + _writable = false; + } + } } } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index e49b44a7328794..5f99fb7972215c 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -10694,6 +10694,9 @@ public MemoryStream(byte[] buffer, int index, int count) { } public MemoryStream(byte[] buffer, int index, int count, bool writable) { } public MemoryStream(byte[] buffer, int index, int count, bool writable, bool publiclyVisible) { } public MemoryStream(int capacity) { } + public MemoryStream(System.Memory memory) { } + public MemoryStream(System.Memory memory, bool writable) { } + public MemoryStream(System.ReadOnlyMemory memory) { } public override bool CanRead { get { throw null; } } public override bool CanSeek { get { throw null; } } public override bool CanWrite { get { throw null; } } From 06ad3930700e4a4fac5b549cb275cc1f4e116f21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:10:27 +0000 Subject: [PATCH 3/4] Add tests for Memory and ReadOnlyMemory MemoryStream constructors Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> --- .../MemoryStream/MemoryStreamTests.cs | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) 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 ee77811f224290..86c8928803eb47 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 @@ -195,5 +195,327 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati return base.WriteAsync(buffer, offset, count, cancellationToken); } } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_BasicRead() + { + byte[] data = new byte[] { 1, 2, 3, 4, 5 }; + ReadOnlyMemory rom = data.AsMemory(); + + using var ms = new MemoryStream(rom); + Assert.True(ms.CanRead); + Assert.True(ms.CanSeek); + Assert.False(ms.CanWrite); + Assert.Equal(5, ms.Length); + Assert.Equal(0, ms.Position); + + byte[] readBuf = new byte[5]; + int bytesRead = ms.Read(readBuf, 0, 5); + Assert.Equal(5, bytesRead); + Assert.Equal(data, readBuf); + Assert.Equal(5, ms.Position); + } + + [Fact] + public static void MemoryStream_Ctor_Memory_BasicReadWrite() + { + byte[] data = new byte[10]; + Memory mem = data.AsMemory(); + + using var ms = new MemoryStream(mem); + Assert.True(ms.CanRead); + Assert.True(ms.CanSeek); + Assert.True(ms.CanWrite); + Assert.Equal(10, ms.Capacity); + + ms.Write(new byte[] { 10, 20, 30 }, 0, 3); + Assert.Equal(3, ms.Position); + + ms.Position = 0; + byte[] readBuf = new byte[3]; + int bytesRead = ms.Read(readBuf, 0, 3); + Assert.Equal(3, bytesRead); + Assert.Equal(new byte[] { 10, 20, 30 }, readBuf); + } + + [Fact] + public static void MemoryStream_Ctor_Memory_NotWritable() + { + Memory mem = new byte[5].AsMemory(); + using var ms = new MemoryStream(mem, writable: false); + + Assert.True(ms.CanRead); + Assert.False(ms.CanWrite); + Assert.Throws(() => ms.WriteByte(1)); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_WriteThrows() + { + ReadOnlyMemory rom = new byte[] { 1, 2, 3 }.AsMemory(); + using var ms = new MemoryStream(rom); + + Assert.Throws(() => ms.Write(new byte[1], 0, 1)); + Assert.Throws(() => ms.Write(new ReadOnlySpan(new byte[1]))); + Assert.Throws(() => ms.WriteByte(1)); + Assert.Throws(() => ms.SetLength(1)); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_ReadByte() + { + ReadOnlyMemory rom = new byte[] { 42, 99 }.AsMemory(); + using var ms = new MemoryStream(rom); + + Assert.Equal(42, ms.ReadByte()); + Assert.Equal(99, ms.ReadByte()); + Assert.Equal(-1, ms.ReadByte()); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_ReadSpan() + { + byte[] data = new byte[] { 1, 2, 3, 4, 5 }; + ReadOnlyMemory rom = data.AsMemory(); + + using var ms = new MemoryStream(rom); + byte[] buf = new byte[3]; + int n = ms.Read(buf.AsSpan()); + Assert.Equal(3, n); + Assert.Equal(new byte[] { 1, 2, 3 }, buf); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_Seek() + { + ReadOnlyMemory rom = new byte[] { 10, 20, 30, 40, 50 }.AsMemory(); + using var ms = new MemoryStream(rom); + + Assert.Equal(2, ms.Seek(2, SeekOrigin.Begin)); + Assert.Equal(30, ms.ReadByte()); + + Assert.Equal(1, ms.Seek(-2, SeekOrigin.Current)); + Assert.Equal(20, ms.ReadByte()); + + Assert.Equal(4, ms.Seek(-1, SeekOrigin.End)); + Assert.Equal(50, ms.ReadByte()); + + Assert.Throws(() => ms.Seek(-1, SeekOrigin.Begin)); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_Position() + { + ReadOnlyMemory rom = new byte[] { 1, 2, 3 }.AsMemory(); + using var ms = new MemoryStream(rom); + + ms.Position = 2; + Assert.Equal(2, ms.Position); + Assert.Equal(3, ms.ReadByte()); + + Assert.Throws(() => ms.Position = -1); + } + + [Fact] + public static void MemoryStream_Ctor_Memory_SetLength() + { + byte[] data = new byte[] { 1, 2, 3, 4, 5 }; + Memory mem = data.AsMemory(); + using var ms = new MemoryStream(mem); + + ms.SetLength(3); + Assert.Equal(3, ms.Length); + + ms.SetLength(5); + Assert.Equal(5, ms.Length); + + Assert.Throws(() => ms.SetLength(11)); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_ToArray() + { + byte[] data = new byte[] { 1, 2, 3, 4, 5 }; + ReadOnlyMemory rom = data.AsMemory(); + using var ms = new MemoryStream(rom); + + byte[] arr = ms.ToArray(); + Assert.Equal(data, arr); + Assert.NotSame(data, arr); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_GetBuffer_Throws() + { + ReadOnlyMemory rom = new byte[5].AsMemory(); + using var ms = new MemoryStream(rom); + + Assert.Throws(() => ms.GetBuffer()); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_TryGetBuffer_ReturnsFalse() + { + ReadOnlyMemory rom = new byte[5].AsMemory(); + using var ms = new MemoryStream(rom); + + Assert.False(ms.TryGetBuffer(out ArraySegment buffer)); + Assert.Equal(0, buffer.Offset); + Assert.Equal(0, buffer.Count); + } + + [Fact] + public static void MemoryStream_Ctor_Memory_WriteTo() + { + byte[] data = new byte[] { 1, 2, 3 }; + Memory mem = data.AsMemory(); + using var ms = new MemoryStream(mem); + using var dest = new MemoryStream(); + + ms.WriteTo(dest); + Assert.Equal(data, dest.ToArray()); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_CopyTo() + { + byte[] data = new byte[] { 10, 20, 30, 40, 50 }; + ReadOnlyMemory rom = data.AsMemory(); + using var ms = new MemoryStream(rom); + using var dest = new MemoryStream(); + + ms.Position = 2; + ms.CopyTo(dest); + Assert.Equal(new byte[] { 30, 40, 50 }, dest.ToArray()); + Assert.Equal(5, ms.Position); + } + + [Fact] + public static async Task MemoryStream_Ctor_ReadOnlyMemory_CopyToAsync() + { + byte[] data = new byte[] { 10, 20, 30, 40, 50 }; + ReadOnlyMemory rom = data.AsMemory(); + using var ms = new MemoryStream(rom); + using var dest = new MemoryStream(); + + ms.Position = 1; + await ms.CopyToAsync(dest); + Assert.Equal(new byte[] { 20, 30, 40, 50 }, dest.ToArray()); + } + + [Fact] + public static async Task MemoryStream_Ctor_ReadOnlyMemory_ReadAsync() + { + byte[] data = new byte[] { 5, 10, 15, 20 }; + ReadOnlyMemory rom = data.AsMemory(); + using var ms = new MemoryStream(rom); + + byte[] buf = new byte[4]; + int n = await ms.ReadAsync(buf, 0, 4); + Assert.Equal(4, n); + Assert.Equal(data, buf); + } + + [Fact] + public static async Task MemoryStream_Ctor_Memory_WriteAsync() + { + byte[] data = new byte[5]; + Memory mem = data.AsMemory(); + using var ms = new MemoryStream(mem); + + await ms.WriteAsync(new byte[] { 1, 2, 3 }, 0, 3); + Assert.Equal(3, ms.Position); + + ms.Position = 0; + byte[] readBuf = new byte[3]; + await ms.ReadAsync(readBuf, 0, 3); + Assert.Equal(new byte[] { 1, 2, 3 }, readBuf); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_Dispose() + { + ReadOnlyMemory rom = new byte[] { 1, 2, 3 }.AsMemory(); + var ms = new MemoryStream(rom); + + ms.Dispose(); + + Assert.False(ms.CanRead); + Assert.False(ms.CanSeek); + Assert.False(ms.CanWrite); + Assert.Throws(() => ms.ReadByte()); + Assert.Throws(() => ms.Position); + + byte[] arr = ms.ToArray(); + Assert.Equal(new byte[] { 1, 2, 3 }, arr); + } + + [Fact] + public static void MemoryStream_Ctor_Memory_NotExpandable() + { + Memory mem = new byte[5].AsMemory(); + using var ms = new MemoryStream(mem); + + Assert.Equal(5, ms.Capacity); + Assert.Throws(() => ms.Capacity = 10); + + ms.Capacity = 5; + } + + [Fact] + public static void MemoryStream_Ctor_Memory_WriteSpan() + { + byte[] data = new byte[10]; + Memory mem = data.AsMemory(); + using var ms = new MemoryStream(mem); + + ms.Write(new ReadOnlySpan(new byte[] { 100, 200 })); + Assert.Equal(2, ms.Position); + + ms.Position = 0; + Span readBuf = stackalloc byte[2]; + int n = ms.Read(readBuf); + Assert.Equal(2, n); + Assert.Equal(100, readBuf[0]); + Assert.Equal(200, readBuf[1]); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_EmptyMemory() + { + ReadOnlyMemory rom = ReadOnlyMemory.Empty; + using var ms = new MemoryStream(rom); + + Assert.Equal(0, ms.Length); + Assert.Equal(0, ms.Position); + Assert.Equal(-1, ms.ReadByte()); + Assert.Equal(0, ms.Read(new byte[5], 0, 5)); + Assert.Empty(ms.ToArray()); + } + + [Fact] + public static void MemoryStream_Ctor_ReadOnlyMemory_SlicedMemory() + { + byte[] data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + ReadOnlyMemory rom = data.AsMemory(3, 4); + + using var ms = new MemoryStream(rom); + Assert.Equal(4, ms.Length); + + byte[] readBuf = new byte[4]; + int n = ms.Read(readBuf, 0, 4); + Assert.Equal(4, n); + Assert.Equal(new byte[] { 4, 5, 6, 7 }, readBuf); + } + + [Fact] + public static void MemoryStream_Ctor_Memory_WriteBeyondCapacity_Throws() + { + Memory mem = new byte[3].AsMemory(); + using var ms = new MemoryStream(mem); + + ms.Write(new byte[] { 1, 2, 3 }, 0, 3); + Assert.Throws(() => ms.WriteByte(4)); + } } } From ef34e71dedb2746021e2c71e76eadd9c4d3a2f1d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:16:33 +0000 Subject: [PATCH 4/4] Fix BinaryReader compatibility with Memory-backed MemoryStream via InternalRead method Co-authored-by: jozkee <16040868+jozkee@users.noreply.github.com> --- .../src/System/IO/BinaryReader.cs | 5 ++- .../src/System/IO/MemoryStream.cs | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs b/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs index 6986e4017081df..b879f265225701 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs @@ -359,9 +359,8 @@ private int InternalReadChars(Span buffer) Debug.Assert(_stream is MemoryStream); MemoryStream mStream = Unsafe.As(_stream); - int position = mStream.InternalGetPosition(); - numBytes = mStream.InternalEmulateRead(numBytes); - byteBuffer = new ReadOnlySpan(mStream.InternalGetBuffer(), position, numBytes); + byteBuffer = mStream.InternalRead(numBytes); + numBytes = byteBuffer.Length; } else { 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 5d0200fe4f4e2e..c6ab0446e98ff0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/MemoryStream.cs @@ -292,6 +292,32 @@ internal int InternalEmulateRead(int count) return n; } + // PERF: Reads up to count bytes from the current position and returns them as a span, advancing the position. + // Unlike InternalReadSpan, does not throw if fewer bytes are available. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ReadOnlySpan InternalRead(int count) + { + if (_memoryMemoryStream is not null) + { + int n = Math.Min(_memoryMemoryStream.RemainingBytes, count); + if (n <= 0) + return default; + return _memoryMemoryStream.InternalReadSpan(n); + } + + EnsureNotClosed(); + + int available = _length - _position; + if (available > count) + available = count; + if (available <= 0) + return default; + + var span = new ReadOnlySpan(_buffer, _position, available); + _position += available; + return span; + } + // Gets & sets the capacity (number of bytes allocated) for this stream. // The capacity cannot be set to a value less than the current length // of the stream. @@ -948,6 +974,16 @@ private void EnsureNotClosed() ThrowHelper.ThrowObjectDisposedException_StreamClosed(null); } + public int RemainingBytes + { + get + { + EnsureNotClosed(); + int n = _length - _position; + return n > 0 ? n : 0; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EnsureWriteable() {