Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions src/SharpCompress/Archives/IArchiveExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ private void WriteToDirectoryInternal(
var bytesRead = 0L;
var seenDirectories = new HashSet<string>();

// When extracting an entire archive, default to extracting with full paths
options ??= new ExtractionOptions { ExtractFullPath = true, Overwrite = true };

Comment on lines +46 to +48

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new default options ??= ...ExtractFullPath=true... is only applied in WriteToDirectoryInternal, which is skipped for solid archives and SevenZip (those go through ExtractAllEntries() + reader.WriteAllToDirectory(...)). As a result, calling archive.WriteToDirectory(dest) without options can still flatten paths for solid/7z archives. Consider applying the same defaulting in the public WriteToDirectory(...) method (before the solid/7z branch), so behavior is consistent across archive types.

Copilot uses AI. Check for mistakes.
foreach (var entry in archive.Entries)
{
if (entry.IsDirectory)
Expand Down
3 changes: 3 additions & 0 deletions src/SharpCompress/Archives/IAsyncArchiveExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ CancellationToken cancellationToken
var bytesRead = 0L;
var seenDirectories = new HashSet<string>();

// When extracting an entire archive, default to extracting with full paths
options ??= new ExtractionOptions { ExtractFullPath = true, Overwrite = true };

Comment on lines +61 to +63

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the sync extension: the default ExtractFullPath=true is only applied in WriteToDirectoryAsyncInternal, but solid archives and SevenZip use ExtractAllEntriesAsync() + reader.WriteAllToDirectoryAsync(...) and still receive options=null. To fully fix path flattening when callers omit options, apply the same defaulting in WriteToDirectoryAsync(...) before choosing the solid/7z path.

Copilot uses AI. Check for mistakes.
await foreach (var entry in archive.EntriesAsync.WithCancellation(cancellationToken))
{
cancellationToken.ThrowIfCancellationRequested();
Expand Down
12 changes: 6 additions & 6 deletions src/SharpCompress/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Comment thread
adamhathcock marked this conversation as resolved.
"contentHash": "kICGrGYEzCNI3wPzfEXcwNHgTvlvVn9yJDhSdRK+oZQy4jvYH529u7O0xf5ocQKzOMjfS07+3z9PKRIjrFMJDA=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
Expand Down Expand Up @@ -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",
Comment thread
adamhathcock marked this conversation as resolved.
"contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
Expand Down
41 changes: 41 additions & 0 deletions tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,47 @@ await entry.WriteToDirectoryAsync(
VerifyFiles();
}

/// <summary>
/// Tests for Issue #1050 - RAR extraction with WriteToDirectoryAsync creates folders
/// but places all files at the top level instead of in their subdirectories.
/// </summary>
[Fact]
public async ValueTask Rar_Issue1050_WriteToDirectoryAsync_ExtractsToSubdirectories()
{
Comment on lines +721 to +727

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same mismatch as the sync test: PR metadata references #1190, but this new async test refers to “Issue #1050” in the XML doc and method name. Align the issue reference so the test is discoverable and correctly attributed.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The PR metadata references sharpcompress#1190, but this new test references Issue #1050. Consider clarifying in the test comment whether this is testing the same issue or a different one, as the mismatch may confuse future maintainers.

var testFile = "Rar.issue1050.rar";
using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Using File.OpenRead() instead of File.Open(..., FileMode.Open) avoids the unnecessary read/write permissions. The current implementation opens the file with read/write access which is unnecessary for reading an archive.

Suggested change
using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));
using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));

await using var archive = RarArchive.OpenAsyncArchive(fileStream);

// Extract using archive.WriteToDirectoryAsync without explicit options
await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH);

// Verify files are in their subdirectories, not at the root
Assert.True(
File.Exists(Path.Combine(SCRATCH_FILES_PATH, "PhysicsBraid", "263825.tr11dtp")),
"File should be in PhysicsBraid subdirectory"
);
Assert.True(
File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Animations", "15441.tr11anim")),
"File should be in Animations subdirectory"
);
Assert.True(
File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766728.tr11dtp")),
"File should be in Braid subdirectory"
);
Assert.True(
File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766832.tr11dtp")),
"File should be in Braid subdirectory"
);
Assert.True(
File.Exists(Path.Combine(SCRATCH_FILES_PATH, "HeadBraid", "321353.tr11modeldata")),
"File should be in HeadBraid subdirectory"
);

// NOTE: The file size check is omitted because there's a separate pre-existing bug
// in the async RAR stream implementation that causes incorrect file sizes.
// This test only verifies the directory structure fix.
}

private async ValueTask ArchiveOpenStreamReadAsync(
ReaderOptions? readerOptions,
params string[] testArchives
Expand Down
41 changes: 41 additions & 0 deletions tests/SharpCompress.Test/Rar/RarArchiveTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,47 @@ public void Rar_StreamValidation_ThrowsOnTruncatedStream()
Assert.Contains("unpacked file size does not match header", exception.Message);
}

/// <summary>
/// Tests for Issue #1050 - RAR extraction with WriteToDirectory creates folders
/// but places all files at the top level instead of in their subdirectories.
/// </summary>
[Fact]
public void Rar_Issue1050_WriteToDirectory_ExtractsToSubdirectories()
{
Comment on lines +725 to +731

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR metadata references sharpcompress#1190, but this new test’s XML doc + method name refer to “Issue #1050”. This makes it hard to trace why the test exists. Update the issue number (and potentially the test name/archive filename) to match the actual tracked issue for this change.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The PR metadata references sharpcompress#1190, but this new test references Issue #1050. Consider clarifying in the test comment whether this is testing the same issue or a different one, as the mismatch may confuse future maintainers.

var testFile = "Rar.issue1050.rar";
using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Using File.OpenRead() instead of File.Open(..., FileMode.Open) avoids the unnecessary read/write permissions. The current implementation opens the file with read/write access which is unnecessary for reading an archive.

Suggested change
using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));
using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile));

using var archive = RarArchive.OpenArchive(fileStream);

// Extract using archive.WriteToDirectory without explicit options
archive.WriteToDirectory(SCRATCH_FILES_PATH);

// Verify files are in their subdirectories, not at the root
Assert.True(
File.Exists(Path.Combine(SCRATCH_FILES_PATH, "PhysicsBraid", "263825.tr11dtp")),
"File should be in PhysicsBraid subdirectory"
);
Assert.True(
File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Animations", "15441.tr11anim")),
"File should be in Animations subdirectory"
);
Assert.True(
File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766728.tr11dtp")),
"File should be in Braid subdirectory"
);
Assert.True(
File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766832.tr11dtp")),
"File should be in Braid subdirectory"
);
Assert.True(
File.Exists(Path.Combine(SCRATCH_FILES_PATH, "HeadBraid", "321353.tr11modeldata")),
"File should be in HeadBraid subdirectory"
);

// Verify the exact file size of 766832.tr11dtp matches the archive entry size
var fileInfo = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766832.tr11dtp"));
Assert.Equal(4867620, fileInfo.Length); // Expected: 4,867,620 bytes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The comment says the extracted file size is being validated, but the assertion doesn't match the comment's stated purpose. The comment says it validates "exact file size...matches the archive entry size" but actually hardcodes 4867620. Consider also verifying against entry.Size to ensure the test validates what the comment describes.

}
Comment on lines +761 to +764

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the extracted file size is being validated against the archive entry size, but the test currently asserts a hard-coded byte count. This is brittle if the test archive is ever regenerated. Prefer reading the expected size from the corresponding archive entry (or otherwise deriving it from the test data) and asserting equality against that value.

Copilot uses AI. Check for mistakes.

/// <summary>
/// Test case for malformed RAR archives that previously caused infinite loops.
/// This test verifies that attempting to read entries from a potentially malformed
Expand Down
Binary file added tests/TestArchives/Archives/Rar.issue1050.rar
Binary file not shown.