From e5ef31bdaf494fbe042c102543c878eac4c7c16f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:11:00 +0000 Subject: [PATCH] Fix XZBlock exception when accessing Position on non-seekable streams XZBlock accesses stream.Position in its constructor and SkipPadding(), which throws NotSupportedException for non-seekable streams (e.g., HTTP). Fix: XZStream now auto-wraps non-seekable streams with SharpCompressStream.Create() to provide position tracking via ring buffer. Add tests for raw non-seekable streams (sync and async). Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Compressors/Xz/XZStream.cs | 3 ++- .../Xz/XZStreamAsyncTests.cs | 20 +++++++++++++++++++ tests/SharpCompress.Test/Xz/XZStreamTests.cs | 20 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/SharpCompress/Compressors/Xz/XZStream.cs b/src/SharpCompress/Compressors/Xz/XZStream.cs index ea193431..e4910993 100644 --- a/src/SharpCompress/Compressors/Xz/XZStream.cs +++ b/src/SharpCompress/Compressors/Xz/XZStream.cs @@ -5,6 +5,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; +using SharpCompress.IO; namespace SharpCompress.Compressors.Xz; @@ -12,7 +13,7 @@ namespace SharpCompress.Compressors.Xz; public sealed partial class XZStream : XZReadOnlyStream { public XZStream(Stream baseStream) - : base(baseStream) { } + : base(baseStream.CanSeek ? baseStream : SharpCompressStream.Create(baseStream)) { } protected override void Dispose(bool disposing) { diff --git a/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs index 8f8e59b8..0da96e49 100644 --- a/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs +++ b/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs @@ -55,4 +55,24 @@ public class XzStreamAsyncTests : XzTestsBase var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false); Assert.Equal(OriginalEmpty, uncompressed); } + + [Fact] + public async ValueTask CanReadRawNonSeekableStreamAsync() + { + var nonSeekable = new ForwardOnlyStream(new MemoryStream(Compressed)); + var xz = new XZStream(nonSeekable); + using var sr = new StreamReader(new AsyncOnlyStream(xz)); + var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false); + Assert.Equal(Original, uncompressed); + } + + [Fact] + public async ValueTask CanReadRawNonSeekableEmptyStreamAsync() + { + var nonSeekable = new ForwardOnlyStream(new MemoryStream(CompressedEmpty)); + var xz = new XZStream(nonSeekable); + using var sr = new StreamReader(new AsyncOnlyStream(xz)); + var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false); + Assert.Equal(OriginalEmpty, uncompressed); + } } diff --git a/tests/SharpCompress.Test/Xz/XZStreamTests.cs b/tests/SharpCompress.Test/Xz/XZStreamTests.cs index 80ad5dc3..75e6c189 100644 --- a/tests/SharpCompress.Test/Xz/XZStreamTests.cs +++ b/tests/SharpCompress.Test/Xz/XZStreamTests.cs @@ -54,4 +54,24 @@ public class XzStreamTests : XzTestsBase var uncompressed = sr.ReadToEnd(); Assert.Equal(OriginalEmpty, uncompressed); } + + [Fact] + public void CanReadRawNonSeekableStream() + { + var nonSeekable = new ForwardOnlyStream(new MemoryStream(Compressed)); + var xz = new XZStream(nonSeekable); + using var sr = new StreamReader(xz); + var uncompressed = sr.ReadToEnd(); + Assert.Equal(Original, uncompressed); + } + + [Fact] + public void CanReadRawNonSeekableEmptyStream() + { + var nonSeekable = new ForwardOnlyStream(new MemoryStream(CompressedEmpty)); + var xz = new XZStream(nonSeekable); + using var sr = new StreamReader(xz); + var uncompressed = sr.ReadToEnd(); + Assert.Equal(OriginalEmpty, uncompressed); + } }