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 ArchiveData + public static TheoryData ArchiveData { get { - return new[] + return new TheoryData { - new object[] { "100Trees.7z", 101 }, - new object[] { "TestData.zip", 5 }, - new object[] { "TestData.7z",3 }, - new object[] { "TestData.tar", 6 }, - new object[] { "TestData.rar",3 }, - new object[] { "TestData.rar4",3 }, - new object[] { "TestData.tar.bz2", 6 }, - new object[] { "TestData.tar.gz", 6 }, - new object[] { "TestData.tar.xz",3 }, - new object[] { "sysvbanner_1.0-17fakesync1_amd64.deb", 8 }, - new object[] { "TestData.a",3 }, - new object[] { "TestData.bsd.ar",3 }, - new object[] { "TestData.iso",3 }, - new object[] { "TestData.vhdx",3 }, - new object[] { "TestData.wim",3 }, - new object[] { "EmptyFile.txt", 1 }, - new object[] { "TestDataArchivesNested.Zip", 54 }, - new object[] { "UdfTest.iso", 3 }, - new object[] { "UdfTestWithMultiSystem.iso", 3 }, -// new object[] { "HfsSampleUDCO.dmg", 2 } + { "100Trees.7z", 101 }, + { "TestData.zip", 5 }, + { "TestData.7z",3 }, + { "TestData.tar", 6 }, + { "TestData.rar",3 }, + { "TestData.rar4",3 }, + { "TestData.tar.bz2", 6 }, + { "TestData.tar.gz", 6 }, + { "TestData.tar.xz",3 }, + { "sysvbanner_1.0-17fakesync1_amd64.deb", 8 }, + { "TestData.a",3 }, + { "TestData.bsd.ar",3 }, + { "TestData.iso",3 }, + { "TestData.vhdx",3 }, + { "TestData.wim",3 }, + { "EmptyFile.txt", 1 }, + { "TestDataArchivesNested.Zip", 54 }, + { "UdfTest.iso", 3 }, + { "UdfTestWithMultiSystem.iso", 3 }, + { "TestData.arj", 1 }, + { "TestData.arc", 1 }, +// { "HfsSampleUDCO.dmg", 2 } }; } } @@ -51,31 +53,33 @@ public static IEnumerable ArchiveData /// /// Mapping from Test archive name to expected number of files to extract when recursion is disabled /// - public static IEnumerable NoRecursionData + public static TheoryData NoRecursionData { get { - return new[] + return new TheoryData { - new object[] { "100Trees.7z", 101 }, - new object[] { "TestData.zip", 5 }, - new object[] { "TestData.7z", 3 }, - new object[] { "TestData.tar", 6 }, - new object[] { "TestData.rar", 3 }, - new object[] { "TestData.rar4", 3 }, - new object[] { "TestData.tar.bz2", 1 }, - new object[] { "TestData.tar.gz", 1 }, - new object[] { "TestData.tar.xz", 1 }, - new object[] { "sysvbanner_1.0-17fakesync1_amd64.deb", 2 }, - new object[] { "TestData.a", 3 }, - new object[] { "TestData.bsd.ar", 3 }, - new object[] { "TestData.iso", 3 }, - new object[] { "TestData.vhdx", 3 }, - new object[] { "TestData.wim", 3 }, - new object[] { "EmptyFile.txt", 1 }, - new object[] { "TestDataArchivesNested.Zip", 14 }, - new object[] { "UdfTestWithMultiSystem.iso", 3 }, -// new object[] { "HfsSampleUDCO.dmg", 2 } + { "100Trees.7z", 101 }, + { "TestData.zip", 5 }, + { "TestData.7z", 3 }, + { "TestData.tar", 6 }, + { "TestData.rar", 3 }, + { "TestData.rar4", 3 }, + { "TestData.tar.bz2", 1 }, + { "TestData.tar.gz", 1 }, + { "TestData.tar.xz", 1 }, + { "sysvbanner_1.0-17fakesync1_amd64.deb", 2 }, + { "TestData.a", 3 }, + { "TestData.bsd.ar", 3 }, + { "TestData.iso", 3 }, + { "TestData.vhdx", 3 }, + { "TestData.wim", 3 }, + { "EmptyFile.txt", 1 }, + { "TestDataArchivesNested.Zip", 14 }, + { "UdfTestWithMultiSystem.iso", 3 }, + { "TestData.arj", 1 }, + { "TestData.arc", 1 }, +// { "HfsSampleUDCO.dmg", 2 } }; } } @@ -85,18 +89,18 @@ private ExtractorOptions GetExtractorOptions(bool parallel = false) return parallel ? defaultExtractorTestOptionsParallel : defaultExtractorTestOptions; } - private ExtractorOptions defaultExtractorTestOptions = new ExtractorOptions() { MaxExtractedBytesRatio = 300 }; - private ExtractorOptions defaultExtractorTestOptionsParallel = new ExtractorOptions() { Parallel = true, MaxExtractedBytesRatio = 300 }; + private readonly ExtractorOptions defaultExtractorTestOptions = new() { MaxExtractedBytesRatio = 300 }; + private readonly ExtractorOptions defaultExtractorTestOptionsParallel = new() { Parallel = true, MaxExtractedBytesRatio = 300 }; - [TestMethod] - [DynamicData(nameof(ArchiveData))] + [Theory] + [MemberData(nameof(ArchiveData))] public void ExtractArchiveToDirectoryParallel(string fileName, int expectedNumFiles) { ExtractArchiveToDirectory(fileName, expectedNumFiles, true); } - [TestMethod] - [DynamicData(nameof(ArchiveData))] + [Theory] + [MemberData(nameof(ArchiveData))] public void ExtractArchiveToDirectorySingleThread(string fileName, int expectedNumFiles) { ExtractArchiveToDirectory(fileName, expectedNumFiles, false); @@ -113,27 +117,27 @@ internal void ExtractArchiveToDirectory(string fileName, int expectedNumFiles, b { files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToArray(); } - Assert.AreEqual(expectedNumFiles, files.Length); + Assert.Equal(expectedNumFiles, files.Length); } - [TestMethod] - [DynamicData(nameof(ArchiveData))] + [Theory] + [MemberData(nameof(ArchiveData))] public async Task ExtractArchiveToDirectoryAsync(string fileName, int expectedNumFiles) { var directory = TestPathHelpers.GetFreshTestDirectory(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); var extractor = new Extractor(); - Assert.AreEqual(ExtractionStatusCode.Ok, await extractor.ExtractToDirectoryAsync(directory, path, GetExtractorOptions()).ConfigureAwait(false)); + Assert.Equal(ExtractionStatusCode.Ok, await extractor.ExtractToDirectoryAsync(directory, path, GetExtractorOptions())); var files = Array.Empty(); if (Directory.Exists(directory)) { files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToArray(); } - Assert.AreEqual(expectedNumFiles, files.Length); + Assert.Equal(expectedNumFiles, files.Length); } - [TestMethod] - [DynamicData(nameof(NoRecursionData))] + [Theory] + [MemberData(nameof(NoRecursionData))] public async Task ExtractArchiveAsyncNoRecursion(string fileName, int expectedNumFiles) { var extractor = new Extractor(); @@ -145,11 +149,11 @@ public async Task ExtractArchiveAsyncNoRecursion(string fileName, int expectedNu { numResults++; } - Assert.AreEqual(expectedNumFiles, numResults); + Assert.Equal(expectedNumFiles, numResults); } - [TestMethod] - [DynamicData(nameof(NoRecursionData))] + [Theory] + [MemberData(nameof(NoRecursionData))] public void ExtractArchiveParallelNoRecursion(string fileName, int expectedNumFiles) { var extractor = new Extractor(); @@ -157,11 +161,11 @@ public void ExtractArchiveParallelNoRecursion(string fileName, int expectedNumFi var opts = GetExtractorOptions(true); opts.Recurse = false; var results = extractor.Extract(path, opts); - Assert.AreEqual(expectedNumFiles, results.Count(entry => entry.EntryStatus == FileEntryStatus.Default)); + Assert.Equal(expectedNumFiles, results.Count(entry => entry.EntryStatus == FileEntryStatus.Default)); } - [TestMethod] - [DynamicData(nameof(NoRecursionData))] + [Theory] + [MemberData(nameof(NoRecursionData))] public void ExtractArchiveNoRecursion(string fileName, int expectedNumFiles) { var extractor = new Extractor(); @@ -169,11 +173,11 @@ public void ExtractArchiveNoRecursion(string fileName, int expectedNumFiles) var opts = GetExtractorOptions(); opts.Recurse = false; var results = extractor.Extract(path, opts); - Assert.AreEqual(expectedNumFiles, results.Count(entry => entry.EntryStatus == FileEntryStatus.Default)); + Assert.Equal(expectedNumFiles, results.Count(entry => entry.EntryStatus == FileEntryStatus.Default)); } - [TestMethod] - [DynamicData(nameof(ArchiveData))] + [Theory] + [MemberData(nameof(ArchiveData))] public void ExtractArchive(string fileName, int expectedNumFiles) { var extractor = new Extractor(); @@ -181,13 +185,13 @@ public void ExtractArchive(string fileName, int expectedNumFiles) var results = extractor.Extract(path, GetExtractorOptions()).ToList(); foreach (var result in results) { - Assert.AreNotEqual(FileEntryStatus.FailedArchive, result.EntryStatus); + Assert.NotEqual(FileEntryStatus.FailedArchive, result.EntryStatus); } - Assert.AreEqual(expectedNumFiles, results.Count); + Assert.Equal(expectedNumFiles, results.Count); } - [TestMethod] - [DynamicData(nameof(ArchiveData))] + [Theory] + [MemberData(nameof(ArchiveData))] public void ExtractArchiveParallel(string fileName, int expectedNumFiles) { var extractor = new Extractor(); @@ -195,11 +199,11 @@ public void ExtractArchiveParallel(string fileName, int expectedNumFiles) var results = extractor.Extract(path, GetExtractorOptions(true)).ToList(); var names = results.Select(x => x.FullPath); var stringOfNames = string.Join("\n", names); - Assert.AreEqual(expectedNumFiles, results.Count()); + Assert.Equal(expectedNumFiles, results.Count); } - [TestMethod] - [DynamicData(nameof(ArchiveData))] + [Theory] + [MemberData(nameof(ArchiveData))] public async Task ExtractArchiveAsync(string fileName, int expectedNumFiles) { var extractor = new Extractor(); @@ -215,41 +219,41 @@ public async Task ExtractArchiveAsync(string fileName, int expectedNumFiles) files++; } } - Assert.AreEqual(expectedNumFiles, numFound); - Assert.AreEqual(expectedNumFiles, files); + Assert.Equal(expectedNumFiles, numFound); + Assert.Equal(expectedNumFiles, files); } - [TestMethod] - [DynamicData(nameof(ArchiveData))] + [Theory] + [MemberData(nameof(ArchiveData))] public async Task ExtractArchiveFromStreamAsync(string fileName, int expectedNumFiles) { - var extractor = new Extractor(); + var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); - using var stream = new FileStream(path, FileMode.Open); + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); var results = extractor.ExtractAsync(path, stream, new ExtractorOptions()); var numFiles = 0; await foreach (var result in results) { numFiles++; } - Assert.AreEqual(expectedNumFiles, numFiles); + Assert.Equal(expectedNumFiles, numFiles); stream.Close(); } - [TestMethod] - [DynamicData(nameof(ArchiveData))] + [Theory] + [MemberData(nameof(ArchiveData))] public void ExtractArchiveFromStream(string fileName, int expectedNumFiles) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); - using var stream = new FileStream(path, FileMode.Open); + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); var results = extractor.Extract(path, stream, GetExtractorOptions()); - Assert.AreEqual(expectedNumFiles, results.Count()); + Assert.Equal(expectedNumFiles, results.Count()); stream.Close(); } - [TestMethod] - [DynamicData(nameof(ArchiveData))] + [Theory] + [MemberData(nameof(ArchiveData))] public void ExtractArchiveSmallBatchSize(string fileName, int expectedNumFiles) { var extractor = new Extractor(); @@ -257,7 +261,7 @@ public void ExtractArchiveSmallBatchSize(string fileName, int expectedNumFiles) var opts = GetExtractorOptions(true); opts.BatchSize = 2; var results = extractor.Extract(path, opts); - Assert.AreEqual(expectedNumFiles, results.Count(entry => entry.EntryStatus == FileEntryStatus.Default)); + Assert.Equal(expectedNumFiles, results.Count(entry => entry.EntryStatus == FileEntryStatus.Default)); } } } \ No newline at end of file diff --git a/RecursiveExtractor.Tests/ExtractorTests/ExtractorTestCollection.cs b/RecursiveExtractor.Tests/ExtractorTests/ExtractorTestCollection.cs new file mode 100644 index 00000000..08796b5c --- /dev/null +++ b/RecursiveExtractor.Tests/ExtractorTests/ExtractorTestCollection.cs @@ -0,0 +1,16 @@ +using Xunit; + +namespace RecursiveExtractor.Tests.ExtractorTests; + +/// +/// Defines a shared test collection so that all extractor test classes share a single +/// fixture instance. The fixture is created once +/// before the first test runs and disposed once after the last test completes, +/// avoiding the race condition where parallel class-level fixtures prematurely +/// delete the shared temp directory while other classes are still running. +/// +[CollectionDefinition(Name)] +public class ExtractorTestCollection : ICollectionFixture +{ + public const string Name = "Extractor Tests"; +} diff --git a/RecursiveExtractor.Tests/ExtractorTests/FilterTests.cs b/RecursiveExtractor.Tests/ExtractorTests/FilterTests.cs index 4fcc28aa..a7442d0a 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/FilterTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/FilterTests.cs @@ -1,32 +1,31 @@ -using Microsoft.CST.RecursiveExtractor; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.CST.RecursiveExtractor; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Xunit; namespace RecursiveExtractor.Tests.ExtractorTests; -[TestClass] -public class FilterTests : BaseExtractorTestClass +public class FilterTests { - [DataTestMethod] - [DataRow("TestData.zip")] - [DataRow("TestData.7z")] - [DataRow("TestData.tar")] - [DataRow("TestData.rar")] - [DataRow("TestData.rar4")] - [DataRow("TestData.tar.bz2")] - [DataRow("TestData.tar.gz")] - [DataRow("TestData.tar.xz")] - [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 0)] - [DataRow("TestData.a", 0)] - [DataRow("TestData.bsd.ar", 0)] - [DataRow("TestData.iso")] - [DataRow("TestData.vhdx")] - [DataRow("TestData.wim")] - [DataRow("EmptyFile.txt", 0)] - [DataRow("TestDataArchivesNested.Zip", 9)] + [Theory] + [InlineData("TestData.zip")] + [InlineData("TestData.7z")] + [InlineData("TestData.tar")] + [InlineData("TestData.rar")] + [InlineData("TestData.rar4")] + [InlineData("TestData.tar.bz2")] + [InlineData("TestData.tar.gz")] + [InlineData("TestData.tar.xz")] + [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 0)] + [InlineData("TestData.a", 0)] + [InlineData("TestData.bsd.ar", 0)] + [InlineData("TestData.iso")] + [InlineData("TestData.vhdx")] + [InlineData("TestData.wim")] + [InlineData("EmptyFile.txt", 0)] + [InlineData("TestDataArchivesNested.Zip", 9)] public async Task ExtractArchiveAsyncAllowFiltered(string fileName, int expectedNumFiles = 1) { var extractor = new Extractor(); @@ -39,129 +38,129 @@ public async Task ExtractArchiveAsyncAllowFiltered(string fileName, int expected numResults++; } - Assert.AreEqual(expectedNumFiles, numResults); + Assert.Equal(expectedNumFiles, numResults); } - [DataTestMethod] - [DataRow("TestData.zip")] - [DataRow("TestData.7z")] - [DataRow("TestData.tar")] - [DataRow("TestData.rar")] - [DataRow("TestData.rar4")] - [DataRow("TestData.tar.bz2")] - [DataRow("TestData.tar.gz")] - [DataRow("TestData.tar.xz")] - [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 0)] - [DataRow("TestData.a", 0)] - [DataRow("TestData.bsd.ar", 0)] - [DataRow("TestData.iso")] - [DataRow("TestData.vhdx")] - [DataRow("TestData.wim")] - [DataRow("EmptyFile.txt", 0)] - [DataRow("TestDataArchivesNested.Zip", 9)] + [Theory] + [InlineData("TestData.zip")] + [InlineData("TestData.7z")] + [InlineData("TestData.tar")] + [InlineData("TestData.rar")] + [InlineData("TestData.rar4")] + [InlineData("TestData.tar.bz2")] + [InlineData("TestData.tar.gz")] + [InlineData("TestData.tar.xz")] + [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 0)] + [InlineData("TestData.a", 0)] + [InlineData("TestData.bsd.ar", 0)] + [InlineData("TestData.iso")] + [InlineData("TestData.vhdx")] + [InlineData("TestData.wim")] + [InlineData("EmptyFile.txt", 0)] + [InlineData("TestDataArchivesNested.Zip", 9)] public void ExtractArchiveAllowFiltered(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() { AllowFilters = new string[] { "**/Bar/**", "**/TestData.tar" } }); - Assert.AreEqual(expectedNumFiles, results.Count()); + Assert.Equal(expectedNumFiles, results.Count()); } - [DataTestMethod] - [DataRow("TestData.zip")] - [DataRow("TestData.7z")] - [DataRow("TestData.tar")] - [DataRow("TestData.rar")] - [DataRow("TestData.rar4")] - [DataRow("TestData.tar.bz2")] - [DataRow("TestData.tar.gz")] - [DataRow("TestData.tar.xz")] - [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 0)] - [DataRow("TestData.a", 0)] - [DataRow("TestData.bsd.ar", 0)] - [DataRow("TestData.iso")] - [DataRow("TestData.vhdx")] - [DataRow("TestData.wim")] - [DataRow("EmptyFile.txt", 0)] - [DataRow("TestDataArchivesNested.Zip", 9)] + [Theory] + [InlineData("TestData.zip")] + [InlineData("TestData.7z")] + [InlineData("TestData.tar")] + [InlineData("TestData.rar")] + [InlineData("TestData.rar4")] + [InlineData("TestData.tar.bz2")] + [InlineData("TestData.tar.gz")] + [InlineData("TestData.tar.xz")] + [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 0)] + [InlineData("TestData.a", 0)] + [InlineData("TestData.bsd.ar", 0)] + [InlineData("TestData.iso")] + [InlineData("TestData.vhdx")] + [InlineData("TestData.wim")] + [InlineData("EmptyFile.txt", 0)] + [InlineData("TestDataArchivesNested.Zip", 9)] public void ExtractArchiveParallelAllowFiltered(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 = true, AllowFilters = new string[] { "**/Bar/**", "**/TestData.tar" } }); - Assert.AreEqual(expectedNumFiles, results.Count()); + Assert.Equal(expectedNumFiles, results.Count()); } - [DataTestMethod] - [DataRow("TestData.zip", 4)] - [DataRow("TestData.7z")] - [DataRow("TestData.tar", 5)] - [DataRow("TestData.rar")] - [DataRow("TestData.rar4")] - [DataRow("TestData.tar.bz2", 5)] - [DataRow("TestData.tar.gz", 5)] - [DataRow("TestData.tar.xz")] - [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8)] - [DataRow("TestData.a", 3)] - [DataRow("TestData.bsd.ar", 3)] - [DataRow("TestData.iso")] - [DataRow("TestData.vhdx")] - [DataRow("TestData.wim")] - [DataRow("EmptyFile.txt", 1)] - [DataRow("TestDataArchivesNested.Zip", 45)] + [Theory] + [InlineData("TestData.zip", 4)] + [InlineData("TestData.7z")] + [InlineData("TestData.tar", 5)] + [InlineData("TestData.rar")] + [InlineData("TestData.rar4")] + [InlineData("TestData.tar.bz2", 5)] + [InlineData("TestData.tar.gz", 5)] + [InlineData("TestData.tar.xz")] + [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 8)] + [InlineData("TestData.a", 3)] + [InlineData("TestData.bsd.ar", 3)] + [InlineData("TestData.iso")] + [InlineData("TestData.vhdx")] + [InlineData("TestData.wim")] + [InlineData("EmptyFile.txt", 1)] + [InlineData("TestDataArchivesNested.Zip", 45)] public void ExtractArchiveDenyFiltered(string fileName, int expectedNumFiles = 2) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); var results = extractor.Extract(path, new ExtractorOptions() { DenyFilters = new string[] { "**/Bar/**" } }); - Assert.AreEqual(expectedNumFiles, results.Count()); + Assert.Equal(expectedNumFiles, results.Count()); } - [DataTestMethod] - [DataRow("TestData.zip", 4)] - [DataRow("TestData.7z")] - [DataRow("TestData.tar", 5)] - [DataRow("TestData.rar")] - [DataRow("TestData.rar4")] - [DataRow("TestData.tar.bz2", 5)] - [DataRow("TestData.tar.gz", 5)] - [DataRow("TestData.tar.xz")] - [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8)] - [DataRow("TestData.a", 3)] - [DataRow("TestData.bsd.ar", 3)] - [DataRow("TestData.iso")] - [DataRow("TestData.vhdx")] - [DataRow("TestData.wim")] - [DataRow("EmptyFile.txt", 1)] - [DataRow("TestDataArchivesNested.Zip", 45)] + [Theory] + [InlineData("TestData.zip", 4)] + [InlineData("TestData.7z")] + [InlineData("TestData.tar", 5)] + [InlineData("TestData.rar")] + [InlineData("TestData.rar4")] + [InlineData("TestData.tar.bz2", 5)] + [InlineData("TestData.tar.gz", 5)] + [InlineData("TestData.tar.xz")] + [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 8)] + [InlineData("TestData.a", 3)] + [InlineData("TestData.bsd.ar", 3)] + [InlineData("TestData.iso")] + [InlineData("TestData.vhdx")] + [InlineData("TestData.wim")] + [InlineData("EmptyFile.txt", 1)] + [InlineData("TestDataArchivesNested.Zip", 45)] public void ExtractArchiveParallelDenyFiltered(string fileName, int expectedNumFiles = 2) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); var results = extractor.Extract(path, new ExtractorOptions() { Parallel = true, DenyFilters = new string[] { "**/Bar/**" } }); - Assert.AreEqual(expectedNumFiles, results.Count()); + Assert.Equal(expectedNumFiles, results.Count()); } - [DataTestMethod] - [DataRow("TestData.zip", 4)] - [DataRow("TestData.7z")] - [DataRow("TestData.tar", 5)] - [DataRow("TestData.rar")] - [DataRow("TestData.rar4")] - [DataRow("TestData.tar.bz2", 5)] - [DataRow("TestData.tar.gz", 5)] - [DataRow("TestData.tar.xz")] - [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8)] - [DataRow("TestData.a", 3)] - [DataRow("TestData.bsd.ar", 3)] - [DataRow("TestData.iso")] - [DataRow("TestData.vhdx")] - [DataRow("TestData.wim")] - [DataRow("EmptyFile.txt", 1)] - [DataRow("TestDataArchivesNested.Zip", 45)] + [Theory] + [InlineData("TestData.zip", 4)] + [InlineData("TestData.7z")] + [InlineData("TestData.tar", 5)] + [InlineData("TestData.rar")] + [InlineData("TestData.rar4")] + [InlineData("TestData.tar.bz2", 5)] + [InlineData("TestData.tar.gz", 5)] + [InlineData("TestData.tar.xz")] + [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", 8)] + [InlineData("TestData.a", 3)] + [InlineData("TestData.bsd.ar", 3)] + [InlineData("TestData.iso")] + [InlineData("TestData.vhdx")] + [InlineData("TestData.wim")] + [InlineData("EmptyFile.txt", 1)] + [InlineData("TestDataArchivesNested.Zip", 45)] public async Task ExtractArchiveAsyncDenyFiltered(string fileName, int expectedNumFiles = 2) { var extractor = new Extractor(); @@ -174,19 +173,19 @@ public async Task ExtractArchiveAsyncDenyFiltered(string fileName, int expectedN numResults++; } - Assert.AreEqual(expectedNumFiles, numResults); + Assert.Equal(expectedNumFiles, numResults); } - [DataTestMethod] - [DataRow(ArchiveFileType.ZIP, new[] { ArchiveFileType.ZIP }, new ArchiveFileType[] { }, false)] - [DataRow(ArchiveFileType.ZIP, new[] { ArchiveFileType.TAR }, new ArchiveFileType[] { }, true)] - [DataRow(ArchiveFileType.ZIP, new ArchiveFileType[] { }, new[] { ArchiveFileType.ZIP }, true)] - [DataRow(ArchiveFileType.TAR, new ArchiveFileType[] { }, new[] { ArchiveFileType.ZIP }, false)] - [DataRow(ArchiveFileType.ZIP, new[] { ArchiveFileType.ZIP }, new[] { ArchiveFileType.ZIP }, false)] + [Theory] + [InlineData(ArchiveFileType.ZIP, new[] { ArchiveFileType.ZIP }, new ArchiveFileType[] { }, false)] + [InlineData(ArchiveFileType.ZIP, new[] { ArchiveFileType.TAR }, new ArchiveFileType[] { }, true)] + [InlineData(ArchiveFileType.ZIP, new ArchiveFileType[] { }, new[] { ArchiveFileType.ZIP }, true)] + [InlineData(ArchiveFileType.TAR, new ArchiveFileType[] { }, new[] { ArchiveFileType.ZIP }, false)] + [InlineData(ArchiveFileType.ZIP, new[] { ArchiveFileType.ZIP }, new[] { ArchiveFileType.ZIP }, false)] public void TestArchiveTypeFilters(ArchiveFileType typeToCheck, IEnumerable denyTypes, IEnumerable allowTypes, bool expected) { ExtractorOptions opts = new() { AllowTypes = allowTypes, DenyTypes = denyTypes }; - Assert.AreEqual(expected, opts.IsAcceptableType(typeToCheck)); + Assert.Equal(expected, opts.IsAcceptableType(typeToCheck)); } -} \ No newline at end of file +} diff --git a/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs b/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs index 016ef133..84afdce0 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs @@ -1,44 +1,45 @@ using Microsoft.CST.RecursiveExtractor; -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.IO; +using Xunit; namespace RecursiveExtractor.Tests.ExtractorTests; -[TestClass] -public class MiniMagicTests : BaseExtractorTestClass +public class MiniMagicTests { - [DataTestMethod] - [DataRow("TestData.zip", ArchiveFileType.ZIP)] - [DataRow("TestData.7z", ArchiveFileType.P7ZIP)] - [DataRow("TestData.Tar", ArchiveFileType.TAR)] - [DataRow("TestData.rar", ArchiveFileType.RAR5)] - [DataRow("TestData.rar4", ArchiveFileType.RAR)] - [DataRow("TestData.tar.bz2", ArchiveFileType.BZIP2)] - [DataRow("TestData.tar.gz", ArchiveFileType.GZIP)] - [DataRow("TestData.tar.xz", ArchiveFileType.XZ)] - [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", ArchiveFileType.DEB)] - [DataRow("TestData.a", ArchiveFileType.AR)] - [DataRow("TestData.iso", ArchiveFileType.ISO_9660)] - [DataRow("UdfTest.iso", ArchiveFileType.UDF)] - [DataRow("TestData.vhdx", ArchiveFileType.VHDX)] - [DataRow("TestData.wim", ArchiveFileType.WIM)] - [DataRow("Empty.vmdk", ArchiveFileType.VMDK)] - [DataRow("HfsSampleUDCO.dmg", ArchiveFileType.DMG)] - [DataRow("EmptyFile.txt", ArchiveFileType.UNKNOWN)] + [Theory] + [InlineData("TestData.zip", ArchiveFileType.ZIP)] + [InlineData("TestData.7z", ArchiveFileType.P7ZIP)] + [InlineData("TestData.Tar", ArchiveFileType.TAR)] + [InlineData("TestData.rar", ArchiveFileType.RAR5)] + [InlineData("TestData.rar4", ArchiveFileType.RAR)] + [InlineData("TestData.tar.bz2", ArchiveFileType.BZIP2)] + [InlineData("TestData.tar.gz", ArchiveFileType.GZIP)] + [InlineData("TestData.tar.xz", ArchiveFileType.XZ)] + [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", ArchiveFileType.DEB)] + [InlineData("TestData.a", ArchiveFileType.AR)] + [InlineData("TestData.iso", ArchiveFileType.ISO_9660)] + [InlineData("UdfTest.iso", ArchiveFileType.UDF)] + [InlineData("TestData.vhdx", ArchiveFileType.VHDX)] + [InlineData("TestData.wim", ArchiveFileType.WIM)] + [InlineData("Empty.vmdk", ArchiveFileType.VMDK)] + [InlineData("HfsSampleUDCO.dmg", ArchiveFileType.DMG)] + [InlineData("TestData.arj", ArchiveFileType.ARJ)] + [InlineData("TestData.arc", ArchiveFileType.ARC)] + [InlineData("EmptyFile.txt", ArchiveFileType.UNKNOWN)] public void TestMiniMagic(string fileName, ArchiveFileType expectedArchiveFileType) { var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); - using var fs = new FileStream(path, FileMode.Open); + using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); // Test just based on the content var fileEntry = new FileEntry("NoName", fs); // We make sure the expected type matches and we have reset the stream - Assert.AreEqual(expectedArchiveFileType, fileEntry.ArchiveType); - Assert.AreEqual(0, fileEntry.Content.Position); + Assert.Equal(expectedArchiveFileType, fileEntry.ArchiveType); + Assert.Equal(0, fileEntry.Content.Position); // Should also work if the stream doesn't start at 0 fileEntry.Content.Position = 10; - Assert.AreEqual(expectedArchiveFileType, fileEntry.ArchiveType); - Assert.AreEqual(10, fileEntry.Content.Position); + Assert.Equal(expectedArchiveFileType, fileEntry.ArchiveType); + Assert.Equal(10, fileEntry.Content.Position); } } \ No newline at end of file diff --git a/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs b/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs index e375904a..789f9bf0 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs @@ -1,20 +1,19 @@ using Microsoft.CST.RecursiveExtractor; -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Xunit; namespace RecursiveExtractor.Tests.ExtractorTests; -[TestClass] public class MiscTests { - [DataTestMethod] - [DataRow("TestDataCorrupt.tar", false, 0, 1)] - [DataRow("TestDataCorrupt.tar", true, 1, 1)] - [DataRow("TestDataCorrupt.tar.zip", false, 0, 2)] - [DataRow("TestDataCorrupt.tar.zip", true, 0, 2)] + [Theory] + [InlineData("TestDataCorrupt.tar", false, 0, 1)] + [InlineData("TestDataCorrupt.tar", true, 1, 1)] + [InlineData("TestDataCorrupt.tar.zip", false, 0, 2)] + [InlineData("TestDataCorrupt.tar.zip", true, 0, 2)] public async Task ExtractCorruptArchiveAsync(string fileName, bool requireTopLevelToBeArchive, int expectedNumFailures, int expectedNumFiles) { var extractor = new Extractor(); @@ -23,61 +22,61 @@ public async Task ExtractCorruptArchiveAsync(string fileName, bool requireTopLev new ExtractorOptions() { RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToListAsync(); - Assert.AreEqual(expectedNumFiles, results.Count); + Assert.Equal(expectedNumFiles, results.Count); var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); - Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); + Assert.Equal(expectedNumFailures, actualNumberOfFailedArchives); } - [DataTestMethod] - [DataRow("Lorem.txt", true, 1)] - [DataRow("Lorem.txt", false, 0)] + [Theory] + [InlineData("Lorem.txt", true, 1)] + [InlineData("Lorem.txt", false, 0)] public async Task ExtractFlatFileAsync(string fileName, bool requireTopLevelToBeArchive, int expectedNumFailures) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestData", fileName); var results = await extractor.ExtractAsync(path, new ExtractorOptions(){ RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToListAsync(); - Assert.AreEqual(1, results.Count); + Assert.Single(results); var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); - Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); + Assert.Equal(expectedNumFailures, actualNumberOfFailedArchives); } - [DataTestMethod] - [DataRow("TestDataCorrupt.tar", false, 0, 1)] - [DataRow("TestDataCorrupt.tar", true, 1, 1)] - [DataRow("TestDataCorrupt.tar.zip", false, 0, 2)] - [DataRow("TestDataCorrupt.tar.zip", true, 0, 2)] - [DataRow("TestDataCorruptWim.zip", true, 0, 0)] + [Theory] + [InlineData("TestDataCorrupt.tar", false, 0, 1)] + [InlineData("TestDataCorrupt.tar", true, 1, 1)] + [InlineData("TestDataCorrupt.tar.zip", false, 0, 2)] + [InlineData("TestDataCorrupt.tar.zip", true, 0, 2)] + [InlineData("TestDataCorruptWim.zip", true, 0, 0)] public void ExtractCorruptArchive(string fileName, bool requireTopLevelToBeArchive, int expectedNumFailures, int expectedNumFiles) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); var results = extractor.Extract(path, new ExtractorOptions(){ RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToList(); - Assert.AreEqual(expectedNumFiles, results.Count); + Assert.Equal(expectedNumFiles, results.Count); var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); - Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); + Assert.Equal(expectedNumFailures, actualNumberOfFailedArchives); } - [DataTestMethod] - [DataRow("Lorem.txt", true, 1)] - [DataRow("Lorem.txt", false, 0)] + [Theory] + [InlineData("Lorem.txt", true, 1)] + [InlineData("Lorem.txt", false, 0)] public void ExtractFlatFile(string fileName, bool requireTopLevelToBeArchive, int expectedNumFailures) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestData", fileName); var results = extractor.Extract(path, new ExtractorOptions(){ RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToList(); - Assert.AreEqual(1, results.Count); + Assert.Single(results); var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); - Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); + Assert.Equal(expectedNumFailures, actualNumberOfFailedArchives); } - [DataTestMethod] - [DataRow("EmptyFile.txt")] - [DataRow("TestData.zip", ".zip")] + [Theory] + [InlineData("EmptyFile.txt")] + [InlineData("TestData.zip", ".zip")] public void ExtractAsRaw(string fileName, string? RawExtension = null) { var extractor = new Extractor(); @@ -88,6 +87,6 @@ public void ExtractAsRaw(string fileName, string? RawExtension = null) var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); var results = extractor.Extract(path, options); - Assert.AreEqual(1, results.Count()); + Assert.Single(results); } } \ No newline at end of file diff --git a/RecursiveExtractor.Tests/ExtractorTests/TestQuinesAndSlip.cs b/RecursiveExtractor.Tests/ExtractorTests/TestQuinesAndSlip.cs index e2da32d4..71823764 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/TestQuinesAndSlip.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/TestQuinesAndSlip.cs @@ -1,85 +1,86 @@ using Microsoft.CST.RecursiveExtractor; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using SharpCompress.Archives.Tar; 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 TestQuinesAndSlip : BaseExtractorTestClass +public class TestQuinesAndSlip { - public static IEnumerable ZipSlipNames + public static TheoryData ZipSlipNames { get { - return new[] + return new TheoryData { - new object [] { "zip-slip-win.zip" }, - new object [] { "zip-slip-win.tar" }, - new object [] { "zip-slip.zip" }, - new object [] { "zip-slip.tar" } + { "zip-slip-win.zip" }, + { "zip-slip-win.tar" }, + { "zip-slip.zip" }, + { "zip-slip.tar" } }; } } - [TestMethod] - [DynamicData(nameof(ZipSlipNames))] + [Theory] + [MemberData(nameof(ZipSlipNames))] public void TestZipSlip(string fileName) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "Bombs", fileName); var results = extractor.Extract(path, new ExtractorOptions()).ToList(); - Assert.IsTrue(results.All(x => !x.FullPath.Contains(".."))); + Assert.True(results.All(x => !x.FullPath.Contains(".."))); } - [TestMethod] - [DynamicData(nameof(ZipSlipNames))] + [Theory] + [MemberData(nameof(ZipSlipNames))] public async Task TestZipSlipAsync(string fileName) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "Bombs", fileName); var results = await extractor.ExtractAsync(path, new ExtractorOptions()).ToListAsync(); - Assert.IsTrue(results.All(x => !x.FullPath.Contains(".."))); + Assert.True(results.All(x => !x.FullPath.Contains(".."))); } - public static IEnumerable QuineBombNames + public static TheoryData QuineBombNames { get { - return new[] + return new TheoryData { - new object [] { "10GB.7z.bz2" }, - new object [] { "10GB.gz.bz2" }, - new object [] { "10GB.rar.bz2" }, - new object [] { "10GB.xz.bz2" }, - new object [] { "10GB.zip.bz2" }, - new object [] { "zblg.zip" }, - new object [] { "zbsm.zip" } + { "10GB.7z.bz2" }, + { "10GB.gz.bz2" }, + { "10GB.rar.bz2" }, + { "10GB.xz.bz2" }, + { "10GB.zip.bz2" }, + { "zblg.zip" }, + { "zbsm.zip" } }; } } - [TestMethod] - [DynamicData(nameof(QuineBombNames))] - [ExpectedException(typeof(OverflowException))] + [Theory] + [MemberData(nameof(QuineBombNames))] public void TestQuineBombs(string fileName) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "Bombs", fileName); - _ = extractor.Extract(path, new ExtractorOptions() { MemoryStreamCutoff = 1024 * 1024 * 1024 }).ToList(); + Assert.Throws(() => + { + _ = extractor.Extract(path, new ExtractorOptions() { MemoryStreamCutoff = 1024 * 1024 * 1024 }).ToList(); + }); } - [TestMethod] - [DynamicData(nameof(QuineBombNames))] - [ExpectedException(typeof(OverflowException))] + [Theory] + [MemberData(nameof(QuineBombNames))] public async Task TestQuineBombsAsync(string fileName) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "Bombs", fileName); - _ = await extractor.ExtractAsync(path, new ExtractorOptions() { MemoryStreamCutoff = 1024 * 1024 * 1024 }).ToListAsync(); + await Assert.ThrowsAsync(async () => + { + _ = await extractor.ExtractAsync(path, new ExtractorOptions() { MemoryStreamCutoff = 1024 * 1024 * 1024 }).ToListAsync(); + }); } } \ No newline at end of file diff --git a/RecursiveExtractor.Tests/ExtractorTests/TimeOutTests.cs b/RecursiveExtractor.Tests/ExtractorTests/TimeOutTests.cs index 2f2c8455..980922c5 100644 --- a/RecursiveExtractor.Tests/ExtractorTests/TimeOutTests.cs +++ b/RecursiveExtractor.Tests/ExtractorTests/TimeOutTests.cs @@ -1,51 +1,50 @@ -using Microsoft.CST.RecursiveExtractor; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.CST.RecursiveExtractor; using System; using System.IO; using System.Threading.Tasks; +using Xunit; namespace RecursiveExtractor.Tests.ExtractorTests; -[TestClass] -public class TimeOutTests : BaseExtractorTestClass +public class TimeOutTests { - [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)] - public void TimeoutTest(string fileName, int expectedNumFiles = 3, bool parallel = false) + [Theory] + [InlineData("TestData.7z", false)] + [InlineData("TestData.tar", false)] + [InlineData("TestData.rar", false)] + [InlineData("TestData.rar4", false)] + [InlineData("TestData.tar.bz2", false)] + [InlineData("TestData.tar.gz", false)] + [InlineData("TestData.tar.xz", false)] + [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", false)] + [InlineData("TestData.a", false)] + [InlineData("TestData.bsd.ar", false)] + [InlineData("TestData.iso", false)] + [InlineData("TestData.vhdx", false)] + [InlineData("TestData.wim", false)] + [InlineData("EmptyFile.txt", false)] + [InlineData("TestData.zip", true)] + [InlineData("TestData.7z", true)] + [InlineData("TestData.tar", true)] + [InlineData("TestData.rar", true)] + [InlineData("TestData.rar4", true)] + [InlineData("TestData.tar.bz2", true)] + [InlineData("TestData.tar.gz", true)] + [InlineData("TestData.tar.xz", true)] + [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", true)] + [InlineData("TestData.a", true)] + [InlineData("TestData.bsd.ar", true)] + [InlineData("TestData.iso", true)] + [InlineData("TestData.vhdx", true)] + [InlineData("TestData.wim", true)] + [InlineData("EmptyFile.txt", true)] + [InlineData("TestDataArchivesNested.Zip", true)] + [InlineData("TestDataArchivesNested.Zip", false)] + public void TimeoutTest(string fileName, bool parallel = false) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); - Assert.ThrowsException(() => + Assert.Throws(() => { var results = extractor.Extract(path, new ExtractorOptions() @@ -59,49 +58,47 @@ public void TimeoutTest(string fileName, int expectedNumFiles = 3, bool parallel } // We should not be able to get to all the files - Assert.Fail(); + Assert.Fail("Should have thrown TimeoutException"); }); } - [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)] - public async Task TimeoutTestAsync(string fileName, int expectedNumFiles = 3, bool parallel = false) + [Theory] + [InlineData("TestData.7z", false)] + [InlineData("TestData.tar", false)] + [InlineData("TestData.rar", false)] + [InlineData("TestData.rar4", false)] + [InlineData("TestData.tar.bz2", false)] + [InlineData("TestData.tar.gz", false)] + [InlineData("TestData.tar.xz", false)] + [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", false)] + [InlineData("TestData.a", false)] + [InlineData("TestData.bsd.ar", false)] + [InlineData("TestData.iso", false)] + [InlineData("TestData.vhdx", false)] + [InlineData("TestData.wim", false)] + [InlineData("EmptyFile.txt", false)] + [InlineData("TestData.zip", true)] + [InlineData("TestData.7z", true)] + [InlineData("TestData.tar", true)] + [InlineData("TestData.rar", true)] + [InlineData("TestData.rar4", true)] + [InlineData("TestData.tar.bz2", true)] + [InlineData("TestData.tar.gz", true)] + [InlineData("TestData.tar.xz", true)] + [InlineData("sysvbanner_1.0-17fakesync1_amd64.deb", true)] + [InlineData("TestData.a", true)] + [InlineData("TestData.bsd.ar", true)] + [InlineData("TestData.iso", true)] + [InlineData("TestData.vhdx", true)] + [InlineData("TestData.wim", true)] + [InlineData("EmptyFile.txt", true)] + [InlineData("TestDataArchivesNested.Zip", true)] + [InlineData("TestDataArchivesNested.Zip", false)] + public async Task TimeoutTestAsync(string fileName, bool parallel = false) { var extractor = new Extractor(); var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); - await Assert.ThrowsExceptionAsync(async () => + await Assert.ThrowsAsync(async () => { var results = extractor.ExtractAsync(path, new ExtractorOptions() @@ -115,7 +112,7 @@ await Assert.ThrowsExceptionAsync(async () => } // We should not be able to get to all the files - Assert.Fail(); + Assert.Fail("Should have thrown TimeoutException"); }); } -} \ No newline at end of file +} diff --git a/RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj b/RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj index fe84518b..aeff1381 100644 --- a/RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj +++ b/RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj @@ -5,18 +5,16 @@ false enable 10.0 - Exe + + $(NoWarn);CS1685 - - - - - - - + + + + @@ -156,6 +154,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/RecursiveExtractor.Tests/SanitizePathTests.cs b/RecursiveExtractor.Tests/SanitizePathTests.cs index f8d56a29..a0fb2f2a 100644 --- a/RecursiveExtractor.Tests/SanitizePathTests.cs +++ b/RecursiveExtractor.Tests/SanitizePathTests.cs @@ -1,38 +1,37 @@ // Copyright (c) Microsoft Corporation. Licensed under the MIT License. using Microsoft.CST.RecursiveExtractor; -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.IO; using System.Runtime.InteropServices; +using Xunit; namespace RecursiveExtractor.Tests { - [TestClass] public class SanitizePathTests { - [DataTestMethod] - [DataRow("a\\file\\with:colon.name", "a\\file\\with_colon.name")] - [DataRow("a\\folder:with\\colon.name", "a\\folder_with\\colon.name")] + [Theory] + [InlineData("a\\file\\with:colon.name", "a\\file\\with_colon.name")] + [InlineData("a\\folder:with\\colon.name", "a\\folder_with\\colon.name")] public void TestSanitizePathWindows(string windowsInputPath, string expectedWindowsPath) { var entry = new FileEntry(windowsInputPath, Stream.Null); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - Assert.AreEqual(expectedWindowsPath, entry.GetSanitizedPath()); + Assert.Equal(expectedWindowsPath, entry.GetSanitizedPath()); } } - [DataTestMethod] - [DataRow("a/file/with:colon.name", "a/file/with_colon.name")] - [DataRow("a/folder:with/colon.name", "a/folder_with/colon.name")] + [Theory] + [InlineData("a/file/with:colon.name", "a/file/with_colon.name")] + [InlineData("a/folder:with/colon.name", "a/folder_with/colon.name")] public void TestSanitizePathLinux(string linuxInputPath, string expectedLinuxPath) { var entry = new FileEntry(linuxInputPath, Stream.Null); if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - Assert.AreEqual(expectedLinuxPath, entry.GetSanitizedPath()); + Assert.Equal(expectedLinuxPath, entry.GetSanitizedPath()); } } diff --git a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.arc b/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.arc new file mode 100644 index 00000000..0c99f274 Binary files /dev/null and b/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.arc differ diff --git a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.arj b/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.arj new file mode 100644 index 00000000..5882d12e Binary files /dev/null and b/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.arj differ diff --git a/RecursiveExtractor.Tests/TestPathHelpers.cs b/RecursiveExtractor.Tests/TestPathHelpers.cs index 97461b2b..f854934a 100644 --- a/RecursiveExtractor.Tests/TestPathHelpers.cs +++ b/RecursiveExtractor.Tests/TestPathHelpers.cs @@ -9,7 +9,10 @@ public static class TestPathHelpers { public const string TestTempFolderName = "RE_Tests"; - public static string TestDirectoryPath => Path.Combine(Path.GetTempPath(), TestTempFolderName); + // Use a process-unique subdirectory to avoid cross-TFM/cross-process interference + private static readonly string ProcessId = System.Diagnostics.Process.GetCurrentProcess().Id.ToString(); + + public static string TestDirectoryPath => Path.Combine(Path.GetTempPath(), TestTempFolderName, ProcessId); public static string GetFreshTestDirectory() { @@ -20,13 +23,13 @@ public static void DeleteTestDirectory() { try { - Directory.Delete(Path.Combine(TestDirectoryPath), true); + Directory.Delete(TestDirectoryPath, true); } catch (DirectoryNotFoundException) { // Not an error. Not every test makes the folder. } - catch (Exception e) + catch (Exception) { // Throwing the exception up may cause tests to fail due to file system oddness so just log Logger.Warn("Failed to delete Test Working Directory at {directory}", TestDirectoryPath); diff --git a/RecursiveExtractor/ArFile.cs b/RecursiveExtractor/ArFile.cs index 01845d93..66728061 100644 --- a/RecursiveExtractor/ArFile.cs +++ b/RecursiveExtractor/ArFile.cs @@ -32,7 +32,7 @@ public static IEnumerable GetFileEntries(FileEntry fileEntry, Extract var headerBuffer = new byte[60]; while (fileEntry.Content.Length - fileEntry.Content.Position >= 60) { - fileEntry.Content.Read(headerBuffer, 0, 60); + fileEntry.Content.ReadExactly(headerBuffer, 0, 60); var headerString = Encoding.ASCII.GetString(headerBuffer); if (long.TryParse(Encoding.ASCII.GetString(headerBuffer[48..58]), out var size))// header size in bytes { @@ -46,7 +46,7 @@ public static IEnumerable GetFileEntries(FileEntry fileEntry, Extract // This should just be a list of names, size should be safe to load in memory and cast // to int var fileNamesBytes = new byte[size]; - fileEntry.Content.Read(fileNamesBytes, 0, (int)size); + fileEntry.Content.ReadExactly(fileNamesBytes, 0, (int)size); var name = new StringBuilder(); var index = 0; @@ -76,7 +76,7 @@ public static IEnumerable GetFileEntries(FileEntry fileEntry, Extract var nameSpan = new byte[nameLength]; // This should move us right to the file - fileEntry.Content.Read(nameSpan, 0, nameLength); + fileEntry.Content.ReadExactly(nameSpan, 0, nameLength); var entryStream = StreamFactory.GenerateAppropriateBackingStream(options, size - nameLength); @@ -93,7 +93,7 @@ public static IEnumerable GetFileEntries(FileEntry fileEntry, Extract // terminated strings "symbol name" (possibly filename) var tableContents = new byte[size]; - fileEntry.Content.Read(tableContents, 0, (int)size); + fileEntry.Content.ReadExactly(tableContents, 0, (int)size); var numEntries = IntFromBigEndianBytes(tableContents[0..4]); var filePositions = new int[numEntries]; @@ -124,7 +124,7 @@ public static IEnumerable GetFileEntries(FileEntry fileEntry, Extract foreach (var entry in fileEntries) { fileEntry.Content.Position = entry.Item1; - fileEntry.Content.Read(headerBuffer, 0, 60); + fileEntry.Content.ReadExactly(headerBuffer, 0, 60); if (long.TryParse(Encoding.ASCII.GetString(headerBuffer[48..58]), out var innerSize))// header size in bytes { @@ -162,14 +162,14 @@ public static IEnumerable GetFileEntries(FileEntry fileEntry, Extract // strings "symbol name" (possibly filename) var buffer = new byte[8]; - fileEntry.Content.Read(buffer, 0, 8); + fileEntry.Content.ReadExactly(buffer, 0, 8); var numEntries = Int64FromBigEndianBytes(buffer); var filePositions = new long[numEntries]; for (var i = 0; i < numEntries; i++) { - fileEntry.Content.Read(buffer, 0, 8); + fileEntry.Content.ReadExactly(buffer, 0, 8); filePositions[i] = Int64FromBigEndianBytes(buffer); } @@ -179,7 +179,7 @@ public static IEnumerable GetFileEntries(FileEntry fileEntry, Extract while (fileEntry.Content.Position < size) { - fileEntry.Content.Read(buffer, 0, 1); + fileEntry.Content.ReadExactly(buffer, 0, 1); if (buffer[0] == '\0') { fileEntries.Add((filePositions[index++], sb.ToString())); @@ -195,7 +195,7 @@ public static IEnumerable GetFileEntries(FileEntry fileEntry, Extract { fileEntry.Content.Position = innerEntry.Item1; - fileEntry.Content.Read(headerBuffer, 0, 60); + fileEntry.Content.ReadExactly(headerBuffer, 0, 60); if (long.TryParse(Encoding.ASCII.GetString(headerBuffer[48..58]), out var innerSize))// header size in bytes { @@ -217,9 +217,9 @@ public static IEnumerable GetFileEntries(FileEntry fileEntry, Extract { filename = innerEntry.Item2; } + var entryStream = StreamFactory.GenerateAppropriateBackingStream(options, innerSize); CopyStreamBytes(fileEntry.Content, entryStream, innerSize); - yield return new FileEntry(filename.TrimEnd('/'), entryStream, fileEntry, true); } } @@ -281,7 +281,7 @@ public static async IAsyncEnumerable GetFileEntriesAsync(FileEntry fi var headerBuffer = new byte[60]; while (fileEntry.Content.Length - fileEntry.Content.Position >= 60) { - fileEntry.Content.Read(headerBuffer, 0, 60); + fileEntry.Content.ReadExactly(headerBuffer, 0, 60); if (long.TryParse(Encoding.ASCII.GetString(headerBuffer[48..58]), out var size))// header size in bytes { governor.CheckResourceGovernor(size); @@ -339,7 +339,7 @@ public static async IAsyncEnumerable GetFileEntriesAsync(FileEntry fi // terminated strings "symbol name" (possibly filename) var tableContents = new byte[size]; - fileEntry.Content.Read(tableContents, 0, (int)size); + fileEntry.Content.ReadExactly(tableContents, 0, (int)size); var numEntries = IntFromBigEndianBytes(tableContents[0..4]); var filePositions = new int[numEntries]; @@ -407,14 +407,14 @@ public static async IAsyncEnumerable GetFileEntriesAsync(FileEntry fi // strings "symbol name" (possibly filename) var buffer = new byte[8]; - fileEntry.Content.Read(buffer, 0, 8); + fileEntry.Content.ReadExactly(buffer, 0, 8); var numEntries = Int64FromBigEndianBytes(buffer); var filePositions = new long[numEntries]; for (var i = 0; i < numEntries; i++) { - fileEntry.Content.Read(buffer, 0, 8); + fileEntry.Content.ReadExactly(buffer, 0, 8); filePositions[i] = Int64FromBigEndianBytes(buffer); } @@ -424,7 +424,7 @@ public static async IAsyncEnumerable GetFileEntriesAsync(FileEntry fi while (fileEntry.Content.Position < size) { - fileEntry.Content.Read(buffer, 0, 1); + fileEntry.Content.ReadExactly(buffer, 0, 1); if (buffer[0] == '\0') { fileEntries.Add((filePositions[index++], sb.ToString())); @@ -440,7 +440,7 @@ public static async IAsyncEnumerable GetFileEntriesAsync(FileEntry fi { fileEntry.Content.Position = innerEntry.Item1; - fileEntry.Content.Read(headerBuffer, 0, 60); + fileEntry.Content.ReadExactly(headerBuffer, 0, 60); if (long.TryParse(Encoding.ASCII.GetString(headerBuffer[48..58]), out var innerSize))// header size in bytes { diff --git a/RecursiveExtractor/DebArchiveFile.cs b/RecursiveExtractor/DebArchiveFile.cs index a7d42602..25139648 100644 --- a/RecursiveExtractor/DebArchiveFile.cs +++ b/RecursiveExtractor/DebArchiveFile.cs @@ -30,7 +30,7 @@ public static IEnumerable GetFileEntries(FileEntry fileEntry, Extract while (fileEntry.Content.Length - fileEntry.Content.Position >= 60) { - fileEntry.Content.Read(headerBytes, 0, 60); + fileEntry.Content.ReadExactly(headerBytes, 0, 60); var filename = Encoding.ASCII.GetString(headerBytes[0..16]).Trim(); // filename is 16 bytes var fileSizeBytes = headerBytes[48..58]; // File size is decimal-encoded, 10 bytes long if (int.TryParse(Encoding.ASCII.GetString(fileSizeBytes).Trim(), out var fileSize)) @@ -39,7 +39,7 @@ public static IEnumerable GetFileEntries(FileEntry fileEntry, Extract governor.AdjustRemainingBytes(-fileSize); var entryContent = new byte[fileSize]; - fileEntry.Content.Read(entryContent, 0, fileSize); + fileEntry.Content.ReadExactly(entryContent, 0, fileSize); var stream = new MemoryStream(entryContent); yield return new FileEntry(filename, stream, fileEntry, true); } @@ -70,7 +70,7 @@ public static async IAsyncEnumerable GetFileEntriesAsync(FileEntry fi while (fileEntry.Content.Length - fileEntry.Content.Position >= 60) { - fileEntry.Content.Read(headerBytes, 0, 60); + fileEntry.Content.ReadExactly(headerBytes, 0, 60); var filename = Encoding.ASCII.GetString(headerBytes[0..16]).Trim(); // filename is 16 bytes var fileSizeBytes = headerBytes[48..58]; // File size is decimal-encoded, 10 bytes long if (int.TryParse(Encoding.ASCII.GetString(fileSizeBytes).Trim(), out var fileSize)) diff --git a/RecursiveExtractor/Extractor.cs b/RecursiveExtractor/Extractor.cs index 15dafc57..de7fd758 100644 --- a/RecursiveExtractor/Extractor.cs +++ b/RecursiveExtractor/Extractor.cs @@ -85,6 +85,8 @@ public void SetDefaultExtractors() SetExtractor(ArchiveFileType.VMDK, new VmdkExtractor(this)); SetExtractor(ArchiveFileType.XZ, new XzExtractor(this)); SetExtractor(ArchiveFileType.ZIP, new ZipExtractor(this)); + SetExtractor(ArchiveFileType.ARJ, new ArjExtractor(this)); + SetExtractor(ArchiveFileType.ARC, new ArcExtractor(this)); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { SetExtractor(ArchiveFileType.WIM, new WimExtractor(this)); @@ -148,13 +150,17 @@ public static bool AreIdentical(FileEntry fileEntry1, FileEntry fileEntry2) var bytesRemaining = stream2.Length; while (bytesRemaining > 0) { - stream1.Read(buffer1, 0, bufferSize); - stream2.Read(buffer2, 0, bufferSize); - if (!buffer1.SequenceEqual(buffer2)) + var bytesToRead = (int)Math.Min(bufferSize, bytesRemaining); + stream1.ReadExactly(buffer1, 0, bytesToRead); + stream2.ReadExactly(buffer2, 0, bytesToRead); + for (int i = 0; i < bytesToRead; i++) { - stream1.Position = position1; - stream2.Position = position2; - return false; + if (buffer1[i] != buffer2[i]) + { + stream1.Position = position1; + stream2.Position = position2; + return false; + } } bytesRemaining = stream2.Length - stream2.Position; } diff --git a/RecursiveExtractor/Extractors/ArcExtractor.cs b/RecursiveExtractor/Extractors/ArcExtractor.cs new file mode 100644 index 00000000..a3d2e44c --- /dev/null +++ b/RecursiveExtractor/Extractors/ArcExtractor.cs @@ -0,0 +1,173 @@ +using SharpCompress.Readers.Arc; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.CST.RecursiveExtractor.Extractors +{ + /// + /// The ARC Archive extractor implementation + /// + public class ArcExtractor : AsyncExtractorInterface + { + /// + /// The constructor takes the Extractor context for recursion. + /// + /// The Extractor context. + public ArcExtractor(Extractor context) + { + Context = context; + } + private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + internal Extractor Context { get; } + + /// + /// Extracts an ARC archive + /// + /// + public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) + { + ArcReader? arcReader = null; + try + { + arcReader = ArcReader.Open(fileEntry.Content, new SharpCompress.Readers.ReaderOptions() + { + LeaveStreamOpen = true + }); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARC, fileEntry.FullPath, string.Empty, e.GetType()); + } + + if (arcReader != null) + { + using (arcReader) + { + while (arcReader.MoveToNextEntry()) + { + var entry = arcReader.Entry; + if (entry.IsDirectory) + { + continue; + } + + var name = entry.Key?.Replace('/', Path.DirectorySeparatorChar); + if (string.IsNullOrEmpty(name)) + { + Logger.Debug(Extractor.ENTRY_MISSING_NAME_ERROR_MESSAGE_STRING, ArchiveFileType.ARC, fileEntry.FullPath); + continue; + } + + var newFileEntry = await FileEntry.FromStreamAsync(name, arcReader.OpenEntryStream(), fileEntry, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); + if (newFileEntry != null) + { + // SharpCompress ARC does not expose entry sizes, so we check the resource governor + // after extraction using the actual decompressed content length. + governor.CheckResourceGovernor(newFileEntry.Content.Length); + + if (options.Recurse || topLevel) + { + await foreach (var innerEntry in Context.ExtractAsync(newFileEntry, options, governor, false)) + { + yield return innerEntry; + } + } + else + { + yield return newFileEntry; + } + } + } + } + } + else + { + if (options.ExtractSelfOnFail) + { + fileEntry.EntryStatus = FileEntryStatus.FailedArchive; + yield return fileEntry; + } + } + } + + /// + /// Extracts an ARC archive + /// + /// + public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) + { + ArcReader? arcReader = null; + try + { + arcReader = ArcReader.Open(fileEntry.Content, new SharpCompress.Readers.ReaderOptions() + { + LeaveStreamOpen = true + }); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARC, fileEntry.FullPath, string.Empty, e.GetType()); + } + + if (arcReader != null) + { + using (arcReader) + { + while (arcReader.MoveToNextEntry()) + { + var entry = arcReader.Entry; + if (entry.IsDirectory) + { + continue; + } + + FileEntry? newFileEntry = null; + try + { + var stream = arcReader.OpenEntryStream(); + var name = entry.Key?.Replace('/', Path.DirectorySeparatorChar); + if (string.IsNullOrEmpty(name)) + { + Logger.Debug(Extractor.ENTRY_MISSING_NAME_ERROR_MESSAGE_STRING, ArchiveFileType.ARC, fileEntry.FullPath); + continue; + } + newFileEntry = new FileEntry(name, stream, fileEntry, false, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARC, fileEntry.FullPath, entry.Key, e.GetType()); + } + if (newFileEntry != null) + { + // SharpCompress ARC does not expose entry sizes, so we check the resource governor + // after extraction using the actual decompressed content length. + governor.CheckResourceGovernor(newFileEntry.Content.Length); + + if (options.Recurse || topLevel) + { + foreach (var innerEntry in Context.Extract(newFileEntry, options, governor, false)) + { + yield return innerEntry; + } + } + else + { + yield return newFileEntry; + } + } + } + } + } + else + { + if (options.ExtractSelfOnFail) + { + fileEntry.EntryStatus = FileEntryStatus.FailedArchive; + yield return fileEntry; + } + } + } + } +} diff --git a/RecursiveExtractor/Extractors/ArjExtractor.cs b/RecursiveExtractor/Extractors/ArjExtractor.cs new file mode 100644 index 00000000..88c38cf2 --- /dev/null +++ b/RecursiveExtractor/Extractors/ArjExtractor.cs @@ -0,0 +1,167 @@ +using SharpCompress.Readers.Arj; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.CST.RecursiveExtractor.Extractors +{ + /// + /// The ARJ Archive extractor implementation + /// + public class ArjExtractor : AsyncExtractorInterface + { + /// + /// The constructor takes the Extractor context for recursion. + /// + /// The Extractor context. + public ArjExtractor(Extractor context) + { + Context = context; + } + private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); + + internal Extractor Context { get; } + + /// + /// Extracts an ARJ archive + /// + /// + public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) + { + ArjReader? arjReader = null; + try + { + arjReader = ArjReader.Open(fileEntry.Content, new SharpCompress.Readers.ReaderOptions() + { + LeaveStreamOpen = true + }); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARJ, fileEntry.FullPath, string.Empty, e.GetType()); + } + + if (arjReader != null) + { + using (arjReader) + { + while (arjReader.MoveToNextEntry()) + { + var entry = arjReader.Entry; + if (entry.IsDirectory) + { + continue; + } + + governor.CheckResourceGovernor(entry.Size); + var name = entry.Key?.Replace('/', Path.DirectorySeparatorChar); + if (string.IsNullOrEmpty(name)) + { + Logger.Debug(Extractor.ENTRY_MISSING_NAME_ERROR_MESSAGE_STRING, ArchiveFileType.ARJ, fileEntry.FullPath); + continue; + } + + var newFileEntry = await FileEntry.FromStreamAsync(name, arjReader.OpenEntryStream(), fileEntry, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); + if (newFileEntry != null) + { + if (options.Recurse || topLevel) + { + await foreach (var innerEntry in Context.ExtractAsync(newFileEntry, options, governor, false)) + { + yield return innerEntry; + } + } + else + { + yield return newFileEntry; + } + } + } + } + } + else + { + if (options.ExtractSelfOnFail) + { + fileEntry.EntryStatus = FileEntryStatus.FailedArchive; + yield return fileEntry; + } + } + } + + /// + /// Extracts an ARJ archive + /// + /// + public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) + { + ArjReader? arjReader = null; + try + { + arjReader = ArjReader.Open(fileEntry.Content, new SharpCompress.Readers.ReaderOptions() + { + LeaveStreamOpen = true + }); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARJ, fileEntry.FullPath, string.Empty, e.GetType()); + } + + if (arjReader != null) + { + using (arjReader) + { + while (arjReader.MoveToNextEntry()) + { + var entry = arjReader.Entry; + if (entry.IsDirectory) + { + continue; + } + + governor.CheckResourceGovernor(entry.Size); + FileEntry? newFileEntry = null; + try + { + var stream = arjReader.OpenEntryStream(); + var name = entry.Key?.Replace('/', Path.DirectorySeparatorChar); + if (string.IsNullOrEmpty(name)) + { + Logger.Debug(Extractor.ENTRY_MISSING_NAME_ERROR_MESSAGE_STRING, ArchiveFileType.ARJ, fileEntry.FullPath); + continue; + } + newFileEntry = new FileEntry(name, stream, fileEntry, false, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff); + } + catch (Exception e) + { + Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.ARJ, fileEntry.FullPath, entry.Key, e.GetType()); + } + if (newFileEntry != null) + { + if (options.Recurse || topLevel) + { + foreach (var innerEntry in Context.Extract(newFileEntry, options, governor, false)) + { + yield return innerEntry; + } + } + else + { + yield return newFileEntry; + } + } + } + } + } + else + { + if (options.ExtractSelfOnFail) + { + fileEntry.EntryStatus = FileEntryStatus.FailedArchive; + yield return fileEntry; + } + } + } + } +} diff --git a/RecursiveExtractor/Extractors/RarExtractor.cs b/RecursiveExtractor/Extractors/RarExtractor.cs index e8b31406..e1096583 100644 --- a/RecursiveExtractor/Extractors/RarExtractor.cs +++ b/RecursiveExtractor/Extractors/RarExtractor.cs @@ -104,7 +104,7 @@ public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, Extra foreach (var entry in rarArchive.Entries.Where(x => x.IsComplete && !x.IsDirectory)) { governor.CheckResourceGovernor(entry.Size); - var name = entry.Key.Replace('/', Path.DirectorySeparatorChar); + var name = (entry.Key ?? string.Empty).Replace('/', Path.DirectorySeparatorChar); var newFileEntry = await FileEntry.FromStreamAsync(name, entry.OpenEntryStream(), fileEntry, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); if (newFileEntry != null) { @@ -149,7 +149,7 @@ public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions opti try { var stream = entry.OpenEntryStream(); - var name = entry.Key.Replace('/', Path.DirectorySeparatorChar); + var name = (entry.Key ?? string.Empty).Replace('/', Path.DirectorySeparatorChar); newFileEntry = new FileEntry(name, stream, fileEntry, false, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff); } catch (Exception e) diff --git a/RecursiveExtractor/Extractors/SevenZipExtractor.cs b/RecursiveExtractor/Extractors/SevenZipExtractor.cs index 50228a55..2f1e3a53 100644 --- a/RecursiveExtractor/Extractors/SevenZipExtractor.cs +++ b/RecursiveExtractor/Extractors/SevenZipExtractor.cs @@ -41,7 +41,7 @@ public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, Extra foreach (var entry in sevenZipArchive.Entries.Where(x => !x.IsDirectory && x.IsComplete).ToList()) { governor.CheckResourceGovernor(entry.Size); - var name = entry.Key.Replace('/', Path.DirectorySeparatorChar); + var name = (entry.Key ?? string.Empty).Replace('/', Path.DirectorySeparatorChar); var newFileEntry = await FileEntry.FromStreamAsync(name, entry.OpenEntryStream(), fileEntry, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); if (newFileEntry != null) @@ -154,7 +154,7 @@ public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions opti foreach (var entry in entries) { governor.CheckResourceGovernor(entry.Size); - var name = entry.Key.Replace('/', Path.DirectorySeparatorChar); + var name = (entry.Key ?? string.Empty).Replace('/', Path.DirectorySeparatorChar); var newFileEntry = new FileEntry(name, entry.OpenEntryStream(), fileEntry, createTime: entry.CreatedTime, modifyTime: entry.LastModifiedTime, accessTime: entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff); if (options.Recurse || topLevel) diff --git a/RecursiveExtractor/Extractors/TarExtractor.cs b/RecursiveExtractor/Extractors/TarExtractor.cs index 8553da07..9de51798 100644 --- a/RecursiveExtractor/Extractors/TarExtractor.cs +++ b/RecursiveExtractor/Extractors/TarExtractor.cs @@ -70,7 +70,7 @@ public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, Extra continue; } // Remove leading ./ - while (name.StartsWith($".{Path.DirectorySeparatorChar}")) + while (name!.StartsWith($".{Path.DirectorySeparatorChar}")) { name = name[2..]; } @@ -140,7 +140,7 @@ public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions opti continue; } // Remove leading ./ - while (name.StartsWith($".{Path.DirectorySeparatorChar}")) + while (name!.StartsWith($".{Path.DirectorySeparatorChar}")) { name = name[2..]; } diff --git a/RecursiveExtractor/Extractors/ZipExtractor.cs b/RecursiveExtractor/Extractors/ZipExtractor.cs index 0c786230..5b706b44 100644 --- a/RecursiveExtractor/Extractors/ZipExtractor.cs +++ b/RecursiveExtractor/Extractors/ZipExtractor.cs @@ -50,7 +50,7 @@ public ZipExtractor(Extractor context) using var testStream = testEntry.OpenEntryStream(); // If we can read without exception, password is correct var buffer = new byte[1]; - testStream.Read(buffer, 0, 1); + testStream.ReadExactly(buffer, 0, 1); return password; } } diff --git a/RecursiveExtractor/MiniMagic.cs b/RecursiveExtractor/MiniMagic.cs index 932ac5db..d0f53ca1 100644 --- a/RecursiveExtractor/MiniMagic.cs +++ b/RecursiveExtractor/MiniMagic.cs @@ -81,10 +81,18 @@ public enum ArchiveFileType /// 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 + + + + +