diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 00000000..90fc64cd
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,151 @@
+# GitHub Copilot Instructions for RecursiveExtractor
+
+## Project Overview
+
+RecursiveExtractor is a cross-platform .NET library and CLI tool for parsing archive files and disk images, including nested archives. It provides a unified interface to extract arbitrary archives using libraries like SharpCompress and DiscUtils.
+
+## Tech Stack
+
+- **Language**: C# 10.0
+- **Target Frameworks**: .NET Standard 2.0, .NET Standard 2.1, .NET 8.0, .NET 9.0, .NET 10.0
+- **Testing Framework**: xUnit (based on project structure)
+- **Key Dependencies**: SharpCompress, LTRData.DiscUtils, NLog, Glob
+
+## Building and Testing
+
+### Build Commands
+```bash
+# Build the entire solution
+dotnet build RecursiveExtractor.sln
+
+# Build a specific project
+dotnet build RecursiveExtractor/RecursiveExtractor.csproj
+```
+
+### Test Commands
+```bash
+# Run all tests
+dotnet test RecursiveExtractor.sln
+
+# Run tests for a specific project
+dotnet test RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj
+dotnet test RecursiveExtractor.Cli.Tests/RecursiveExtractor.Cli.Tests.csproj
+```
+
+### Restore Packages
+```bash
+dotnet restore RecursiveExtractor.sln
+```
+
+## NuGet Configuration
+
+⚠️ **Important**: The repository uses a private NuGet feed configured in `nuget.config`:
+- The `nuget.config` file points to a private Azure DevOps feed: `https://pkgs.dev.azure.com/microsoft-sdl/General/_packaging/PublicRegistriesFeed/nuget/v3/index.json`
+- **When working as an agent, you may need to temporarily modify `nuget.config` to use public NuGet feeds** (e.g., `https://api.nuget.org/v3/index.json`) to restore packages successfully
+- **ALWAYS restore the `nuget.config` to its original configuration before completing your work**
+- The original configuration must be preserved to maintain consistency with the team's workflow
+
+Example of temporarily switching to public feed:
+```xml
+
+
+
+
+
+
+
+```
+
+## Code Style Guidelines
+
+### Follow .editorconfig Settings
+- Use 4 spaces for indentation (no tabs)
+- CRLF line endings
+- Open braces on new lines
+- Use `var` for local variables when type is apparent
+- Follow PascalCase for types, methods, and properties
+- Interfaces should begin with 'I'
+- Do not use `this.` qualifier unless necessary
+
+### Naming Conventions
+- **Interfaces**: Start with 'I' (e.g., `ICustomAsyncExtractor`)
+- **Classes**: PascalCase (e.g., `FileEntry`, `Extractor`)
+- **Methods**: PascalCase (e.g., `Extract`, `ExtractAsync`)
+- **Properties**: PascalCase (e.g., `FullPath`, `Content`)
+- **Parameters**: camelCase (e.g., `fileEntry`, `options`)
+
+### C# Best Practices
+- Enable nullable reference types (project uses `Enable`)
+- Prefer pattern matching over `as` with null checks
+- Use expression-bodied members for simple properties and accessors
+- Prefer `null` propagation (`?.`) when appropriate
+- Use async/await for I/O operations
+- Implement both synchronous and asynchronous versions of extraction methods
+
+## Testing Practices
+
+### Test Organization
+- Unit tests go in `RecursiveExtractor.Tests` project
+- CLI tests go in `RecursiveExtractor.Cli.Tests` project
+- Use xUnit as the testing framework
+- Test files should mirror the structure of source files
+
+### Test Naming
+- Use descriptive test names that explain what is being tested
+- Follow pattern: `MethodName_StateUnderTest_ExpectedBehavior`
+
+### Test Data
+- Test archives and files should be placed in appropriate test data directories
+- Include edge cases: nested archives, encrypted files, malformed content, zip bombs
+
+## Security Considerations
+
+- The library includes protections against ZipSlip, Quines, and Zip Bombs
+- Always validate file paths to prevent directory traversal attacks
+- Handle malformed archives gracefully without crashes
+- Implement proper resource cleanup (dispose streams, file handles)
+
+## Documentation
+
+- Add XML documentation comments for public APIs
+- Keep README.md updated with new features or changes
+- Document breaking changes clearly
+- Include code examples for new public APIs
+
+## Project Structure
+
+```
+RecursiveExtractor/ # Main library project
+RecursiveExtractor.Tests/ # Unit tests for library
+RecursiveExtractor.Cli/ # Command-line interface project
+RecursiveExtractor.Cli.Tests/ # Tests for CLI
+```
+
+## Common Patterns
+
+### Extraction Pattern
+- Use `Extractor` class as the main entry point
+- Support both `Extract()` (sync) and `ExtractAsync()` (async) methods
+- Return `IEnumerable` or `IAsyncEnumerable`
+- Each `FileEntry` contains a Stream of content that should be disposed properly
+
+### Custom Extractors
+- Implement `ICustomAsyncExtractor` for new archive formats
+- Include `CanExtract()` method to detect file format via magic bytes
+- Preserve stream position in `CanExtract()`
+- Support both sync and async extraction
+
+### Error Handling
+- Throw `OverflowException` for detected quines or zip bombs
+- Throw `TimeoutException` when timing limits are exceeded
+- Log errors and skip invalid files during extraction
+- Use `ExtractSelfOnFail` option to return original archive on failure
+
+## Important Notes
+
+- Multi-targeting means code must be compatible with .NET Standard 2.0
+- Some features (like WIM support) are Windows-only
+- The library automatically detects archive types
+- Streams in FileEntry objects should be disposed by consumers
+- Avoid multiple enumeration of extraction results
+- For parallel processing, use batching mechanism as documented in README
diff --git a/Directory.Build.props b/Directory.Build.props
index 907effa3..e685da3b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -7,10 +7,5 @@
-
-
- true
- true
- Recommended
-
+
\ No newline at end of file
diff --git a/Pipelines/recursive-extractor-pr.yml b/Pipelines/recursive-extractor-pr.yml
index 3476419e..97a45149 100644
--- a/Pipelines/recursive-extractor-pr.yml
+++ b/Pipelines/recursive-extractor-pr.yml
@@ -42,12 +42,16 @@ extends:
poolName: MSSecurity-1ES-Build-Agents-Pool
poolImage: MSSecurity-1ES-Windows-2022
poolOs: windows
- dotnetTestArgs: '-- --coverage --report-trx'
+ dotnetTestArgs: '--settings coverlet.runsettings --collect:"XPlat Code Coverage"'
includeNuGetOrg: false
nugetFeedsToUse: 'config'
nugetConfigPath: 'nuget.config'
onInit:
- task: NuGetAuthenticate@1
+ postTest:
+ - task: PublishCodeCoverageResults@2
+ inputs:
+ summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
- template: dotnet-test-job.yml@templates
parameters:
@@ -57,12 +61,16 @@ extends:
poolName: MSSecurity-1ES-Build-Agents-Pool
poolImage: MSSecurity-1ES-Windows-2022
poolOs: windows
- dotnetTestArgs: '-- --coverage --report-trx'
+ dotnetTestArgs: '--settings coverlet.runsettings --collect:"XPlat Code Coverage"'
includeNuGetOrg: false
nugetFeedsToUse: 'config'
nugetConfigPath: 'nuget.config'
onInit:
- task: NuGetAuthenticate@1
+ postTest:
+ - task: PublishCodeCoverageResults@2
+ inputs:
+ summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
- stage: Build
dependsOn:
diff --git a/Pipelines/recursive-extractor-release.yml b/Pipelines/recursive-extractor-release.yml
index b2a85fc8..6cdd928a 100644
--- a/Pipelines/recursive-extractor-release.yml
+++ b/Pipelines/recursive-extractor-release.yml
@@ -43,12 +43,16 @@ extends:
poolName: MSSecurity-1ES-Build-Agents-Pool
poolImage: MSSecurity-1ES-Windows-2022
poolOs: windows
- dotnetTestArgs: '-- --coverage --report-trx'
+ dotnetTestArgs: '--settings coverlet.runsettings --collect:"XPlat Code Coverage"'
includeNuGetOrg: false
nugetFeedsToUse: 'config'
nugetConfigPath: 'nuget.config'
onInit:
- task: NuGetAuthenticate@1
+ postTest:
+ - task: PublishCodeCoverageResults@2
+ inputs:
+ summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
- template: dotnet-test-job.yml@templates
parameters:
jobName: 'cli_dotnet_test_windows'
@@ -57,12 +61,16 @@ extends:
poolName: MSSecurity-1ES-Build-Agents-Pool
poolImage: MSSecurity-1ES-Windows-2022
poolOs: windows
- dotnetTestArgs: '-- --coverage --report-trx'
+ dotnetTestArgs: '--settings coverlet.runsettings --collect:"XPlat Code Coverage"'
includeNuGetOrg: false
nugetFeedsToUse: 'config'
nugetConfigPath: 'nuget.config'
onInit:
- task: NuGetAuthenticate@1
+ postTest:
+ - task: PublishCodeCoverageResults@2
+ inputs:
+ summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
- stage: Build
dependsOn:
diff --git a/RecursiveExtractor.Cli.Tests/CliTests/CliTests.cs b/RecursiveExtractor.Cli.Tests/CliTests/CliTests.cs
index d430bca3..a2d633ad 100644
--- a/RecursiveExtractor.Cli.Tests/CliTests/CliTests.cs
+++ b/RecursiveExtractor.Cli.Tests/CliTests/CliTests.cs
@@ -3,41 +3,40 @@
using Microsoft.CST.RecursiveExtractor;
using Microsoft.CST.RecursiveExtractor.Cli;
using Microsoft.CST.RecursiveExtractor.Tests;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
using RecursiveExtractor.Tests.ExtractorTests;
using System;
using System.IO;
using System.Linq;
using System.Threading;
+using Xunit;
namespace RecursiveExtractor.Tests.CliTests
{
- [TestClass]
- public class CliTests : BaseExtractorTestClass
+ public class CliTests : IClassFixture
{
- [DataTestMethod]
- [DataRow("TestData.zip", 5)]
- [DataRow("TestData.7z")]
- [DataRow("TestData.tar", 6)]
- [DataRow("TestData.rar")]
- [DataRow("TestData.rar4")]
- [DataRow("TestData.tar.bz2", 6)]
- [DataRow("TestData.tar.gz", 6)]
- [DataRow("TestData.tar.xz")]
- [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8)]
- [DataRow("TestData.a")]
- [DataRow("TestData.bsd.ar")]
- [DataRow("TestData.iso")]
- [DataRow("TestData.vhdx")]
- [DataRow("TestData.wim")]
- [DataRow("EmptyFile.txt", 1)]
- [DataRow("TestDataArchivesNested.Zip", 54)]
+ [Theory]
+ [InlineData("TestData.zip", 5)]
+ [InlineData("TestData.7z")]
+ [InlineData("TestData.tar", 6)]
+ [InlineData("TestData.rar")]
+ [InlineData("TestData.rar4")]
+ [InlineData("TestData.tar.bz2", 6)]
+ [InlineData("TestData.tar.gz", 6)]
+ [InlineData("TestData.tar.xz")]
+ [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 8)]
+ [InlineData("TestData.a")]
+ [InlineData("TestData.bsd.ar")]
+ [InlineData("TestData.iso")]
+ [InlineData("TestData.vhdx")]
+ [InlineData("TestData.wim")]
+ [InlineData("EmptyFile.txt", 1)]
+ [InlineData("TestDataArchivesNested.Zip", 54)]
public void ExtractArchiveParallel(string fileName, int expectedNumFiles = 3)
{
- ExtractArchive(fileName, expectedNumFiles, false);
+ CliTests.ExtractArchive(fileName, expectedNumFiles, false);
}
- internal void ExtractArchive(string fileName, int expectedNumFiles, bool singleThread)
+ internal static void ExtractArchive(string fileName, int expectedNumFiles, bool singleThread)
{
var directory = TestPathHelpers.GetFreshTestDirectory();
var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName);
@@ -46,35 +45,35 @@ internal void ExtractArchive(string fileName, int expectedNumFiles, bool singleT
Thread.Sleep(100);
if (Directory.Exists(directory))
{
- files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToArray();
+ files = [.. Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories)];
}
- Assert.AreEqual(expectedNumFiles, files.Length);
+ Assert.Equal(expectedNumFiles, files.Length);
}
- [DataTestMethod]
- [DataRow("TestData.zip", 5)]
- [DataRow("TestData.7z")]
- [DataRow("TestData.tar", 6)]
- [DataRow("TestData.rar")]
- [DataRow("TestData.rar4")]
- [DataRow("TestData.tar.bz2", 6)]
- [DataRow("TestData.tar.gz", 6)]
- [DataRow("TestData.tar.xz")]
- [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8)]
- [DataRow("TestData.a")]
- [DataRow("TestData.bsd.ar")]
- [DataRow("TestData.iso")]
- [DataRow("TestData.vhdx")]
- [DataRow("TestData.wim")]
- [DataRow("EmptyFile.txt", 1)]
- [DataRow("TestDataArchivesNested.Zip", 54)]
+ [Theory]
+ [InlineData("TestData.zip", 5)]
+ [InlineData("TestData.7z")]
+ [InlineData("TestData.tar", 6)]
+ [InlineData("TestData.rar")]
+ [InlineData("TestData.rar4")]
+ [InlineData("TestData.tar.bz2", 6)]
+ [InlineData("TestData.tar.gz", 6)]
+ [InlineData("TestData.tar.xz")]
+ [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 8)]
+ [InlineData("TestData.a")]
+ [InlineData("TestData.bsd.ar")]
+ [InlineData("TestData.iso")]
+ [InlineData("TestData.vhdx")]
+ [InlineData("TestData.wim")]
+ [InlineData("EmptyFile.txt", 1)]
+ [InlineData("TestDataArchivesNested.Zip", 54)]
public void ExtractArchiveSingleThread(string fileName, int expectedNumFiles = 3)
{
- ExtractArchive(fileName, expectedNumFiles, true);
+ CliTests.ExtractArchive(fileName, expectedNumFiles, true);
}
- [DataTestMethod]
- [DataRow("TestDataForFilters.7z")]
+ [Theory]
+ [InlineData("TestDataForFilters.7z")]
public void ExtractArchiveWithAllowFilters(string fileName, int expectedNumFiles = 1)
{
var directory = TestPathHelpers.GetFreshTestDirectory();
@@ -86,21 +85,21 @@ public void ExtractArchiveWithAllowFilters(string fileName, int expectedNumFiles
Input = newpath,
Output = directory,
Verbose = true,
- AllowFilters = new string[]
- {
+ AllowFilters =
+ [
"*.cs"
- }
+ ]
});
var files = Array.Empty();
if (Directory.Exists(directory))
{
- files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToArray();
+ files = [.. Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories)];
}
- Assert.AreEqual(expectedNumFiles, files.Length);
+ Assert.Equal(expectedNumFiles, files.Length);
}
- [DataTestMethod]
- [DataRow("TestDataForFilters.7z")]
+ [Theory]
+ [InlineData("TestDataForFilters.7z")]
public void ExtractArchiveWithDenyFilters(string fileName, int expectedNumFiles = 2)
{
var directory = TestPathHelpers.GetFreshTestDirectory();
@@ -112,33 +111,31 @@ public void ExtractArchiveWithDenyFilters(string fileName, int expectedNumFiles
Input = newpath,
Output = directory,
Verbose = true,
- DenyFilters = new string[]
- {
+ DenyFilters =
+ [
"*.cs"
- }
+ ]
});
var files = Array.Empty();
if (Directory.Exists(directory))
{
- files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToArray();
+ files = [.. Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories)];
}
- Assert.AreEqual(expectedNumFiles, files.Length);
+ Assert.Equal(expectedNumFiles, files.Length);
}
- [DataTestMethod]
- [DataRow("TestDataEncrypted.7z")]
- [DataRow("TestDataEncryptedAes.zip")]
- [DataRow("TestDataEncrypted.rar4")]
+ [Theory]
+ [InlineData("TestDataEncrypted.7z")]
+ [InlineData("TestDataEncryptedAes.zip")]
+ [InlineData("TestDataEncrypted.rar4")]
public void ExtractEncryptedArchive(string fileName, int expectedNumFiles = 3)
{
var directory = TestPathHelpers.GetFreshTestDirectory();
var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName);
var passwords = EncryptedArchiveTests.TestArchivePasswords.Values.SelectMany(x => x);
RecursiveExtractorClient.ExtractCommand(new ExtractCommandOptions() { Input = path, Output = directory, Verbose = true, Passwords = passwords });
- string[] files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToArray();
- Assert.AreEqual(expectedNumFiles, files.Length);
- }
-
- protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();
+ string[] files = [.. Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories)];
+ Assert.Equal(expectedNumFiles, files.Length);
+ }
}
}
\ No newline at end of file
diff --git a/RecursiveExtractor.Cli.Tests/RecursiveExtractor.Cli.Tests.csproj b/RecursiveExtractor.Cli.Tests/RecursiveExtractor.Cli.Tests.csproj
index f9792e34..41d07b76 100644
--- a/RecursiveExtractor.Cli.Tests/RecursiveExtractor.Cli.Tests.csproj
+++ b/RecursiveExtractor.Cli.Tests/RecursiveExtractor.Cli.Tests.csproj
@@ -6,13 +6,13 @@
enable
false
- Exe
-
-
-
+
+
+
+
diff --git a/RecursiveExtractor.Cli.Tests/Usings.cs b/RecursiveExtractor.Cli.Tests/Usings.cs
index bed1fa29..2af8a547 100644
--- a/RecursiveExtractor.Cli.Tests/Usings.cs
+++ b/RecursiveExtractor.Cli.Tests/Usings.cs
@@ -1,2 +1,2 @@
-global using Microsoft.VisualStudio.TestTools.UnitTesting;
+global using Xunit;
diff --git a/RecursiveExtractor.Tests/AssemblyInfo.cs b/RecursiveExtractor.Tests/AssemblyInfo.cs
new file mode 100644
index 00000000..b928bc0e
--- /dev/null
+++ b/RecursiveExtractor.Tests/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using Xunit;
+
+[assembly: CollectionBehavior(DisableTestParallelization = false)]
diff --git a/RecursiveExtractor.Tests/ExtractorTests/BaseExtractorTestClass.cs b/RecursiveExtractor.Tests/ExtractorTests/BaseExtractorTestClass.cs
index d223a1a1..3c6ad5e9 100644
--- a/RecursiveExtractor.Tests/ExtractorTests/BaseExtractorTestClass.cs
+++ b/RecursiveExtractor.Tests/ExtractorTests/BaseExtractorTestClass.cs
@@ -1,21 +1,19 @@
using Microsoft.CST.RecursiveExtractor.Tests;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
using NLog;
using NLog.Config;
using NLog.Targets;
+using System;
namespace RecursiveExtractor.Tests.ExtractorTests;
-public class BaseExtractorTestClass
+///
+/// XUnit test fixture class for extractor tests. Sets up logging and test directories. Tests should use this class as a fixture via IClassFixture<BaseExtractorTestClass> to get the benefits of the setup and teardown.
+///
+public class BaseExtractorTestClass : IDisposable
{
- [ClassCleanup]
- public static void ClassCleanup()
- {
- TestPathHelpers.DeleteTestDirectory();
- }
+ protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();
- [ClassInitialize]
- public static void ClassInitialize(TestContext context)
+ static BaseExtractorTestClass()
{
var config = new LoggingConfiguration();
var consoleTarget = new ConsoleTarget
@@ -27,5 +25,11 @@ public static void ClassInitialize(TestContext context)
LogManager.Configuration = config;
}
- protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger();
+
+ ///
+ public void Dispose()
+ {
+ TestPathHelpers.DeleteTestDirectory();
+ GC.SuppressFinalize(this);
+ }
}
\ No newline at end of file
diff --git a/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs b/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs
index 547ba87d..f107709f 100644
--- a/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs
+++ b/RecursiveExtractor.Tests/ExtractorTests/CustomExtractorTests.cs
@@ -1,15 +1,14 @@
using Microsoft.CST.RecursiveExtractor;
using Microsoft.CST.RecursiveExtractor.Extractors;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Xunit;
namespace RecursiveExtractor.Tests.ExtractorTests;
-[TestClass]
public class CustomExtractorTests
{
///
@@ -121,43 +120,43 @@ public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, Extra
}
}
- [TestMethod]
+ [Fact]
public void Constructor_WithCustomExtractors_RegistersExtractors()
{
var customExtractor = new TestCustomExtractor(null!);
var extractor = new Extractor(new[] { customExtractor });
- Assert.AreEqual(1, extractor.CustomExtractors.Count);
+ Assert.Single(extractor.CustomExtractors);
}
- [TestMethod]
+ [Fact]
public void Constructor_WithMultipleCustomExtractors_RegistersAll()
{
var customExtractor1 = new TestCustomExtractor(null!);
var customExtractor2 = new SecondTestCustomExtractor(null!);
var extractor = new Extractor(new ICustomAsyncExtractor[] { customExtractor1, customExtractor2 });
- Assert.AreEqual(2, extractor.CustomExtractors.Count);
+ Assert.Equal(2, extractor.CustomExtractors.Count());
}
- [TestMethod]
+ [Fact]
public void Constructor_WithNullInCollection_IgnoresNull()
{
var customExtractor = new TestCustomExtractor(null!);
var extractor = new Extractor(new ICustomAsyncExtractor[] { customExtractor, null! });
- Assert.AreEqual(1, extractor.CustomExtractors.Count);
+ Assert.Single(extractor.CustomExtractors);
}
- [TestMethod]
+ [Fact]
public void Constructor_WithNullCollection_CreatesEmptyExtractor()
{
var extractor = new Extractor((IEnumerable)null!);
- Assert.AreEqual(0, extractor.CustomExtractors.Count);
+ Assert.Empty(extractor.CustomExtractors);
}
- [TestMethod]
+ [Fact]
public void Extract_WithMatchingCustomExtractor_UsesCustomExtractor()
{
var customExtractor = new TestCustomExtractor(null!);
@@ -167,17 +166,17 @@ public void Extract_WithMatchingCustomExtractor_UsesCustomExtractor()
var testData = System.Text.Encoding.ASCII.GetBytes("CUSTOM1 This is test data");
var results = extractor.Extract("test.custom", testData).ToList();
- Assert.AreEqual(1, results.Count);
- Assert.AreEqual("extracted_from_custom.txt", results[0].Name);
+ Assert.Single(results);
+ Assert.Equal("extracted_from_custom.txt", results[0].Name);
// Read the content to verify it was processed by our custom extractor
using var reader = new StreamReader(results[0].Content);
results[0].Content.Position = 0;
var content = reader.ReadToEnd();
- Assert.AreEqual("Extracted by TestCustomExtractor", content);
+ Assert.Equal("Extracted by TestCustomExtractor", content);
}
- [TestMethod]
+ [Fact]
public async Task ExtractAsync_WithMatchingCustomExtractor_UsesCustomExtractor()
{
var customExtractor = new TestCustomExtractor(null!);
@@ -187,17 +186,17 @@ public async Task ExtractAsync_WithMatchingCustomExtractor_UsesCustomExtractor()
var testData = System.Text.Encoding.ASCII.GetBytes("CUSTOM1 This is test data");
var results = await extractor.ExtractAsync("test.custom", testData).ToListAsync();
- Assert.AreEqual(1, results.Count);
- Assert.AreEqual("extracted_from_custom.txt", results[0].Name);
+ Assert.Single(results);
+ Assert.Equal("extracted_from_custom.txt", results[0].Name);
// Read the content to verify it was processed by our custom extractor
using var reader = new StreamReader(results[0].Content);
results[0].Content.Position = 0;
var content = reader.ReadToEnd();
- Assert.AreEqual("Extracted by TestCustomExtractor", content);
+ Assert.Equal("Extracted by TestCustomExtractor", content);
}
- [TestMethod]
+ [Fact]
public void Extract_WithoutMatchingCustomExtractor_ReturnsOriginalFile()
{
var customExtractor = new TestCustomExtractor(null!);
@@ -208,17 +207,17 @@ public void Extract_WithoutMatchingCustomExtractor_ReturnsOriginalFile()
var results = extractor.Extract("test.txt", testData).ToList();
// Should return the original file since no custom extractor matched
- Assert.AreEqual(1, results.Count);
- Assert.AreEqual("test.txt", results[0].Name);
+ Assert.Single(results);
+ Assert.Equal("test.txt", results[0].Name);
// Verify it's the original content
using var reader = new StreamReader(results[0].Content);
results[0].Content.Position = 0;
var content = reader.ReadToEnd();
- Assert.AreEqual("NOTCUSTOM This is test data", content);
+ Assert.Equal("NOTCUSTOM This is test data", content);
}
- [TestMethod]
+ [Fact]
public void Extract_MultipleCustomExtractors_UsesCorrectOne()
{
var extractor = new Extractor(new ICustomAsyncExtractor[]
@@ -230,17 +229,17 @@ public void Extract_MultipleCustomExtractors_UsesCorrectOne()
// Test with first custom format
var testData1 = System.Text.Encoding.ASCII.GetBytes("CUSTOM1 data");
var results1 = extractor.Extract("test1.custom", testData1).ToList();
- Assert.AreEqual(1, results1.Count);
- Assert.AreEqual("extracted_from_custom.txt", results1[0].Name);
+ Assert.Single(results1);
+ Assert.Equal("extracted_from_custom.txt", results1[0].Name);
// Test with second custom format
var testData2 = System.Text.Encoding.ASCII.GetBytes("CUSTOM2 data");
var results2 = extractor.Extract("test2.custom", testData2).ToList();
- Assert.AreEqual(1, results2.Count);
- Assert.AreEqual("extracted_from_second_custom.txt", results2[0].Name);
+ Assert.Single(results2);
+ Assert.Equal("extracted_from_second_custom.txt", results2[0].Name);
}
- [TestMethod]
+ [Fact]
public void Extract_NoCustomExtractors_ReturnsOriginalFile()
{
var extractor = new Extractor();
@@ -250,11 +249,11 @@ public void Extract_NoCustomExtractors_ReturnsOriginalFile()
var results = extractor.Extract("test.custom", testData).ToList();
// Should return the original file since no custom extractor is registered
- Assert.AreEqual(1, results.Count);
- Assert.AreEqual("test.custom", results[0].Name);
+ Assert.Single(results);
+ Assert.Equal("test.custom", results[0].Name);
}
- [TestMethod]
+ [Fact]
public void Extract_CustomExtractorForKnownFormat_UsesBuiltInExtractor()
{
var customExtractor = new TestCustomExtractor(null!);
@@ -267,8 +266,8 @@ public void Extract_CustomExtractorForKnownFormat_UsesBuiltInExtractor()
var results = extractor.Extract(path).ToList();
// Should extract the ZIP normally, not use the custom extractor
- Assert.IsTrue(results.Count > 0);
- Assert.IsTrue(results.Any(r => r.Name.Contains("EmptyFile")));
+ Assert.True(results.Count > 0);
+ Assert.Contains(results, r => r.Name.Contains("EmptyFile"));
}
}
}
diff --git a/RecursiveExtractor.Tests/ExtractorTests/DisposeBehaviorTests.cs b/RecursiveExtractor.Tests/ExtractorTests/DisposeBehaviorTests.cs
index bab30610..4d7a6277 100644
--- a/RecursiveExtractor.Tests/ExtractorTests/DisposeBehaviorTests.cs
+++ b/RecursiveExtractor.Tests/ExtractorTests/DisposeBehaviorTests.cs
@@ -1,50 +1,48 @@
using Microsoft.CST.RecursiveExtractor;
using Microsoft.CST.RecursiveExtractor.Tests;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
+using Xunit;
namespace RecursiveExtractor.Tests.ExtractorTests;
-[TestClass]
-public class DisposeBehaviorTests : BaseExtractorTestClass
+[Collection(ExtractorTestCollection.Name)]
+public class DisposeBehaviorTests
{
- [DataTestMethod]
- [DataRow("TestData.7z", 3, false)]
- [DataRow("TestData.tar", 6, false)]
- [DataRow("TestData.rar", 3, false)]
- [DataRow("TestData.rar4", 3, false)]
- [DataRow("TestData.tar.bz2", 6, false)]
- [DataRow("TestData.tar.gz", 6, false)]
- [DataRow("TestData.tar.xz", 3, false)]
- [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, false)]
- [DataRow("TestData.a", 3, false)]
- [DataRow("TestData.bsd.ar", 3, false)]
- [DataRow("TestData.iso", 3, false)]
- [DataRow("TestData.vhdx", 3, false)]
- [DataRow("TestData.wim", 3, false)]
- [DataRow("EmptyFile.txt", 1, false)]
- [DataRow("TestData.zip", 5, true)]
- [DataRow("TestData.7z", 3, true)]
- [DataRow("TestData.tar", 6, true)]
- [DataRow("TestData.rar", 3, true)]
- [DataRow("TestData.rar4", 3, true)]
- [DataRow("TestData.tar.bz2", 6, true)]
- [DataRow("TestData.tar.gz", 6, true)]
- [DataRow("TestData.tar.xz", 3, true)]
- [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, true)]
- [DataRow("TestData.a", 3, true)]
- [DataRow("TestData.bsd.ar", 3, true)]
- [DataRow("TestData.iso", 3, true)]
- [DataRow("TestData.vhdx", 3, true)]
- [DataRow("TestData.wim", 3, true)]
- [DataRow("EmptyFile.txt", 1, true)]
- [DataRow("TestDataArchivesNested.Zip", 54, true)]
- [DataRow("TestDataArchivesNested.Zip", 54, false)]
- [DataRow("TestDataArchivesNested.Zip", 54, true)]
- [DataRow("TestDataArchivesNested.Zip", 54, false)]
+ [Theory]
+ [InlineData("TestData.7z", 3, false)]
+ [InlineData("TestData.tar", 6, false)]
+ [InlineData("TestData.rar", 3, false)]
+ [InlineData("TestData.rar4", 3, false)]
+ [InlineData("TestData.tar.bz2", 6, false)]
+ [InlineData("TestData.tar.gz", 6, false)]
+ [InlineData("TestData.tar.xz", 3, false)]
+ [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 8, false)]
+ [InlineData("TestData.a", 3, false)]
+ [InlineData("TestData.bsd.ar", 3, false)]
+ [InlineData("TestData.iso", 3, false)]
+ [InlineData("TestData.vhdx", 3, false)]
+ [InlineData("TestData.wim", 3, false)]
+ [InlineData("EmptyFile.txt", 1, false)]
+ [InlineData("TestData.zip", 5, true)]
+ [InlineData("TestData.7z", 3, true)]
+ [InlineData("TestData.tar", 6, true)]
+ [InlineData("TestData.rar", 3, true)]
+ [InlineData("TestData.rar4", 3, true)]
+ [InlineData("TestData.tar.bz2", 6, true)]
+ [InlineData("TestData.tar.gz", 6, true)]
+ [InlineData("TestData.tar.xz", 3, true)]
+ [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 8, true)]
+ [InlineData("TestData.a", 3, true)]
+ [InlineData("TestData.bsd.ar", 3, true)]
+ [InlineData("TestData.iso", 3, true)]
+ [InlineData("TestData.vhdx", 3, true)]
+ [InlineData("TestData.wim", 3, true)]
+ [InlineData("EmptyFile.txt", 1, true)]
+ [InlineData("TestDataArchivesNested.Zip", 54, true)]
+ [InlineData("TestDataArchivesNested.Zip", 54, false)]
public void ExtractArchiveAndDisposeWhileEnumerating(string fileName, int expectedNumFiles = 3,
bool parallel = false)
{
@@ -60,47 +58,45 @@ public void ExtractArchiveAndDisposeWhileEnumerating(string fileName, int expect
_ = theStream.ReadByte();
}
- Assert.AreEqual(expectedNumFiles, disposedResults.Count);
+ Assert.Equal(expectedNumFiles, disposedResults.Count);
foreach (var disposedResult in disposedResults)
{
- Assert.ThrowsException(() => disposedResult.Content.Position);
+ Assert.Throws(() => disposedResult.Content.Position);
}
}
- [DataTestMethod]
- [DataRow("TestData.7z", 3, false)]
- [DataRow("TestData.tar", 6, false)]
- [DataRow("TestData.rar", 3, false)]
- [DataRow("TestData.rar4", 3, false)]
- [DataRow("TestData.tar.bz2", 6, false)]
- [DataRow("TestData.tar.gz", 6, false)]
- [DataRow("TestData.tar.xz", 3, false)]
- [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, false)]
- [DataRow("TestData.a", 3, false)]
- [DataRow("TestData.bsd.ar", 3, false)]
- [DataRow("TestData.iso", 3, false)]
- [DataRow("TestData.vhdx", 3, false)]
- [DataRow("TestData.wim", 3, false)]
- [DataRow("EmptyFile.txt", 1, false)]
- [DataRow("TestData.zip", 5, true)]
- [DataRow("TestData.7z", 3, true)]
- [DataRow("TestData.tar", 6, true)]
- [DataRow("TestData.rar", 3, true)]
- [DataRow("TestData.rar4", 3, true)]
- [DataRow("TestData.tar.bz2", 6, true)]
- [DataRow("TestData.tar.gz", 6, true)]
- [DataRow("TestData.tar.xz", 3, true)]
- [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, true)]
- [DataRow("TestData.a", 3, true)]
- [DataRow("TestData.bsd.ar", 3, true)]
- [DataRow("TestData.iso", 3, true)]
- [DataRow("TestData.vhdx", 3, true)]
- [DataRow("TestData.wim", 3, true)]
- [DataRow("EmptyFile.txt", 1, true)]
- [DataRow("TestDataArchivesNested.Zip", 54, true)]
- [DataRow("TestDataArchivesNested.Zip", 54, false)]
- [DataRow("TestDataArchivesNested.Zip", 54, true)]
- [DataRow("TestDataArchivesNested.Zip", 54, false)]
+ [Theory]
+ [InlineData("TestData.7z", 3, false)]
+ [InlineData("TestData.tar", 6, false)]
+ [InlineData("TestData.rar", 3, false)]
+ [InlineData("TestData.rar4", 3, false)]
+ [InlineData("TestData.tar.bz2", 6, false)]
+ [InlineData("TestData.tar.gz", 6, false)]
+ [InlineData("TestData.tar.xz", 3, false)]
+ [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 8, false)]
+ [InlineData("TestData.a", 3, false)]
+ [InlineData("TestData.bsd.ar", 3, false)]
+ [InlineData("TestData.iso", 3, false)]
+ [InlineData("TestData.vhdx", 3, false)]
+ [InlineData("TestData.wim", 3, false)]
+ [InlineData("EmptyFile.txt", 1, false)]
+ [InlineData("TestData.zip", 5, true)]
+ [InlineData("TestData.7z", 3, true)]
+ [InlineData("TestData.tar", 6, true)]
+ [InlineData("TestData.rar", 3, true)]
+ [InlineData("TestData.rar4", 3, true)]
+ [InlineData("TestData.tar.bz2", 6, true)]
+ [InlineData("TestData.tar.gz", 6, true)]
+ [InlineData("TestData.tar.xz", 3, true)]
+ [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 8, true)]
+ [InlineData("TestData.a", 3, true)]
+ [InlineData("TestData.bsd.ar", 3, true)]
+ [InlineData("TestData.iso", 3, true)]
+ [InlineData("TestData.vhdx", 3, true)]
+ [InlineData("TestData.wim", 3, true)]
+ [InlineData("EmptyFile.txt", 1, true)]
+ [InlineData("TestDataArchivesNested.Zip", 54, true)]
+ [InlineData("TestDataArchivesNested.Zip", 54, false)]
public async Task ExtractArchiveAndDisposeWhileEnumeratingAsync(string fileName, int expectedNumFiles = 3,
bool parallel = false)
{
@@ -116,15 +112,15 @@ public async Task ExtractArchiveAndDisposeWhileEnumeratingAsync(string fileName,
_ = theStream.ReadByte();
}
- Assert.AreEqual(expectedNumFiles, disposedResults.Count);
+ Assert.Equal(expectedNumFiles, disposedResults.Count);
foreach (var disposedResult in disposedResults)
{
- Assert.ThrowsException(() => disposedResult.Content.Position);
+ Assert.Throws(() => disposedResult.Content.Position);
}
}
- [DataTestMethod]
- [DataRow("TestData.zip")]
+ [Theory]
+ [InlineData("TestData.zip")]
public void EnsureDisposedWithExtractToDirectory(string fileName)
{
var directory = TestPathHelpers.GetFreshTestDirectory();
@@ -137,17 +133,26 @@ public void EnsureDisposedWithExtractToDirectory(string fileName)
var extractor = new Extractor();
extractor.ExtractToDirectory(directory, copyPath);
File.Delete(copyPath);
- if (Directory.Exists(directory))
+ try
{
- Directory.Delete(directory, true);
+ if (Directory.Exists(directory))
+ {
+ Directory.Delete(directory, true);
+ }
}
- if (Directory.Exists(copyDirectory)) {
- Directory.Delete(copyDirectory, true);
+ catch (DirectoryNotFoundException) { }
+ try
+ {
+ if (Directory.Exists(copyDirectory))
+ {
+ Directory.Delete(copyDirectory, true);
+ }
}
+ catch (DirectoryNotFoundException) { }
}
- [DataTestMethod]
- [DataRow("TestData.zip")]
+ [Theory]
+ [InlineData("TestData.zip")]
public async Task EnsureDisposedWithExtractToDirectoryAsync(string fileName)
{
var directory = TestPathHelpers.GetFreshTestDirectory();
@@ -160,13 +165,21 @@ public async Task EnsureDisposedWithExtractToDirectoryAsync(string fileName)
var extractor = new Extractor();
await extractor.ExtractToDirectoryAsync(directory, copyPath);
File.Delete(copyPath);
- if (Directory.Exists(directory))
+ try
{
- Directory.Delete(directory, true);
+ if (Directory.Exists(directory))
+ {
+ Directory.Delete(directory, true);
+ }
}
- if (Directory.Exists(copyDirectory))
+ catch (DirectoryNotFoundException) { }
+ try
{
- Directory.Delete(copyDirectory, true);
+ if (Directory.Exists(copyDirectory))
+ {
+ Directory.Delete(copyDirectory, true);
+ }
}
+ catch (DirectoryNotFoundException) { }
}
}
\ No newline at end of file
diff --git a/RecursiveExtractor.Tests/ExtractorTests/EncryptedArchiveTests.cs b/RecursiveExtractor.Tests/ExtractorTests/EncryptedArchiveTests.cs
index 9d034e73..50a5d003 100644
--- a/RecursiveExtractor.Tests/ExtractorTests/EncryptedArchiveTests.cs
+++ b/RecursiveExtractor.Tests/ExtractorTests/EncryptedArchiveTests.cs
@@ -1,37 +1,36 @@
using Microsoft.CST.RecursiveExtractor;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using Xunit;
namespace RecursiveExtractor.Tests.ExtractorTests;
-[TestClass]
-public class EncryptedArchiveTests : BaseExtractorTestClass
+public class EncryptedArchiveTests
{
- [DataTestMethod]
- [DataRow("TestDataEncryptedZipCrypto.zip")]
- [DataRow("TestDataEncryptedAes.zip")]
- [DataRow("TestDataEncrypted.7z")]
- [DataRow("TestDataEncrypted.rar4")]
- [DataRow("TestDataEncrypted.rar")]
+ [Theory]
+ [InlineData("TestDataEncryptedZipCrypto.zip")]
+ [InlineData("TestDataEncryptedAes.zip")]
+ [InlineData("TestDataEncrypted.7z")]
+ [InlineData("TestDataEncrypted.rar4")]
+ [InlineData("TestDataEncrypted.rar")]
public void FileTypeSetCorrectlyForEncryptedArchives(string fileName, int expectedNumFiles = 1)
{
var extractor = new Extractor();
var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName);
var results = extractor.Extract(path, new ExtractorOptions() { Parallel = false }).ToList();
- Assert.AreEqual(expectedNumFiles, results.Count());
- Assert.AreEqual(FileEntryStatus.EncryptedArchive, results.First().EntryStatus);
+ Assert.Equal(expectedNumFiles, results.Count());
+ Assert.Equal(FileEntryStatus.EncryptedArchive, results.First().EntryStatus);
}
- [DataTestMethod]
- [DataRow("TestDataEncryptedZipCrypto.zip")]
- [DataRow("TestDataEncryptedAes.zip")]
- [DataRow("TestDataEncrypted.7z")]
- [DataRow("TestDataEncrypted.rar4")]
- [DataRow("TestDataEncrypted.rar")]
+ [Theory]
+ [InlineData("TestDataEncryptedZipCrypto.zip")]
+ [InlineData("TestDataEncryptedAes.zip")]
+ [InlineData("TestDataEncrypted.7z")]
+ [InlineData("TestDataEncrypted.rar4")]
+ [InlineData("TestDataEncrypted.rar")]
public async Task FileTypeSetCorrectlyForEncryptedArchivesAsync(string fileName, int expectedNumFiles = 1)
{
var extractor = new Extractor();
@@ -42,36 +41,36 @@ public async Task FileTypeSetCorrectlyForEncryptedArchivesAsync(string fileName,
results.Add(entry);
}
- Assert.AreEqual(expectedNumFiles, results.Count);
- Assert.AreEqual(FileEntryStatus.EncryptedArchive, results.First().EntryStatus);
+ Assert.Equal(expectedNumFiles, results.Count);
+ Assert.Equal(FileEntryStatus.EncryptedArchive, results.First().EntryStatus);
}
- [DataTestMethod]
- [DataRow("TestDataEncryptedZipCrypto.zip")]
- [DataRow("TestDataEncryptedAes.zip")]
- [DataRow("TestDataEncrypted.7z")]
- [DataRow("TestDataEncrypted.rar4")]
- [DataRow("EncryptedWithPlainNames.7z")]
- [DataRow("EncryptedWithPlainNames.rar4")]
- //[DataRow("TestDataEncrypted.rar")] // RAR5 is not yet supported by SharpCompress: https://github.com/adamhathcock/sharpcompress/issues/517
+ [Theory]
+ [InlineData("TestDataEncryptedZipCrypto.zip")]
+ [InlineData("TestDataEncryptedAes.zip")]
+ [InlineData("TestDataEncrypted.7z")]
+ [InlineData("TestDataEncrypted.rar4")]
+ [InlineData("EncryptedWithPlainNames.7z")]
+ [InlineData("EncryptedWithPlainNames.rar4")]
+ //[InlineData("TestDataEncrypted.rar")] // RAR5 is not yet supported by SharpCompress: https://github.com/adamhathcock/sharpcompress/issues/517
public void ExtractEncryptedArchive(string fileName, int expectedNumFiles = 3)
{
var extractor = new Extractor();
var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName);
var results = extractor.Extract(path, new ExtractorOptions() { Passwords = TestArchivePasswords, ExtractSelfOnFail = false })
.ToList(); // Make this a list so it fully populates
- Assert.AreEqual(expectedNumFiles, results.Count);
- Assert.AreEqual(0, results.Count(x => x.EntryStatus == FileEntryStatus.EncryptedArchive || x.EntryStatus == FileEntryStatus.FailedArchive));
+ Assert.Equal(expectedNumFiles, results.Count);
+ Assert.Equal(0, results.Count(x => x.EntryStatus == FileEntryStatus.EncryptedArchive || x.EntryStatus == FileEntryStatus.FailedArchive));
}
- [DataTestMethod]
- [DataRow("TestDataEncryptedZipCrypto.zip")]
- [DataRow("TestDataEncryptedAes.zip")]
- [DataRow("TestDataEncrypted.7z")]
- [DataRow("TestDataEncrypted.rar4")]
- [DataRow("EncryptedWithPlainNames.7z")]
- [DataRow("EncryptedWithPlainNames.rar4")]
- //[DataRow("TestDataEncrypted.rar")] // RAR5 is not yet supported by SharpCompress: https://github.com/adamhathcock/sharpcompress/issues/517
+ [Theory]
+ [InlineData("TestDataEncryptedZipCrypto.zip")]
+ [InlineData("TestDataEncryptedAes.zip")]
+ [InlineData("TestDataEncrypted.7z")]
+ [InlineData("TestDataEncrypted.rar4")]
+ [InlineData("EncryptedWithPlainNames.7z")]
+ [InlineData("EncryptedWithPlainNames.rar4")]
+ //[InlineData("TestDataEncrypted.rar")] // RAR5 is not yet supported by SharpCompress: https://github.com/adamhathcock/sharpcompress/issues/517
public async Task ExtractEncryptedArchiveAsync(string fileName, int expectedNumFiles = 3)
{
var extractor = new Extractor();
@@ -88,8 +87,8 @@ public async Task ExtractEncryptedArchiveAsync(string fileName, int expectedNumF
}
}
- Assert.AreEqual(expectedNumFiles, numEntries);
- Assert.AreEqual(0, numEntriesEncrypted);
+ Assert.Equal(expectedNumFiles, numEntries);
+ Assert.Equal(0, numEntriesEncrypted);
}
diff --git a/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs b/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs
index ab9544cf..5f56e2f0 100644
--- a/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs
+++ b/RecursiveExtractor.Tests/ExtractorTests/ExpectedNumFilesTests.cs
@@ -2,48 +2,50 @@
using Microsoft.CST.RecursiveExtractor;
using Microsoft.CST.RecursiveExtractor.Tests;
-using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Xunit;
namespace RecursiveExtractor.Tests.ExtractorTests
{
- [TestClass]
- public class ExpectedNumFilesTests : BaseExtractorTestClass
+ [Collection(ExtractorTestCollection.Name)]
+ public class ExpectedNumFilesTests
{
///
/// Mapping from Test archive name to expected number of files to extract
///
- public static IEnumerable
VMDK,
///
- /// A DMG disc image.
+ /// A DMG disc image.
///
DMG,
///
+ /// An ARJ compressed archive.
+ ///
+ ARJ,
+ ///
+ /// An ARC compressed archive.
+ ///
+ ARC,
+ ///
/// Unused.
///
INVALID
@@ -128,7 +136,7 @@ public static ArchiveFileType DetectFileType(Stream fileStream)
{
var dmgFooterMagic = new byte[] { 0x6b, 0x6f, 0x6c, 0x79 };
fileStream.Position = fileStream.Length - 0x200; // Footer position
- fileStream.Read(buffer, 0, 4);
+ fileStream.ReadExactly(buffer, 0, 4);
fileStream.Position = initialPosition;
if (dmgFooterMagic.SequenceEqual(buffer[0..4]))
@@ -140,7 +148,7 @@ public static ArchiveFileType DetectFileType(Stream fileStream)
if (fileStream.Length >= 9)
{
fileStream.Position = 0;
- fileStream.Read(buffer, 0, 9);
+ fileStream.ReadExactly(buffer, 0, 9);
fileStream.Position = initialPosition;
if (buffer[0] == 0x50 && buffer[1] == 0x4B && buffer[2] == 0x03 && buffer[3] == 0x04)
@@ -173,6 +181,16 @@ public static ArchiveFileType DetectFileType(Stream fileStream)
{
return ArchiveFileType.P7ZIP;
}
+ // ARJ archive header starts with 0x60, 0xEA
+ if (buffer[0] == 0x60 && buffer[1] == 0xEA)
+ {
+ return ArchiveFileType.ARJ;
+ }
+ // ARC archive: marker byte 0x1A, then compression method (valid: 0x01-0x09 or 0x7F)
+ if (buffer[0] == 0x1A && ((buffer[1] >= 0x01 && buffer[1] <= 0x09) || buffer[1] == 0x7F))
+ {
+ return ArchiveFileType.ARC;
+ }
if (Encoding.ASCII.GetString(buffer[0..8]) == "MSWIM\0\0\0" || Encoding.ASCII.GetString(buffer[0..8]) == "WLPWM\0\0\0")
{
return ArchiveFileType.WIM;
@@ -181,7 +199,7 @@ public static ArchiveFileType DetectFileType(Stream fileStream)
{
fileStream.Position = 512;
var secondToken = new byte[21];
- fileStream.Read(secondToken, 0, 21);
+ fileStream.ReadExactly(secondToken, 0, 21);
fileStream.Position = initialPosition;
if (Encoding.ASCII.GetString(secondToken) == "# Disk DescriptorFile")
@@ -194,7 +212,7 @@ public static ArchiveFileType DetectFileType(Stream fileStream)
{
// .deb https://manpages.debian.org/unstable/dpkg-dev/deb.5.en.html
fileStream.Position = 68;
- fileStream.Read(buffer, 0, 4);
+ fileStream.ReadExactly(buffer, 0, 4);
fileStream.Position = initialPosition;
var encoding = new ASCIIEncoding();
@@ -208,7 +226,7 @@ public static ArchiveFileType DetectFileType(Stream fileStream)
// Created by GNU ar https://en.wikipedia.org/wiki/Ar_(Unix)#System_V_(or_GNU)_variant
fileStream.Position = 8;
- fileStream.Read(headerBuffer, 0, 60);
+ fileStream.ReadExactly(headerBuffer, 0, 60);
fileStream.Position = initialPosition;
// header size in bytes
@@ -232,7 +250,7 @@ public static ArchiveFileType DetectFileType(Stream fileStream)
if (fileStream.Length >= 262)
{
fileStream.Position = 257;
- fileStream.Read(buffer, 0, 5);
+ fileStream.ReadExactly(buffer, 0, 5);
fileStream.Position = initialPosition;
if (buffer[0] == 0x75 && buffer[1] == 0x73 && buffer[2] == 0x74 && buffer[3] == 0x61 && buffer[4] == 0x72)
@@ -245,7 +263,7 @@ public static ArchiveFileType DetectFileType(Stream fileStream)
if (fileStream.Length > 32768 + 2048)
{
fileStream.Position = 32769;
- fileStream.Read(buffer, 0, 5);
+ fileStream.ReadExactly(buffer, 0, 5);
fileStream.Position = initialPosition;
if (buffer[0] == 'C' && buffer[1] == 'D' && buffer[2] == '0' && buffer[3] == '0' && buffer[4] == '1')
@@ -266,7 +284,7 @@ public static ArchiveFileType DetectFileType(Stream fileStream)
var vhdFooterCookie = new byte[] { 0x63, 0x6F, 0x6E, 0x65, 0x63, 0x74, 0x69, 0x78 };
fileStream.Position = fileStream.Length - 0x200; // Footer position
- fileStream.Read(buffer, 0, 8);
+ fileStream.ReadExactly(buffer, 0, 8);
fileStream.Position = initialPosition;
if (vhdFooterCookie.SequenceEqual(buffer[0..8]))
@@ -275,7 +293,7 @@ public static ArchiveFileType DetectFileType(Stream fileStream)
}
fileStream.Position = fileStream.Length - 0x1FF; //If created on legacy platform footer is 511 bytes instead
- fileStream.Read(buffer, 0, 8);
+ fileStream.ReadExactly(buffer, 0, 8);
fileStream.Position = initialPosition;
if (vhdFooterCookie.SequenceEqual(buffer[0..8]))
diff --git a/RecursiveExtractor/Range.cs b/RecursiveExtractor/Range.cs
index ac5b2147..18638669 100644
--- a/RecursiveExtractor/Range.cs
+++ b/RecursiveExtractor/Range.cs
@@ -1,238 +1,7 @@
-// https://github.com/dotnet/corefx/blob/1597b894a2e9cac668ce6e484506eca778a85197/src/Common/src/CoreLib/System/Index.cs
-// https://github.com/dotnet/corefx/blob/1597b894a2e9cac668ce6e484506eca778a85197/src/Common/src/CoreLib/System/Range.cs
+// Polyfill for RuntimeHelpers.GetSubArray needed by the C# compiler for array range syntax on netstandard2.0.
+// Index and Range types are now provided by Microsoft.Bcl.Memory (transitive dependency).
#if NETSTANDARD2_0
-using System.Runtime.CompilerServices;
-
-namespace System
-{
- /// Represent a type can be used to index a collection either from the start or the end.
- ///
- /// Index is used by the C# compiler to support the new index syntax
- ///
- /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ;
- /// int lastElement = someArray[^1]; // lastElement = 5
- ///
- ///
- internal readonly struct Index : IEquatable
- {
- private readonly int _value;
-
- /// Construct an Index using a value and indicating if the index is from the start or from the end.
- /// The index value. it has to be zero or positive number.
- /// Indicating if the index is from the start or from the end.
- ///
- /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public Index(int value, bool fromEnd = false)
- {
- if (value < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative");
- }
-
- if (fromEnd)
- _value = ~value;
- else
- _value = value;
- }
-
- // The following private constructors mainly created for perf reason to avoid the checks
- private Index(int value)
- {
- _value = value;
- }
-
- /// Create an Index pointing at first element.
- public static Index Start => new Index(0);
-
- /// Create an Index pointing at beyond last element.
- public static Index End => new Index(~0);
-
- /// Create an Index from the start at the position indicated by the value.
- /// The index value from the start.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Index FromStart(int value)
- {
- if (value < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative");
- }
-
- return new Index(value);
- }
-
- /// Create an Index from the end at the position indicated by the value.
- /// The index value from the end.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Index FromEnd(int value)
- {
- if (value < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(value), "value must be non-negative");
- }
-
- return new Index(~value);
- }
-
- /// Returns the index value.
- public int Value
- {
- get
- {
- if (_value < 0)
- {
- return ~_value;
- }
- else
- {
- return _value;
- }
- }
- }
-
- /// Indicates whether the index is from the start or the end.
- public bool IsFromEnd => _value < 0;
-
- /// Calculate the offset from the start using the giving collection length.
- /// The length of the collection that the Index will be used with. length has to be a positive value
- ///
- /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values.
- /// we don't validate either the returned offset is greater than the input length.
- /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and
- /// then used to index a collection will get out of range exception which will be same affect as the validation.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public int GetOffset(int length)
- {
- var offset = _value;
- if (IsFromEnd)
- {
- // offset = length - (~value)
- // offset = length + (~(~value) + 1)
- // offset = length + value + 1
-
- offset += length + 1;
- }
- return offset;
- }
-
- /// Indicates whether the current Index object is equal to another object of the same type.
- /// An object to compare with this object
- public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value;
-
- /// Indicates whether the current Index object is equal to another Index object.
- /// An object to compare with this object
- public bool Equals(Index other) => _value == other._value;
-
- /// Returns the hash code for this instance.
- public override int GetHashCode() => _value;
-
- /// Converts integer number to an Index.
- public static implicit operator Index(int value) => FromStart(value);
-
- /// Converts the value of the current Index object to its equivalent string representation.
- public override string ToString()
- {
- if (IsFromEnd)
- return "^" + ((uint)Value).ToString();
-
- return ((uint)Value).ToString();
- }
- }
-
- /// Represent a range has start and end indexes.
- ///
- /// Range is used by the C# compiler to support the range syntax.
- ///
- /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 };
- /// int[] subArray1 = someArray[0..2]; // { 1, 2 }
- /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 }
- ///
- ///
- internal readonly struct Range : IEquatable
- {
- /// Represent the inclusive start index of the Range.
- public Index Start { get; }
-
- /// Represent the exclusive end index of the Range.
- public Index End { get; }
-
- /// Construct a Range object using the start and end indexes.
- /// Represent the inclusive start index of the range.
- /// Represent the exclusive end index of the range.
- public Range(Index start, Index end)
- {
- Start = start;
- End = end;
- }
-
- /// Indicates whether the current Range object is equal to another object of the same type.
- /// An object to compare with this object
- public override bool Equals(object? value) =>
- value is Range r &&
- r.Start.Equals(Start) &&
- r.End.Equals(End);
-
- /// Indicates whether the current Range object is equal to another Range object.
- /// An object to compare with this object
- public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End);
-
- /// Returns the hash code for this instance.
- public override int GetHashCode()
- {
- return Start.GetHashCode() * 31 + End.GetHashCode();
- }
-
- /// Converts the value of the current Range object to its equivalent string representation.
- public override string ToString()
- {
- return Start + ".." + End;
- }
-
- /// Create a Range object starting from start index to the end of the collection.
- public static Range StartAt(Index start) => new Range(start, Index.End);
-
- /// Create a Range object starting from first element in the collection to the end Index.
- public static Range EndAt(Index end) => new Range(Index.Start, end);
-
- /// Create a Range object starting from first element to the end.
- public static Range All => new Range(Index.Start, Index.End);
-
- /// Calculate the start offset and length of range object using a collection length.
- /// The length of the collection that the range will be used with. length has to be a positive value.
- ///
- /// For performance reason, we don't validate the input length parameter against negative values.
- /// It is expected Range will be used with collections which always have non negative length/count.
- /// We validate the range is inside the length scope though.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public (int Offset, int Length) GetOffsetAndLength(int length)
- {
- int start;
- var startIndex = Start;
- if (startIndex.IsFromEnd)
- start = length - startIndex.Value;
- else
- start = startIndex.Value;
-
- int end;
- var endIndex = End;
- if (endIndex.IsFromEnd)
- end = length - endIndex.Value;
- else
- end = endIndex.Value;
-
- if ((uint)end > (uint)length || (uint)start > (uint)end)
- {
- throw new ArgumentOutOfRangeException(nameof(length));
- }
-
- return (start, end - start);
- }
- }
-}
namespace System.Runtime.CompilerServices
{
diff --git a/RecursiveExtractor/RecursiveExtractor.csproj b/RecursiveExtractor/RecursiveExtractor.csproj
index a45aeb8c..c7b1f2eb 100644
--- a/RecursiveExtractor/RecursiveExtractor.csproj
+++ b/RecursiveExtractor/RecursiveExtractor.csproj
@@ -48,7 +48,7 @@
-
+
diff --git a/RecursiveExtractor/StreamReadExactlyPolyfill.cs b/RecursiveExtractor/StreamReadExactlyPolyfill.cs
new file mode 100644
index 00000000..1a2a0f55
--- /dev/null
+++ b/RecursiveExtractor/StreamReadExactlyPolyfill.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
+
+#if !NET7_0_OR_GREATER
+using System.IO;
+
+namespace Microsoft.CST.RecursiveExtractor
+{
+ internal static class StreamReadExactlyPolyfill
+ {
+ internal static void ReadExactly(this Stream stream, byte[] buffer, int offset, int count)
+ {
+ int totalRead = 0;
+ while (totalRead < count)
+ {
+ int read = stream.Read(buffer, offset + totalRead, count - totalRead);
+ if (read == 0)
+ {
+ throw new EndOfStreamException();
+ }
+ totalRead += read;
+ }
+ }
+ }
+}
+#endif
diff --git a/coverlet.runsettings b/coverlet.runsettings
new file mode 100644
index 00000000..d7c98c23
--- /dev/null
+++ b/coverlet.runsettings
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+ cobertura,opencover
+ **/obj/**,**/bin/**
+ false
+ true
+ false
+ true
+
+
+
+
+