Compare commits

...

3 Commits

Author SHA1 Message Date
Adam Hathcock
998ce961c2 new interfaces 2026-02-17 07:41:11 +00:00
Adam Hathcock
a7ba77cc85 Merge branch 'adam/conslidate-exceptions' into adam/rework-7z-extraction 2026-02-16 16:30:02 +00:00
Adam Hathcock
e27ae35c44 Add IExtractableArchiveEntry 2026-02-14 10:00:35 +00:00
48 changed files with 564 additions and 301 deletions

View File

@@ -6,7 +6,6 @@ using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Factories;
using SharpCompress.IO;
using SharpCompress.Readers;
namespace SharpCompress.Archives;

View File

@@ -14,7 +14,9 @@ using SharpCompress.Writers.GZip;
namespace SharpCompress.Archives.GZip;
public partial class GZipArchive
: AbstractWritableArchive<GZipArchiveEntry, GZipVolume, GZipWriterOptions>
: AbstractWritableArchive<GZipArchiveEntry, GZipVolume, GZipWriterOptions>,
IGZipArchive,
IGZipAsyncArchive
{
private GZipArchive(SourceStream sourceStream)
: base(ArchiveType.GZip, sourceStream) { }
@@ -93,4 +95,10 @@ public partial class GZipArchive
stream.Position = 0;
return GZipReader.OpenReader(stream, ReaderOptions);
}
IEnumerable<IExtractableArchiveEntry> IExtractableArchive.Entries =>
Entries.Cast<IExtractableArchiveEntry>();
IAsyncEnumerable<IExtractableArchiveEntry> IExtractableAsyncArchive.EntriesAsync =>
EntriesAsync.CastToExtractableEntry<GZipArchiveEntry>();
}

View File

@@ -7,7 +7,7 @@ using SharpCompress.Common.Options;
namespace SharpCompress.Archives.GZip;
public class GZipArchiveEntry : GZipEntry, IArchiveEntry
public class GZipArchiveEntry : GZipEntry, IExtractableArchiveEntry
{
internal GZipArchiveEntry(GZipArchive archive, GZipFilePart? part, IReaderOptions readerOptions)
: base(part, readerOptions) => Archive = archive;

View File

@@ -0,0 +1,13 @@
using SharpCompress.Common;
namespace SharpCompress.Archives.GZip;
/// <summary>
/// GZip archive supporting random access and entry stream extraction.
/// </summary>
public interface IGZipArchive : IExtractableArchive { }
/// <summary>
/// Async GZip archive supporting random access and entry stream extraction.
/// </summary>
public interface IGZipAsyncArchive : IExtractableAsyncArchive { }

View File

@@ -1,24 +1,9 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
namespace SharpCompress.Archives;
public interface IArchiveEntry : IEntry
{
/// <summary>
/// Opens the current entry as a stream that will decompress as it is read.
/// Read the entire stream or use SkipEntry on EntryStream.
/// </summary>
Stream OpenEntryStream();
/// <summary>
/// Opens the current entry as a stream that will decompress as it is read asynchronously.
/// Read the entire stream or use SkipEntry on EntryStream.
/// </summary>
ValueTask<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default);
/// <summary>
/// The archive can find all the parts of the archive needed to extract this entry.
/// </summary>

View File

@@ -12,6 +12,20 @@ public static class IArchiveEntryExtensions
/// <param name="archiveEntry">The archive entry to extract.</param>
extension(IArchiveEntry archiveEntry)
{
/// <summary>
/// Opens the current entry as a stream that will decompress as it is read.
/// For formats that only support sequential extraction, use archive.ExtractAllEntries().
/// </summary>
public Stream OpenEntryStream() => GetExtractableEntry(archiveEntry).OpenEntryStream();
/// <summary>
/// Opens the current entry as a stream that will decompress as it is read asynchronously.
/// For formats that only support sequential extraction, use archive.ExtractAllEntriesAsync().
/// </summary>
public ValueTask<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
) => GetExtractableEntry(archiveEntry).OpenEntryStreamAsync(cancellationToken);
/// <summary>
/// Extract entry to the specified stream.
/// </summary>
@@ -24,7 +38,8 @@ public static class IArchiveEntryExtensions
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
}
using var entryStream = archiveEntry.OpenEntryStream();
var extractable = GetExtractableEntry(archiveEntry);
using var entryStream = extractable.OpenEntryStream();
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
sourceStream.CopyTo(streamToWriteTo, Constants.BufferSize);
}
@@ -47,11 +62,11 @@ public static class IArchiveEntryExtensions
}
#if LEGACY_DOTNET
using var entryStream = await archiveEntry
using var entryStream = await GetExtractableEntry(archiveEntry)
.OpenEntryStreamAsync(cancellationToken)
.ConfigureAwait(false);
#else
await using var entryStream = await archiveEntry
await using var entryStream = await GetExtractableEntry(archiveEntry)
.OpenEntryStreamAsync(cancellationToken)
.ConfigureAwait(false);
#endif
@@ -62,6 +77,18 @@ public static class IArchiveEntryExtensions
}
}
private static IExtractableArchiveEntry GetExtractableEntry(IArchiveEntry entry)
{
if (entry is IExtractableArchiveEntry extractableEntry)
{
return extractableEntry;
}
throw new NotSupportedException(
$"Entry stream extraction is not supported for {entry.Archive.Type} archive entries. Use archive.ExtractAllEntries() for sequential extraction."
);
}
private static Stream WrapWithProgress(
Stream source,
IArchiveEntry entry,

View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
using SharpCompress.Common;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
/// <summary>
/// Archive that supports extracting individual entries via OpenEntryStream.
/// </summary>
public interface IExtractableArchive : IArchive
{
/// <summary>
/// Entries that support opening a decompressed content stream directly.
/// </summary>
new IEnumerable<IExtractableArchiveEntry> Entries { get; }
}
/// <summary>
/// Async archive that supports extracting individual entries via OpenEntryStream.
/// </summary>
public interface IExtractableAsyncArchive : IAsyncArchive
{
/// <summary>
/// Entries that support opening a decompressed content stream directly.
/// </summary>
new IAsyncEnumerable<IExtractableArchiveEntry> EntriesAsync { get; }
}

View File

@@ -0,0 +1,23 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Archives;
/// <summary>
/// Archive entry that supports opening a decompressed content stream directly.
/// </summary>
public interface IExtractableArchiveEntry : IArchiveEntry
{
/// <summary>
/// Opens the current entry as a stream that will decompress as it is read.
/// Read the entire stream or use SkipEntry on EntryStream.
/// </summary>
Stream OpenEntryStream();
/// <summary>
/// Opens the current entry as a stream that will decompress as it is read asynchronously.
/// Read the entire stream or use SkipEntry on EntryStream.
/// </summary>
ValueTask<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default);
}

View File

@@ -20,9 +20,9 @@ public interface IRarArchiveCommon
int MaxVersion { get; }
}
public interface IRarArchive : IArchive, IRarArchiveCommon { }
public interface IRarArchive : IExtractableArchive, IRarArchiveCommon { }
public interface IRarAsyncArchive : IAsyncArchive, IRarArchiveCommon { }
public interface IRarAsyncArchive : IExtractableAsyncArchive, IRarArchiveCommon { }
public partial class RarArchive
: AbstractArchive<RarArchiveEntry, RarVolume>,
@@ -104,4 +104,10 @@ public partial class RarArchive
public virtual int MinVersion => Volumes.First().MinVersion;
public virtual int MaxVersion => Volumes.First().MaxVersion;
IEnumerable<IExtractableArchiveEntry> IExtractableArchive.Entries =>
Entries.Cast<IExtractableArchiveEntry>();
IAsyncEnumerable<IExtractableArchiveEntry> IExtractableAsyncArchive.EntriesAsync =>
EntriesAsync.CastToExtractableEntry<RarArchiveEntry>();
}

View File

@@ -12,7 +12,7 @@ using SharpCompress.Readers;
namespace SharpCompress.Archives.Rar;
public partial class RarArchiveEntry : RarEntry, IArchiveEntry
public partial class RarArchiveEntry : RarEntry, IExtractableArchiveEntry
{
private readonly ICollection<RarFilePart> parts;
private readonly RarArchive archive;

View File

@@ -0,0 +1,15 @@
using SharpCompress.Common;
namespace SharpCompress.Archives.SevenZip;
/// <summary>
/// 7Zip archive supporting metadata access and sequential extraction only.
/// Does NOT support IExtractableArchive because 7Zip requires sequential decompression.
/// </summary>
public interface ISevenZipArchive : IArchive { }
/// <summary>
/// Async 7Zip archive supporting metadata access and sequential extraction only.
/// Does NOT support IExtractableAsyncArchive because 7Zip requires sequential decompression.
/// </summary>
public interface ISevenZipAsyncArchive : IAsyncArchive { }

View File

@@ -0,0 +1,13 @@
using SharpCompress.Readers;
namespace SharpCompress.Archives.SevenZip;
/// <summary>
/// Reader for 7Zip archives - supports sequential extraction only.
/// </summary>
public interface ISevenZipReader : IReader { }
/// <summary>
/// Async reader for 7Zip archives - supports sequential extraction only.
/// </summary>
public interface ISevenZipAsyncReader : IAsyncReader { }

View File

@@ -12,7 +12,10 @@ using SharpCompress.Readers;
namespace SharpCompress.Archives.SevenZip;
public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVolume>
public partial class SevenZipArchive
: AbstractArchive<SevenZipArchiveEntry, SevenZipVolume>,
ISevenZipArchive,
ISevenZipAsyncArchive
{
private ArchiveDatabase? _database;
@@ -103,7 +106,10 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
public override long TotalSize =>
_database?._packSizes.Aggregate(0L, (total, packSize) => total + packSize) ?? 0;
internal sealed class SevenZipReader : AbstractReader<SevenZipEntry, SevenZipVolume>
internal sealed class SevenZipReader
: AbstractReader<SevenZipEntry, SevenZipVolume>,
ISevenZipReader,
ISevenZipAsyncReader
{
private readonly SevenZipArchive _archive;
private SevenZipEntry? _currentEntry;

View File

@@ -1,6 +1,3 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Options;
using SharpCompress.Common.SevenZip;
@@ -15,15 +12,6 @@ public class SevenZipArchiveEntry : SevenZipEntry, IArchiveEntry
)
: base(part, readerOptions) => Archive = archive;
public Stream OpenEntryStream() => FilePart.GetCompressedStream();
public async ValueTask<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
) =>
(
await FilePart.GetCompressedStreamAsync(cancellationToken).ConfigureAwait(false)
).NotNull();
public IArchive Archive { get; }
public bool IsComplete => true;

View File

@@ -0,0 +1,13 @@
using SharpCompress.Common;
namespace SharpCompress.Archives.Tar;
/// <summary>
/// TAR archive supporting random access and entry stream extraction.
/// </summary>
public interface ITarArchive : IExtractableArchive { }
/// <summary>
/// Async TAR archive supporting random access and entry stream extraction.
/// </summary>
public interface ITarAsyncArchive : IExtractableAsyncArchive { }

View File

@@ -17,7 +17,9 @@ using Constants = SharpCompress.Common.Constants;
namespace SharpCompress.Archives.Tar;
public partial class TarArchive
: AbstractWritableArchive<TarArchiveEntry, TarVolume, TarWriterOptions>
: AbstractWritableArchive<TarArchiveEntry, TarVolume, TarWriterOptions>,
ITarArchive,
ITarAsyncArchive
{
private readonly CompressionType _compressionType;
@@ -237,4 +239,10 @@ public partial class TarArchive
stream.Position = 0;
return new TarReader(stream, ReaderOptions, _compressionType);
}
IEnumerable<IExtractableArchiveEntry> IExtractableArchive.Entries =>
Entries.Cast<IExtractableArchiveEntry>();
IAsyncEnumerable<IExtractableArchiveEntry> IExtractableAsyncArchive.EntriesAsync =>
EntriesAsync.CastToExtractableEntry<TarArchiveEntry>();
}

View File

@@ -8,7 +8,7 @@ using SharpCompress.Common.Tar;
namespace SharpCompress.Archives.Tar;
public class TarArchiveEntry : TarEntry, IArchiveEntry
public class TarArchiveEntry : TarEntry, IExtractableArchiveEntry
{
internal TarArchiveEntry(
TarArchive archive,

View File

@@ -0,0 +1,13 @@
using SharpCompress.Common;
namespace SharpCompress.Archives.Zip;
/// <summary>
/// ZIP archive supporting random access and entry stream extraction.
/// </summary>
public interface IZipArchive : IExtractableArchive { }
/// <summary>
/// Async ZIP archive supporting random access and entry stream extraction.
/// </summary>
public interface IZipAsyncArchive : IExtractableAsyncArchive { }

View File

@@ -18,7 +18,9 @@ using SharpCompress.Writers.Zip;
namespace SharpCompress.Archives.Zip;
public partial class ZipArchive
: AbstractWritableArchive<ZipArchiveEntry, ZipVolume, ZipWriterOptions>
: AbstractWritableArchive<ZipArchiveEntry, ZipVolume, ZipWriterOptions>,
IZipArchive,
IZipAsyncArchive
{
private readonly SeekableZipHeaderFactory? headerFactory;
@@ -173,4 +175,10 @@ public partial class ZipArchive
stream.Position = 0;
return new((IAsyncReader)ZipReader.OpenReader(stream, ReaderOptions, Entries));
}
IEnumerable<IExtractableArchiveEntry> IExtractableArchive.Entries =>
Entries.Cast<IExtractableArchiveEntry>();
IAsyncEnumerable<IExtractableArchiveEntry> IExtractableAsyncArchive.EntriesAsync =>
EntriesAsync.CastToExtractableEntry<ZipArchiveEntry>();
}

View File

@@ -1,13 +1,11 @@
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Options;
using SharpCompress.Common.Zip;
namespace SharpCompress.Archives.Zip;
public partial class ZipArchiveEntry : ZipEntry, IArchiveEntry
public partial class ZipArchiveEntry : ZipEntry, IExtractableArchiveEntry
{
internal ZipArchiveEntry(
ZipArchive archive,

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
namespace SharpCompress.Common;
/// <summary>
/// Extension methods for async enumerables.
/// </summary>
public static class AsyncEnumerableExtensions
{
/// <summary>
/// Casts an IAsyncEnumerable to another type.
/// </summary>
public static async IAsyncEnumerable<TResult> CastAsync<TResult>(
this IAsyncEnumerable<object> source
)
{
await foreach (var item in source)
{
yield return (TResult)item;
}
}
/// <summary>
/// Casts an IAsyncEnumerable of TEntry to IAsyncEnumerable of IExtractableArchiveEntry.
/// </summary>
public static async IAsyncEnumerable<IExtractableArchiveEntry> CastToExtractableEntry<TEntry>(
this IAsyncEnumerable<TEntry> source
)
where TEntry : IArchiveEntry
{
await foreach (var item in source)
{
yield return (IExtractableArchiveEntry)(object)item;
}
}
}

View File

@@ -18,7 +18,10 @@ namespace SharpCompress.Readers.Ace;
/// - Recovery record support
/// - Additional header flags
/// </remarks>
public abstract partial class AceReader : AbstractReader<AceEntry, AceVolume>
public abstract partial class AceReader
: AbstractReader<AceEntry, AceVolume>,
IAceReader,
IAceAsyncReader
{
private readonly IArchiveEncoding _archiveEncoding;

View File

@@ -0,0 +1,13 @@
using SharpCompress.Readers;
namespace SharpCompress.Readers.Ace;
/// <summary>
/// Reader for ACE archives.
/// </summary>
public interface IAceReader : IReader { }
/// <summary>
/// Async reader for ACE archives.
/// </summary>
public interface IAceAsyncReader : IAsyncReader { }

View File

@@ -9,7 +9,7 @@ using SharpCompress.Common.Arc;
namespace SharpCompress.Readers.Arc;
public partial class ArcReader : AbstractReader<ArcEntry, ArcVolume>
public partial class ArcReader : AbstractReader<ArcEntry, ArcVolume>, IArcReader, IArcAsyncReader
{
private ArcReader(Stream stream, ReaderOptions options)
: base(options, ArchiveType.Arc) => Volume = new ArcVolume(stream, options, 0);

View File

@@ -0,0 +1,13 @@
using SharpCompress.Readers;
namespace SharpCompress.Readers.Arc;
/// <summary>
/// Reader for ARC archives.
/// </summary>
public interface IArcReader : IReader { }
/// <summary>
/// Async reader for ARC archives.
/// </summary>
public interface IArcAsyncReader : IAsyncReader { }

View File

@@ -10,7 +10,10 @@ using SharpCompress.Readers.Rar;
namespace SharpCompress.Readers.Arj;
public abstract partial class ArjReader : AbstractReader<ArjEntry, ArjVolume>
public abstract partial class ArjReader
: AbstractReader<ArjEntry, ArjVolume>,
IArjReader,
IArjAsyncReader
{
internal ArjReader(ReaderOptions options)
: base(options, ArchiveType.Arj) { }

View File

@@ -0,0 +1,13 @@
using SharpCompress.Readers;
namespace SharpCompress.Readers.Arj;
/// <summary>
/// Reader for ARJ archives.
/// </summary>
public interface IArjReader : IReader { }
/// <summary>
/// Async reader for ARJ archives.
/// </summary>
public interface IArjAsyncReader : IAsyncReader { }

View File

@@ -5,7 +5,10 @@ using SharpCompress.Common.GZip;
namespace SharpCompress.Readers.GZip;
public partial class GZipReader : AbstractReader<GZipEntry, GZipVolume>
public partial class GZipReader
: AbstractReader<GZipEntry, GZipVolume>,
IGZipReader,
IGZipAsyncReader
{
private GZipReader(Stream stream, ReaderOptions options)
: base(options, ArchiveType.GZip) => Volume = new GZipVolume(stream, options, 0);

View File

@@ -0,0 +1,13 @@
using SharpCompress.Readers;
namespace SharpCompress.Readers.GZip;
/// <summary>
/// Reader for GZip archives.
/// </summary>
public interface IGZipReader : IReader { }
/// <summary>
/// Async reader for GZip archives.
/// </summary>
public interface IGZipAsyncReader : IAsyncReader { }

View File

@@ -0,0 +1,13 @@
using SharpCompress.Readers;
namespace SharpCompress.Readers.Lzw;
/// <summary>
/// Reader for LZW archives.
/// </summary>
public interface ILzwReader : IReader { }
/// <summary>
/// Async reader for LZW archives.
/// </summary>
public interface ILzwAsyncReader : IAsyncReader { }

View File

@@ -5,7 +5,7 @@ using SharpCompress.Common.Lzw;
namespace SharpCompress.Readers.Lzw;
public partial class LzwReader : AbstractReader<LzwEntry, LzwVolume>
public partial class LzwReader : AbstractReader<LzwEntry, LzwVolume>, ILzwReader, ILzwAsyncReader
{
private LzwReader(Stream stream, ReaderOptions options)
: base(options, ArchiveType.Lzw) => Volume = new LzwVolume(stream, options, 0);

View File

@@ -0,0 +1,13 @@
using SharpCompress.Readers;
namespace SharpCompress.Readers.Rar;
/// <summary>
/// Reader for RAR archives.
/// </summary>
public interface IRarReader : IReader { }
/// <summary>
/// Async reader for RAR archives.
/// </summary>
public interface IRarAsyncReader : IAsyncReader { }

View File

@@ -11,7 +11,10 @@ namespace SharpCompress.Readers.Rar;
/// <summary>
/// This class faciliates Reading a Rar Archive in a non-seekable forward-only manner
/// </summary>
public abstract partial class RarReader : AbstractReader<RarReaderEntry, RarVolume>
public abstract partial class RarReader
: AbstractReader<RarReaderEntry, RarVolume>,
IRarReader,
IRarAsyncReader
{
private bool _disposed;
private RarVolume? volume;

View File

@@ -0,0 +1,13 @@
using SharpCompress.Readers;
namespace SharpCompress.Readers.Tar;
/// <summary>
/// Reader for TAR archives.
/// </summary>
public interface ITarReader : IReader { }
/// <summary>
/// Async reader for TAR archives.
/// </summary>
public interface ITarAsyncReader : IAsyncReader { }

View File

@@ -17,7 +17,7 @@ using SharpCompress.Providers;
namespace SharpCompress.Readers.Tar;
public partial class TarReader : AbstractReader<TarEntry, TarVolume>
public partial class TarReader : AbstractReader<TarEntry, TarVolume>, ITarReader, ITarAsyncReader
{
private readonly CompressionType compressionType;

View File

@@ -0,0 +1,13 @@
using SharpCompress.Readers;
namespace SharpCompress.Readers.Zip;
/// <summary>
/// Reader for ZIP archives.
/// </summary>
public interface IZipReader : IReader { }
/// <summary>
/// Async reader for ZIP archives.
/// </summary>
public interface IZipAsyncReader : IAsyncReader { }

View File

@@ -9,7 +9,7 @@ using SharpCompress.Common.Zip.Headers;
namespace SharpCompress.Readers.Zip;
public partial class ZipReader : AbstractReader<ZipEntry, ZipVolume>
public partial class ZipReader : AbstractReader<ZipEntry, ZipVolume>, IZipReader, IZipAsyncReader
{
private readonly StreamingZipHeaderFactory _headerFactory;

View File

@@ -216,9 +216,9 @@
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.2, )",
"resolved": "10.0.2",
"contentHash": "sXdDtMf2qcnbygw9OdE535c2lxSxrZP8gO4UhDJ0xiJbl1wIqXS1OTcTDFTIJPOFd6Mhcm8gPEthqWGUxBsTqw=="
"requested": "[10.0.0, )",
"resolved": "10.0.0",
"contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
@@ -264,9 +264,9 @@
"net8.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.23, )",
"resolved": "8.0.23",
"contentHash": "GqHiB1HbbODWPbY/lc5xLQH8siEEhNA0ptpJCC6X6adtAYNEzu5ZlqV3YHA3Gh7fuEwgA8XqVwMtH2KNtuQM1Q=="
"requested": "[8.0.22, )",
"resolved": "8.0.22",
"contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",

View File

@@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Readers;

View File

@@ -25,9 +25,15 @@ public class SevenZipBenchmarks : ArchiveBenchmarkBase
{
using var stream = new MemoryStream(_lzmaBytes);
using var archive = SevenZipArchive.OpenArchive(stream);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
using var reader = archive.ExtractAllEntries();
while (reader.MoveToNextEntry())
{
using var entryStream = entry.OpenEntryStream();
if (reader.Entry.IsDirectory)
{
continue;
}
using var entryStream = reader.OpenEntryStream();
entryStream.CopyTo(Stream.Null);
}
}
@@ -39,9 +45,15 @@ public class SevenZipBenchmarks : ArchiveBenchmarkBase
await using var archive = await SevenZipArchive
.OpenAsyncArchive(stream)
.ConfigureAwait(false);
await foreach (var entry in archive.EntriesAsync.Where(e => !e.IsDirectory))
await using var reader = await archive.ExtractAllEntriesAsync().ConfigureAwait(false);
while (await reader.MoveToNextEntryAsync().ConfigureAwait(false))
{
await using var entryStream = await entry.OpenEntryStreamAsync().ConfigureAwait(false);
if (reader.Entry.IsDirectory)
{
continue;
}
await using var entryStream = await reader.OpenEntryStreamAsync().ConfigureAwait(false);
await entryStream.CopyToAsync(Stream.Null).ConfigureAwait(false);
}
}
@@ -51,9 +63,15 @@ public class SevenZipBenchmarks : ArchiveBenchmarkBase
{
using var stream = new MemoryStream(_lzma2Bytes);
using var archive = SevenZipArchive.OpenArchive(stream);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
using var reader = archive.ExtractAllEntries();
while (reader.MoveToNextEntry())
{
using var entryStream = entry.OpenEntryStream();
if (reader.Entry.IsDirectory)
{
continue;
}
using var entryStream = reader.OpenEntryStream();
entryStream.CopyTo(Stream.Null);
}
}
@@ -65,9 +83,15 @@ public class SevenZipBenchmarks : ArchiveBenchmarkBase
await using var archive = await SevenZipArchive
.OpenAsyncArchive(stream)
.ConfigureAwait(false);
await foreach (var entry in archive.EntriesAsync.Where(e => !e.IsDirectory))
await using var reader = await archive.ExtractAllEntriesAsync().ConfigureAwait(false);
while (await reader.MoveToNextEntryAsync().ConfigureAwait(false))
{
await using var entryStream = await entry.OpenEntryStreamAsync().ConfigureAwait(false);
if (reader.Entry.IsDirectory)
{
continue;
}
await using var entryStream = await reader.OpenEntryStreamAsync().ConfigureAwait(false);
await entryStream.CopyToAsync(Stream.Null).ConfigureAwait(false);
}
}
@@ -78,9 +102,14 @@ public class SevenZipBenchmarks : ArchiveBenchmarkBase
using var stream = new MemoryStream(_lzma2Bytes);
using var archive = SevenZipArchive.OpenArchive(stream);
using var reader = archive.ExtractAllEntries();
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
while (reader.MoveToNextEntry())
{
using var entryStream = entry.OpenEntryStream();
if (reader.Entry.IsDirectory)
{
continue;
}
using var entryStream = reader.OpenEntryStream();
entryStream.CopyTo(Stream.Null);
}
}

View File

@@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SharpCompress.Archives;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Compressors;

View File

@@ -3,6 +3,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Compressors;

View File

@@ -60,10 +60,7 @@ public class ArchiveTests : ReaderTests
stream.ThrowOnDispose = false;
return;
}
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
entry.WriteToDirectory(SCRATCH_FILES_PATH);
}
WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH);
stream.ThrowOnDispose = false;
}
catch (Exception)
@@ -146,10 +143,7 @@ public class ArchiveTests : ReaderTests
{
try
{
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
entry.WriteToDirectory(SCRATCH_FILES_PATH);
}
WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH);
}
catch (IndexOutOfRangeException)
{
@@ -184,10 +178,7 @@ public class ArchiveTests : ReaderTests
)
)
{
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
entry.WriteToDirectory(SCRATCH_FILES_PATH);
}
WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH);
}
VerifyFiles();
}
@@ -213,10 +204,7 @@ public class ArchiveTests : ReaderTests
)
)
{
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
entry.WriteToDirectory(SCRATCH_FILES_PATH);
}
WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH);
}
VerifyFiles();
}
@@ -285,10 +273,7 @@ public class ArchiveTests : ReaderTests
ExtensionTest(testArchive, archiveFactory);
using (var archive = archiveFactory.OpenArchive(new FileInfo(testArchive), readerOptions))
{
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
entry.WriteToDirectory(SCRATCH_FILES_PATH);
}
WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH);
}
VerifyFiles();
}
@@ -329,10 +314,7 @@ public class ArchiveTests : ReaderTests
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
using (var archive = ArchiveFactory.OpenArchive(testArchive))
{
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
entry.WriteToDirectory(SCRATCH_FILES_PATH);
}
WriteArchiveEntriesToDirectory(archive, SCRATCH_FILES_PATH);
}
VerifyFilesEx();
}
@@ -341,6 +323,26 @@ public class ArchiveTests : ReaderTests
{
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
using var archive = ArchiveFactory.OpenArchive(testArchive);
var hasNonExtractableEntries = archive.Entries.Any(entry =>
!entry.IsDirectory && entry is not IExtractableArchiveEntry
);
if (hasNonExtractableEntries)
{
using var reader = archive.ExtractAllEntries();
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
using var memory = new MemoryStream();
reader.WriteEntryTo(memory);
VerifyDeltaDistanceStream(memory);
}
}
return;
}
foreach (var entry in archive.Entries)
{
if (!entry.IsDirectory)
@@ -348,17 +350,7 @@ public class ArchiveTests : ReaderTests
var memory = new MemoryStream();
entry.WriteTo(memory);
memory.Position = 0;
for (var y = 0; y < 9; y++)
{
for (var x = 0; x < 256; x++)
{
Assert.Equal(x, memory.ReadByte());
}
}
Assert.Equal(-1, memory.ReadByte());
VerifyDeltaDistanceStream(memory);
}
}
}
@@ -413,7 +405,9 @@ public class ArchiveTests : ReaderTests
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
using var entryStream = entry.OpenEntryStream();
using var entryStream = Assert
.IsAssignableFrom<IExtractableArchiveEntry>(entry)
.OpenEntryStream();
using var extractedStream = new MemoryStream();
entryStream.CopyTo(extractedStream);
var extractedData = extractedStream.ToArray();
@@ -581,7 +575,9 @@ public class ArchiveTests : ReaderTests
var entry = archive.Entries.FirstOrDefault(e => e.Key == entryName && !e.IsDirectory);
Assert.NotNull(entry);
using var entryStream = entry.OpenEntryStream();
using var entryStream = Assert
.IsAssignableFrom<IExtractableArchiveEntry>(entry)
.OpenEntryStream();
using var extractedStream = new MemoryStream();
entryStream.CopyTo(extractedStream);
@@ -622,12 +618,7 @@ public class ArchiveTests : ReaderTests
{
try
{
await foreach (
var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)
)
{
await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH);
}
await WriteArchiveEntriesToDirectoryAsync(archive, SCRATCH_FILES_PATH);
}
catch (IndexOutOfRangeException)
{
@@ -640,4 +631,67 @@ public class ArchiveTests : ReaderTests
VerifyFiles();
}
}
private static void VerifyDeltaDistanceStream(MemoryStream memory)
{
memory.Position = 0;
for (var y = 0; y < 9; y++)
{
for (var x = 0; x < 256; x++)
{
Assert.Equal(x, memory.ReadByte());
}
}
Assert.Equal(-1, memory.ReadByte());
}
private static void WriteArchiveEntriesToDirectory(
IArchive archive,
string destinationDirectory
)
{
if (
archive.Entries.Any(entry =>
!entry.IsDirectory && entry is not IExtractableArchiveEntry
)
)
{
archive.WriteToDirectory(destinationDirectory);
return;
}
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
entry.WriteToDirectory(destinationDirectory);
}
}
private static async Task WriteArchiveEntriesToDirectoryAsync(
IAsyncArchive archive,
string destinationDirectory
)
{
var hasNonExtractableEntries = false;
await foreach (var entry in archive.EntriesAsync)
{
if (!entry.IsDirectory && entry is not IExtractableArchiveEntry)
{
hasNonExtractableEntries = true;
break;
}
}
if (hasNonExtractableEntries)
{
await archive.WriteToDirectoryAsync(destinationDirectory).ConfigureAwait(false);
return;
}
await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory))
{
await entry.WriteToDirectoryAsync(destinationDirectory).ConfigureAwait(false);
}
}
}

View File

@@ -5,6 +5,7 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using AwesomeAssertions;
using SharpCompress.Archives;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Common.Options;

View File

@@ -5,7 +5,6 @@ using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Readers;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.SevenZip;
@@ -16,215 +15,35 @@ public class SevenZipArchiveAsyncTests : ArchiveTests
public async Task SevenZipArchive_LZMA_AsyncStreamExtraction()
{
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA.7z");
#if NETFRAMEWORK
using var stream = File.OpenRead(testArchive);
#else
await using var stream = File.OpenRead(testArchive);
#endif
await using var archive = await ArchiveFactory.OpenAsyncArchive(
new AsyncOnlyStream(stream)
);
await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory))
{
var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
var targetDir = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
#if NETFRAMEWORK
using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#else
await using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#endif
#if NETFRAMEWORK
using var targetStream = File.Create(targetPath);
#else
await using var targetStream = File.Create(targetPath);
#endif
#if NETFRAMEWORK
await sourceStream.CopyToAsync(targetStream, 81920, CancellationToken.None);
#else
await sourceStream.CopyToAsync(targetStream, CancellationToken.None);
#endif
}
VerifyFiles();
await ExtractAllEntriesSequentiallyAsync(testArchive);
}
//[Fact]
public async Task SevenZipArchive_LZMA2_AsyncStreamExtraction()
{
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA2.7z");
#if NETFRAMEWORK
using var stream = File.OpenRead(testArchive);
#else
await using var stream = File.OpenRead(testArchive);
#endif
await using var archive = await ArchiveFactory.OpenAsyncArchive(
new AsyncOnlyStream(stream)
);
await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory))
{
var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
var targetDir = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
#if NETFRAMEWORK
using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#else
await using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#endif
#if NETFRAMEWORK
using var targetStream = File.Create(targetPath);
#else
await using var targetStream = File.Create(targetPath);
#endif
#if NETFRAMEWORK
await sourceStream.CopyToAsync(targetStream, 81920, CancellationToken.None);
#else
await sourceStream.CopyToAsync(targetStream, CancellationToken.None);
#endif
}
VerifyFiles();
await ExtractAllEntriesSequentiallyAsync(testArchive);
}
[Fact]
public async Task SevenZipArchive_Solid_AsyncStreamExtraction()
{
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.7z");
#if NETFRAMEWORK
using var stream = File.OpenRead(testArchive);
#else
await using var stream = File.OpenRead(testArchive);
#endif
await using var archive = await ArchiveFactory.OpenAsyncArchive(
new AsyncOnlyStream(stream)
);
await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory))
{
var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
var targetDir = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
#if NETFRAMEWORK
using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#else
await using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#endif
#if NETFRAMEWORK
using var targetStream = File.Create(targetPath);
#else
await using var targetStream = File.Create(targetPath);
#endif
#if NETFRAMEWORK
await sourceStream.CopyToAsync(targetStream, 81920, CancellationToken.None);
#else
await sourceStream.CopyToAsync(targetStream, CancellationToken.None);
#endif
}
VerifyFiles();
await ExtractAllEntriesSequentiallyAsync(testArchive);
}
[Fact]
public async Task SevenZipArchive_BZip2_AsyncStreamExtraction()
{
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.BZip2.7z");
#if NETFRAMEWORK
using var stream = File.OpenRead(testArchive);
#else
await using var stream = File.OpenRead(testArchive);
#endif
await using var archive = await ArchiveFactory.OpenAsyncArchive(
new AsyncOnlyStream(stream)
);
await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory))
{
var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
var targetDir = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
#if NETFRAMEWORK
using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#else
await using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#endif
#if NETFRAMEWORK
using var targetStream = File.Create(targetPath);
#else
await using var targetStream = File.Create(targetPath);
#endif
#if NETFRAMEWORK
await sourceStream.CopyToAsync(targetStream, 81920, CancellationToken.None);
#else
await sourceStream.CopyToAsync(targetStream, CancellationToken.None);
#endif
}
VerifyFiles();
await ExtractAllEntriesSequentiallyAsync(testArchive);
}
[Fact]
public async Task SevenZipArchive_PPMd_AsyncStreamExtraction()
{
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.PPMd.7z");
#if NETFRAMEWORK
using var stream = File.OpenRead(testArchive);
#else
await using var stream = File.OpenRead(testArchive);
#endif
await using var archive = await ArchiveFactory.OpenAsyncArchive(
new AsyncOnlyStream(stream)
);
await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory))
{
var targetPath = Path.Combine(SCRATCH_FILES_PATH, entry.Key!);
var targetDir = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
#if NETFRAMEWORK
using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#else
await using var sourceStream = await entry.OpenEntryStreamAsync(CancellationToken.None);
#endif
#if NETFRAMEWORK
using var targetStream = File.Create(targetPath);
#else
await using var targetStream = File.Create(targetPath);
#endif
#if NETFRAMEWORK
await sourceStream.CopyToAsync(targetStream, 81920, CancellationToken.None);
#else
await sourceStream.CopyToAsync(targetStream, CancellationToken.None);
#endif
}
VerifyFiles();
await ExtractAllEntriesSequentiallyAsync(testArchive);
}
[Fact]
@@ -317,4 +136,51 @@ public class SevenZipArchiveAsyncTests : ArchiveTests
// The critical check: within a single folder, the stream should NEVER be recreated
Assert.Equal(0, streamRecreationsWithinFolder); // Folder stream should remain the same for all entries in the same folder
}
private async Task ExtractAllEntriesSequentiallyAsync(string testArchive)
{
#if NETFRAMEWORK
using var stream = File.OpenRead(testArchive);
#else
await using var stream = File.OpenRead(testArchive);
#endif
await using var archive = await ArchiveFactory.OpenAsyncArchive(stream);
await using var reader = await archive.ExtractAllEntriesAsync();
while (await reader.MoveToNextEntryAsync())
{
if (reader.Entry.IsDirectory)
{
continue;
}
var targetPath = Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key!);
var targetDir = Path.GetDirectoryName(targetPath);
if (!string.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
#if NETFRAMEWORK
using var sourceStream = await reader.OpenEntryStreamAsync(CancellationToken.None);
#else
await using var sourceStream = await reader.OpenEntryStreamAsync(
CancellationToken.None
);
#endif
#if NETFRAMEWORK
using var targetStream = File.Create(targetPath);
#else
await using var targetStream = File.Create(targetPath);
#endif
#if NETFRAMEWORK
await sourceStream.CopyToAsync(targetStream, 81920, CancellationToken.None);
#else
await sourceStream.CopyToAsync(targetStream, CancellationToken.None);
#endif
}
VerifyFiles();
}
}

View File

@@ -209,8 +209,11 @@ public class SevenZipArchiveTests : ArchiveTests
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "7Zip.Tar.tar.7z")))
using (var archive = SevenZipArchive.OpenArchive(stream))
{
using var reader = archive.ExtractAllEntries();
Assert.True(reader.MoveToNextEntry());
reader.WriteEntryToDirectory(SCRATCH_FILES_PATH);
var entry = archive.Entries.First();
entry.WriteToFile(Path.Combine(SCRATCH_FILES_PATH, entry.Key.NotNull()));
var size = entry.Size;
var scratch = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "7Zip.Tar.tar"));
@@ -366,12 +369,12 @@ public class SevenZipArchiveTests : ArchiveTests
{
emptyStreamFileCount++;
}
// This should not throw NullReferenceException
entry.WriteToDirectory(SCRATCH_FILES_PATH);
}
}
// Use archive-level extraction for SevenZip since entries don't support direct stream extraction
archive.WriteToDirectory(SCRATCH_FILES_PATH);
// Ensure we actually tested empty-stream entries
Assert.True(
emptyStreamFileCount > 0,

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Compressors.Deflate;

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Compressors.Deflate;