-
Notifications
You must be signed in to change notification settings - Fork 2
Add MemoryBackedStream #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 15 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 9319ccf
Fix test coverage and clarify docs for ToMemoryStream fast-path
Copilot c066a9c
Address code review feedback: remarks section and explicit CopyToAsyn…
Copilot 107ffcd
Re-architect stream materialization as a lazy MemoryBackedStream wrapper
Copilot 98c2118
Address code review: clamp position, document Flush no-op, truncate o…
Copilot fa8f4c4
Merge remote-tracking branch 'origin/prime' into copilot/add-to-memor…
Copilot e0a459b
Simplify EnsureBuffer, delegate Flush, replace stream wrappers with f…
Copilot e2caac0
Merge branch 'prime' into copilot/add-to-memory-stream-extension
Tyrrrz c1451ea
Make MemoryBackedStream a public class instead of an extension method
Copilot 974d6e6
Retain source's initial position in buffer when buffering a seekable …
Copilot 16270b3
Treat all source streams as unseekable in MemoryBackedStream
Copilot 1b4499e
Document trailing-data behavior and add write-back position test
Copilot 4f0c450
Split MemoryBackedStream into MemoryReadStream and MemoryWriteStream
Copilot 1c0883f
MemoryWriteStream: flush to source on Flush() instead of Dispose()
Copilot a064ed6
Consolidate stream tests to one focused test each
Copilot 3804c2e
Fix MemoryReadStream test to use a genuinely non-seekable source
Copilot 4dc8071
Extract NonSeekableStream as shared test utility; use in both read an…
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| using System.IO; | ||
| using FluentAssertions; | ||
| using Xunit; | ||
|
|
||
| namespace PowerKit.Tests; | ||
|
|
||
| public class MemoryReadStreamTests | ||
| { | ||
| [Fact] | ||
| public void MemoryReadStream_MakesUnseekableStreamSeekable_Test() | ||
| { | ||
| // Arrange — write known data and open as a non-seekable network-style stream | ||
| var data = new byte[] { 1, 2, 3, 4, 5 }; | ||
| using var tempFile = TempFile.Create(); | ||
| File.WriteAllBytes(tempFile.Path, data); | ||
| using var source = File.OpenRead(tempFile.Path); | ||
|
|
||
| // 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); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| using System.IO; | ||
| using FluentAssertions; | ||
| using Xunit; | ||
|
|
||
| namespace PowerKit.Tests; | ||
|
|
||
| public class MemoryWriteStreamTests | ||
| { | ||
| [Fact] | ||
| public void MemoryWriteStream_MakesUnseekableStreamSeekable_Test() | ||
| { | ||
| // Arrange — write-only file stream is not seekable after the initial position | ||
| var data = new byte[] { 1, 2, 3, 4, 5 }; | ||
| using var tempFile = TempFile.Create(); | ||
|
|
||
| // Act — wrapper buffers writes in memory; Flush() commits them to the source | ||
| using (var source = File.OpenWrite(tempFile.Path)) | ||
| 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 file | ||
| File.ReadAllBytes(tempFile.Path).Should().Equal(new byte[] { 10, 20, 3, 4, 5 }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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."); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure a file stream is unseekable? @copilot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right —
FileStreamis seekable. Replaced it with a privateNonSeekableStreamhelper that wraps aMemoryStreamand returnsfalseforCanSeek, so the test now genuinely exercises the unseekable-source path.