From 3ed94dd462158ceb8dcf8e25cd48ad18dea0ae85 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 09:56:26 +0100 Subject: [PATCH 1/6] 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 fde9a93db..0d5415e15 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -99,6 +99,8 @@ public static async ValueTask OpenAsyncArchive( 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 ec044e0e8..308aaa63d 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 IArchive OpenArchive(IReadOnlyList streams, ReaderOptions? 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 9dad1ae89..6a0b6bacd 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -77,6 +77,7 @@ public static IWritableArchive OpenArchive( ) { 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 df820e2fc..79d897181 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -92,6 +92,7 @@ public static IRarArchive OpenArchive( ) { 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 df7fa8c49..011b9a863 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -72,6 +72,7 @@ public static IArchive OpenArchive( ) { 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 e1464dbf6..37abd21c5 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -67,6 +67,7 @@ public static IWritableArchive OpenArchive( ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; var sourceStream = new SourceStream( strms[0], @@ -103,6 +104,10 @@ public static async ValueTask> OpenAsync ) { 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 static async ValueTask> OpenAsync { 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 bdd18669d..18d28114c 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -68,6 +68,7 @@ public static IWritableArchive OpenArchive( ) { streams.NotNull(nameof(streams)); + SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); var strms = streams; return new ZipArchive( new SourceStream( @@ -132,6 +133,7 @@ public static ValueTask> OpenAsyncArchiv ) { 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 96249f8ef..af5ab6920 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 @@ System.Type expectedFactoryType 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 a33256a6d..59e8b4fe8 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs @@ -5,6 +5,7 @@ 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 void TestGzArchiveTypeGzip() 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 20b5faa01..d13660c61 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -122,6 +122,15 @@ ReaderOptions.ForFilePath with [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 e3016d964..f16036df5 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -7,6 +7,7 @@ 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 30b511fae..0ab211f47 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 ebb721f5a..a0dcd87a1 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -34,6 +34,15 @@ public void TarArchiveStreamRead_Throws_On_NonSeekable_Stream() 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 82192b224..8f4d4a206 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"); From be4b6cdd7f7892e57cf19fb8723610f7b3ed2fbb Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 09:56:53 +0100 Subject: [PATCH 2/6] ignore opencode stuff --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 661476594..80f4154a7 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ profiler-snapshots/ .DS_Store *.snupkg benchmark-results/ +/.opencode From 2bae46e28aa1f9fcbd30bffbf9c034a7971cb816 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 10:11:54 +0100 Subject: [PATCH 3/6] add extension methods for checks --- .../Archives/ArchiveFactory.Async.cs | 9 +-- src/SharpCompress/Archives/ArchiveFactory.cs | 41 +++---------- .../Archives/GZip/GZipArchive.Factory.cs | 9 +-- .../Archives/Rar/RarArchive.Factory.cs | 9 +-- .../SevenZip/SevenZipArchive.Factory.cs | 9 +-- .../Archives/Tar/TarArchive.Factory.cs | 16 ++--- .../Archives/Zip/ZipArchive.Factory.cs | 11 +--- .../Readers/Ace/AceReader.Factory.cs | 10 ++-- .../Readers/Ace/SingleVolumeAceReader.cs | 2 +- src/SharpCompress/Readers/Arc/ArcReader.cs | 2 +- src/SharpCompress/Readers/Arj/ArjReader.cs | 6 +- .../Readers/Arj/SingleVolumeArjReader.cs | 2 +- .../Readers/GZip/GZipReader.Factory.cs | 2 +- .../Readers/Lzw/LzwReader.Factory.cs | 2 +- src/SharpCompress/Readers/Rar/RarReader.cs | 6 +- .../Readers/ReaderFactory.Async.cs | 2 +- src/SharpCompress/Readers/ReaderFactory.cs | 2 +- .../Readers/Tar/TarReader.Factory.cs | 2 +- src/SharpCompress/Readers/Zip/ZipReader.cs | 4 +- .../StreamValidationExtensions.cs | 58 +++++++++++++++++++ .../SharpCompress.Test/ReaderFactoryTests.cs | 39 +++++++++++++ 21 files changed, 141 insertions(+), 102 deletions(-) create mode 100644 src/SharpCompress/StreamValidationExtensions.cs create mode 100644 tests/SharpCompress.Test/ReaderFactoryTests.cs diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 0d5415e15..39ac27957 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -99,7 +99,7 @@ public static async ValueTask OpenAsyncArchive( throw new ArchiveOperationException("No streams"); } - EnsureSeekable(streamsArray); + streamsArray.RequireSeekable(); var firstStream = streamsArray[0]; if (streamsArray.Count == 1) @@ -145,11 +145,8 @@ internal static async ValueTask FindFactoryAsync( ) where T : IFactory { - stream.NotNull(nameof(stream)); - if (!stream.CanRead || !stream.CanSeek) - { - throw new ArgumentException("Stream should be readable and seekable"); - } + stream.RequireReadable(); + stream.RequireSeekable(); var factories = Factory.Factories.OfType(); diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 308aaa63d..eb7a68150 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -13,22 +13,6 @@ 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; @@ -96,7 +80,7 @@ public static IArchive OpenArchive(IReadOnlyList streams, ReaderOptions? throw new ArchiveOperationException("No streams"); } - EnsureSeekable(streamsArray); + streamsArray.RequireSeekable(); var firstStream = streamsArray[0]; if (streamsArray.Count == 1) @@ -139,11 +123,8 @@ public static T FindFactory(FileInfo finfo) public static T FindFactory(Stream stream) where T : IFactory { - stream.NotNull(nameof(stream)); - if (!stream.CanRead || !stream.CanSeek) - { - throw new ArgumentException("Stream should be readable and seekable"); - } + stream.RequireReadable(); + stream.RequireSeekable(); var factories = Factory.Factories.OfType(); @@ -178,12 +159,8 @@ public static bool IsArchive(string filePath, out ArchiveType? type) public static bool IsArchive(Stream stream, out ArchiveType? type) { type = null; - stream.NotNull(nameof(stream)); - - if (!stream.CanRead || !stream.CanSeek) - { - throw new ArgumentException("Stream should be readable and seekable"); - } + stream.RequireReadable(); + stream.RequireSeekable(); var startPosition = stream.Position; @@ -217,12 +194,8 @@ public static bool IsArchive(Stream stream, out ArchiveType? type) CancellationToken cancellationToken = default ) { - stream.NotNull(nameof(stream)); - - if (!stream.CanRead || !stream.CanSeek) - { - throw new ArgumentException("Stream should be readable and seekable"); - } + stream.RequireReadable(); + stream.RequireSeekable(); var startPosition = stream.Position; diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index 6a0b6bacd..c7f57f889 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -77,7 +77,7 @@ public static IWritableArchive OpenArchive( ) { streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); var strms = streams; return new GZipArchive( new SourceStream( @@ -93,12 +93,7 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { - stream.NotNull(nameof(stream)); - - if (stream is not { CanSeek: true }) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); return new GZipArchive( new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream) diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index 79d897181..b97e74978 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -58,12 +58,7 @@ public static IRarArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOp public static IRarArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); - - if (stream is not { CanSeek: true }) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); return new RarArchive( new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream) @@ -92,7 +87,7 @@ public static IRarArchive OpenArchive( ) { streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); 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 011b9a863..e15354b13 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -72,7 +72,7 @@ public static IArchive OpenArchive( ) { streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); var strms = streams; return new SevenZipArchive( new SourceStream( @@ -85,12 +85,7 @@ public static IArchive OpenArchive( public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); - - if (stream is not { CanSeek: true }) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); return new SevenZipArchive( new SourceStream(stream, _ => null, readerOptions ?? ReaderOptions.ForExternalStream) diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index 37abd21c5..5eacb1171 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -67,7 +67,7 @@ public static IWritableArchive OpenArchive( ) { streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); var strms = streams; var sourceStream = new SourceStream( strms[0], @@ -87,12 +87,7 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { - stream.NotNull(nameof(stream)); - - if (stream is not { CanSeek: true }) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); return OpenArchive([stream], readerOptions); } @@ -104,10 +99,7 @@ public static async ValueTask> OpenAsync ) { stream.NotNull(nameof(stream)); - if (!stream.CanSeek) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); var sourceStream = new SourceStream( stream, i => null, @@ -164,7 +156,7 @@ public static async ValueTask> OpenAsync { cancellationToken.ThrowIfCancellationRequested(); streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); 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 18d28114c..d83d00e1f 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -68,7 +68,7 @@ public static IWritableArchive OpenArchive( ) { streams.NotNull(nameof(streams)); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); var strms = streams; return new ZipArchive( new SourceStream( @@ -84,12 +84,7 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { - stream.NotNull(nameof(stream)); - - if (stream is not { CanSeek: true }) - { - throw new ArgumentException("Stream must be seekable", nameof(stream)); - } + stream.RequireSeekable(); return new ZipArchive( new SourceStream(stream, i => null, readerOptions ?? ReaderOptions.ForExternalStream) @@ -133,7 +128,7 @@ public static ValueTask> OpenAsyncArchiv ) { cancellationToken.ThrowIfCancellationRequested(); - SharpCompress.Archives.ArchiveFactory.EnsureSeekable(streams); + streams.RequireSeekable(); return new((IWritableAsyncArchive)OpenArchive(streams, readerOptions)); } diff --git a/src/SharpCompress/Readers/Ace/AceReader.Factory.cs b/src/SharpCompress/Readers/Ace/AceReader.Factory.cs index 5977e8ded..98c3124ad 100644 --- a/src/SharpCompress/Readers/Ace/AceReader.Factory.cs +++ b/src/SharpCompress/Readers/Ace/AceReader.Factory.cs @@ -19,7 +19,7 @@ public partial class AceReader /// An AceReader instance. public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new SingleVolumeAceReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } @@ -31,8 +31,8 @@ public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = n /// public static IReader OpenReader(IEnumerable streams, ReaderOptions? options = null) { - streams.NotNull(nameof(streams)); - return new MultiVolumeAceReader(streams, options ?? ReaderOptions.ForExternalStream); + var streamArray = streams.RequireReadable(); + return new MultiVolumeAceReader(streamArray, options ?? ReaderOptions.ForExternalStream); } public static ValueTask OpenAsyncReader( @@ -61,8 +61,8 @@ public static IAsyncReader OpenAsyncReader( ReaderOptions? options = null ) { - streams.NotNull(nameof(streams)); - return new MultiVolumeAceReader(streams, options ?? ReaderOptions.ForExternalStream); + var streamArray = streams.RequireReadable(); + return new MultiVolumeAceReader(streamArray, options ?? ReaderOptions.ForExternalStream); } public static ValueTask OpenAsyncReader( diff --git a/src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs b/src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs index ce42c6e1d..ba782d23f 100644 --- a/src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs +++ b/src/SharpCompress/Readers/Ace/SingleVolumeAceReader.cs @@ -12,7 +12,7 @@ internal class SingleVolumeAceReader : AceReader internal SingleVolumeAceReader(Stream stream, ReaderOptions options) : base(options) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); _stream = stream; } diff --git a/src/SharpCompress/Readers/Arc/ArcReader.cs b/src/SharpCompress/Readers/Arc/ArcReader.cs index c1b533665..e2a8303d5 100644 --- a/src/SharpCompress/Readers/Arc/ArcReader.cs +++ b/src/SharpCompress/Readers/Arc/ArcReader.cs @@ -24,7 +24,7 @@ private ArcReader(Stream stream, ReaderOptions options) /// public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new ArcReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } diff --git a/src/SharpCompress/Readers/Arj/ArjReader.cs b/src/SharpCompress/Readers/Arj/ArjReader.cs index a0c4899c4..79389b872 100644 --- a/src/SharpCompress/Readers/Arj/ArjReader.cs +++ b/src/SharpCompress/Readers/Arj/ArjReader.cs @@ -31,7 +31,7 @@ internal ArjReader(ReaderOptions options) /// public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new SingleVolumeArjReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } @@ -43,8 +43,8 @@ public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = n /// public static IReader OpenReader(IEnumerable streams, ReaderOptions? options = null) { - streams.NotNull(nameof(streams)); - return new MultiVolumeArjReader(streams, options ?? ReaderOptions.ForExternalStream); + var streamArray = streams.RequireReadable(); + return new MultiVolumeArjReader(streamArray, options ?? ReaderOptions.ForExternalStream); } protected abstract void ValidateArchive(ArjVolume archive); diff --git a/src/SharpCompress/Readers/Arj/SingleVolumeArjReader.cs b/src/SharpCompress/Readers/Arj/SingleVolumeArjReader.cs index 71eb46e4b..0128f60eb 100644 --- a/src/SharpCompress/Readers/Arj/SingleVolumeArjReader.cs +++ b/src/SharpCompress/Readers/Arj/SingleVolumeArjReader.cs @@ -12,7 +12,7 @@ internal class SingleVolumeArjReader : ArjReader internal SingleVolumeArjReader(Stream stream, ReaderOptions options) : base(options) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); _stream = stream; } diff --git a/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs b/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs index bd593f209..a6bca9f67 100644 --- a/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs +++ b/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs @@ -55,7 +55,7 @@ public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new GZipReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } } diff --git a/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs b/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs index e2166e37d..a1535c891 100644 --- a/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs +++ b/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs @@ -55,7 +55,7 @@ public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new LzwReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } } diff --git a/src/SharpCompress/Readers/Rar/RarReader.cs b/src/SharpCompress/Readers/Rar/RarReader.cs index 81a5457c7..43925d648 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.cs @@ -71,7 +71,7 @@ public static IReader OpenReader(IEnumerable fileInfos, ReaderOptions? /// public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new SingleVolumeRarReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } @@ -83,8 +83,8 @@ public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = n /// public static IReader OpenReader(IEnumerable streams, ReaderOptions? options = null) { - streams.NotNull(nameof(streams)); - return new MultiVolumeRarReader(streams, options ?? ReaderOptions.ForExternalStream); + var streamArray = streams.RequireReadable(); + return new MultiVolumeRarReader(streamArray, options ?? ReaderOptions.ForExternalStream); } protected override IEnumerable GetEntries(Stream stream) diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index 803885776..72bd7b09c 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -56,7 +56,7 @@ public static async ValueTask OpenAsyncReader( CancellationToken cancellationToken = default ) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); options ??= ReaderOptions.ForExternalStream; var sharpCompressStream = SharpCompressStream.Create( diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 2a84913cd..72c4dd71f 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -29,7 +29,7 @@ public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? options = nul /// public static IReader OpenReader(Stream stream, ReaderOptions? options = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); options ??= ReaderOptions.ForExternalStream; var sharpCompressStream = SharpCompressStream.Create( diff --git a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs index aae20bb4c..aa13c779b 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs @@ -170,7 +170,7 @@ public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions /// public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); readerOptions ??= ReaderOptions.ForExternalStream; var sharpCompressStream = SharpCompressStream.Create( stream, diff --git a/src/SharpCompress/Readers/Zip/ZipReader.cs b/src/SharpCompress/Readers/Zip/ZipReader.cs index e2019de65..a9ca5c50f 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.cs @@ -47,7 +47,7 @@ private ZipReader(Stream stream, ReaderOptions options, IEnumerable en /// public static IReader OpenReader(Stream stream, ReaderOptions? readerOptions = null) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new ZipReader(stream, readerOptions ?? ReaderOptions.ForExternalStream); } @@ -57,7 +57,7 @@ public static IReader OpenReader( IEnumerable entries ) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); return new ZipReader(stream, options ?? ReaderOptions.ForExternalStream, entries); } diff --git a/src/SharpCompress/StreamValidationExtensions.cs b/src/SharpCompress/StreamValidationExtensions.cs new file mode 100644 index 000000000..7420bcec6 --- /dev/null +++ b/src/SharpCompress/StreamValidationExtensions.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace SharpCompress; + +internal static class StreamValidationExtensions +{ + internal static Stream RequireReadable(this Stream stream) + { + stream.NotNull(nameof(stream)); + + if (!stream.CanRead) + { + throw new ArgumentException("Stream must be readable", nameof(stream)); + } + + return stream; + } + + internal static Stream RequireSeekable(this Stream stream) + { + stream.NotNull(nameof(stream)); + + if (!stream.CanSeek) + { + throw new ArgumentException("Stream must be seekable", nameof(stream)); + } + + return stream; + } + + internal static IReadOnlyList RequireSeekable(this IReadOnlyList streams) + { + streams.NotNull(nameof(streams)); + + foreach (var stream in streams) + { + stream.RequireSeekable(); + } + + return streams; + } + + internal static IReadOnlyList RequireReadable(this IEnumerable streams) + { + streams.NotNull(nameof(streams)); + + var streamArray = streams as IReadOnlyList ?? streams.ToArray(); + foreach (var stream in streamArray) + { + stream.RequireReadable(); + } + + return streamArray; + } +} diff --git a/tests/SharpCompress.Test/ReaderFactoryTests.cs b/tests/SharpCompress.Test/ReaderFactoryTests.cs new file mode 100644 index 000000000..f77fb2f92 --- /dev/null +++ b/tests/SharpCompress.Test/ReaderFactoryTests.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Readers; +using SharpCompress.Readers.Rar; +using SharpCompress.Test.Mocks; +using Xunit; + +namespace SharpCompress.Test; + +public class ReaderFactoryTests +{ + [Fact] + public void OpenReader_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => ReaderFactory.OpenReader(unreadable)); + } + + [Fact] + public async ValueTask OpenAsyncReader_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + await Assert.ThrowsAsync(() => + ReaderFactory.OpenAsyncReader(unreadable).AsTask() + ); + } + + [Fact] + public void RarReader_StreamCollection_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + using var readable = new MemoryStream(); + + Assert.Throws(() => RarReader.OpenReader([unreadable, readable])); + } +} From 5d14c96fb03699f2bfdcc44dbe4a2597995cbcdb Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 10:19:37 +0100 Subject: [PATCH 4/6] clean up seekable checks --- .../Archives/ArchiveFactory.Async.cs | 6 ++---- src/SharpCompress/Archives/ArchiveFactory.cs | 6 ++---- .../Archives/GZip/GZipArchive.Factory.cs | 6 +++--- .../Archives/Rar/RarArchive.Factory.cs | 6 +++--- .../SevenZip/SevenZipArchive.Factory.cs | 6 +++--- .../Archives/Tar/TarArchive.Factory.cs | 13 ++++++------- .../Archives/Zip/ZipArchive.Factory.cs | 7 +++---- .../SharpCompress.Test/ArchiveFactoryTests.cs | 18 ++++++++++++++++++ .../GZip/GZipArchiveTests.cs | 8 ++++++++ .../SharpCompress.Test/Rar/RarArchiveTests.cs | 8 ++++++++ .../SevenZip/SevenZipArchiveTests.cs | 8 ++++++++ .../Tar/TarArchiveAsyncTests.cs | 10 ++++++++++ .../SharpCompress.Test/Tar/TarArchiveTests.cs | 13 +++++++++++++ .../SharpCompress.Test/Zip/ZipArchiveTests.cs | 9 +++++++++ 14 files changed, 96 insertions(+), 28 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 39ac27957..6518952fb 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -92,15 +92,13 @@ public static async ValueTask OpenAsyncArchive( ) { cancellationToken.ThrowIfCancellationRequested(); - streams.NotNull(nameof(streams)); - var streamsArray = streams; + var streamsArray = streams.RequireReadable(); + streamsArray.RequireSeekable(); if (streamsArray.Count == 0) { throw new ArchiveOperationException("No streams"); } - streamsArray.RequireSeekable(); - var firstStream = streamsArray[0]; if (streamsArray.Count == 1) { diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index eb7a68150..7f1834d17 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -73,15 +73,13 @@ public static IArchive OpenArchive( public static IArchive OpenArchive(IReadOnlyList streams, ReaderOptions? options = null) { - streams.NotNull(nameof(streams)); - var streamsArray = streams; + var streamsArray = streams.RequireReadable(); + streamsArray.RequireSeekable(); if (streamsArray.Count == 0) { throw new ArchiveOperationException("No streams"); } - streamsArray.RequireSeekable(); - 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 c7f57f889..9caa068e7 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -76,9 +76,8 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); return new GZipArchive( new SourceStream( strms[0], @@ -93,6 +92,7 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { + stream.RequireReadable(); stream.RequireSeekable(); return new GZipArchive( diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index b97e74978..31d53b0ae 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -58,6 +58,7 @@ public static IRarArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOp public static IRarArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { + stream.RequireReadable(); stream.RequireSeekable(); return new RarArchive( @@ -86,9 +87,8 @@ public static IRarArchive OpenArchive( ReaderOptions? readerOptions = null ) { - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); return new RarArchive( new SourceStream( strms[0], diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index e15354b13..161b0ed25 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -71,9 +71,8 @@ public static IArchive OpenArchive( ReaderOptions? readerOptions = null ) { - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); return new SevenZipArchive( new SourceStream( strms[0], @@ -85,6 +84,7 @@ public static IArchive OpenArchive( public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { + stream.RequireReadable(); stream.RequireSeekable(); return new SevenZipArchive( diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index 5eacb1171..9d60a6e84 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -66,9 +66,8 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); var sourceStream = new SourceStream( strms[0], i => i < strms.Count ? strms[i] : null, @@ -87,6 +86,7 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { + stream.RequireReadable(); stream.RequireSeekable(); return OpenArchive([stream], readerOptions); @@ -98,7 +98,7 @@ public static async ValueTask> OpenAsync CancellationToken cancellationToken = default ) { - stream.NotNull(nameof(stream)); + stream.RequireReadable(); stream.RequireSeekable(); var sourceStream = new SourceStream( stream, @@ -155,9 +155,8 @@ public static async ValueTask> OpenAsync ) { cancellationToken.ThrowIfCancellationRequested(); - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); var sourceStream = new SourceStream( strms[0], i => i < strms.Count ? strms[i] : null, diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index d83d00e1f..a015272ab 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -67,9 +67,8 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { - streams.NotNull(nameof(streams)); - streams.RequireSeekable(); - var strms = streams; + var strms = streams.RequireReadable(); + strms.RequireSeekable(); return new ZipArchive( new SourceStream( strms[0], @@ -84,6 +83,7 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { + stream.RequireReadable(); stream.RequireSeekable(); return new ZipArchive( @@ -128,7 +128,6 @@ public static ValueTask> OpenAsyncArchiv ) { cancellationToken.ThrowIfCancellationRequested(); - streams.RequireSeekable(); return new((IWritableAsyncArchive)OpenArchive(streams, readerOptions)); } diff --git a/tests/SharpCompress.Test/ArchiveFactoryTests.cs b/tests/SharpCompress.Test/ArchiveFactoryTests.cs index af5ab6920..d6326badc 100644 --- a/tests/SharpCompress.Test/ArchiveFactoryTests.cs +++ b/tests/SharpCompress.Test/ArchiveFactoryTests.cs @@ -93,6 +93,24 @@ await ArchiveFactory.FindFactoryAsync(stream) ); } + [Fact] + public void OpenArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => ArchiveFactory.OpenArchive(unreadable)); + } + + [Fact] + public async ValueTask OpenAsyncArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + await Assert.ThrowsAsync(() => + ArchiveFactory.OpenAsyncArchive(unreadable).AsTask() + ); + } + [Theory] [InlineData("Zip.deflate.zip", ArchiveType.Zip)] [InlineData("Tar.noEmptyDirs.tar", ArchiveType.Tar)] diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs index 59e8b4fe8..5f9f50946 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs @@ -137,6 +137,14 @@ public void GZipArchive_StreamCollection_Throws_On_NonSeekable_Stream() Assert.Throws(() => GZipArchive.OpenArchive([nonSeekable, seekable])); } + [Fact] + public void GZipArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => GZipArchive.OpenArchive(unreadable)); + } + [Fact] public void GZip_Archive_NonSeekableStream() { diff --git a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs index d13660c61..c4ef386cc 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -131,6 +131,14 @@ public void RarArchive_StreamCollection_Throws_On_NonSeekable_Stream() Assert.Throws(() => RarArchive.OpenArchive([nonSeekable, seekable])); } + [Fact] + public void RarArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => RarArchive.OpenArchive(unreadable)); + } + [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 f16036df5..0fc86c3e0 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -37,6 +37,14 @@ public void SevenZipArchive_StreamCollection_Throws_On_NonSeekable_Stream() ); } + [Fact] + public void SevenZipArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => SevenZipArchive.OpenArchive(unreadable)); + } + [Fact] public void SevenZipArchive_LZMAAES_StreamRead() => ArchiveStreamRead( diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index 0ab211f47..a2c065167 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -32,6 +32,16 @@ await Assert.ThrowsAsync(() => ); } + [Fact] + public async ValueTask TarArchiveOpenAsyncStream_Throws_On_Unreadable_Stream() + { + using var stream = new TestStream(new MemoryStream(), false, true, true); + + 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 a0dcd87a1..5f522e7ce 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -34,6 +34,19 @@ public void TarArchiveStreamRead_Throws_On_NonSeekable_Stream() Assert.Throws(() => ArchiveFactory.OpenArchive(stream)); } + [Fact] + public void TarArchiveStreamRead_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream( + File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")), + false, + true, + true + ); + + Assert.Throws(() => TarArchive.OpenArchive(unreadable)); + } + [Fact] public void TarArchive_StreamCollection_Throws_On_NonSeekable_Stream() { diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs index 8f4d4a206..ca80917f2 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs @@ -7,6 +7,7 @@ using SharpCompress.Common; using SharpCompress.Common.Zip; using SharpCompress.Readers; +using SharpCompress.Test.Mocks; using SharpCompress.Writers; using SharpCompress.Writers.Zip; using Xunit; @@ -37,6 +38,14 @@ public void ZipArchive_StreamCollection_Throws_On_NonSeekable_Stream() Assert.Throws(() => ZipArchive.OpenArchive([nonSeekable, seekable])); } + [Fact] + public void ZipArchive_Stream_Throws_On_Unreadable_Stream() + { + using var unreadable = new TestStream(new MemoryStream(), false, true, true); + + Assert.Throws(() => ZipArchive.OpenArchive(unreadable)); + } + [Fact] public void Zip_Deflate_Streamed2_ArchiveStreamRead() => ArchiveStreamRead("Zip.deflate.dd-.zip"); From c9a68593ea9ce2b0880e53203b640b501e212aae Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 11:19:09 +0100 Subject: [PATCH 5/6] add writable checks --- .../StreamValidationExtensions.cs | 12 +++++++ .../Writers/GZip/GZipWriter.Factory.cs | 2 +- .../SevenZip/SevenZipWriter.Factory.cs | 2 +- .../Writers/Tar/TarWriter.Factory.cs | 2 +- src/SharpCompress/Writers/WriterFactory.cs | 4 +++ .../Writers/Zip/ZipWriter.Factory.cs | 2 +- .../SharpCompress.Test/WriterFactoryTests.cs | 34 +++++++++++++++++++ 7 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 tests/SharpCompress.Test/WriterFactoryTests.cs diff --git a/src/SharpCompress/StreamValidationExtensions.cs b/src/SharpCompress/StreamValidationExtensions.cs index 7420bcec6..8c2b87db1 100644 --- a/src/SharpCompress/StreamValidationExtensions.cs +++ b/src/SharpCompress/StreamValidationExtensions.cs @@ -31,6 +31,18 @@ internal static Stream RequireSeekable(this Stream stream) return stream; } + internal static Stream RequireWritable(this Stream stream) + { + stream.NotNull(nameof(stream)); + + if (!stream.CanWrite) + { + throw new ArgumentException("Stream must be writable", nameof(stream)); + } + + return stream; + } + internal static IReadOnlyList RequireSeekable(this IReadOnlyList streams) { streams.NotNull(nameof(streams)); diff --git a/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs b/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs index f4f2b902a..cae0b91d0 100644 --- a/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs +++ b/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs @@ -22,7 +22,7 @@ public static IWriter OpenWriter(FileInfo fileInfo, GZipWriterOptions writerOpti public static IWriter OpenWriter(Stream stream, GZipWriterOptions writerOptions) { - stream.NotNull(nameof(stream)); + stream.RequireWritable(); return new GZipWriter(stream, writerOptions); } diff --git a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs index e84d74017..1191a83a2 100644 --- a/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs +++ b/src/SharpCompress/Writers/SevenZip/SevenZipWriter.Factory.cs @@ -36,7 +36,7 @@ writerOptions with /// public static IWriter OpenWriter(Stream stream, SevenZipWriterOptions writerOptions) { - stream.NotNull(nameof(stream)); + stream.RequireWritable(); return new SevenZipWriter(stream, writerOptions); } diff --git a/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs b/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs index b00ed13ef..7613374b2 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs @@ -22,7 +22,7 @@ public static IWriter OpenWriter(FileInfo fileInfo, TarWriterOptions writerOptio public static IWriter OpenWriter(Stream stream, TarWriterOptions writerOptions) { - stream.NotNull(nameof(stream)); + stream.RequireWritable(); return new TarWriter(stream, writerOptions); } diff --git a/src/SharpCompress/Writers/WriterFactory.cs b/src/SharpCompress/Writers/WriterFactory.cs index 48ef1d474..847766f91 100644 --- a/src/SharpCompress/Writers/WriterFactory.cs +++ b/src/SharpCompress/Writers/WriterFactory.cs @@ -75,6 +75,8 @@ public static IWriter OpenWriter( IWriterOptions writerOptions ) { + stream.RequireWritable(); + var factory = Factories .Factory.Factories.OfType() .FirstOrDefault(item => item.KnownArchiveType == archiveType); @@ -102,6 +104,8 @@ public static async ValueTask OpenAsyncWriter( CancellationToken cancellationToken = default ) { + stream.RequireWritable(); + var factory = Factories .Factory.Factories.OfType() .FirstOrDefault(item => item.KnownArchiveType == archiveType); diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs b/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs index b6667842b..825de8709 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs @@ -22,7 +22,7 @@ public static IWriter OpenWriter(FileInfo fileInfo, ZipWriterOptions writerOptio public static IWriter OpenWriter(Stream stream, ZipWriterOptions writerOptions) { - stream.NotNull(nameof(stream)); + stream.RequireWritable(); return new ZipWriter(stream, writerOptions); } diff --git a/tests/SharpCompress.Test/WriterFactoryTests.cs b/tests/SharpCompress.Test/WriterFactoryTests.cs new file mode 100644 index 000000000..ca7a45a52 --- /dev/null +++ b/tests/SharpCompress.Test/WriterFactoryTests.cs @@ -0,0 +1,34 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Test.Mocks; +using SharpCompress.Writers; +using Xunit; + +namespace SharpCompress.Test; + +public class WriterFactoryTests +{ + [Fact] + public void OpenWriter_Stream_Throws_On_Unwritable_Stream() + { + using var unwritable = new TestStream(new MemoryStream(), true, false, true); + + Assert.Throws(() => + WriterFactory.OpenWriter(unwritable, ArchiveType.Zip, WriterOptions.ForZip()) + ); + } + + [Fact] + public async ValueTask OpenAsyncWriter_Stream_Throws_On_Unwritable_Stream() + { + using var unwritable = new TestStream(new MemoryStream(), true, false, true); + + await Assert.ThrowsAsync(() => + WriterFactory + .OpenAsyncWriter(unwritable, ArchiveType.Zip, WriterOptions.ForZip()) + .AsTask() + ); + } +} From 41432ab0624bbf19dc5497fd37430df4c49d24f7 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 23 Apr 2026 11:43:10 +0100 Subject: [PATCH 6/6] human fix --- .../Archives/ArchiveFactory.Async.cs | 8 +---- src/SharpCompress/Archives/ArchiveFactory.cs | 3 +- .../Archives/GZip/GZipArchive.Factory.cs | 3 +- .../Archives/Rar/RarArchive.Factory.cs | 3 +- .../SevenZip/SevenZipArchive.Factory.cs | 3 +- .../Archives/Tar/TarArchive.Factory.cs | 6 ++-- .../Archives/Zip/ZipArchive.Factory.cs | 3 +- .../StreamValidationExtensions.cs | 30 +++++-------------- src/SharpCompress/packages.lock.json | 12 ++++---- .../SharpCompress.Test/ReaderFactoryTests.cs | 4 ++- 10 files changed, 25 insertions(+), 50 deletions(-) diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 6518952fb..84bb671dc 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -92,13 +92,7 @@ public static async ValueTask OpenAsyncArchive( ) { cancellationToken.ThrowIfCancellationRequested(); - var streamsArray = streams.RequireReadable(); - streamsArray.RequireSeekable(); - if (streamsArray.Count == 0) - { - throw new ArchiveOperationException("No streams"); - } - + var streamsArray = streams.RequireReadable().RequireSeekable().ToList(); var firstStream = streamsArray[0]; if (streamsArray.Count == 1) { diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 7f1834d17..34cc5bb1e 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -73,8 +73,7 @@ public static IArchive OpenArchive( public static IArchive OpenArchive(IReadOnlyList streams, ReaderOptions? options = null) { - var streamsArray = streams.RequireReadable(); - streamsArray.RequireSeekable(); + var streamsArray = streams.RequireReadable().RequireSeekable().ToList(); if (streamsArray.Count == 0) { throw new ArchiveOperationException("No streams"); diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index 9caa068e7..2f5e5e336 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -76,8 +76,7 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); return new GZipArchive( new SourceStream( strms[0], diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index 31d53b0ae..2c333c5c5 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -87,8 +87,7 @@ public static IRarArchive OpenArchive( ReaderOptions? readerOptions = null ) { - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); return new RarArchive( new SourceStream( strms[0], diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index 161b0ed25..a338e363d 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -71,8 +71,7 @@ public static IArchive OpenArchive( ReaderOptions? readerOptions = null ) { - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); return new SevenZipArchive( new SourceStream( strms[0], diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index 9d60a6e84..d166f8c8f 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -66,8 +66,7 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); var sourceStream = new SourceStream( strms[0], i => i < strms.Count ? strms[i] : null, @@ -155,8 +154,7 @@ public static async ValueTask> OpenAsync ) { cancellationToken.ThrowIfCancellationRequested(); - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); var sourceStream = new SourceStream( strms[0], i => i < strms.Count ? strms[i] : null, diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index a015272ab..1e02ad6f1 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -67,8 +67,7 @@ public static IWritableArchive OpenArchive( ReaderOptions? readerOptions = null ) { - var strms = streams.RequireReadable(); - strms.RequireSeekable(); + var strms = streams.RequireReadable().RequireSeekable().ToList(); return new ZipArchive( new SourceStream( strms[0], diff --git a/src/SharpCompress/StreamValidationExtensions.cs b/src/SharpCompress/StreamValidationExtensions.cs index 8c2b87db1..28bd91167 100644 --- a/src/SharpCompress/StreamValidationExtensions.cs +++ b/src/SharpCompress/StreamValidationExtensions.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; namespace SharpCompress; internal static class StreamValidationExtensions { - internal static Stream RequireReadable(this Stream stream) + internal static void RequireReadable(this Stream stream) { stream.NotNull(nameof(stream)); @@ -15,11 +14,9 @@ internal static Stream RequireReadable(this Stream stream) { throw new ArgumentException("Stream must be readable", nameof(stream)); } - - return stream; } - internal static Stream RequireSeekable(this Stream stream) + internal static void RequireSeekable(this Stream stream) { stream.NotNull(nameof(stream)); @@ -27,11 +24,9 @@ internal static Stream RequireSeekable(this Stream stream) { throw new ArgumentException("Stream must be seekable", nameof(stream)); } - - return stream; } - internal static Stream RequireWritable(this Stream stream) + internal static void RequireWritable(this Stream stream) { stream.NotNull(nameof(stream)); @@ -39,32 +34,23 @@ internal static Stream RequireWritable(this Stream stream) { throw new ArgumentException("Stream must be writable", nameof(stream)); } - - return stream; } - internal static IReadOnlyList RequireSeekable(this IReadOnlyList streams) + internal static IEnumerable RequireSeekable(this IEnumerable streams) { - streams.NotNull(nameof(streams)); - foreach (var stream in streams) { stream.RequireSeekable(); + yield return stream; } - - return streams; } - internal static IReadOnlyList RequireReadable(this IEnumerable streams) + internal static IEnumerable RequireReadable(this IEnumerable streams) { - streams.NotNull(nameof(streams)); - - var streamArray = streams as IReadOnlyList ?? streams.ToArray(); - foreach (var stream in streamArray) + foreach (var stream in streams) { stream.RequireReadable(); + yield return stream; } - - return streamArray; } } diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index a401c7021..e82196f83 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -268,9 +268,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.0, )", - "resolved": "10.0.0", - "contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA==" + "requested": "[10.0.6, )", + "resolved": "10.0.6", + "contentHash": "QKuvS0LWX4fjFqeDkyM7Kqt8P3wYTiPD4nwU+9y59n0sCiG714fxDgbbN82vDnzq89AF/PiHl92TP2C4aFDUQA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -400,9 +400,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.22, )", - "resolved": "8.0.22", - "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" + "requested": "[8.0.26, )", + "resolved": "8.0.26", + "contentHash": "o7/yVssM2r9Wyln2s9edBd5ANZXqdSdBI+g7JqXkyJmXrhs2WsJp25K5yPnYrTgdKBCjKB8bg+O2oew4sgzFaA==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/ReaderFactoryTests.cs b/tests/SharpCompress.Test/ReaderFactoryTests.cs index f77fb2f92..2c5236c8d 100644 --- a/tests/SharpCompress.Test/ReaderFactoryTests.cs +++ b/tests/SharpCompress.Test/ReaderFactoryTests.cs @@ -34,6 +34,8 @@ public void RarReader_StreamCollection_Throws_On_Unreadable_Stream() using var unreadable = new TestStream(new MemoryStream(), false, true, true); using var readable = new MemoryStream(); - Assert.Throws(() => RarReader.OpenReader([unreadable, readable])); + Assert.Throws(() => + RarReader.OpenReader([unreadable, readable]).MoveToNextEntry() + ); } }