From 3ed94dd462158ceb8dcf8e25cd48ad18dea0ae85 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 09:56:26 +0100 Subject: [PATCH] add seekable checks --- .../Archives/ArchiveFactory.Async.cs | 2 ++ src/SharpCompress/Archives/ArchiveFactory.cs | 18 +++++++++++++++ .../Archives/GZip/GZipArchive.Factory.cs | 1 + .../Archives/Rar/RarArchive.Factory.cs | 1 + .../SevenZip/SevenZipArchive.Factory.cs | 1 + .../Archives/Tar/TarArchive.Factory.cs | 6 +++++ .../Archives/Zip/ZipArchive.Factory.cs | 2 ++ .../SharpCompress.Test/ArchiveFactoryTests.cs | 22 +++++++++++++++++++ .../GZip/GZipArchiveTests.cs | 10 +++++++++ .../SharpCompress.Test/Rar/RarArchiveTests.cs | 9 ++++++++ .../SevenZip/SevenZipArchiveTests.cs | 12 ++++++++++ .../Tar/TarArchiveAsyncTests.cs | 10 +++++++++ .../SharpCompress.Test/Tar/TarArchiveTests.cs | 9 ++++++++ .../SharpCompress.Test/Zip/ZipArchiveTests.cs | 9 ++++++++ 14 files changed, 112 insertions(+) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index fde9a93d..0d5415e1 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -99,6 +99,8 @@ public static partial class ArchiveFactory throw new ArchiveOperationException("No streams"); } + EnsureSeekable(streamsArray); + var firstStream = streamsArray[0]; if (streamsArray.Count == 1) { diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index ec044e0e..308aaa63 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -13,6 +13,22 @@ namespace SharpCompress.Archives; public static partial class ArchiveFactory { + internal static void EnsureSeekable(Stream stream) + { + if (stream is null || !stream.CanSeek) + { + throw new ArgumentException("Stream must be seekable", nameof(stream)); + } + } + + internal static void EnsureSeekable(IReadOnlyList streams) + { + foreach (var stream in streams) + { + EnsureSeekable(stream); + } + } + public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { readerOptions ??= ReaderOptions.ForExternalStream; @@ -80,6 +96,8 @@ public static partial class ArchiveFactory throw new ArchiveOperationException("No streams"); } + EnsureSeekable(streamsArray); + var firstStream = streamsArray[0]; if (streamsArray.Count == 1) { diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index 9dad1ae8..6a0b6bac 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -77,6 +77,7 @@ public partial class GZipArchive ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; return new GZipArchive( new SourceStream( diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index df820e2f..79d89718 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -92,6 +92,7 @@ public partial class RarArchive ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; return new RarArchive( new SourceStream( diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index df7fa8c4..011b9a86 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -72,6 +72,7 @@ public partial class SevenZipArchive ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; return new SevenZipArchive( new SourceStream( diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index e1464dbf..37abd21c 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -67,6 +67,7 @@ public partial class TarArchive ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; var sourceStream = new SourceStream( strms[0], @@ -103,6 +104,10 @@ public partial class TarArchive ) { stream.NotNull(nameof(stream)); + if (!stream.CanSeek) + { + throw new ArgumentException("Stream must be seekable", nameof(stream)); + } var sourceStream = new SourceStream( stream, i => null, @@ -159,6 +164,7 @@ public partial class TarArchive { cancellationToken.ThrowIfCancellationRequested(); streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; var sourceStream = new SourceStream( strms[0], diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index bdd18669..18d28114 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -68,6 +68,7 @@ public partial class ZipArchive ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; return new ZipArchive( new SourceStream( @@ -132,6 +133,7 @@ public partial class ZipArchive ) { cancellationToken.ThrowIfCancellationRequested(); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); return new((IWritableAsyncArchive)OpenArchive(streams, readerOptions)); } diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index 96249f8e..af5ab692 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -1,9 +1,11 @@ +using System; using System.IO; using System.Text; using System.Threading.Tasks; using SharpCompress.Archives; using SharpCompress.Common; using SharpCompress.Factories; +using SharpCompress.Test.Mocks; using Xunit; namespace SharpCompress.Test; @@ -61,6 +63,26 @@ public class ArchiveFactoryTests : TestBase Assert.Equal(startPosition, stream.Position); } + [Fact] + public void OpenArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + Assert.Throws(() => ArchiveFactory.OpenArchive([nonSeekable, seekable])); + } + + [Fact] + public async ValueTask OpenAsyncArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + await Assert.ThrowsAsync(() => + ArchiveFactory.OpenAsyncArchive([nonSeekable, seekable]).AsTask() + ); + } + [Fact] public async ValueTask FindFactoryAsync_InvalidData_ThrowsArchiveOperationException() { diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs index a33256a6..59e8b4fe 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs @@ -5,6 +5,7 @@ using SharpCompress.Archives; using SharpCompress.Archives.GZip; using SharpCompress.Archives.Tar; using SharpCompress.Common; +using SharpCompress.Test.Mocks; using SharpCompress.Writers.GZip; using Xunit; @@ -127,6 +128,15 @@ public class GZipArchiveTests : ArchiveTests Assert.Equal(archive.Type, ArchiveType.GZip); } + [Fact] + public void GZipArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + Assert.Throws(() => GZipArchive.OpenArchive([nonSeekable, seekable])); + } + [Fact] public void GZip_Archive_NonSeekableStream() { diff --git a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs index 20b5faa0..d13660c6 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -122,6 +122,15 @@ public class RarArchiveTests : ArchiveTests [Fact] public void Rar_ArchiveStreamRead() => ArchiveStreamRead("Rar.rar"); + [Fact] + public void RarArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + Assert.Throws(() => RarArchive.OpenArchive([nonSeekable, seekable])); + } + [Fact] public void Rar5_ArchiveStreamRead() => ArchiveStreamRead("Rar5.rar"); diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs index e3016d96..f16036df 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -7,6 +7,7 @@ using SharpCompress.Common; using SharpCompress.Common.SevenZip; using SharpCompress.Factories; using SharpCompress.Readers; +using SharpCompress.Test.Mocks; using Xunit; namespace SharpCompress.Test.SevenZip; @@ -25,6 +26,17 @@ public class SevenZipArchiveTests : ArchiveTests [Fact] public void SevenZipArchive_LZMA_PathRead() => ArchiveFileRead("7Zip.LZMA.7z"); + [Fact] + public void SevenZipArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + Assert.Throws(() => + SevenZipArchive.OpenArchive([nonSeekable, seekable]) + ); + } + [Fact] public void SevenZipArchive_LZMAAES_StreamRead() => ArchiveStreamRead( diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 30b511fa..0ab211f4 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -22,6 +22,16 @@ public class TarArchiveAsyncTests : ArchiveTests [Fact] public async ValueTask TarArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Tar.tar"); + [Fact] + public async ValueTask TarArchiveOpenAsyncStream_Throws_On_NonSeekable_Stream() + { + using var stream = new ForwardOnlyStream(new MemoryStream()); + + await Assert.ThrowsAsync(() => + TarArchive.OpenAsyncArchive(stream).AsTask() + ); + } + [Fact] public async ValueTask Tar_FileName_Exactly_100_Characters_Async() { diff --git a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs index ebb721f5..a0dcd87a 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -34,6 +34,15 @@ public class TarArchiveTests : ArchiveTests Assert.Throws(() => ArchiveFactory.OpenArchive(stream)); } + [Fact] + public void TarArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new ForwardOnlyStream(new MemoryStream()); + using var seekable = new MemoryStream(); + + Assert.Throws(() => TarArchive.OpenArchive([nonSeekable, seekable])); + } + [Fact] public void Tar_FileName_Exactly_100_Characters() { diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs index 82192b22..8f4d4a20 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs @@ -28,6 +28,15 @@ public class ZipArchiveTests : ArchiveTests [Fact] public void Zip_BZip2_ArchiveStreamRead() => ArchiveStreamRead("Zip.bzip2.zip"); + [Fact] + public void ZipArchive_StreamCollection_Throws_On_NonSeekable_Stream() + { + using var nonSeekable = new NonSeekableMemoryStream(); + using var seekable = new MemoryStream(); + + Assert.Throws(() => ZipArchive.OpenArchive([nonSeekable, seekable])); + } + [Fact] public void Zip_Deflate_Streamed2_ArchiveStreamRead() => ArchiveStreamRead("Zip.deflate.dd-.zip");