Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d4380b6
Initial plan
Copilot Nov 27, 2025
c082d42
Changes before error encountered
Copilot Nov 27, 2025
0d487df
Add IProgress support for compression operations with tests
Copilot Nov 27, 2025
0f374b2
Address code review: ProgressReportingStream now throws on writes
Copilot Nov 27, 2025
aa0356d
Changes before error encountered
Copilot Nov 29, 2025
8fc5ca5
Unify progress reporting: remove IExtractionListener and add IProgres…
Copilot Nov 29, 2025
7af029b
Address code review: properly handle zero-sized entries in progress r…
Copilot Nov 29, 2025
e2df789
Remove IArchiveExtractionListener and add IProgress support to Archiv…
Copilot Nov 30, 2025
0fdf9c7
Address code review: Replace dynamic with IArchiveProgressInfo interface
Copilot Nov 30, 2025
14d432e
Pass progress as parameter to WriteTo/WriteToAsync instead of storing…
Copilot Nov 30, 2025
85f3b17
Merge remote-tracking branch 'origin/master' into copilot/add-progres…
Copilot Dec 5, 2025
9291f58
Merge master and add comprehensive tests for archive and reader progress
Copilot Dec 5, 2025
3396f8f
Refactor to use ProgressReportingStream for progress tracking
Copilot Dec 5, 2025
c2f6055
format
adamhathcock Dec 8, 2025
c294071
Update src/SharpCompress/Archives/IArchiveEntryExtensions.cs
adamhathcock Dec 8, 2025
fd453e9
Update src/SharpCompress/IO/ProgressReportingStream.cs
adamhathcock Dec 8, 2025
1eaf3e6
format with csharpier
adamhathcock Dec 8, 2025
618b4bb
try to tell agents to format
adamhathcock Dec 8, 2025
40e72ad
fix AI edit
adamhathcock Dec 8, 2025
aaa97e2
Merge master branch - add ZStandard compression support and TarHeader…
Copilot Dec 19, 2025
6d3e4e8
Merge remote-tracking branch 'origin/master' into copilot/add-progres…
adamhathcock Dec 19, 2025
ca743ea
fix for running net 10 tests
adamhathcock Dec 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ SharpCompress is a pure C# compression library supporting multiple archive forma

## Code Formatting

**Copilot agents: You MUST run the `format` task after making code changes to ensure consistency.**

- Use CSharpier for code formatting to ensure consistent style across the project
- CSharpier is configured as a local tool in `.config/dotnet-tools.json`
- **To format code, run the task: `format` task (which runs `dotnet csharpier .` from project root)**
- Restore tools with: `dotnet tool restore`
- Format files from the project root with: `dotnet csharpier .`
- **Run `dotnet csharpier .` from the project root after making code changes before committing**
- Configure your IDE to format on save using CSharpier for the best experience
- The project also uses `.editorconfig` for editor settings (indentation, encoding, etc.)
- Let CSharpier handle code style while `.editorconfig` handles editor behavior
Expand Down
11 changes: 10 additions & 1 deletion build/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
const string Build = "build";
const string Test = "test";
const string Format = "format";
const string CheckFormat = "check-format";
const string Publish = "publish";

Target(
Expand Down Expand Up @@ -42,12 +43,20 @@ void RemoveDirectory(string d)
Target(
Format,
() =>
{
Run("dotnet", "tool restore");
Run("dotnet", "csharpier format .");
}
);
Target(
CheckFormat,
() =>
{
Run("dotnet", "tool restore");
Run("dotnet", "csharpier check .");
}
);
Target(Restore, [Format], () => Run("dotnet", "restore"));
Target(Restore, [CheckFormat], () => Run("dotnet", "restore"));

Target(
Build,
Expand Down
45 changes: 4 additions & 41 deletions src/SharpCompress/Archives/AbstractArchive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace SharpCompress.Archives;

public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtractionListener
public abstract class AbstractArchive<TEntry, TVolume> : IArchive
where TEntry : IArchiveEntry
where TVolume : IVolume
{
Expand All @@ -17,11 +17,6 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
private bool _disposed;
private readonly SourceStream? _sourceStream;

public event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>>? EntryExtractionBegin;
public event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>>? EntryExtractionEnd;

public event EventHandler<CompressedBytesReadEventArgs>? CompressedBytesRead;
public event EventHandler<FilePartExtractionBeginEventArgs>? FilePartExtractionBegin;
protected ReaderOptions ReaderOptions { get; }

internal AbstractArchive(ArchiveType type, SourceStream sourceStream)
Expand All @@ -43,12 +38,6 @@ internal AbstractArchive(ArchiveType type)

public ArchiveType Type { get; }

void IArchiveExtractionListener.FireEntryExtractionBegin(IArchiveEntry entry) =>
EntryExtractionBegin?.Invoke(this, new ArchiveExtractionEventArgs<IArchiveEntry>(entry));

void IArchiveExtractionListener.FireEntryExtractionEnd(IArchiveEntry entry) =>
EntryExtractionEnd?.Invoke(this, new ArchiveExtractionEventArgs<IArchiveEntry>(entry));

private static Stream CheckStreams(Stream stream)
{
if (!stream.CanSeek || !stream.CanRead)
Expand Down Expand Up @@ -99,38 +88,12 @@ public virtual void Dispose()
}
}

void IArchiveExtractionListener.EnsureEntriesLoaded()
private void EnsureEntriesLoaded()
{
_lazyEntries.EnsureFullyLoaded();
_lazyVolumes.EnsureFullyLoaded();
}

void IExtractionListener.FireCompressedBytesRead(
long currentPartCompressedBytes,
long compressedReadBytes
) =>
CompressedBytesRead?.Invoke(
this,
new CompressedBytesReadEventArgs(
currentFilePartCompressedBytesRead: currentPartCompressedBytes,
compressedBytesRead: compressedReadBytes
)
);

void IExtractionListener.FireFilePartExtractionBegin(
string name,
long size,
long compressedSize
) =>
FilePartExtractionBegin?.Invoke(
this,
new FilePartExtractionBeginEventArgs(
compressedSize: compressedSize,
size: size,
name: name
)
);

/// <summary>
/// Use this method to extract all entries in an archive in order.
/// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be
Expand All @@ -150,7 +113,7 @@ public IReader ExtractAllEntries()
"ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)."
);
}
((IArchiveExtractionListener)this).EnsureEntriesLoaded();
EnsureEntriesLoaded();
return CreateReaderForSolidExtraction();
}

Expand All @@ -173,7 +136,7 @@ public bool IsComplete
{
get
{
((IArchiveExtractionListener)this).EnsureEntriesLoaded();
EnsureEntriesLoaded();
return Entries.All(x => x.IsComplete);
}
}
Expand Down
6 changes: 0 additions & 6 deletions src/SharpCompress/Archives/IArchive.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ namespace SharpCompress.Archives;

public interface IArchive : IDisposable
{
event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>> EntryExtractionBegin;
event EventHandler<ArchiveExtractionEventArgs<IArchiveEntry>> EntryExtractionEnd;

event EventHandler<CompressedBytesReadEventArgs> CompressedBytesRead;
event EventHandler<FilePartExtractionBeginEventArgs> FilePartExtractionBegin;

IEnumerable<IArchiveEntry> Entries { get; }
IEnumerable<IVolume> Volumes { get; }

Expand Down
96 changes: 66 additions & 30 deletions src/SharpCompress/Archives/IArchiveEntryExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -8,56 +9,89 @@ namespace SharpCompress.Archives;

public static class IArchiveEntryExtensions
{
public static void WriteTo(this IArchiveEntry archiveEntry, Stream streamToWriteTo)
private const int BufferSize = 81920;

/// <summary>
/// Extract entry to the specified stream.
/// </summary>
/// <param name="archiveEntry">The archive entry to extract.</param>
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
public static void WriteTo(
this IArchiveEntry archiveEntry,
Stream streamToWriteTo,
IProgress<ProgressReport>? progress = null
)
{
if (archiveEntry.IsDirectory)
{
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
}

var streamListener = (IArchiveExtractionListener)archiveEntry.Archive;
streamListener.EnsureEntriesLoaded();
streamListener.FireEntryExtractionBegin(archiveEntry);
streamListener.FireFilePartExtractionBegin(
archiveEntry.Key ?? "Key",
archiveEntry.Size,
archiveEntry.CompressedSize
);
var entryStream = archiveEntry.OpenEntryStream();
using (entryStream)
{
using Stream s = new ListeningStream(streamListener, entryStream);
s.CopyTo(streamToWriteTo);
}
streamListener.FireEntryExtractionEnd(archiveEntry);
using var entryStream = archiveEntry.OpenEntryStream();
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
sourceStream.CopyTo(streamToWriteTo, BufferSize);
}

/// <summary>
/// Extract entry to the specified stream asynchronously.
/// </summary>
/// <param name="archiveEntry">The archive entry to extract.</param>
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
public static async Task WriteToAsync(
this IArchiveEntry archiveEntry,
Stream streamToWriteTo,
CancellationToken cancellationToken = default
CancellationToken cancellationToken = default,
IProgress<ProgressReport>? progress = null
)
{
if (archiveEntry.IsDirectory)
{
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
}

var streamListener = (IArchiveExtractionListener)archiveEntry.Archive;
streamListener.EnsureEntriesLoaded();
streamListener.FireEntryExtractionBegin(archiveEntry);
streamListener.FireFilePartExtractionBegin(
archiveEntry.Key ?? "Key",
archiveEntry.Size,
archiveEntry.CompressedSize
using var entryStream = archiveEntry.OpenEntryStream();
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
await sourceStream
.CopyToAsync(streamToWriteTo, BufferSize, cancellationToken)
.ConfigureAwait(false);
}

private static Stream WrapWithProgress(
Stream source,
IArchiveEntry entry,
IProgress<ProgressReport>? progress
)
{
if (progress is null)
{
return source;
}

var entryPath = entry.Key ?? string.Empty;
long? totalBytes = GetEntrySizeSafe(entry);
return new ProgressReportingStream(
source,
progress,
entryPath,
totalBytes,
leaveOpen: true
);
var entryStream = archiveEntry.OpenEntryStream();
using (entryStream)
}

private static long? GetEntrySizeSafe(IArchiveEntry entry)
{
try
{
var size = entry.Size;
return size >= 0 ? size : null;
}
catch (NotImplementedException)
{
using Stream s = new ListeningStream(streamListener, entryStream);
await s.CopyToAsync(streamToWriteTo, 81920, cancellationToken).ConfigureAwait(false);
return null;
}
streamListener.FireEntryExtractionEnd(archiveEntry);
}

/// <summary>
Expand Down Expand Up @@ -127,7 +161,9 @@ public static Task WriteToFileAsync(
async (x, fm) =>
{
using var fs = File.Open(destinationFileName, fm);
await entry.WriteToAsync(fs, cancellationToken).ConfigureAwait(false);
await entry
.WriteToAsync(fs, progress: null, cancellationToken: cancellationToken)
.ConfigureAwait(false);
},
cancellationToken
);
Expand Down
10 changes: 0 additions & 10 deletions src/SharpCompress/Archives/IArchiveExtractionListener.cs

This file was deleted.

8 changes: 4 additions & 4 deletions src/SharpCompress/Archives/Rar/RarArchiveEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ public Stream OpenEntryStream()
stream = new RarStream(
archive.UnpackV1.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
);
}
else
{
stream = new RarStream(
archive.UnpackV2017.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
);
}

Expand All @@ -100,15 +100,15 @@ public async Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationTok
stream = new RarStream(
archive.UnpackV1.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
);
}
else
{
stream = new RarStream(
archive.UnpackV2017.Value,
FileHeader,
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>(), archive)
new MultiVolumeReadOnlyStream(Parts.Cast<RarFilePart>())
);
}

Expand Down
10 changes: 0 additions & 10 deletions src/SharpCompress/Common/ArchiveExtractionEventArgs.cs

This file was deleted.

25 changes: 0 additions & 25 deletions src/SharpCompress/Common/CompressedBytesReadEventArgs.cs

This file was deleted.

28 changes: 0 additions & 28 deletions src/SharpCompress/Common/FilePartExtractionBeginEventArgs.cs

This file was deleted.

Loading