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.Private.CoreLib/src/System/Threading/Tasks/CachedCompletedInt32Task.cs b/src/libraries/Common/src/System/Threading/Tasks/CachedCompletedInt32Task.cs similarity index 100% rename from src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/CachedCompletedInt32Task.cs rename to src/libraries/Common/src/System/Threading/Tasks/CachedCompletedInt32Task.cs diff --git a/src/libraries/System.Memory/ref/System.Memory.cs b/src/libraries/System.Memory/ref/System.Memory.cs index 7d53b7a8da7630..86078321d5c729 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 source) { } + public override bool CanRead { get { throw null; } } + public override bool CanSeek { get { throw null; } } + public override bool CanWrite { get { throw null; } } + public override long Length { get { throw null; } } + public override long Position { get { throw null; } set { } } + public override void 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..b377b90f62ae90 100644 --- a/src/libraries/System.Memory/src/System.Memory.csproj +++ b/src/libraries/System.Memory/src/System.Memory.csproj @@ -28,6 +28,7 @@ + @@ -40,6 +41,8 @@ + diff --git a/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs new file mode 100644 index 00000000000000..59ba86a3ce1786 --- /dev/null +++ b/src/libraries/System.Memory/src/System/Buffers/ReadOnlySequenceStream.cs @@ -0,0 +1,296 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace System.Buffers +{ + /// + /// Provides a seekable, read-only over a . + /// + /// + /// The underlying sequence is not copied; reads are served directly from its segments. + /// This type is not thread-safe. Synchronize access if the stream is used concurrently. + /// The stream cannot be written to. always returns . + /// + public sealed class ReadOnlySequenceStream : Stream + { + private ReadOnlySequence _sequence; + private SequencePosition _position; + private long _absolutePosition; + private bool _isDisposed; + private CachedCompletedInt32Task _lastReadTask; + + /// + /// Initializes a new instance of the class over the specified . + /// + /// The to wrap. + public ReadOnlySequenceStream(ReadOnlySequence source) + { + _sequence = source; + _position = source.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(); + + byte b = 0; + return Read(new Span(ref b)) > 0 ? b : -1; + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBufferArguments(buffer, offset, count); + EnsureNotDisposed(); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + int n = Read(buffer, offset, count); + return _lastReadTask.GetTask(n); + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + EnsureNotDisposed(); + + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + try + { + int n = Read(buffer.Span); + return new ValueTask(n); + } + catch (OperationCanceledException oce) + { + return ValueTask.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } + } + + /// + 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); + + /// + 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)) + }; + + if (absolutePosition < 0) + { + throw new IOException(SR.IO_SeekBeforeBegin); + } + + 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..0d6f40f83723f5 --- /dev/null +++ b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStream.ConformanceTests.cs @@ -0,0 +1,56 @@ +// 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 +{ + public class ROSequenceStreamConformanceTests : StandaloneStreamConformanceTests + { + protected override bool CanSeek => true; + 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 ReadOnlySequenceStream(ReadOnlySequence.Empty)); + } + + return Task.FromResult( + new ReadOnlySequenceStream(CreateSequence(initialData))); + } + + protected virtual ReadOnlySequence CreateSequence(byte[] data) + => new ReadOnlySequence(data); + + protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) + => Task.FromResult(null); + + protected override Task CreateReadWriteStreamCore(byte[]? initialData) + => Task.FromResult(null); + } + + /// + /// Runs the full conformance suite against multi-segment sequences (3 segments). + /// + public class ROSequenceStreamMultiSegmentConformanceTests : ROSequenceStreamConformanceTests + { + protected override ReadOnlySequence CreateSequence(byte[] data) + => ReadOnlySequenceFactory.SplitInThree.CreateWithContent(data); + } + + /// + /// Runs the full conformance suite against maximally-fragmented sequences + /// (one byte per segment with empty segments interspersed). + /// + public class ROSequenceStreamSegmentPerItemConformanceTests : ROSequenceStreamConformanceTests + { + protected override ReadOnlySequence CreateSequence(byte[] data) + => ReadOnlySequenceFactory.SegmentPerItemFactory.CreateWithContent(data); + } +} diff --git a/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs new file mode 100644 index 00000000000000..85a3b3015aea23 --- /dev/null +++ b/src/libraries/System.Memory/tests/ReadOnlyBuffer/ReadOnlySequenceStreamTests.cs @@ -0,0 +1,31 @@ +// 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 Xunit; + +namespace System.Memory.Tests +{ + public class ReadOnlySequenceStreamTests + { + [Fact] + public void SeekingBeyondEmptyBufferIsAllowed() + { + 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); + + stream.Seek(0, SeekOrigin.Begin); + Assert.Equal(0, stream.Position); + + long newPosition = stream.Seek(1, SeekOrigin.Begin); + Assert.Equal(1, newPosition); + Assert.Equal(1, stream.Position); + } + } +} diff --git a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj index 5ef352b8d1f6d9..48a81b6938b84e 100644 --- a/src/libraries/System.Memory/tests/System.Memory.Tests.csproj +++ b/src/libraries/System.Memory/tests/System.Memory.Tests.csproj @@ -135,6 +135,8 @@ + + @@ -291,4 +293,7 @@ + + + diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index c2a4d472ccaa09..47186f9d79ae5c 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -130,8 +130,6 @@ Link="Common\System\Runtime\CompilerServices\RuntimeAsyncMethodGenerationAttribute.cs" /> - + + + @@ -1307,7 +1310,8 @@ - + diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs new file mode 100644 index 00000000000000..ce4fe91bd02342 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/ReadOnlyMemoryStream.cs @@ -0,0 +1,299 @@ +// 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; + private CachedCompletedInt32Task _lastReadTask; + + /// + /// Initializes a new instance of the class over the specified . + /// + /// The to wrap. + public ReadOnlyMemoryStream(ReadOnlyMemory source) : base() + { + _buffer = source; + _isOpen = true; + } + + /// + public override bool CanRead => _isOpen; + + /// + public override bool 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); + } + + try + { + int n = Read(buffer, offset, count); + return _lastReadTask.GetTask(n); + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + + /// + 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 (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + 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, nameof(origin)) + }; + + // Order matches MemoryStream.SeekCore / Common/src/System/IO/ReadOnlyMemoryStream.cs: + // overflow check first, then seek-before-begin. + ArgumentOutOfRangeException.ThrowIfGreaterThan(newPosition, int.MaxValue, nameof(offset)); + + if (newPosition < 0) + { + throw new IOException(SR.IO_SeekBeforeBegin); + } + + _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(); + + 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..61b416488706e8 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/StringStream.cs @@ -0,0 +1,380 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.IO +{ + /// + /// Provides a read-only, non-seekable that encodes a or + /// into bytes on-the-fly using a specified . + /// + /// + /// This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves. + /// This type is not thread-safe. Synchronize access if the stream is used concurrently. + /// + public sealed class StringStream : Stream + { + private readonly ReadOnlyMemory _text; + // Lazily created on the encoder slow path. The single-shot fast path in Read + // uses stateless Encoding.GetBytes and never touches this field. + private Encoder? _encoder; + private readonly Encoding _encoding; + private readonly int _maxBytesPerChar; + private int _charPosition; + private bool _disposed; + private bool _encoderFlushed; + + // Spillover buffer for multibyte encodings: when the caller's buffer is too small + // to hold even one encoded scalar (e.g., ReadByte with UTF-16), we encode into + // this buffer and serve bytes from it across subsequent Read/ReadByte calls. + // Also used to hold final encoder flush bytes when the caller's buffer had no room. + private byte[]? _pendingBytes; + private int _pendingOffset; + private int _pendingCount; + + /// + /// Initializes a new instance of the class with the specified string and encoding. + /// + /// The string to read from. + /// The encoding to use when converting the string to bytes. + /// or is . + public StringStream(string text, Encoding encoding) + { + ArgumentNullException.ThrowIfNull(text); + ArgumentNullException.ThrowIfNull(encoding); + + _text = text.AsMemory(); + _encoding = encoding; + _maxBytesPerChar = encoding.GetMaxByteCount(1); + } + + /// + /// 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; + _maxBytesPerChar = encoding.GetMaxByteCount(1); + } + + /// + /// 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)); + } + + /// + // All Read overloads funnel here; ObjectDisposedException guards every path (TranscodingStream pattern). + public override int Read(Span buffer) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (buffer.Length == 0 || (_charPosition >= _text.Length && _pendingCount == 0 && _encoderFlushed)) + { + return 0; + } + + // Fast path: nothing emitted yet and the caller's buffer is guaranteed + // large enough to hold the entire encoded payload in a single shot. + // Encoding.GetBytes is stateless and emits any reset/shift sequences + // required by stateful encodings for a complete conversion, so we can + // mark the encoder as flushed without ever allocating an Encoder. + // + // Guard against Encoding.GetMaxByteCount overflowing for very large inputs + // ((_text.Length + 1) * _maxBytesPerChar must fit in int). If it would, + // fall through to the streaming path instead of throwing from Read. + if (_charPosition == 0 && _pendingCount == 0 && + _text.Length <= (int.MaxValue / _maxBytesPerChar) - 1 && + buffer.Length >= _encoding.GetMaxByteCount(_text.Length)) + { + int written = _encoding.GetBytes(_text.Span, buffer); + _charPosition = _text.Length; + _encoderFlushed = true; + return written; + } + + int totalBytesWritten = 0; + int bufferBytesWritten = 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 < _maxBytesPerChar) + { + // Instance field — ArrayPool not appropriate. Contents are + // always overwritten by Encoder.Convert before being read out, + // so we can skip the JIT-emitted zero-init (mirrors TranscodingStream). + _pendingBytes ??= GC.AllocateUninitializedArray(_encoding.GetMaxByteCount(2)); + int charsToEncode = Math.Min(2, remaining.Length); + GetEncoder().Convert(remaining.Slice(0, charsToEncode), _pendingBytes, flush: false, out int charsUsed, out int bytesUsed, out _); + _charPosition += charsUsed; + + int toCopy = Math.Min(bytesUsed, buffer.Length); + _pendingBytes.AsSpan(0, toCopy).CopyTo(buffer); + totalBytesWritten += toCopy; + bufferBytesWritten += toCopy; + + _pendingOffset = toCopy; + _pendingCount = bytesUsed - toCopy; + } + else + { + // Encode directly into the caller's buffer. + // Only flush on the final block to preserve encoder state + // for stateful encodings. + GetEncoder().Convert(remaining, buffer, flush: false, out int charsUsed, out int bytesUsed, out _); + _charPosition += charsUsed; + totalBytesWritten += bytesUsed; + bufferBytesWritten += bytesUsed; + } + } + + // If all input chars are consumed but the encoder hasn't been flushed, + // flush any remaining encoder state (e.g., stateful encoding reset sequences). + // Always flush into _pendingBytes (which is guaranteed large enough) to + // avoid ArgumentException if the caller's remaining buffer is too small. + if (_charPosition >= _text.Length && !_encoderFlushed && _pendingCount == 0) + { + _pendingBytes ??= GC.AllocateUninitializedArray(_encoding.GetMaxByteCount(2)); + GetEncoder().Convert(ReadOnlySpan.Empty, _pendingBytes, flush: true, out _, out int flushBytes, out _); + _encoderFlushed = true; + + if (flushBytes > 0) + { + Span flushTarget = buffer.Slice(bufferBytesWritten); + int toCopy = Math.Min(flushBytes, flushTarget.Length); + if (toCopy > 0) + { + _pendingBytes.AsSpan(0, toCopy).CopyTo(flushTarget); + totalBytesWritten += toCopy; + } + + if (toCopy < flushBytes) + { + _pendingOffset = toCopy; + _pendingCount = flushBytes - toCopy; + } + } + } + + return totalBytesWritten; + } + + /// + public override int ReadByte() + { + byte b = 0; + return Read(new Span(ref b)) > 0 ? b : -1; + } + + private Encoder GetEncoder() => _encoder ??= _encoding.GetEncoder(); + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBufferArguments(buffer, offset, count); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + return Task.FromResult(Read(buffer, offset, count)); + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + try + { + return new ValueTask(Read(buffer.Span)); + } + catch (OperationCanceledException oce) + { + return ValueTask.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } + } + + /// + 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; + + /// + public override void CopyTo(Stream destination, int bufferSize) + { + ValidateCopyToArguments(destination, bufferSize); + ObjectDisposedException.ThrowIf(_disposed, this); + + int rentSize = ComputeCopyToRentSize(bufferSize); + byte[] buffer = ArrayPool.Shared.Rent(rentSize); + try + { + int n; + while ((n = Read(buffer, 0, buffer.Length)) != 0) + { + destination.Write(buffer, 0, n); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + ValidateCopyToArguments(destination, bufferSize); + ObjectDisposedException.ThrowIf(_disposed, this); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + int rentSize = ComputeCopyToRentSize(bufferSize); + return CopyToAsyncCore(destination, rentSize, cancellationToken); + } + + // Size the rented buffer to the remaining encoded payload (capped by bufferSize). + // Stream.CopyTo's default buffer is 80 KB because this stream is non-seekable; + // sizing here avoids that waste and lets the single-shot Read fast path consume + // the entire input in one call when the rented buffer is large enough. + // Falls back to bufferSize when Encoding.GetMaxByteCount would overflow. + private int ComputeCopyToRentSize(int bufferSize) + { + int remainingChars = _text.Length - _charPosition; + if (remainingChars <= (int.MaxValue / _maxBytesPerChar) - 1) + { + int maxBytes = _encoding.GetMaxByteCount(remainingChars); + return Math.Max(1, Math.Min(maxBytes, bufferSize)); + } + + return bufferSize; + } + + private async Task CopyToAsyncCore(Stream destination, int rentSize, CancellationToken cancellationToken) + { + byte[] buffer = ArrayPool.Shared.Rent(rentSize); + try + { + int n; + // Read is synchronous and CPU-bound; no underlying IO to await. + while ((n = Read(buffer, 0, buffer.Length)) != 0) + { + await destination.WriteAsync(buffer.AsMemory(0, n), cancellationToken).ConfigureAwait(false); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + /// + protected override void Dispose(bool disposing) + { + _disposed = true; + base.Dispose(disposing); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs new file mode 100644 index 00000000000000..d51b1e83b86be1 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/IO/WritableMemoryStream.cs @@ -0,0 +1,396 @@ +// 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; + private CachedCompletedInt32Task _lastReadTask; + + /// + /// Initializes a new instance of the class over the specified . + /// + /// The to wrap. + public WritableMemoryStream(Memory buffer) : base() + { + _buffer = buffer; + _length = 0; + _isOpen = true; + } + + /// + public override bool CanRead => _isOpen; + + /// + public override bool 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); + _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); + } + + try + { + int n = Read(buffer, offset, count); + return _lastReadTask.GetTask(n); + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception exception) + { + return Task.FromException(exception); + } + } + + /// + 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(_buffer.Span.Slice(_position, _length - _position)); + _position = _length; + } + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + ValidateCopyToArguments(destination, bufferSize); + EnsureNotClosed(); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + if (_length > _position) + { + ReadOnlyMemory content = _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); + } + + if (_position > _length) + { + _buffer.Span.Slice(_length, _position - _length).Clear(); + } + + _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); + } + + if (_position > _length) + { + _buffer.Span.Slice(_length, _position - _length).Clear(); + } + + 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); + EnsureNotClosed(); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + try + { + Write(buffer, offset, count); + return Task.CompletedTask; + } + catch (OperationCanceledException oce) + { + return Task.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + EnsureNotClosed(); + + if (cancellationToken.IsCancellationRequested) + { + return ValueTask.FromCanceled(cancellationToken); + } + + try + { + Write(buffer.Span); + return default; + } + catch (OperationCanceledException oce) + { + return ValueTask.FromCanceled(oce.CancellationToken); + } + catch (Exception ex) + { + return ValueTask.FromException(ex); + } + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + EnsureNotClosed(); + + long newPosition = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _length + offset, + _ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin)) + }; + + // Order matches MemoryStream.SeekCore / Common/src/System/IO/ReadOnlyMemoryStream.cs: + // overflow check first, then seek-before-begin. + ArgumentOutOfRangeException.ThrowIfGreaterThan(newPosition, int.MaxValue, nameof(offset)); + + if (newPosition < 0) + { + throw new IOException(SR.IO_SeekBeforeBegin); + } + + _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); + _buffer.Span.Slice(0, _length).CopyTo(copy); + return copy; + } + + /// + public override void WriteTo(Stream stream) + { + ArgumentNullException.ThrowIfNull(stream); + EnsureNotClosed(); + + stream.Write(_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..a66f8da44cfcac 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Json/JsonXmlDataContract.cs @@ -30,16 +30,16 @@ public JsonXmlDataContract(XmlDataContract traditionalXmlDataContract) DataContractSerializer dataContractSerializer = new DataContractSerializer(TraditionalDataContract.UnderlyingType, GetKnownTypesFromContext(context, context?.SerializerKnownTypeList), 1, false, false); // maxItemsInObjectGraph // ignoreExtensionDataObject // preserveObjectReferences - MemoryStream memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(xmlContent)); + StringStream stream = new(xmlContent, Encoding.UTF8); object? xmlValue; XmlDictionaryReaderQuotas? quotas = ((JsonReaderDelegator)jsonReader).ReaderQuotas; if (quotas == null) { - xmlValue = dataContractSerializer.ReadObject(memoryStream); + xmlValue = dataContractSerializer.ReadObject(stream); } else { - xmlValue = dataContractSerializer.ReadObject(XmlDictionaryReader.CreateTextReader(memoryStream, quotas)); + xmlValue = dataContractSerializer.ReadObject(XmlDictionaryReader.CreateTextReader(stream, quotas)); } context?.AddNewObject(xmlValue); return xmlValue; diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index f6d583e721bda0..5ad54a8b9a8477 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -11031,6 +11031,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..c8a0929de319a7 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamConformanceTests.cs @@ -0,0 +1,28 @@ +// 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 +{ + public class ReadOnlyMemoryStreamConformanceTests : StandaloneStreamConformanceTests + { + protected override bool CanSeek => true; + 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 ReadOnlyMemoryStream(ReadOnlyMemory.Empty)); + } + + return Task.FromResult(new ReadOnlyMemoryStream(new ReadOnlyMemory(initialData))); + } + + 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/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs new file mode 100644 index 00000000000000..fa1800d9b765d0 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/ReadOnlyMemoryStream/ReadOnlyMemoryStreamTests.cs @@ -0,0 +1,67 @@ +// 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 Xunit; + +namespace System.IO.Tests +{ + public class ReadOnlyMemoryStreamTests + { + [Fact] + public void ConstructorFromMemoryImplicitConversion() + { + byte[] buffer = { 1, 2, 3, 4, 5 }; + Memory memory = buffer; + Stream stream = new ReadOnlyMemoryStream(memory); + + Assert.Equal(5, stream.Length); + Assert.True(stream.CanRead); + } + + [Fact] + public void WorksWithSlicedMemory() + { + byte[] largeBuffer = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + ReadOnlyMemory slice = largeBuffer.AsMemory(3, 4); + 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 ReadFromUnmanagedMemory() + { + byte[] expected = [1, 2, 3, 4, 5]; + + using var manager = new NativeMemoryManager(expected.Length); + expected.CopyTo(manager.GetSpan()); + + using var stream = new ReadOnlyMemoryStream(manager.Memory); + + byte[] result = new byte[expected.Length]; + int bytesRead = stream.Read(result); + + Assert.Equal(expected.Length, bytesRead); + Assert.Equal(expected, result); + } + + [Fact] + public void GetBuffer_Throws_TryGetBuffer_ReturnsFalse() + { + var stream = new ReadOnlyMemoryStream(new byte[] { 1, 2, 3 }); + + Assert.Throws(() => stream.GetBuffer()); + Assert.False(stream.TryGetBuffer(out ArraySegment segment)); + Assert.Null(segment.Array); + Assert.Equal(0, segment.Offset); + Assert.Equal(0, segment.Count); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs new file mode 100644 index 00000000000000..8d89332bd15a0c --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamConformanceTests.cs @@ -0,0 +1,131 @@ +// 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 +{ + 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)); + } + + char[] chars = new char[initialData.Length]; + for (int i = 0; i < initialData.Length; i++) + chars[i] = (char)initialData[i]; + + return Task.FromResult( + new StringStream(chars.AsMemory(), IdentityEncoding.Instance)); + } + + protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); + + protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); + } + + 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("", IdentityEncoding.Instance)); + } + + char[] chars = new char[initialData.Length]; + for (int i = 0; i < initialData.Length; i++) + chars[i] = (char)initialData[i]; + + return Task.FromResult( + new StringStream(new string(chars), IdentityEncoding.Instance)); + } + + protected override Task CreateWriteOnlyStreamCore(byte[]? initialData) => Task.FromResult(null); + + protected override Task CreateReadWriteStreamCore(byte[]? initialData) => Task.FromResult(null); + } + + /// + /// Maps each char to/from a single byte (1:1), allowing conformance tests to + /// exercise StringStream with arbitrary byte data without UTF-8 round-trip issues. + /// Provides a custom Encoder because the base Encoder.Convert throws + /// "Conversion buffer overflow" when called with 0 chars (encoder flush path). + /// + internal sealed class IdentityEncoding : Encoding + { + public static IdentityEncoding Instance { get; } = new(); + + public override int GetByteCount(char[] chars, int index, int count) => count; + + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) + { + for (int i = 0; i < charCount; i++) + { + bytes[byteIndex + i] = (byte)chars[charIndex + i]; + } + + return charCount; + } + + public override int GetCharCount(byte[] bytes, int index, int count) => count; + + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + { + for (int i = 0; i < byteCount; i++) + { + chars[charIndex + i] = (char)bytes[byteIndex + i]; + } + + return byteCount; + } + + public override int GetMaxByteCount(int charCount) => charCount; + + public override int GetMaxCharCount(int byteCount) => byteCount; + + public override byte[] GetPreamble() => Array.Empty(); + + public override Encoder GetEncoder() => new IdentityEncoder(); + + private sealed class IdentityEncoder : Encoder + { + public override int GetByteCount(char[] chars, int index, int count, bool flush) => count; + + public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex, bool flush) + { + for (int i = 0; i < charCount; i++) + { + bytes[byteIndex + i] = (byte)chars[charIndex + i]; + } + + return charCount; + } + + public override void Convert(ReadOnlySpan chars, Span bytes, bool flush, out int charsUsed, out int bytesUsed, out bool completed) + { + int count = Math.Min(chars.Length, bytes.Length); + for (int i = 0; i < count; i++) + { + bytes[i] = (byte)chars[i]; + } + + charsUsed = count; + bytesUsed = count; + completed = chars.Length <= bytes.Length; + } + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTestBase.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTestBase.cs new file mode 100644 index 00000000000000..05dee7d5f16253 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTestBase.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Xunit; + +namespace System.IO.Tests +{ + public abstract class StringStreamTestBase + { + protected abstract Stream CreateStream(string input, Encoding encoding); + protected abstract int ReadFromStream(Stream stream, byte[] buffer, int offset, int count); + + private int ReadToEnd(Stream stream, byte[] buffer) + { + int totalRead = 0; + int bytesRead; + while ((bytesRead = ReadFromStream(stream, buffer, totalRead, buffer.Length - totalRead)) > 0) + { + totalRead += bytesRead; + } + + return totalRead; + } + + [Theory] + [InlineData("Hello, World! ")] + [InlineData("Unicode: 你好世界 🌍")] + [InlineData("Multi\nLine\r\nText")] + public void ReadsCorrectBytesForDifferentStrings(string input) + { + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + using Stream stream = CreateStream(input, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length + 100]; + int totalRead = ReadToEnd(stream, actualBytes); + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Theory] + [InlineData("ASCII text")] + [InlineData("Ñoño español")] + public void WorksWithDifferentEncodings(string input) + { + Encoding[] encodings = [Encoding.UTF8, Encoding.Unicode, Encoding.UTF32]; + + foreach (Encoding encoding in encodings) + { + byte[] expectedBytes = encoding.GetBytes(input); + using Stream stream = CreateStream(input, encoding); + + byte[] actualBytes = new byte[expectedBytes.Length * 2]; + int totalRead = ReadToEnd(stream, actualBytes); + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + } + + [Fact] + public void HandlesEmptyInput() + { + using Stream stream = CreateStream("", Encoding.UTF8); + byte[] buffer = new byte[10]; + int bytesRead = ReadFromStream(stream, buffer, 0, 10); + Assert.Equal(0, bytesRead); + } + + [Fact] + public void HandlesSurrogatePairs() + { + string input = "😀😁😂🤣😃😄"; + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + using Stream stream = CreateStream(input, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length + 100]; + int totalRead = ReadToEnd(stream, actualBytes); + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Fact] + public void MultiByteCharactersAcrossChunkBoundary() + { + string input = new string('A', 1023) + "你"; + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + using Stream stream = CreateStream(input, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length + 100]; + int totalRead = ReadToEnd(stream, actualBytes); + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Fact] + public void HandlesChunkedReading() + { + string input = new string('A', 10000); + byte[] expectedBytes = Encoding.UTF8.GetBytes(input); + using Stream stream = CreateStream(input, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length]; + int totalRead = 0; + int chunkSize = 512; + + int bytesRead; + while ((bytesRead = ReadFromStream(stream, actualBytes, totalRead, + Math.Min(chunkSize, expectedBytes.Length - totalRead))) > 0) + { + totalRead += bytesRead; + } + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes); + } + + [Fact] + public void MultipleReadsEventuallyReturnZero() + { + using Stream stream = CreateStream("small", Encoding.UTF8); + byte[] buffer = new byte[100]; + + int totalRead = ReadToEnd(stream, buffer); + Assert.Equal(5, totalRead); + + int finalRead = ReadFromStream(stream, buffer, 0, buffer.Length); + Assert.Equal(0, finalRead); + } + + [Fact] + public void PendingBytesDrainAcrossSingleByteReads() + { + // '你' encodes to 3 UTF-8 bytes (0xE4 0xBD 0xA0). A 1-byte buffer forces + // the encoder spillover path; subsequent reads must drain _pendingBytes + // before encoding the next character. + string input = "你"; + byte[] expected = Encoding.UTF8.GetBytes(input); + + using Stream stream = CreateStream(input, Encoding.UTF8); + byte[] buffer = new byte[1]; + + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(1, ReadFromStream(stream, buffer, 0, 1)); + Assert.Equal(expected[i], buffer[0]); + } + + Assert.Equal(0, ReadFromStream(stream, buffer, 0, 1)); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs new file mode 100644 index 00000000000000..3e30a4d8803ec1 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_Memory.cs @@ -0,0 +1,137 @@ +// 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 +{ + public class StringStreamTests_Memory_Read : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input.AsMemory(), encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.Read(buffer, offset, count); + } + + public class StringStreamTests_Memory_ReadSpan : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input.AsMemory(), encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.Read(buffer.AsSpan(offset, count)); + } + + public class StringStreamTests_Memory_ReadByte : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input.AsMemory(), encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + { + int b = stream.ReadByte(); + if (b == -1) return 0; + buffer[offset] = (byte)b; + return 1; + } + } + + public class StringStreamTests_Memory_ReadAsyncMemory : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input.AsMemory(), encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.ReadAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult(); + } + + public class StringStreamTests_Memory_ReadAsyncArray : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input.AsMemory(), encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); + } + + public class StringStreamTests_Memory_Misc + { + [Fact] + public async Task WorksWithMemorySlice() + { + string source = "0123456789ABCDEFGHIJ"; + ReadOnlyMemory slice = source.AsMemory(5, 10); + + byte[] expectedBytes = Encoding.UTF8.GetBytes("56789ABCDE"); + using var stream = new StringStream(slice, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length + 10]; + int totalRead = 0; + int bytesRead; + while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) + totalRead += bytesRead; + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Fact] + public async Task WorksWithCharArray() + { + char[] charArray = ['H', 'e', 'l', 'l', 'o']; + var memory = new ReadOnlyMemory(charArray); + + byte[] expectedBytes = Encoding.UTF8.GetBytes("Hello"); + using var stream = new StringStream(memory, Encoding.UTF8); + + byte[] actualBytes = new byte[expectedBytes.Length + 10]; + int totalRead = 0; + int bytesRead; + while ((bytesRead = await stream.ReadAsync(actualBytes.AsMemory(totalRead))) > 0) + totalRead += bytesRead; + + Assert.Equal(expectedBytes.Length, totalRead); + Assert.Equal(expectedBytes, actualBytes.AsSpan(0, totalRead).ToArray()); + } + + [Theory] + [InlineData("Hello")] + [InlineData("Unicode: 你好")] + [InlineData("Emoji: 😀")] + public async Task ProducesSameOutputAsStringOverload(string input) + { + using var memoryStream = new StringStream(input.AsMemory(), Encoding.UTF8); + using 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()); + } + + [Fact] + public void TruncatedSurrogatePairProducesReplacementChar() + { + // "🌍" is U+1F30D = surrogate pair (0xD83C, 0xDF0D) + // Slice after the high surrogate to create an unpaired surrogate + string emoji = "A\U0001F30D"; + ReadOnlyMemory truncated = emoji.AsMemory(0, 2); // 'A' + high surrogate only + + using var stream = new StringStream(truncated, Encoding.UTF8); + byte[] buffer = new byte[64]; + int totalRead = 0; + int bytesRead; + + while ((bytesRead = stream.Read(buffer, totalRead, buffer.Length - totalRead)) > 0) + totalRead += bytesRead; + + // Encoder should produce U+FFFD replacement character for the unpaired surrogate + byte[] expected = Encoding.UTF8.GetBytes("A\uFFFD"); + Assert.Equal(expected, buffer.AsSpan(0, totalRead).ToArray()); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs new file mode 100644 index 00000000000000..b123499b6132ca --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/StringStream/StringStreamTests_String.cs @@ -0,0 +1,99 @@ +// 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; +using Xunit; + +namespace System.IO.Tests +{ + public class StringStreamTests_String_Read : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input, encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.Read(buffer, offset, count); + } + + public class StringStreamTests_String_ReadSpan : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input, encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.Read(buffer.AsSpan(offset, count)); + } + + public class StringStreamTests_String_ReadByte : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input, encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + { + int b = stream.ReadByte(); + if (b == -1) return 0; + buffer[offset] = (byte)b; + return 1; + } + } + + public class StringStreamTests_String_ReadAsyncMemory : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input, encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.ReadAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult(); + } + + public class StringStreamTests_String_ReadAsyncArray : StringStreamTestBase + { + protected override Stream CreateStream(string input, Encoding encoding) + => new StringStream(input, encoding); + protected override int ReadFromStream(Stream stream, byte[] buffer, int offset, int count) + => stream.ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); + } + + public class StringStreamTests_String_Misc + { + [Fact] + public void ThrowsOnNullString() + { + Assert.Throws(() => new StringStream((string)null!, Encoding.UTF8)); + } + + [Fact] + public void ThrowsOnNullEncoding() + { + Assert.Throws(() => new StringStream("test", null!)); + } + + [Fact] + public void StreamCapabilities() + { + var stream = new StringStream("test", Encoding.UTF8); + Assert.True(stream.CanRead); + Assert.False(stream.CanSeek); + Assert.False(stream.CanWrite); + } + + [Fact] + public void EncodingPropertyReturnsCorrectEncoding() + { + var stream = new StringStream("test", Encoding.UTF32); + Assert.Equal(Encoding.UTF32, stream.Encoding); + } + + [Fact] + public async Task CopyToAsync_HonorsCancellation() + { + using var stream = new StringStream("hello", Encoding.UTF8); + using var destination = new MemoryStream(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAsync( + () => stream.CopyToAsync(destination, bufferSize: 81920, cts.Token)); + } + + } +} 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..d61f4473c9c7f0 100644 --- a/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/System.IO.Tests.csproj @@ -33,6 +33,14 @@ + + + + + + + + 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..1125961cf0bb63 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamConformanceTests.cs @@ -0,0 +1,40 @@ +// 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; + 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) + { + if (initialData is null) + { + return Task.FromResult(null); + } + + if (initialData.Length == 0) + { + return Task.FromResult( + new WritableMemoryStream(new Memory(new byte[1024]))); + } + + var memory = new Memory(new byte[initialData.Length]); + var stream = new WritableMemoryStream(memory); + stream.Write(initialData, 0, initialData.Length); + 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 new file mode 100644 index 00000000000000..dc9770c13c9232 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System.IO.Tests/WritableMemoryStream/WritableMemoryStreamTests.cs @@ -0,0 +1,129 @@ +// 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 Xunit; + +namespace System.IO.Tests +{ + public class WritableMemoryStreamTests + { + [Fact] + public void WriteBeyondCapacityThrows() + { + byte[] buffer = new byte[10]; + Stream stream = new WritableMemoryStream(new Memory(buffer)); + + byte[] data = new byte[15]; + Assert.Throws(() => stream.Write(data, 0, data.Length)); + } + + [Fact] + public void WriteByteBeyondCapacityThrows() + { + 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 WriteUpToExactCapacitySucceeds() + { + byte[] buffer = new byte[10]; + Stream stream = new WritableMemoryStream(new Memory(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 WritePastCapacityThrowsWithoutSideEffects() + { + byte[] buffer = new byte[10]; + Stream stream = new WritableMemoryStream(buffer); + + stream.Write(new byte[8], 0, 8); + Assert.Equal(8, stream.Position); + + byte[] data = new byte[5]; + Assert.Throws(() => stream.Write(data, 0, 5)); + + Assert.Equal(8, stream.Position); + } + + [Fact] + public void SeekPastCapacitySucceeds() + { + byte[] buffer = new byte[10]; + Stream stream = new WritableMemoryStream(buffer); + + stream.Seek(100, SeekOrigin.Begin); + Assert.Equal(100, stream.Position); + + Assert.Equal(-1, stream.ReadByte()); + Assert.Throws(() => stream.WriteByte(42)); + } + + [Fact] + public void WriteOverExistingDataReplacesData() + { + 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); + + Assert.Equal(10, bytesRead); + Assert.Equal(new byte[] { 1, 2, 3, 100, 101, 102, 7, 8, 9, 10 }, result); + } + + [Fact] + public void WriteToUnmanagedMemory() + { + byte[] data = [10, 20, 30, 40, 50]; + + using var manager = new NativeMemoryManager(data.Length); + manager.GetSpan().Clear(); + + using var stream = new WritableMemoryStream(manager.Memory); + stream.Write(data); + + Assert.Equal(data, manager.GetSpan().ToArray()); + Assert.Equal(data.Length, stream.Position); + } + + [Fact] + public void GetBuffer_Throws_TryGetBuffer_ReturnsFalse() + { + using var stream = new WritableMemoryStream(new byte[8]); + + Assert.Throws(() => stream.GetBuffer()); + Assert.False(stream.TryGetBuffer(out ArraySegment segment)); + Assert.Null(segment.Array); + Assert.Equal(0, segment.Offset); + Assert.Equal(0, segment.Count); + } + } +}