mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-08 13:34:57 +00:00
Compare commits
19 Commits
adam/open-
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f59b14a278 | ||
|
|
3870cc8d34 | ||
|
|
242e442a8c | ||
|
|
d95d1e928b | ||
|
|
2a3086a0d7 | ||
|
|
41c3cc1a18 | ||
|
|
1b1df86a11 | ||
|
|
e0660e7775 | ||
|
|
99a6c4de88 | ||
|
|
ffa765bd97 | ||
|
|
b1696524b3 | ||
|
|
6a37c55085 | ||
|
|
9c1c6fff9f | ||
|
|
db8c6f4bcb | ||
|
|
ff17ecda7d | ||
|
|
692058677c | ||
|
|
1e90d69912 | ||
|
|
64a1cc68e1 | ||
|
|
20353f35ff |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -15,7 +15,6 @@ tests/TestArchives/*/Scratch
|
||||
tests/TestArchives/*/Scratch2
|
||||
.vs
|
||||
tools
|
||||
.vscode
|
||||
.idea/
|
||||
|
||||
.DS_Store
|
||||
|
||||
9
.vscode/extensions.json
vendored
Normal file
9
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-dotnettools.csharp",
|
||||
"ms-dotnettools.vscode-dotnet-runtime",
|
||||
"csharpier.csharpier-vscode",
|
||||
"formulahendry.dotnet-test-explorer"
|
||||
]
|
||||
}
|
||||
97
.vscode/launch.json
vendored
Normal file
97
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Tests (net10.0)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net10.0",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Debug Specific Test (net10.0)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net10.0",
|
||||
"--no-build",
|
||||
"--filter",
|
||||
"FullyQualifiedName~${input:testName}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Debug Performance Tests",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/tests/SharpCompress.Performance/SharpCompress.Performance.csproj",
|
||||
"--no-build"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
},
|
||||
{
|
||||
"name": "Debug Build Script",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"program": "dotnet",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/build/build.csproj",
|
||||
"--",
|
||||
"${input:buildTarget}"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
{
|
||||
"id": "testName",
|
||||
"type": "promptString",
|
||||
"description": "Enter test name or pattern (e.g., TestMethodName or ClassName)",
|
||||
"default": ""
|
||||
},
|
||||
{
|
||||
"id": "buildTarget",
|
||||
"type": "pickString",
|
||||
"description": "Select build target",
|
||||
"options": [
|
||||
"clean",
|
||||
"restore",
|
||||
"build",
|
||||
"test",
|
||||
"format",
|
||||
"publish",
|
||||
"default"
|
||||
],
|
||||
"default": "build"
|
||||
}
|
||||
]
|
||||
}
|
||||
29
.vscode/settings.json
vendored
Normal file
29
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"dotnet.defaultSolution": "SharpCompress.sln",
|
||||
"files.exclude": {
|
||||
"**/bin": true,
|
||||
"**/obj": true
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/bin/**": true,
|
||||
"**/obj/**": true,
|
||||
"**/artifacts/**": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"**/bin": true,
|
||||
"**/obj": true,
|
||||
"**/artifacts": true
|
||||
},
|
||||
"editor.formatOnSave": false,
|
||||
"[csharp]": {
|
||||
"editor.defaultFormatter": "csharpier.csharpier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
}
|
||||
},
|
||||
"csharpier.enableDebugLogs": false,
|
||||
"omnisharp.enableRoslynAnalyzers": true,
|
||||
"omnisharp.enableEditorConfigSupport": true,
|
||||
"dotnet-test-explorer.testProjectPath": "tests/**/*.csproj"
|
||||
}
|
||||
178
.vscode/tasks.json
vendored
Normal file
178
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/SharpCompress.sln",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "build-release",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/SharpCompress.sln",
|
||||
"-c",
|
||||
"Release",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "build-library",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/src/SharpCompress/SharpCompress.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "restore",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"restore",
|
||||
"${workspaceFolder}/SharpCompress.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "clean",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"clean",
|
||||
"${workspaceFolder}/SharpCompress.sln"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "test",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test-net10",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net10.0",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "test",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test-net48",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/SharpCompress.Test/SharpCompress.Test.csproj",
|
||||
"-f",
|
||||
"net48",
|
||||
"--no-build",
|
||||
"--verbosity=normal"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "test",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "format",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"csharpier",
|
||||
"."
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "format-check",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"csharpier",
|
||||
"check",
|
||||
"."
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "run-build-script",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/build/build.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "pack",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"pack",
|
||||
"${workspaceFolder}/src/SharpCompress/SharpCompress.csproj",
|
||||
"-c",
|
||||
"Release",
|
||||
"-o",
|
||||
"${workspaceFolder}/artifacts/"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"dependsOn": "build-release"
|
||||
},
|
||||
{
|
||||
"label": "performance-tests",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"run",
|
||||
"--project",
|
||||
"${workspaceFolder}/tests/SharpCompress.Performance/SharpCompress.Performance.csproj",
|
||||
"-c",
|
||||
"Release"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
39
AGENTS.md
39
AGENTS.md
@@ -49,6 +49,30 @@ SharpCompress is a pure C# compression library supporting multiple archive forma
|
||||
- Use `dotnet test` to run tests
|
||||
- Solution file: `SharpCompress.sln`
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
src/SharpCompress/
|
||||
├── Archives/ # IArchive implementations (Zip, Tar, Rar, 7Zip, GZip)
|
||||
├── Readers/ # IReader implementations (forward-only)
|
||||
├── Writers/ # IWriter implementations (forward-only)
|
||||
├── Compressors/ # Low-level compression streams (BZip2, Deflate, LZMA, etc.)
|
||||
├── Factories/ # Format detection and factory pattern
|
||||
├── Common/ # Shared types (ArchiveType, Entry, Options)
|
||||
├── Crypto/ # Encryption implementations
|
||||
└── IO/ # Stream utilities and wrappers
|
||||
|
||||
tests/SharpCompress.Test/
|
||||
├── Zip/, Tar/, Rar/, SevenZip/, GZip/, BZip2/ # Format-specific tests
|
||||
├── TestBase.cs # Base test class with helper methods
|
||||
└── TestArchives/ # Test data (not checked into main test project)
|
||||
```
|
||||
|
||||
### Factory Pattern
|
||||
All format types implement factory interfaces (`IArchiveFactory`, `IReaderFactory`, `IWriterFactory`) for auto-detection:
|
||||
- `ReaderFactory.Open()` - Auto-detects format by probing stream
|
||||
- `WriterFactory.Open()` - Creates writer for specified `ArchiveType`
|
||||
- Factories located in: `src/SharpCompress/Factories/`
|
||||
|
||||
## Nullable Reference Types
|
||||
|
||||
- Declare variables non-nullable, and check for `null` at entry points.
|
||||
@@ -116,3 +140,18 @@ SharpCompress supports multiple archive and compression formats:
|
||||
- Use test archives from `tests/TestArchives` directory for consistency.
|
||||
- Test stream disposal and `LeaveStreamOpen` behavior.
|
||||
- Test edge cases: empty archives, large files, corrupted archives, encrypted archives.
|
||||
|
||||
### Test Organization
|
||||
- Base class: `TestBase` - Provides `TEST_ARCHIVES_PATH`, `SCRATCH_FILES_PATH`, temp directory management
|
||||
- Framework: xUnit with AwesomeAssertions
|
||||
- Test archives: `tests/TestArchives/` - Use existing archives, don't create new ones unnecessarily
|
||||
- Match naming style of nearby test files
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Don't mix Archive and Reader APIs** - Archive needs seekable stream, Reader doesn't
|
||||
2. **Solid archives (Rar, 7Zip)** - Use `ExtractAllEntries()` for best performance, not individual entry extraction
|
||||
3. **Stream disposal** - Always set `LeaveStreamOpen` explicitly when needed (default is to close)
|
||||
4. **Tar + non-seekable stream** - Must provide file size or it will throw
|
||||
5. **Multi-framework differences** - Some features differ between .NET Framework and modern .NET (e.g., Mono.Posix)
|
||||
6. **Format detection** - Use `ReaderFactory.Open()` for auto-detection, test with actual archive files
|
||||
|
||||
@@ -161,6 +161,11 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveExtra
|
||||
/// </summary>
|
||||
public virtual bool IsSolid => false;
|
||||
|
||||
/// <summary>
|
||||
/// Archive is ENCRYPTED (this means the Archive has password-protected files).
|
||||
/// </summary>
|
||||
public virtual bool IsEncrypted => false;
|
||||
|
||||
/// <summary>
|
||||
/// The archive can find all the parts of the archive needed to fully extract the archive. This forces the parsing of the entire archive.
|
||||
/// </summary>
|
||||
|
||||
@@ -3,6 +3,10 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Tar.Headers;
|
||||
using SharpCompress.Compressors;
|
||||
using SharpCompress.Compressors.BZip2;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.Factories;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
@@ -131,10 +135,10 @@ public static class ArchiveFactory
|
||||
{
|
||||
finfo.NotNull(nameof(finfo));
|
||||
using Stream stream = finfo.OpenRead();
|
||||
return FindFactory<T>(stream);
|
||||
return FindFactory<T>(stream, finfo.Name);
|
||||
}
|
||||
|
||||
private static T FindFactory<T>(Stream stream)
|
||||
private static T FindFactory<T>(Stream stream, string? fileName = null)
|
||||
where T : IFactory
|
||||
{
|
||||
stream.NotNull(nameof(stream));
|
||||
@@ -159,6 +163,16 @@ public static class ArchiveFactory
|
||||
}
|
||||
}
|
||||
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
|
||||
// Check if this is a compressed tar file (tar.bz2, tar.lz, etc.)
|
||||
// These formats are supported by ReaderFactory but not by ArchiveFactory
|
||||
var compressedTarMessage = TryGetCompressedTarMessage(stream, fileName);
|
||||
if (compressedTarMessage != null)
|
||||
{
|
||||
throw new InvalidOperationException(compressedTarMessage);
|
||||
}
|
||||
|
||||
var extensions = string.Join(", ", factories.Select(item => item.Name));
|
||||
|
||||
throw new InvalidOperationException(
|
||||
@@ -248,4 +262,111 @@ public static class ArchiveFactory
|
||||
}
|
||||
|
||||
public static IArchiveFactory AutoFactory { get; } = new AutoArchiveFactory();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the stream is a compressed tar file (tar.bz2, tar.lz, etc.) that should use ReaderFactory instead.
|
||||
/// Returns an error message if detected, null otherwise.
|
||||
/// </summary>
|
||||
private static string? TryGetCompressedTarMessage(Stream stream, string? fileName)
|
||||
{
|
||||
var startPosition = stream.Position;
|
||||
try
|
||||
{
|
||||
// Check if it's a BZip2 file
|
||||
if (BZip2Stream.IsBZip2(stream))
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
|
||||
// Try to decompress and check if it contains a tar archive
|
||||
using var decompressed = new BZip2Stream(stream, CompressionMode.Decompress, true);
|
||||
if (IsTarStream(decompressed))
|
||||
{
|
||||
return "This appears to be a tar.bz2 archive. The Archive API requires seekable streams, but decompression streams are not seekable. "
|
||||
+ "Please use ReaderFactory.Open() instead for forward-only extraction, "
|
||||
+ "or decompress the file first and then open the resulting tar file with ArchiveFactory.Open().";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
|
||||
// Check if it's an LZip file
|
||||
if (LZipStream.IsLZipFile(stream))
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
|
||||
// Try to decompress and check if it contains a tar archive
|
||||
using var decompressed = new LZipStream(stream, CompressionMode.Decompress);
|
||||
if (IsTarStream(decompressed))
|
||||
{
|
||||
return "This appears to be a tar.lz archive. The Archive API requires seekable streams, but decompression streams are not seekable. "
|
||||
+ "Please use ReaderFactory.Open() instead for forward-only extraction, "
|
||||
+ "or decompress the file first and then open the resulting tar file with ArchiveFactory.Open().";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check file extension as a fallback for other compressed tar formats
|
||||
if (fileName != null)
|
||||
{
|
||||
var lowerFileName = fileName.ToLowerInvariant();
|
||||
if (
|
||||
lowerFileName.EndsWith(".tar.bz2")
|
||||
|| lowerFileName.EndsWith(".tbz")
|
||||
|| lowerFileName.EndsWith(".tbz2")
|
||||
|| lowerFileName.EndsWith(".tb2")
|
||||
|| lowerFileName.EndsWith(".tz2")
|
||||
|| lowerFileName.EndsWith(".tar.lz")
|
||||
|| lowerFileName.EndsWith(".tar.xz")
|
||||
|| lowerFileName.EndsWith(".txz")
|
||||
|| lowerFileName.EndsWith(".tar.zst")
|
||||
|| lowerFileName.EndsWith(".tar.zstd")
|
||||
|| lowerFileName.EndsWith(".tzst")
|
||||
|| lowerFileName.EndsWith(".tzstd")
|
||||
|| lowerFileName.EndsWith(".tar.z")
|
||||
|| lowerFileName.EndsWith(".tz")
|
||||
|| lowerFileName.EndsWith(".taz")
|
||||
)
|
||||
{
|
||||
return $"The file '{fileName}' appears to be a compressed tar archive. The Archive API requires seekable streams, but decompression streams are not seekable. "
|
||||
+ "Please use ReaderFactory.Open() instead for forward-only extraction, "
|
||||
+ "or decompress the file first and then open the resulting tar file with ArchiveFactory.Open().";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If we can't determine, just return null and let the normal error handling proceed
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore seek failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a stream contains a tar archive by trying to read a tar header.
|
||||
/// </summary>
|
||||
private static bool IsTarStream(Stream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tarHeader = new TarHeader(new ArchiveEncoding());
|
||||
return tarHeader.Read(new BinaryReader(stream));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
|
||||
|
||||
public override bool IsSolid => Volumes.First().IsSolidArchive;
|
||||
|
||||
public override bool IsEncrypted => Entries.First(x => !x.IsDirectory).IsEncrypted;
|
||||
|
||||
public virtual int MinVersion => Volumes.First().MinVersion;
|
||||
public virtual int MaxVersion => Volumes.First().MaxVersion;
|
||||
|
||||
|
||||
@@ -205,6 +205,8 @@ public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVol
|
||||
.GroupBy(x => x.FilePart.Folder)
|
||||
.Any(folder => folder.Count() > 1);
|
||||
|
||||
public override bool IsEncrypted => Entries.First(x => !x.IsDirectory).IsEncrypted;
|
||||
|
||||
public override long TotalSize =>
|
||||
_database?._packSizes.Aggregate(0L, (total, packSize) => total + packSize) ?? 0;
|
||||
|
||||
|
||||
@@ -57,19 +57,238 @@ public class TarFactory
|
||||
Stream stream,
|
||||
string? password = null,
|
||||
int bufferSize = ReaderOptions.DefaultBufferSize
|
||||
) => TarArchive.IsTarFile(stream);
|
||||
)
|
||||
{
|
||||
if (!stream.CanSeek)
|
||||
{
|
||||
return TarArchive.IsTarFile(stream); // For non-seekable streams, just check if it's a tar file
|
||||
}
|
||||
|
||||
var startPosition = stream.Position;
|
||||
|
||||
// First check if it's a regular tar file
|
||||
if (TarArchive.IsTarFile(stream))
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin); // Seek back for consistency
|
||||
return true;
|
||||
}
|
||||
|
||||
// Seek back after the tar file check
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
|
||||
if (compressionOptions == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Try each compression option to see if it contains a tar file
|
||||
foreach (var testOption in compressionOptions)
|
||||
{
|
||||
if (testOption.Type == CompressionType.None)
|
||||
{
|
||||
continue; // Skip uncompressed
|
||||
}
|
||||
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
|
||||
try
|
||||
{
|
||||
if (testOption.CanHandle(stream))
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
|
||||
// Try to decompress and check if it contains a tar archive
|
||||
// For compression formats that don't support leaveOpen, we need to save/restore position
|
||||
var positionBeforeDecompress = stream.Position;
|
||||
Stream? decompressedStream = null;
|
||||
bool streamWasClosed = false;
|
||||
|
||||
try
|
||||
{
|
||||
decompressedStream = testOption.Type switch
|
||||
{
|
||||
CompressionType.BZip2 => new BZip2Stream(stream, CompressionMode.Decompress, true),
|
||||
_ => testOption.CreateStream(stream) // For other types, may close the stream
|
||||
};
|
||||
|
||||
if (TarArchive.IsTarFile(decompressedStream))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
streamWasClosed = true;
|
||||
throw; // Stream was closed, can't continue
|
||||
}
|
||||
finally
|
||||
{
|
||||
decompressedStream?.Dispose();
|
||||
|
||||
if (!streamWasClosed && stream.CanSeek)
|
||||
{
|
||||
try
|
||||
{
|
||||
stream.Seek(positionBeforeDecompress, SeekOrigin.Begin);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If seek fails, the stream might have been closed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Seek back to start after decompression attempt
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If decompression fails, it's not this format - continue to next option
|
||||
try
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore seek failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore seek failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IArchiveFactory
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) =>
|
||||
TarArchive.Open(stream, readerOptions);
|
||||
public IArchive Open(Stream stream, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
readerOptions ??= new ReaderOptions();
|
||||
|
||||
// Try to detect and handle compressed tar formats
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
var startPosition = stream.Position;
|
||||
|
||||
// Try each compression option to see if we can decompress it
|
||||
foreach (var testOption in compressionOptions)
|
||||
{
|
||||
if (testOption.Type == CompressionType.None)
|
||||
{
|
||||
continue; // Skip uncompressed
|
||||
}
|
||||
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
|
||||
if (testOption.CanHandle(stream))
|
||||
{
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
|
||||
// Decompress the entire stream into a seekable MemoryStream
|
||||
using var decompressedStream = testOption.CreateStream(stream);
|
||||
var memoryStream = new MemoryStream();
|
||||
decompressedStream.CopyTo(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
// Verify it's actually a tar file
|
||||
if (TarArchive.IsTarFile(memoryStream))
|
||||
{
|
||||
memoryStream.Position = 0;
|
||||
// Return a TarArchive from the decompressed memory stream
|
||||
// The TarArchive will own the MemoryStream and dispose it when disposed
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
LeaveStreamOpen = false, // Ensure the MemoryStream is disposed with the archive
|
||||
ArchiveEncoding = readerOptions?.ArchiveEncoding ?? new ArchiveEncoding()
|
||||
};
|
||||
return TarArchive.Open(memoryStream, options);
|
||||
}
|
||||
|
||||
memoryStream.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
stream.Seek(startPosition, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
// Fall back to normal tar archive opening
|
||||
return TarArchive.Open(stream, readerOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>
|
||||
TarArchive.Open(fileInfo, readerOptions);
|
||||
public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
|
||||
{
|
||||
readerOptions ??= new ReaderOptions();
|
||||
|
||||
// Try to detect and handle compressed tar formats by file extension and content
|
||||
using var fileStream = fileInfo.OpenRead();
|
||||
|
||||
// Try each compression option
|
||||
foreach (var testOption in compressionOptions)
|
||||
{
|
||||
if (testOption.Type == CompressionType.None)
|
||||
{
|
||||
continue; // Skip uncompressed
|
||||
}
|
||||
|
||||
// Check if file extension matches
|
||||
var fileName = fileInfo.Name.ToLowerInvariant();
|
||||
if (testOption.KnownExtensions.Any(ext => fileName.EndsWith(ext)))
|
||||
{
|
||||
fileStream.Position = 0;
|
||||
|
||||
// Verify it's the right compression format
|
||||
if (testOption.CanHandle(fileStream))
|
||||
{
|
||||
fileStream.Position = 0;
|
||||
|
||||
// Decompress the entire file into a seekable MemoryStream
|
||||
using var decompressedStream = testOption.CreateStream(fileStream);
|
||||
var memoryStream = new MemoryStream();
|
||||
decompressedStream.CopyTo(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
// Verify it's actually a tar file
|
||||
if (TarArchive.IsTarFile(memoryStream))
|
||||
{
|
||||
memoryStream.Position = 0;
|
||||
// Return a TarArchive from the decompressed memory stream
|
||||
// The TarArchive will own the MemoryStream and dispose it when disposed
|
||||
var options = new ReaderOptions
|
||||
{
|
||||
LeaveStreamOpen = false, // Ensure the MemoryStream is disposed with the archive
|
||||
ArchiveEncoding = readerOptions?.ArchiveEncoding ?? new ArchiveEncoding()
|
||||
};
|
||||
return TarArchive.Open(memoryStream, options);
|
||||
}
|
||||
|
||||
memoryStream.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fileStream will be closed by the using statement
|
||||
|
||||
// Fall back to normal tar archive opening
|
||||
return TarArchive.Open(fileInfo, readerOptions);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
63
tests/SharpCompress.Test/ArchiveFactoryCompressedTarTests.cs
Normal file
63
tests/SharpCompress.Test/ArchiveFactoryCompressedTarTests.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Archives;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test;
|
||||
|
||||
public class ArchiveFactoryCompressedTarTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void ArchiveFactory_Open_TarBz2_ThrowsHelpfulException()
|
||||
{
|
||||
var testFile = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2");
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
using var archive = ArchiveFactory.Open(testFile);
|
||||
});
|
||||
|
||||
Assert.Contains("tar.bz2", exception.Message);
|
||||
Assert.Contains("ReaderFactory", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArchiveFactory_Open_TarLz_ThrowsHelpfulException()
|
||||
{
|
||||
var testFile = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.lz");
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
using var archive = ArchiveFactory.Open(testFile);
|
||||
});
|
||||
|
||||
Assert.Contains("tar.lz", exception.Message);
|
||||
Assert.Contains("ReaderFactory", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArchiveFactory_Open_TarBz2Stream_ThrowsHelpfulException()
|
||||
{
|
||||
var testFile = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2");
|
||||
using var stream = File.OpenRead(testFile);
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
using var archive = ArchiveFactory.Open(stream);
|
||||
});
|
||||
|
||||
Assert.Contains("tar.bz2", exception.Message);
|
||||
Assert.Contains("ReaderFactory", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArchiveFactory_Open_TarLzStream_ThrowsHelpfulException()
|
||||
{
|
||||
var testFile = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.lz");
|
||||
using var stream = File.OpenRead(testFile);
|
||||
var exception = Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
using var archive = ArchiveFactory.Open(stream);
|
||||
});
|
||||
|
||||
Assert.Contains("tar.lz", exception.Message);
|
||||
Assert.Contains("ReaderFactory", exception.Message);
|
||||
}
|
||||
}
|
||||
@@ -633,4 +633,13 @@ public class RarArchiveTests : ArchiveTests
|
||||
"Rar5.encrypted_filesOnly.rar",
|
||||
"Failure jpg exe Empty тест.txt jpg\\test.jpg exe\\test.exe"
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public void Rar_TestEncryptedDetection()
|
||||
{
|
||||
using var passwordProtectedFilesArchive = RarArchive.Open(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Rar.encrypted_filesOnly.rar")
|
||||
);
|
||||
Assert.True(passwordProtectedFilesArchive.IsEncrypted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,6 +224,15 @@ public class SevenZipArchiveTests : ArchiveTests
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SevenZipArchive_TestEncryptedDetection()
|
||||
{
|
||||
using var passwordProtectedFilesArchive = SevenZipArchive.Open(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "7Zip.encryptedFiles.7z")
|
||||
);
|
||||
Assert.True(passwordProtectedFilesArchive.IsEncrypted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SevenZipArchive_TestSolidDetection()
|
||||
{
|
||||
|
||||
BIN
tests/TestArchives/Archives/7Zip.encryptedFiles.7z
Normal file
BIN
tests/TestArchives/Archives/7Zip.encryptedFiles.7z
Normal file
Binary file not shown.
Reference in New Issue
Block a user