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));
+ }
+}