diff --git a/PowerKit.Tests/StreamPortalTests.cs b/PowerKit.Tests/StreamPortalTests.cs new file mode 100644 index 0000000..f6cc661 --- /dev/null +++ b/PowerKit.Tests/StreamPortalTests.cs @@ -0,0 +1,59 @@ +using System.IO; +using FluentAssertions; +using PowerKit.Extensions; +using Xunit; + +namespace PowerKit.Tests; + +public class StreamPortalTests +{ + [Fact] + public void CreatePortal_AtCurrentPosition_Test() + { + // Arrange + var data = new byte[] { 1, 2, 3, 4, 5 }; + using var stream = new MemoryStream(data); + stream.Seek(2, SeekOrigin.Begin); + + // Act + var portal = stream.CreatePortal(); + + // Assert + portal.Position.Should().Be(2); + } + + [Fact] + public void CreatePortal_AtSpecifiedPosition_Test() + { + // Arrange + var data = new byte[] { 1, 2, 3, 4, 5 }; + using var stream = new MemoryStream(data); + + // Act + var portal = stream.CreatePortal(3); + + // Assert + portal.Position.Should().Be(3); + } + + [Fact] + public void Jump_SeeksToPortalPositionAndRestoresOnDispose_Test() + { + // Arrange + var data = new byte[] { 1, 2, 3, 4, 5 }; + using var stream = new MemoryStream(data); + stream.Seek(1, SeekOrigin.Begin); + var portal = stream.CreatePortal(4); + + // Act + var jump = portal.Jump(); + + // Assert - seeks to portal position + stream.Position.Should().Be(4); + + jump.Dispose(); + + // Assert - restores original position on dispose + stream.Position.Should().Be(1); + } +} diff --git a/PowerKit/Extensions/StreamExtensions.cs b/PowerKit/Extensions/StreamExtensions.cs index 3a9402f..7280775 100644 --- a/PowerKit/Extensions/StreamExtensions.cs +++ b/PowerKit/Extensions/StreamExtensions.cs @@ -13,6 +13,16 @@ public static class StreamExtensions { extension(Stream source) { + /// + /// Creates a portal to the specified position in the stream. + /// + public StreamPortal CreatePortal(long position) => new(source, position); + + /// + /// Creates a portal to the current position in the stream. + /// + public StreamPortal CreatePortal() => source.CreatePortal(source.Position); + #if NET40_OR_GREATER || NETSTANDARD || NET /// /// Copies the contents of the stream to the destination stream, optionally flushing after each write. diff --git a/PowerKit/StreamPortal.cs b/PowerKit/StreamPortal.cs new file mode 100644 index 0000000..34e2c8c --- /dev/null +++ b/PowerKit/StreamPortal.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; + +namespace PowerKit; + +/// +/// Represents a saved position in a stream that can be jumped to and returned from. +/// +public class StreamPortal(Stream stream, long position) +{ + /// + /// Gets the position this portal points to. + /// + public long Position { get; } = + stream.CanSeek + ? position >= 0 + ? position + : throw new ArgumentOutOfRangeException( + nameof(position), + "Position must be non-negative." + ) + : throw new ArgumentException("Stream must support seeking.", nameof(stream)); + + /// + /// Seeks the stream to the portal's position and returns a disposable that, + /// when disposed, seeks back to the original position. + /// + public IDisposable Jump() + { + var oldPosition = stream.Position; + stream.Seek(Position, SeekOrigin.Begin); + + return Disposable.Create(() => stream.Seek(oldPosition, SeekOrigin.Begin)); + } +}