Skip to content
Merged
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
80b1a53
Add ToMemoryStream and ToMemoryStreamAsync extensions with tests
Copilot Jul 20, 2026
9319ccf
Fix test coverage and clarify docs for ToMemoryStream fast-path
Copilot Jul 20, 2026
c066a9c
Address code review feedback: remarks section and explicit CopyToAsyn…
Copilot Jul 20, 2026
107ffcd
Re-architect stream materialization as a lazy MemoryBackedStream wrapper
Copilot Jul 20, 2026
98c2118
Address code review: clamp position, document Flush no-op, truncate o…
Copilot Jul 20, 2026
fa8f4c4
Merge remote-tracking branch 'origin/prime' into copilot/add-to-memor…
Copilot Jul 20, 2026
e0a459b
Simplify EnsureBuffer, delegate Flush, replace stream wrappers with f…
Copilot Jul 21, 2026
e2caac0
Merge branch 'prime' into copilot/add-to-memory-stream-extension
Tyrrrz Jul 22, 2026
c1451ea
Make MemoryBackedStream a public class instead of an extension method
Copilot Jul 22, 2026
974d6e6
Retain source's initial position in buffer when buffering a seekable …
Copilot Jul 22, 2026
16270b3
Treat all source streams as unseekable in MemoryBackedStream
Copilot Jul 23, 2026
1b4499e
Document trailing-data behavior and add write-back position test
Copilot Jul 23, 2026
4f0c450
Split MemoryBackedStream into MemoryReadStream and MemoryWriteStream
Copilot Jul 23, 2026
1c0883f
MemoryWriteStream: flush to source on Flush() instead of Dispose()
Copilot Jul 23, 2026
a064ed6
Consolidate stream tests to one focused test each
Copilot Jul 23, 2026
3804c2e
Fix MemoryReadStream test to use a genuinely non-seekable source
Copilot Jul 23, 2026
4dc8071
Extract NonSeekableStream as shared test utility; use in both read an…
Copilot Jul 23, 2026
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
32 changes: 32 additions & 0 deletions PowerKit.Tests/MemoryReadStreamTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.IO;
using FluentAssertions;
using Xunit;

namespace PowerKit.Tests;

public class MemoryReadStreamTests
{
[Fact]
public void MemoryReadStream_MakesUnseekableStreamSeekable_Test()
{
// Arrange — wrap a MemoryStream in a non-seekable facade
var data = new byte[] { 1, 2, 3, 4, 5 };
using var source = new NonSeekableStream(new MemoryStream(data));
source.CanSeek.Should().BeFalse();

// Act
using var result = new MemoryReadStream(source);

// Assert — wrapper is always seekable and can re-read from the beginning
result.CanSeek.Should().BeTrue();

var partial = new byte[2];
result.ReadExactly(partial);

result.Seek(0, SeekOrigin.Begin);

var full = new byte[data.Length];
result.ReadExactly(full);
full.Should().Equal(data);
}
}
32 changes: 32 additions & 0 deletions PowerKit.Tests/MemoryWriteStreamTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.IO;
using FluentAssertions;
using Xunit;

namespace PowerKit.Tests;

public class MemoryWriteStreamTests
{
[Fact]
public void MemoryWriteStream_MakesUnseekableStreamSeekable_Test()
{
// Arrange — wrap a MemoryStream in a non-seekable facade
var data = new byte[] { 1, 2, 3, 4, 5 };
var inner = new MemoryStream();
using var source = new NonSeekableStream(inner);
source.CanSeek.Should().BeFalse();

// Act — wrapper buffers writes in memory; Flush() commits them to the source
using var wrapper = new MemoryWriteStream(source);
wrapper.CanSeek.Should().BeTrue();

// Write all bytes then seek back and overwrite the first two
wrapper.Write(data, 0, data.Length);
wrapper.Seek(0, SeekOrigin.Begin);
wrapper.Write(new byte[] { 10, 20 }, 0, 2);

wrapper.Flush();

// Assert — overwritten prefix is reflected in the underlying stream
inner.ToArray().Should().Equal(new byte[] { 10, 20, 3, 4, 5 });
}
}
36 changes: 36 additions & 0 deletions PowerKit.Tests/NonSeekableStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.IO;

namespace PowerKit.Tests;

internal sealed class NonSeekableStream(Stream inner) : Stream
{
public override bool CanRead => inner.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => inner.CanWrite;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}

public override void Flush() => inner.Flush();

public override int Read(byte[] buffer, int offset, int count) =>
inner.Read(buffer, offset, count);

public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();

public override void SetLength(long value) => throw new NotSupportedException();

public override void Write(byte[] buffer, int offset, int count) =>
inner.Write(buffer, offset, count);

protected override void Dispose(bool disposing)
{
if (disposing)
inner.Dispose();
base.Dispose(disposing);
}
}
74 changes: 74 additions & 0 deletions PowerKit/MemoryReadStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.IO;

namespace PowerKit;

/// <summary>
/// A <see cref="Stream" /> wrapper that lazily loads a readable stream into an in-memory buffer,
/// making it fully seekable regardless of whether the underlying stream supports seeking.
/// </summary>
/// <remarks>
/// <para>
/// The underlying stream is read into memory on the first access to
/// <see cref="Read" />, <see cref="Seek" />, <see cref="Length" />, or <see cref="Position" />.
/// Subsequent operations work directly against the in-memory buffer.
/// </para>
/// <para>
/// Content is buffered from the source stream's current position when the buffer is first
/// initialized. The buffer position always starts at 0.
/// </para>
/// </remarks>
public sealed class MemoryReadStream(Stream source) : Stream
{
private MemoryStream? _buffer;

private MemoryStream EnsureBuffer()
{
if (_buffer is not null)
return _buffer;

var capacity = source.CanSeek ? (int)(source.Length - source.Position) : 0;
_buffer = new MemoryStream(capacity);
source.CopyTo(_buffer);
_buffer.Position = 0;

return _buffer;
}

/// <inheritdoc />
public override bool CanRead => true;

/// <inheritdoc />
public override bool CanSeek => true;

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

/// <inheritdoc />
public override long Length => EnsureBuffer().Length;

/// <inheritdoc />
public override long Position
{
get => EnsureBuffer().Position;
set => EnsureBuffer().Position = value;
}

/// <inheritdoc />
public override void Flush() => _buffer?.Flush();

/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count) =>
EnsureBuffer().Read(buffer, offset, count);

/// <inheritdoc />
public override long Seek(long offset, SeekOrigin origin) =>
EnsureBuffer().Seek(offset, origin);

/// <inheritdoc />
public override void SetLength(long value) =>
throw new System.NotSupportedException("Stream does not support writing.");

/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count) =>
throw new System.NotSupportedException("Stream does not support writing.");
}
64 changes: 64 additions & 0 deletions PowerKit/MemoryWriteStream.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.IO;

namespace PowerKit;

/// <summary>
/// A <see cref="Stream" /> wrapper that buffers all writes in memory and flushes them to the
/// underlying stream on <see cref="Flush" />.
/// </summary>
/// <remarks>
/// <para>
/// Writes go to an in-memory buffer and do not touch the underlying stream until
/// <see cref="Flush" /> is called. This makes the wrapper always seekable and allows writes to
/// be reordered freely before the final flush.
/// </para>
/// <para>
/// On <see cref="Flush" />, the entire in-memory buffer is written to the underlying stream
/// starting at its current position.
/// </para>
/// </remarks>
public sealed class MemoryWriteStream(Stream source) : Stream
{
private readonly MemoryStream _buffer = new();

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

/// <inheritdoc />
public override bool CanSeek => true;

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

/// <inheritdoc />
public override long Length => _buffer.Length;

/// <inheritdoc />
public override long Position
{
get => _buffer.Position;
set => _buffer.Position = value;
}

/// <inheritdoc />
public override void Flush()
{
_buffer.Position = 0;
_buffer.CopyTo(source);
source.Flush();
}

/// <inheritdoc />
public override int Read(byte[] buffer, int offset, int count) =>
throw new System.NotSupportedException("Stream does not support reading.");

/// <inheritdoc />
public override long Seek(long offset, SeekOrigin origin) => _buffer.Seek(offset, origin);

/// <inheritdoc />
public override void SetLength(long value) => _buffer.SetLength(value);

/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count) =>
_buffer.Write(buffer, offset, count);
}