Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions src/libraries/System.Memory/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,10 @@
<data name="BufferMaximumSizeExceeded" xml:space="preserve">
<value>Cannot allocate a buffer of size {0}.</value>
</data>
<data name="NotSupported_UnseekableStream" xml:space="preserve">
<value>Stream does not support seeking.</value>
</data>
<data name="NotSupported_UnwritableStream" xml:space="preserve">
<value>Stream does not support writing.</value>
</data>
<data name="IO_SeekBeforeBegin" xml:space="preserve">
<value>An attempt was made to move the position before the beginning of the stream.</value>
</data>
<data name="Argument_InvalidSeekOrigin" xml:space="preserve">
<value>Invalid seek origin.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
namespace System.Buffers
{
/// <summary>
/// Provides a seekable, read-only <see cref="Stream"/> over a <see cref="ReadOnlySequence{Byte}"/>.
/// Provides a read-only, non-seekable <see cref="Stream"/> for reading from a <see cref="ReadOnlySequence{Byte}"/>.
/// </summary>
/// <remarks>
/// <para>The underlying sequence is not copied; reads are served directly from its segments.</para>
Expand All @@ -18,7 +18,6 @@ public sealed class ReadOnlySequenceStream : Stream
{
private ReadOnlySequence<byte> _sequence;
private SequencePosition _position;
private long _absolutePosition;
private bool _isDisposed;
private CachedCompletedInt32Task _lastReadTask;

Expand All @@ -30,59 +29,36 @@ public ReadOnlySequenceStream(ReadOnlySequence<byte> source)
{
_sequence = source;
_position = source.Start;
_absolutePosition = 0;
_isDisposed = false;
}

/// <inheritdoc />
public override bool CanRead => !_isDisposed;

/// <inheritdoc />
public override bool CanSeek => !_isDisposed;
// Keep this intentionally non-seekable: backward positioning requires traversing segments
// again from the beginning, making repeated seeks worst-case O(N). ReadOnlySequence<T>
// segment boundaries may be indirectly controlled by an untrusted network client through
// packet framing, so even correct stitching logic can produce adversarial fragmentation.
// Consumers must remain resilient against the worst technically compliant implementation
// rather than assuming ASP.NET-like segmentation.
public override bool CanSeek => false;

Comment thread
jozkee marked this conversation as resolved.
/// <inheritdoc />
public override bool CanWrite => false;

private void EnsureNotDisposed() => ObjectDisposedException.ThrowIf(_isDisposed, this);

/// <inheritdoc />
public override long Length
{
get
{
EnsureNotDisposed();
return _sequence.Length;
}
}
// Keep Length and Position unsupported to match the standard contract encoded by the
// stream conformance tests for streams where CanSeek is false, even though the underlying
// sequence can provide its length cheaply.
public override long Length => throw new NotSupportedException(SR.NotSupported_UnseekableStream);

/// <inheritdoc />
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;
}
get => throw new NotSupportedException(SR.NotSupported_UnseekableStream);
set => throw new NotSupportedException(SR.NotSupported_UnseekableStream);
}

/// <inheritdoc />
Expand All @@ -97,11 +73,6 @@ public override int Read(Span<byte> buffer)
{
EnsureNotDisposed();

if (_absolutePosition >= _sequence.Length)
{
return 0;
}

ReadOnlySequence<byte> remaining = _sequence.Slice(_position);
int n = (int)Math.Min(remaining.Length, buffer.Length);
if (n <= 0)
Expand All @@ -111,7 +82,6 @@ public override int Read(Span<byte> buffer)

remaining.Slice(0, n).CopyTo(buffer);
_position = _sequence.GetPosition(n, _position);
_absolutePosition += n;
return n;
}

Expand Down Expand Up @@ -159,19 +129,18 @@ public override void CopyTo(Stream destination, int bufferSize)
ValidateCopyToArguments(destination, bufferSize);
EnsureNotDisposed();

if (_absolutePosition >= _sequence.Length)
ReadOnlySequence<byte> remaining = _sequence.Slice(_position);
if (remaining.IsEmpty)
{
return;
}

ReadOnlySequence<byte> remaining = _sequence.Slice(_position);
foreach (ReadOnlyMemory<byte> segment in remaining)
{
destination.Write(segment.Span);
}

_position = _sequence.End;
_absolutePosition = _sequence.Length;
}

/// <inheritdoc />
Expand All @@ -185,24 +154,23 @@ public override Task CopyToAsync(Stream destination, int bufferSize, Cancellatio
return Task.FromCanceled(cancellationToken);
}

if (_absolutePosition >= _sequence.Length)
ReadOnlySequence<byte> remaining = _sequence.Slice(_position);
if (remaining.IsEmpty)
{
return Task.CompletedTask;
}

return CopyToAsyncCore(destination, cancellationToken);
return CopyToAsyncCore(remaining, destination, cancellationToken);
}

private async Task CopyToAsyncCore(Stream destination, CancellationToken cancellationToken)
private async Task CopyToAsyncCore(ReadOnlySequence<byte> remaining, Stream destination, CancellationToken cancellationToken)
{
ReadOnlySequence<byte> remaining = _sequence.Slice(_position);
foreach (ReadOnlyMemory<byte> segment in remaining)
{
await destination.WriteAsync(segment, cancellationToken).ConfigureAwait(false);
}

_position = _sequence.End;
_absolutePosition = _sequence.Length;
}

/// <inheritdoc />
Expand All @@ -218,43 +186,7 @@ private async Task CopyToAsyncCore(Stream destination, CancellationToken cancell
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);

/// <inheritdoc/>
public override long Seek(long offset, SeekOrigin origin)
{
EnsureNotDisposed();

long basePosition = origin switch
{
SeekOrigin.Begin => 0L,
SeekOrigin.Current => _absolutePosition,
SeekOrigin.End => _sequence.Length,
_ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin))
};

ArgumentOutOfRangeException.ThrowIfGreaterThan(offset, long.MaxValue - basePosition);

long absolutePosition = basePosition + offset;

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 long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(SR.NotSupported_UnseekableStream);

/// <inheritdoc />
public override void Flush() { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
using System.Buffers;
using System.IO.Tests;
using System.Threading.Tasks;
using Xunit;

namespace System.Memory.Tests
{
public class ROSequenceStreamConformanceTests : StandaloneStreamConformanceTests
{
protected override bool CanSeek => true;
protected override bool CanSeek => false;
protected override bool CanSetLength => false;
protected override bool NopFlushCompletesSynchronously => true;

Expand All @@ -34,27 +33,6 @@ protected virtual ReadOnlySequence<byte> CreateSequence(byte[] data)

protected override Task<Stream?> CreateReadWriteStreamCore(byte[]? initialData)
=> Task.FromResult<Stream?>(null);

public override async Task Seek_PastEnd_ReadReturns0(SeekMode mode)
{
await base.Seek_PastEnd_ReadReturns0(mode);

// ReadOnlySequenceStream-specific: seeking past end against an empty sequence
// is allowed, Position reflects the requested offset, and reads stay at 0.
var stream = new ReadOnlySequenceStream(ReadOnlySequence<byte>.Empty);
Assert.Equal(0, stream.Length);
Assert.Equal(0, stream.Position);

byte[] buffer = new byte[10];
Assert.Equal(0, stream.Read(buffer, 0, 10));

stream.Seek(0, SeekOrigin.Begin);
Assert.Equal(0, stream.Position);

long newPosition = stream.Seek(1, SeekOrigin.Begin);
Assert.Equal(1, newPosition);
Assert.Equal(1, stream.Position);
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@
namespace System.IO
{
/// <summary>
/// Provides a seekable, read-only <see cref="Stream"/> over a <see cref="ReadOnlyMemory{Byte}"/>.
/// Provides a seekable, read-only <see cref="Stream"/> for reading from a <see cref="ReadOnlyMemory{Byte}"/>.
/// </summary>
/// <remarks>
/// <para>The stream cannot be written to. <see cref="CanWrite"/> always returns <see langword="false"/>.</para>
/// </remarks>
public sealed class ReadOnlyMemoryStream : Stream
{
private ReadOnlyMemory<byte> _memory;
Expand All @@ -22,7 +19,11 @@ public sealed class ReadOnlyMemoryStream : Stream
/// <summary>
/// Initializes a new instance of the <see cref="ReadOnlyMemoryStream"/> class over the specified <see cref="ReadOnlyMemory{Byte}"/>.
/// </summary>
/// <param name="source">The <see cref="ReadOnlyMemory{Byte}"/> to wrap.</param>
/// <param name="source">The memory region from which to create the stream.</param>
/// <remarks>
/// The existing contents of <paramref name="source"/> are immediately readable.
/// Clear rented or reused memory before constructing the stream if its existing contents should not be exposed.
/// </remarks>
public ReadOnlyMemoryStream(ReadOnlyMemory<byte> source)
{
_memory = source;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace System.IO
{
/// <summary>
/// Provides a read-only, non-seekable <see cref="Stream"/> that encodes a <see cref="string"/> or
/// <see cref="ReadOnlyMemory{Char}"/> into bytes on-the-fly using a specified <see cref="System.Text.Encoding"/>.
/// <see cref="ReadOnlyMemory{Char}"/> into bytes on-the-fly using a specified <see cref="Encoding"/>.
/// </summary>
/// <remarks>
/// <para>This stream never emits a byte order mark (BOM). Callers who need a BOM can prepend it themselves.</para>
Expand Down Expand Up @@ -74,6 +74,8 @@ public StringStream(ReadOnlyMemory<char> text, Encoding encoding)
public override bool CanRead => !_disposed;

/// <inheritdoc/>
// Keep this intentionally non-seekable: backward positioning requires re-running the
// encoder from the beginning, making repeated seeks worst-case O(N).
public override bool CanSeek => false;

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace System.IO
{
/// <summary>
/// Provides a seekable, writable <see cref="Stream"/> over a <see cref="Memory{Byte}"/>.
/// Provides a seekable <see cref="Stream"/> for reading from and writing to a <see cref="Memory{Byte}"/>.
/// </summary>
public sealed class WritableMemoryStream : Stream
{
Expand All @@ -20,7 +20,11 @@ public sealed class WritableMemoryStream : Stream
/// <summary>
/// Initializes a new instance of the <see cref="WritableMemoryStream"/> class over the specified <see cref="Memory{Byte}"/>.
/// </summary>
/// <param name="buffer">The <see cref="Memory{Byte}"/> to wrap.</param>
/// <param name="buffer">The memory region from which to create the stream.</param>
/// <remarks>
/// The existing contents of <paramref name="buffer"/> are immediately readable.
/// Clear rented or reused memory before constructing the stream if its existing contents should not be exposed.
/// </remarks>
public WritableMemoryStream(Memory<byte> buffer)
{
_memory = buffer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,15 @@ public void ThrowsOnNullEncoding()
[Fact]
public void StreamCapabilities()
{
var stream = new StringStream("test", Encoding.UTF8);
using var stream = new StringStream("test", Encoding.UTF8);

Assert.True(stream.CanRead);
Assert.False(stream.CanSeek);
Assert.False(stream.CanWrite);
Assert.Throws<NotSupportedException>(() => stream.Length);
Assert.Throws<NotSupportedException>(() => stream.Position);
Assert.Throws<NotSupportedException>(() => stream.Position = 0);
Assert.Throws<NotSupportedException>(() => stream.Seek(0, SeekOrigin.Begin));
}

[Fact]
Expand Down
Loading