Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
18 changes: 18 additions & 0 deletions src/libraries/System.Memory/ref/System.Memory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,24 @@ public void Rewind(long count) { }
public bool TryReadExact(int count, out System.Buffers.ReadOnlySequence<T> sequence) { throw null; }
}
}
namespace System.Buffers
{
public sealed partial class ReadOnlySequenceStream : System.IO.Stream
{
public ReadOnlySequenceStream(System.Buffers.ReadOnlySequence<byte> sequence) { }
public override bool CanRead { get { throw null; } }
public override bool CanSeek { get { throw null; } }
public override bool CanWrite { get { throw null; } }
public override long Length { get { throw null; } }
public override long Position { get { throw null; } set { } }
public override void Flush() { }
public override int Read(byte[] buffer, int offset, int count) { throw null; }
public override int Read(System.Span<byte> buffer) { 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) { }
}
}
namespace System.Runtime.InteropServices
{
public static partial class SequenceMarshal
Expand Down
9 changes: 9 additions & 0 deletions src/libraries/System.Memory/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,13 @@
<data name="BufferMaximumSizeExceeded" xml:space="preserve">
<value>Cannot allocate a buffer of size {0}.</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>
3 changes: 2 additions & 1 deletion src/libraries/System.Memory/src/System.Memory.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
Expand Down Expand Up @@ -26,6 +26,7 @@
<Compile Include="System\Buffers\ReadOnlySequence.cs" />
<Compile Include="System\Buffers\ReadOnlySequenceDebugView.cs" />
<Compile Include="System\Buffers\ReadOnlySequenceSegment.cs" />
<Compile Include="System\Buffers\ReadOnlySequenceStream.cs" />
<Compile Include="System\Buffers\ReadOnlySequence.Helpers.cs" />
<Compile Include="System\Buffers\SequenceReader.cs" />
<Compile Include="System\Buffers\SequenceReader.Search.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Threading;
using System.IO;
using System.Threading.Tasks;

namespace System.Buffers
{
/// <summary>
/// Provides a seekable, read-only <see cref="Stream"/> implementation over a <see cref="ReadOnlySequence{T}"/> of bytes.
/// </summary>
/// <remarks>
/// This type is not thread-safe. Synchronize access if the stream is used concurrently.
/// The underlying sequence should not be modified while the stream is in use.
/// Seeking beyond the end of the stream is supported; subsequent reads will return zero bytes.
/// </remarks>
// Seekable Stream from ReadOnlySequence<byte>
public sealed class ReadOnlySequenceStream : Stream
{
private ReadOnlySequence<byte> _sequence;
private SequencePosition _position;
private long _positionPastEnd; // -1 if within bounds, or the actual position if past end
private bool _isDisposed;

/// <summary>
/// Initializes a new instance of the <see cref="ReadOnlySequenceStream"/> class over the specified <see cref="ReadOnlySequence{Byte}"/>.
/// </summary>
/// <param name="sequence">The <see cref="ReadOnlySequence{Byte}"/> to wrap.</param>
public ReadOnlySequenceStream(ReadOnlySequence<byte> sequence)
{
_sequence = sequence;
_position = sequence.Start;
_positionPastEnd = -1;
_isDisposed = false;
}

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

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

/// <inheritdoc />
public override bool CanWrite => false;

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

/// <inheritdoc />
public override long Length
{
get
{
EnsureNotDisposed();
return _sequence.Length;
}
}

/// <inheritdoc />
public override long Position
{
get
{
EnsureNotDisposed();
return _positionPastEnd >= 0 ? _positionPastEnd : _sequence.Slice(_sequence.Start, _position).Length;
}
set
{
EnsureNotDisposed();
ArgumentOutOfRangeException.ThrowIfNegative(value);

// Allow seeking past the end
if (value >= Length)
{
_position = _sequence.End;
_positionPastEnd = value;
}
else
{
_position = _sequence.GetPosition(value, _sequence.Start);
_positionPastEnd = -1;
}
}
}

/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count)
{
ValidateBufferArguments(buffer, offset, count);
return Read(buffer.AsSpan(offset, count));
}

/// <inheritdoc />
public override int Read(Span<byte> buffer)
{
EnsureNotDisposed();

if (_positionPastEnd >= 0)
{
return 0;
}

ReadOnlySequence<byte> 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);
return n;
}

/// <inheritdoc/>
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ValidateBufferArguments(buffer, offset, count);

// If cancellation was requested, bail early
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled<int>(cancellationToken);

int n = Read(buffer, offset, count);
return Task.FromResult(n);
}

/// <inheritdoc/>
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
{
return ValueTask.FromCanceled<int>(cancellationToken);
}

int bytesRead = Read(buffer.Span);
return new ValueTask<int>(bytesRead);
}

/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);

/// <inheritdoc/>
public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);

/// <inheritdoc/>
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);

/// <inheritdoc/>
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);

/// <summary>
/// Sets the position within the current stream.
/// </summary>
/// <param name="offset">A byte offset relative to the <paramref name="origin"/> parameter.</param>
/// <param name="origin">A value of type <see cref="SeekOrigin"/> indicating the reference point used to obtain the new position.</param>
/// <returns>The new position within the stream.</returns>
public override long Seek(long offset, SeekOrigin origin)
{
EnsureNotDisposed();

long absolutePosition = origin switch
{
SeekOrigin.Begin => offset,
SeekOrigin.Current => (_positionPastEnd >= 0 ? _positionPastEnd : _sequence.Slice(_sequence.Start, _position).Length) + offset,
SeekOrigin.End => Length + offset,
_ => throw new ArgumentException(SR.Argument_InvalidSeekOrigin, nameof(origin))
};

// Negative positions are invalid
if (absolutePosition < 0)
{
throw new IOException(SR.IO_SeekBeforeBegin);
}

// Update position - seeking past end is allowed
if (absolutePosition >= Length)
{
_position = _sequence.End;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_position = _sequence.End;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the catch! You're right, since _position is never read when _positionPastEnd >= 0, the assignment is currently redundant. Although, it still feels like a safer fallback than leaving it stale (in case future code reads _position without checking first).

_positionPastEnd = absolutePosition;
}
else
{
_position = _sequence.GetPosition(absolutePosition, _sequence.Start);
_positionPastEnd = -1;
}

return absolutePosition;
}

/// <inheritdoc />
public override void Flush() { }

/// <inheritdoc />
public override void SetLength(long value) => throw new NotSupportedException(SR.NotSupported_UnwritableStream);

/// <inheritdoc />
protected override void Dispose(bool disposing)
{
_isDisposed = true;
base.Dispose(disposing);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
using System.Buffers;
using System.IO.Tests;
using System.Threading.Tasks;

namespace System.Memory.Tests
{
/// <summary>
/// Conformance tests for ReadOnlySequenceStream - a read-only, seekable stream
/// wrapper around ReadOnlySequence{byte}.
/// </summary>
public class ROSequenceStreamConformanceTests : StandaloneStreamConformanceTests
Comment thread
ViveliDuCh marked this conversation as resolved.
{
// StreamConformanceTests flags to specify capabilities
protected override bool CanSeek => true;
// SetLength() is not supported because ReadOnlySequence{byte} is immutable.
protected override bool CanSetLength => false;
Comment thread
ViveliDuCh marked this conversation as resolved.
// ReadOnlySequenceStream doesn't buffer writes (it's read-only),
protected override bool NopFlushCompletesSynchronously => true;

protected override Task<Stream?> CreateReadOnlyStreamCore(byte[]? initialData)
{
if (initialData == null || initialData.Length == 0)
{
// Create empty sequence for null or empty data
var emptySequence = ReadOnlySequence<byte>.Empty;
return Task.FromResult<Stream?>(new ReadOnlySequenceStream(emptySequence));
}

// ReadOnlySequence<byte> can be constructed from:
// 1. ReadOnlyMemory<byte> (single segment)
// 2. ReadOnlySequenceSegment<byte> chain (multi-segment)
var sequence = new ReadOnlySequence<byte>(initialData); // Single segment
return Task.FromResult<Stream?>(new ReadOnlySequenceStream(sequence));
}

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


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