├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── Directory.Build.props ├── LICENSE.txt ├── NOTICE.txt ├── PRIVACY.md ├── Pipelines ├── recursive-extractor-pr.yml └── recursive-extractor-release.yml ├── README.md ├── RecursiveExtractor.Cli.Tests ├── CliTests │ └── CliTests.cs ├── RecursiveExtractor.Cli.Tests.csproj └── Usings.cs ├── RecursiveExtractor.Cli ├── ExtractCommandOptions.cs ├── Properties │ └── launchSettings.json ├── RecursiveExtractor.Cli.csproj ├── RecursiveExtractor.Cli.csproj.user └── RecursiveExtractorClient.cs ├── RecursiveExtractor.Tests ├── ExtractorTests │ ├── BaseExtractorTestClass.cs │ ├── DisposeBehaviorTests.cs │ ├── EncryptedArchiveTests.cs │ ├── ExpectedNumFilesTests.cs │ ├── FilterTests.cs │ ├── MiniMagicTests.cs │ ├── MiscTests.cs │ ├── TestQuinesAndSlip.cs │ └── TimeOutTests.cs ├── RecursiveExtractor.Tests.csproj ├── SanitizePathTests.cs ├── TestData │ ├── Bombs │ │ ├── 10GB.7z.bz2 │ │ ├── 10GB.bz2 │ │ ├── 10GB.gz.bz2 │ │ ├── 10GB.rar.bz2 │ │ ├── 10GB.xz.bz2 │ │ ├── 10GB.zip.bz2 │ │ ├── zblg.zip │ │ ├── zbsm.zip │ │ ├── zip-slip-win.tar │ │ ├── zip-slip-win.zip │ │ ├── zip-slip.tar │ │ └── zip-slip.zip │ ├── TestData │ │ ├── Foo │ │ │ ├── Bar │ │ │ │ └── Dolor.txt │ │ │ └── Ipsum.txt │ │ └── Lorem.txt │ └── TestDataArchives │ │ ├── .gitattributes │ │ ├── 100trees.7z │ │ ├── Empty.vmdk │ │ ├── EmptyFile.txt │ │ ├── EncryptedWithPlainNames.7z │ │ ├── EncryptedWithPlainNames.rar4 │ │ ├── HfsSampleUDCO.dmg │ │ ├── Lorem.txt │ │ ├── TestData.7z │ │ ├── TestData.a │ │ ├── TestData.bsd.ar │ │ ├── TestData.cdr │ │ ├── TestData.iso │ │ ├── TestData.rar │ │ ├── TestData.rar4 │ │ ├── TestData.tar │ │ ├── TestData.tar.bz2 │ │ ├── TestData.tar.gz │ │ ├── TestData.tar.xz │ │ ├── TestData.vhdx │ │ ├── TestData.wim │ │ ├── TestData.zip │ │ ├── TestDataArchivesNested.zip │ │ ├── TestDataCorrupt.tar │ │ ├── TestDataCorrupt.tar.zip │ │ ├── TestDataCorruptWim.zip │ │ ├── TestDataEncrypted.7z │ │ ├── TestDataEncrypted.rar │ │ ├── TestDataEncrypted.rar4 │ │ ├── TestDataEncryptedAES.zip │ │ ├── TestDataEncryptedZipCrypto.zip │ │ ├── TestDataForFilters.7z │ │ ├── UdfTest.iso │ │ ├── UdfTestWithMultiSystem.iso │ │ └── sysvbanner_1.0-17fakesync1_amd64.deb └── TestPathHelpers.cs ├── RecursiveExtractor.sln ├── RecursiveExtractor ├── ArFile.cs ├── DebArchiveFile.cs ├── ExtractionStatusCode.cs ├── Extractor.cs ├── ExtractorOptions.cs ├── Extractors │ ├── AsyncExtractorInterface.cs │ ├── BZip2Extractor.cs │ ├── DebExtractor.cs │ ├── DiscCommon.cs │ ├── ExtractorInterface.cs │ ├── GnuArExtractor.cs │ ├── GzipExtractor.cs │ ├── IsoExtractor.cs │ ├── RarExtractor.cs │ ├── SevenZipExtractor.cs │ ├── TarExtractor.cs │ ├── UdfExtractor.cs │ ├── VhdExtractor.cs │ ├── VhdxExtractor.cs │ ├── VmdkExtractor.cs │ ├── WimExtractor.cs │ ├── XzExtractor.cs │ └── ZipExtractor.cs ├── FileEntry.cs ├── FileEntryInfo.cs ├── MiniMagic.cs ├── README.md ├── Range.cs ├── RecursiveExtractor.csproj ├── ResourceGovernor.cs ├── StreamFactory.cs └── TempPath.cs ├── SECURITY.md ├── icon-128.png └── version.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Don't mess with .ar files 5 | *.ar binary 6 | *.deb binary -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | paths: 6 | - RecursiveExtractor.Blazor 7 | - RecursiveExtractor.Cli 8 | - RecursiveExtractor.Tests 9 | - RecursiveExtractor 10 | pull_request: 11 | schedule: 12 | - cron: '0 5 * * 2' 13 | 14 | jobs: 15 | CodeQL-Build: 16 | 17 | strategy: 18 | fail-fast: false 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v2 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Setup .NET Core SDK 29 | uses: actions/setup-dotnet@v1.7.2 30 | with: 31 | dotnet-version: 8.0.x 32 | 33 | - name: Initialize CodeQL 34 | uses: github/codeql-action/init@v2 35 | with: 36 | languages: csharp 37 | 38 | - name: Build RecursiveExtractor 39 | run: | 40 | dotnet restore RecursiveExtractor.sln 41 | dotnet build -c Release 42 | 43 | - name: Perform CodeQL Analysis 44 | uses: github/codeql-action/analyze@v2 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | .idea/ 4 | .vs 5 | RecursiveExtractor.sln.DotSettings.user 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/RecursiveExtractor.Cli/bin/Debug/netcoreapp3.1/RecursiveExtractor.Cli.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/RecursiveExtractor.Cli", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet-test-explorer.testProjectPath": "**/*Tests.csproj" 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "--framework", 11 | "netcoreapp3.1", 12 | "${workspaceFolder}/RecursiveExtractor.Cli/RecursiveExtractor.Cli.csproj", 13 | "/property:GenerateFullPaths=true", 14 | "/consoleloggerparameters:NoSummary" 15 | ], 16 | "problemMatcher": "$msCompile" 17 | }, 18 | { 19 | "label": "publish", 20 | "command": "dotnet", 21 | "type": "process", 22 | "args": [ 23 | "publish", 24 | "${workspaceFolder}/RecursiveExtractor.Cli/RecursiveExtractor.Cli.csproj", 25 | "/property:GenerateFullPaths=true", 26 | "/consoleloggerparameters:NoSummary" 27 | ], 28 | "problemMatcher": "$msCompile" 29 | }, 30 | { 31 | "label": "watch", 32 | "command": "dotnet", 33 | "type": "process", 34 | "args": [ 35 | "watch", 36 | "run", 37 | "${workspaceFolder}/RecursiveExtractor.Cli/RecursiveExtractor.Cli.csproj", 38 | "/property:GenerateFullPaths=true", 39 | "/consoleloggerparameters:NoSummary" 40 | ], 41 | "problemMatcher": "$msCompile" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 3.7.115 6 | all 7 | 8 | 9 | 10 | 11 | 12 | true 13 | true 14 | Recommended 15 | 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation. All rights reserved. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PRIVACY.md: -------------------------------------------------------------------------------- 1 | # Privacy and Telemetry Notice 2 | 3 | ## Data Collection 4 | 5 | The Recursive Extractor software itself does *not* collect information about your 6 | use of the software, and therefore does not send such information to any source, 7 | including to Microsoft. Nevertheless, our privacy statement is located at 8 | https://go.microsoft.com/fwlink/?LinkID=824704. -------------------------------------------------------------------------------- /Pipelines/recursive-extractor-pr.yml: -------------------------------------------------------------------------------- 1 | # Azure Pipelines 2 | # https://aka.ms/yaml 3 | 4 | name: RecursiveExtractor_PR_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) 5 | trigger: none 6 | pr: 7 | branches: 8 | include: 9 | - main 10 | 11 | resources: 12 | repositories: 13 | - repository: templates 14 | type: git 15 | name: SecurityEngineering/OSS-Tools-Pipeline-Templates 16 | ref: refs/tags/v2.0.1 17 | - repository: 1esPipelines 18 | type: git 19 | name: 1ESPipelineTemplates/1ESPipelineTemplates 20 | ref: refs/tags/release 21 | 22 | variables: 23 | BuildConfiguration: 'Release' 24 | DotnetVersion: '9.0.x' 25 | 26 | extends: 27 | template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines 28 | parameters: 29 | pool: 30 | name: MSSecurity-1ES-Build-Agents-Pool 31 | image: MSSecurity-1ES-Windows-2022 32 | os: windows 33 | sdl: 34 | armory: 35 | enabled: false 36 | stages: 37 | - stage: Test 38 | dependsOn: [] 39 | jobs: 40 | - template: dotnet-test-job.yml@templates 41 | parameters: 42 | jobName: 'lib_dotnet_test_windows' 43 | dotnetVersions: ['8.0.x', '9.0.x'] 44 | projectPath: 'RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj' 45 | poolName: MSSecurity-1ES-Build-Agents-Pool 46 | poolImage: MSSecurity-1ES-Windows-2022 47 | poolOs: windows 48 | dotnetTestArgs: '-- --coverage --report-trx' 49 | - template: dotnet-test-job.yml@templates 50 | parameters: 51 | jobName: 'cli_dotnet_test_windows' 52 | dotnetVersions: ['8.0.x', '9.0.x'] 53 | projectPath: 'RecursiveExtractor.Cli.Tests/RecursiveExtractor.Cli.Tests.csproj' 54 | poolName: MSSecurity-1ES-Build-Agents-Pool 55 | poolImage: MSSecurity-1ES-Windows-2022 56 | poolOs: windows 57 | dotnetTestArgs: '-- --coverage --report-trx' 58 | 59 | - stage: Build 60 | dependsOn: 61 | - Test 62 | jobs: 63 | - template: nuget-build-job.yml@templates 64 | parameters: 65 | jobName: 'pack_lib' 66 | buildConfiguration: ${{ variables.BuildConfiguration }} 67 | dotnetVersion: ${{ variables.DotnetVersion }} 68 | projectPath: 'RecursiveExtractor/RecursiveExtractor.csproj' 69 | projectName: 'RecursiveExtractor' 70 | customPackFlags: '/p:ContinuousIntegrationBuild=true' 71 | artifactName: 'lib-archive' 72 | preBuild: 73 | - template: nbgv-set-version-steps.yml@templates 74 | - template: nuget-build-job.yml@templates 75 | parameters: 76 | jobName: 'pack_cli' 77 | buildConfiguration: ${{ variables.BuildConfiguration }} 78 | dotnetVersion: ${{ variables.DotnetVersion }} 79 | projectPath: 'RecursiveExtractor.Cli/RecursiveExtractor.Cli.csproj' 80 | projectName: 'RecursiveExtractor_CLI' 81 | customPackFlags: '/p:ContinuousIntegrationBuild=true' 82 | artifactName: 'cli-archive' 83 | preBuild: 84 | - template: nbgv-set-version-steps.yml@templates 85 | -------------------------------------------------------------------------------- /Pipelines/recursive-extractor-release.yml: -------------------------------------------------------------------------------- 1 | name: RecursiveExtractor_Release_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:.r) 2 | trigger: none 3 | pr: none 4 | 5 | resources: 6 | repositories: 7 | - repository: templates 8 | type: git 9 | name: Data/OSS-Tools-Pipeline-Templates 10 | ref: refs/tags/v2.0.1 11 | - repository: 1esPipelines 12 | type: git 13 | name: 1ESPipelineTemplates/1ESPipelineTemplates 14 | ref: refs/tags/release 15 | 16 | variables: 17 | BuildConfiguration: 'Release' 18 | DotnetVersion: '9.0.x' 19 | 20 | extends: 21 | template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines 22 | parameters: 23 | pool: 24 | name: MSSecurity-1ES-Build-Agents-Pool 25 | image: MSSecurity-1ES-Windows-2022 26 | os: windows 27 | sdl: 28 | armory: 29 | enabled: false 30 | sourceRepositoriesToScan: 31 | exclude: 32 | - repository: 1esPipelines 33 | - repository: templates 34 | stages: 35 | - stage: Test 36 | dependsOn: [] 37 | jobs: 38 | - template: dotnet-test-job.yml@templates 39 | parameters: 40 | jobName: 'lib_dotnet_test_windows' 41 | dotnetVersions: ['8.0.x', '9.0.x'] 42 | projectPath: 'RecursiveExtractor.Tests/RecursiveExtractor.Tests.csproj' 43 | poolName: MSSecurity-1ES-Build-Agents-Pool 44 | poolImage: MSSecurity-1ES-Windows-2022 45 | poolOs: windows 46 | dotnetTestArgs: '-- --coverage --report-trx' 47 | - template: dotnet-test-job.yml@templates 48 | parameters: 49 | jobName: 'cli_dotnet_test_windows' 50 | dotnetVersions: ['8.0.x', '9.0.x'] 51 | projectPath: 'RecursiveExtractor.Cli.Tests/RecursiveExtractor.Cli.Tests.csproj' 52 | poolName: MSSecurity-1ES-Build-Agents-Pool 53 | poolImage: MSSecurity-1ES-Windows-2022 54 | poolOs: windows 55 | dotnetTestArgs: '-- --coverage --report-trx' 56 | 57 | - stage: Build 58 | dependsOn: 59 | - Test 60 | jobs: 61 | - template: nuget-build-job.yml@templates 62 | parameters: 63 | jobName: 'pack_lib' 64 | buildConfiguration: ${{ variables.BuildConfiguration }} 65 | dotnetVersion: ${{ variables.DotnetVersion }} 66 | projectPath: 'RecursiveExtractor/RecursiveExtractor.csproj' 67 | projectName: 'RecursiveExtractor' 68 | customPackFlags: '/p:ContinuousIntegrationBuild=true' 69 | artifactName: 'lib-archive' 70 | preBuild: 71 | - template: nbgv-set-version-steps.yml@templates 72 | - template: nuget-build-job.yml@templates 73 | parameters: 74 | jobName: 'pack_cli' 75 | buildConfiguration: ${{ variables.BuildConfiguration }} 76 | dotnetVersion: ${{ variables.DotnetVersion }} 77 | projectPath: 'RecursiveExtractor.Cli/RecursiveExtractor.Cli.csproj' 78 | projectName: 'RecursiveExtractor_CLI' 79 | customPackFlags: '/p:ContinuousIntegrationBuild=true' 80 | artifactName: 'cli-archive' 81 | preBuild: 82 | - template: nbgv-set-version-steps.yml@templates 83 | 84 | - stage: Release 85 | dependsOn: 86 | - Build 87 | condition: succeeded() 88 | jobs: 89 | - job: sign_hash_release 90 | displayName: Code Sign, Generate Hashes, Publish Public Releases 91 | templateContext: 92 | outputs: 93 | - output: pipelineArtifact 94 | path: '$(Build.StagingDirectory)' 95 | artifact: 'Signed_Binaries_$(System.JobId)_$(System.JobAttempt)' 96 | # see https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-docs/1es-pipeline-templates/features/outputs/nuget-packages 97 | - output: nuget 98 | useDotNetTask: false 99 | packagesToPush: '$(Build.StagingDirectory)/*.nupkg' 100 | packageParentPath: '$(Build.StagingDirectory)' 101 | nuGetFeedType: external 102 | publishPackageMetadata: true 103 | publishFeedCredentials: 'sdl-oss-nuget-publish' 104 | steps: 105 | - template: nbgv-set-version-steps.yml@templates 106 | - task: UseDotNet@2 107 | inputs: 108 | packageType: 'sdk' 109 | version: '6.0.x' # ESRP requires a specific version. 110 | - task: DownloadPipelineArtifact@2 111 | inputs: 112 | displayName: 'Download lib-archive' 113 | buildType: 'current' 114 | artifactName: 'lib-archive' 115 | targetPath: $(Build.BinariesDirectory)\Unsigned_Binaries\ 116 | - task: DownloadPipelineArtifact@2 117 | inputs: 118 | displayName: 'Download cli-archive' 119 | buildType: 'current' 120 | artifactName: 'cli-archive' 121 | targetPath: $(Build.BinariesDirectory)\Unsigned_Binaries\ 122 | - task: ExtractFiles@1 123 | displayName: Extract Artifacts for Signing 124 | inputs: 125 | archiveFilePatterns: '$(Build.BinariesDirectory)\Unsigned_Binaries\*.zip' 126 | destinationFolder: '$(Build.BinariesDirectory)' 127 | cleanDestinationFolder: false 128 | overwriteExistingFiles: true 129 | - task: AntiMalware@4 130 | displayName: Anti-Malware Scan 131 | inputs: 132 | InputType: 'Basic' 133 | ScanType: 'CustomScan' 134 | FileDirPath: '$(Build.BinariesDirectory)' 135 | EnableServices: true 136 | SupportLogOnError: true 137 | TreatSignatureUpdateFailureAs: 'Warning' 138 | SignatureFreshness: 'UpToDate' 139 | TreatStaleSignatureAs: 'Warning' 140 | - task: EsrpCodeSigning@5 141 | displayName: Code Sign Nuget Packages 142 | inputs: 143 | ConnectedServiceName: 'oss-esrp-signing-recext-v5-connection' 144 | AppRegistrationClientId: 'caf746ee-b288-4155-8cc0-0bedca65f230' 145 | AppRegistrationTenantId: '33e01921-4d64-4f8c-a055-5bdaffd5e33d' 146 | AuthAKVName: 'oss-signing-vault' 147 | AuthCertName: 'oss-recursive-auth-cert' 148 | AuthSignCertName: 'oss-recursive-signing-cert' 149 | FolderPath: '$(Build.BinariesDirectory)' 150 | Pattern: '*.nupkg, *.snupkg' 151 | signConfigType: 'inlineSignParams' 152 | inlineOperation: | 153 | [ 154 | { 155 | "KeyCode" : "CP-401405", 156 | "OperationCode" : "NuGetSign", 157 | "Parameters" : {}, 158 | "ToolName" : "sign", 159 | "ToolVersion" : "1.0" 160 | }, 161 | { 162 | "KeyCode" : "CP-401405", 163 | "OperationCode" : "NuGetVerify", 164 | "Parameters" : {}, 165 | "ToolName" : "sign", 166 | "ToolVersion" : "1.0" 167 | } 168 | ] 169 | SessionTimeout: '60' 170 | MaxConcurrency: '50' 171 | MaxRetryAttempts: '5' 172 | - powershell: 'Get-ChildItem -Path ''$(Build.BinariesDirectory)'' -Recurse CodeSign* | foreach { Remove-Item -Path $_.FullName }' 173 | displayName: 'Delete Code Sign Summaries' 174 | - task: PowerShell@2 175 | displayName: Move NuGet Packages 176 | inputs: 177 | targetType: 'inline' 178 | script: | 179 | mv $env:BUILD_BINARIESDIRECTORY/*.nupkg $env:BUILD_STAGINGDIRECTORY/ 180 | mv $env:BUILD_BINARIESDIRECTORY/*.snupkg $env:BUILD_STAGINGDIRECTORY/ -------------------------------------------------------------------------------- /RecursiveExtractor.Cli.Tests/CliTests/CliTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. Licensed under the MIT License. 2 | 3 | using Microsoft.CST.RecursiveExtractor; 4 | using Microsoft.CST.RecursiveExtractor.Cli; 5 | using Microsoft.CST.RecursiveExtractor.Tests; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | using RecursiveExtractor.Tests.ExtractorTests; 8 | using System; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Threading; 12 | 13 | namespace RecursiveExtractor.Tests.CliTests 14 | { 15 | [TestClass] 16 | public class CliTests : BaseExtractorTestClass 17 | { 18 | [DataTestMethod] 19 | [DataRow("TestData.zip", 5)] 20 | [DataRow("TestData.7z")] 21 | [DataRow("TestData.tar", 6)] 22 | [DataRow("TestData.rar")] 23 | [DataRow("TestData.rar4")] 24 | [DataRow("TestData.tar.bz2", 6)] 25 | [DataRow("TestData.tar.gz", 6)] 26 | [DataRow("TestData.tar.xz")] 27 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8)] 28 | [DataRow("TestData.a")] 29 | [DataRow("TestData.bsd.ar")] 30 | [DataRow("TestData.iso")] 31 | [DataRow("TestData.vhdx")] 32 | [DataRow("TestData.wim")] 33 | [DataRow("EmptyFile.txt", 1)] 34 | [DataRow("TestDataArchivesNested.Zip", 54)] 35 | public void ExtractArchiveParallel(string fileName, int expectedNumFiles = 3) 36 | { 37 | ExtractArchive(fileName, expectedNumFiles, false); 38 | } 39 | 40 | internal void ExtractArchive(string fileName, int expectedNumFiles, bool singleThread) 41 | { 42 | var directory = TestPathHelpers.GetFreshTestDirectory(); 43 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 44 | RecursiveExtractorClient.ExtractCommand(new ExtractCommandOptions() { Input = path, Output = directory, Verbose = true, SingleThread = singleThread}); 45 | var files = Array.Empty(); 46 | Thread.Sleep(100); 47 | if (Directory.Exists(directory)) 48 | { 49 | files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToArray(); 50 | } 51 | Assert.AreEqual(expectedNumFiles, files.Length); 52 | } 53 | 54 | [DataTestMethod] 55 | [DataRow("TestData.zip", 5)] 56 | [DataRow("TestData.7z")] 57 | [DataRow("TestData.tar", 6)] 58 | [DataRow("TestData.rar")] 59 | [DataRow("TestData.rar4")] 60 | [DataRow("TestData.tar.bz2", 6)] 61 | [DataRow("TestData.tar.gz", 6)] 62 | [DataRow("TestData.tar.xz")] 63 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8)] 64 | [DataRow("TestData.a")] 65 | [DataRow("TestData.bsd.ar")] 66 | [DataRow("TestData.iso")] 67 | [DataRow("TestData.vhdx")] 68 | [DataRow("TestData.wim")] 69 | [DataRow("EmptyFile.txt", 1)] 70 | [DataRow("TestDataArchivesNested.Zip", 54)] 71 | public void ExtractArchiveSingleThread(string fileName, int expectedNumFiles = 3) 72 | { 73 | ExtractArchive(fileName, expectedNumFiles, true); 74 | } 75 | 76 | [DataTestMethod] 77 | [DataRow("TestDataForFilters.7z")] 78 | public void ExtractArchiveWithAllowFilters(string fileName, int expectedNumFiles = 1) 79 | { 80 | var directory = TestPathHelpers.GetFreshTestDirectory(); 81 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 82 | var newpath = TempPath.GetTempFilePath(); 83 | File.Copy(path, newpath,true); 84 | RecursiveExtractorClient.ExtractCommand(new ExtractCommandOptions() 85 | { 86 | Input = newpath, 87 | Output = directory, 88 | Verbose = true, 89 | AllowFilters = new string[] 90 | { 91 | "*.cs" 92 | } 93 | }); 94 | var files = Array.Empty(); 95 | if (Directory.Exists(directory)) 96 | { 97 | files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToArray(); 98 | } 99 | Assert.AreEqual(expectedNumFiles, files.Length); 100 | } 101 | 102 | [DataTestMethod] 103 | [DataRow("TestDataForFilters.7z")] 104 | public void ExtractArchiveWithDenyFilters(string fileName, int expectedNumFiles = 2) 105 | { 106 | var directory = TestPathHelpers.GetFreshTestDirectory(); 107 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 108 | var newpath = TempPath.GetTempFilePath(); 109 | File.Copy(path, newpath, true); 110 | RecursiveExtractorClient.ExtractCommand(new ExtractCommandOptions() 111 | { 112 | Input = newpath, 113 | Output = directory, 114 | Verbose = true, 115 | DenyFilters = new string[] 116 | { 117 | "*.cs" 118 | } 119 | }); 120 | var files = Array.Empty(); 121 | if (Directory.Exists(directory)) 122 | { 123 | files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToArray(); 124 | } 125 | Assert.AreEqual(expectedNumFiles, files.Length); 126 | } 127 | 128 | [DataTestMethod] 129 | [DataRow("TestDataEncrypted.7z")] 130 | [DataRow("TestDataEncryptedAes.zip")] 131 | [DataRow("TestDataEncrypted.rar4")] 132 | public void ExtractEncryptedArchive(string fileName, int expectedNumFiles = 3) 133 | { 134 | var directory = TestPathHelpers.GetFreshTestDirectory(); 135 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 136 | var passwords = EncryptedArchiveTests.TestArchivePasswords.Values.SelectMany(x => x); 137 | RecursiveExtractorClient.ExtractCommand(new ExtractCommandOptions() { Input = path, Output = directory, Verbose = true, Passwords = passwords }); 138 | string[] files = Directory.EnumerateFiles(directory, "*.*", SearchOption.AllDirectories).ToArray(); 139 | Assert.AreEqual(expectedNumFiles, files.Length); 140 | } 141 | 142 | protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 143 | } 144 | } -------------------------------------------------------------------------------- /RecursiveExtractor.Cli.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | -------------------------------------------------------------------------------- /RecursiveExtractor.Cli/ExtractCommandOptions.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using System.Collections.Generic; 3 | 4 | namespace Microsoft.CST.RecursiveExtractor.Cli 5 | { 6 | [Verb("extract", true, HelpText = "Extract an archive recursively.")] 7 | public class ExtractCommandOptions 8 | { 9 | [Option('i',"input", HelpText = "The name of the archive to extract.", Required = true)] 10 | public string Input { get; set; } = string.Empty; 11 | 12 | [Option('o', "output", HelpText = "The directory to extract to.", Default = ".")] 13 | public string Output { get; set; } = string.Empty; 14 | 15 | [Option('p', "passwords", Required = false, HelpText = "Comma-separated list of passwords to use.", Separator = ',')] 16 | public IEnumerable? Passwords { get; set; } 17 | 18 | [Option('a', "allow-globs", Required = false, HelpText = "Comma-separated list of glob expressions. When set, files are ONLY written to disk if they match one of these filters.", Separator = ',')] 19 | public IEnumerable? AllowFilters { get; set; } 20 | 21 | [Option('d', "deny-globs", Required = false, HelpText = "Comma-separated list of glob expressions. When set, files are NOT written to disk if they match one of these filters.", Separator = ',')] 22 | public IEnumerable? DenyFilters { get; set; } 23 | 24 | [Option('R', "raw-extensions", Required = false, HelpText = "Comma-separated list of file extensions to treat as raw files (don't recurse into).", Separator = ',')] 25 | public IEnumerable? RawExtensions { get; set; } 26 | 27 | [Option('n', "no-recursion", Required = false, HelpText = "Disable recursive extraction.")] 28 | public bool DisableRecursion { get; set; } 29 | 30 | [Option('s', "single-thread", Required = false, HelpText = "Disable parallelized extraction.")] 31 | public bool SingleThread { get; set; } 32 | 33 | [Option(HelpText = "Set logging to 'verbose'.")] 34 | public bool Verbose { get; set; } 35 | 36 | [Option(HelpText = "Set logging to 'debug'.")] 37 | public bool Debug { get; set; } 38 | 39 | [Option(HelpText = "Output the names of all files extracted.")] 40 | public bool PrintNames { get; set; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /RecursiveExtractor.Cli/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "RecursiveExtractor.Cli": { 4 | "commandName": "Project", 5 | "commandLineArgs": "--input C:\\Users\\gstocco\\ExampleFaultyNumberOfEntries.tar --output C:\\Users\\gstocco\\ExampleFaultyNumberOfEntriesOut" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /RecursiveExtractor.Cli/RecursiveExtractor.Cli.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0;net9.0 6 | 10.0 7 | enable 8 | Microsoft.CST.RecursiveExtractor.Cli 9 | © Microsoft Corporation. All rights reserved. 10 | Recursive Extractor 11 | Microsoft 12 | Microsoft 13 | RecursiveExtractor is able to process the following formats: ar, bzip2, deb, gzip, iso, tar, vhd, vhdx, vmdk, wim, xzip, and zip. RecursiveExtractor automatically detects the archive type and fails gracefully when attempting to process malformed content. 14 | false 15 | true 16 | Microsoft.CST.RecursiveExtractor.CLI 17 | 0.0.0 18 | https://github.com/microsoft/RecursiveExtractor 19 | unzip extract extractor 20 | RecursiveExtractor 21 | LICENSE.txt 22 | icon-128.png 23 | true 24 | snupkg 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /RecursiveExtractor.Cli/RecursiveExtractor.Cli.csproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Project 5 | --input /Users/gabe/Documents/GitHub/RecursiveExtractor/RecursiveExtractor.Tests/TestData/Nested.zip --output /Users/gabe/Documents/GitHub/RecursiveExtractor/NestedExtracted --verbose 6 | true 7 | 8 | -------------------------------------------------------------------------------- /RecursiveExtractor.Cli/RecursiveExtractorClient.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | using NLog; 8 | using NLog.Config; 9 | using NLog.Targets; 10 | namespace Microsoft.CST.RecursiveExtractor.Cli 11 | { 12 | public static class RecursiveExtractorClient 13 | { 14 | public static int Main(string[] args) 15 | { 16 | return CommandLine.Parser.Default.ParseArguments(args) 17 | .MapResult( 18 | (ExtractCommandOptions opts) => ExtractCommand(opts), 19 | _ => 1); 20 | } 21 | 22 | public static int ExtractCommand(ExtractCommandOptions options) 23 | { 24 | var config = new LoggingConfiguration(); 25 | var consoleTarget = new ConsoleTarget 26 | { 27 | Name = "console", 28 | Layout = "${longdate}|${level:uppercase=true}|${logger}|${message}", 29 | }; 30 | if (options.Verbose) 31 | { 32 | config.AddRule(LogLevel.Trace, LogLevel.Fatal, consoleTarget, "*"); 33 | } 34 | else if (options.Debug) 35 | { 36 | config.AddRule(LogLevel.Debug, LogLevel.Fatal, consoleTarget, "*"); 37 | } 38 | else 39 | { 40 | config.AddRule(LogLevel.Info, LogLevel.Fatal, consoleTarget, "*"); 41 | } 42 | 43 | LogManager.Configuration = config; 44 | 45 | var extractor = new Extractor(); 46 | var extractorOptions = new ExtractorOptions() 47 | { 48 | ExtractSelfOnFail = true, 49 | Parallel = !options.SingleThread, 50 | RawExtensions = options.RawExtensions ?? Array.Empty(), 51 | Recurse = !options.DisableRecursion, 52 | AllowFilters = options.AllowFilters ?? Array.Empty(), 53 | DenyFilters = options.DenyFilters ?? Array.Empty() 54 | }; 55 | if (options.Passwords?.Any() ?? false) 56 | { 57 | extractorOptions.Passwords = new Dictionary>() 58 | { 59 | { 60 | new Regex(".*",RegexOptions.Compiled), 61 | options.Passwords.ToList() 62 | } 63 | }; 64 | } 65 | var exitCode = ExtractionStatusCode.Ok; 66 | try 67 | { 68 | exitCode = extractor.ExtractToDirectory(options.Output, options.Input, extractorOptions, options.PrintNames); 69 | } 70 | catch(Exception e) 71 | { 72 | Logger.Error($"Exception while extracting. {e.GetType()}:{e.Message} ({e.StackTrace})"); 73 | exitCode = ExtractionStatusCode.Failure; 74 | } 75 | 76 | return (int)exitCode; 77 | } 78 | private readonly static NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/ExtractorTests/BaseExtractorTestClass.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CST.RecursiveExtractor.Tests; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NLog; 4 | using NLog.Config; 5 | using NLog.Targets; 6 | 7 | namespace RecursiveExtractor.Tests.ExtractorTests; 8 | 9 | public class BaseExtractorTestClass 10 | { 11 | [ClassCleanup] 12 | public static void ClassCleanup() 13 | { 14 | TestPathHelpers.DeleteTestDirectory(); 15 | } 16 | 17 | [ClassInitialize] 18 | public static void ClassInitialize(TestContext context) 19 | { 20 | var config = new LoggingConfiguration(); 21 | var consoleTarget = new ConsoleTarget 22 | { 23 | Name = "console", 24 | Layout = "${longdate}|${level:uppercase=true}|${logger}|${message}", 25 | }; 26 | config.AddRule(LogLevel.Trace, LogLevel.Fatal, consoleTarget, "*"); 27 | 28 | LogManager.Configuration = config; 29 | } 30 | protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 31 | } -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/ExtractorTests/DisposeBehaviorTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CST.RecursiveExtractor; 2 | using Microsoft.CST.RecursiveExtractor.Tests; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Threading.Tasks; 8 | 9 | namespace RecursiveExtractor.Tests.ExtractorTests; 10 | 11 | [TestClass] 12 | public class DisposeBehaviorTests : BaseExtractorTestClass 13 | { 14 | [DataTestMethod] 15 | [DataRow("TestData.7z", 3, false)] 16 | [DataRow("TestData.tar", 6, false)] 17 | [DataRow("TestData.rar", 3, false)] 18 | [DataRow("TestData.rar4", 3, false)] 19 | [DataRow("TestData.tar.bz2", 6, false)] 20 | [DataRow("TestData.tar.gz", 6, false)] 21 | [DataRow("TestData.tar.xz", 3, false)] 22 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, false)] 23 | [DataRow("TestData.a", 3, false)] 24 | [DataRow("TestData.bsd.ar", 3, false)] 25 | [DataRow("TestData.iso", 3, false)] 26 | [DataRow("TestData.vhdx", 3, false)] 27 | [DataRow("TestData.wim", 3, false)] 28 | [DataRow("EmptyFile.txt", 1, false)] 29 | [DataRow("TestData.zip", 5, true)] 30 | [DataRow("TestData.7z", 3, true)] 31 | [DataRow("TestData.tar", 6, true)] 32 | [DataRow("TestData.rar", 3, true)] 33 | [DataRow("TestData.rar4", 3, true)] 34 | [DataRow("TestData.tar.bz2", 6, true)] 35 | [DataRow("TestData.tar.gz", 6, true)] 36 | [DataRow("TestData.tar.xz", 3, true)] 37 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, true)] 38 | [DataRow("TestData.a", 3, true)] 39 | [DataRow("TestData.bsd.ar", 3, true)] 40 | [DataRow("TestData.iso", 3, true)] 41 | [DataRow("TestData.vhdx", 3, true)] 42 | [DataRow("TestData.wim", 3, true)] 43 | [DataRow("EmptyFile.txt", 1, true)] 44 | [DataRow("TestDataArchivesNested.Zip", 54, true)] 45 | [DataRow("TestDataArchivesNested.Zip", 54, false)] 46 | [DataRow("TestDataArchivesNested.Zip", 54, true)] 47 | [DataRow("TestDataArchivesNested.Zip", 54, false)] 48 | public void ExtractArchiveAndDisposeWhileEnumerating(string fileName, int expectedNumFiles = 3, 49 | bool parallel = false) 50 | { 51 | var extractor = new Extractor(); 52 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 53 | var results = extractor.Extract(path, new ExtractorOptions() { Parallel = parallel }); 54 | var disposedResults = new List(); 55 | foreach (var file in results) 56 | { 57 | disposedResults.Add(file); 58 | using var theStream = file.Content; 59 | // Do something with the stream. 60 | _ = theStream.ReadByte(); 61 | } 62 | 63 | Assert.AreEqual(expectedNumFiles, disposedResults.Count); 64 | foreach (var disposedResult in disposedResults) 65 | { 66 | Assert.ThrowsException(() => disposedResult.Content.Position); 67 | } 68 | } 69 | 70 | [DataTestMethod] 71 | [DataRow("TestData.7z", 3, false)] 72 | [DataRow("TestData.tar", 6, false)] 73 | [DataRow("TestData.rar", 3, false)] 74 | [DataRow("TestData.rar4", 3, false)] 75 | [DataRow("TestData.tar.bz2", 6, false)] 76 | [DataRow("TestData.tar.gz", 6, false)] 77 | [DataRow("TestData.tar.xz", 3, false)] 78 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, false)] 79 | [DataRow("TestData.a", 3, false)] 80 | [DataRow("TestData.bsd.ar", 3, false)] 81 | [DataRow("TestData.iso", 3, false)] 82 | [DataRow("TestData.vhdx", 3, false)] 83 | [DataRow("TestData.wim", 3, false)] 84 | [DataRow("EmptyFile.txt", 1, false)] 85 | [DataRow("TestData.zip", 5, true)] 86 | [DataRow("TestData.7z", 3, true)] 87 | [DataRow("TestData.tar", 6, true)] 88 | [DataRow("TestData.rar", 3, true)] 89 | [DataRow("TestData.rar4", 3, true)] 90 | [DataRow("TestData.tar.bz2", 6, true)] 91 | [DataRow("TestData.tar.gz", 6, true)] 92 | [DataRow("TestData.tar.xz", 3, true)] 93 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, true)] 94 | [DataRow("TestData.a", 3, true)] 95 | [DataRow("TestData.bsd.ar", 3, true)] 96 | [DataRow("TestData.iso", 3, true)] 97 | [DataRow("TestData.vhdx", 3, true)] 98 | [DataRow("TestData.wim", 3, true)] 99 | [DataRow("EmptyFile.txt", 1, true)] 100 | [DataRow("TestDataArchivesNested.Zip", 54, true)] 101 | [DataRow("TestDataArchivesNested.Zip", 54, false)] 102 | [DataRow("TestDataArchivesNested.Zip", 54, true)] 103 | [DataRow("TestDataArchivesNested.Zip", 54, false)] 104 | public async Task ExtractArchiveAndDisposeWhileEnumeratingAsync(string fileName, int expectedNumFiles = 3, 105 | bool parallel = false) 106 | { 107 | var extractor = new Extractor(); 108 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 109 | var results = extractor.ExtractAsync(path, new ExtractorOptions() { Parallel = parallel }); 110 | var disposedResults = new List(); 111 | await foreach (var file in results) 112 | { 113 | disposedResults.Add(file); 114 | using var theStream = file.Content; 115 | // Do something with the stream. 116 | _ = theStream.ReadByte(); 117 | } 118 | 119 | Assert.AreEqual(expectedNumFiles, disposedResults.Count); 120 | foreach (var disposedResult in disposedResults) 121 | { 122 | Assert.ThrowsException(() => disposedResult.Content.Position); 123 | } 124 | } 125 | 126 | [DataTestMethod] 127 | [DataRow("TestData.zip")] 128 | public void EnsureDisposedWithExtractToDirectory(string fileName) 129 | { 130 | var directory = TestPathHelpers.GetFreshTestDirectory(); 131 | var copyDirectory = TestPathHelpers.GetFreshTestDirectory(); 132 | Directory.CreateDirectory(copyDirectory); 133 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 134 | // Make a copy of the archive so it can be deleted later to confirm properly disposed 135 | var copyPath = Path.Combine(copyDirectory, fileName); 136 | File.Copy(path, copyPath); 137 | var extractor = new Extractor(); 138 | extractor.ExtractToDirectory(directory, copyPath); 139 | File.Delete(copyPath); 140 | if (Directory.Exists(directory)) 141 | { 142 | Directory.Delete(directory, true); 143 | } 144 | if (Directory.Exists(copyDirectory)) { 145 | Directory.Delete(copyDirectory, true); 146 | } 147 | } 148 | 149 | [DataTestMethod] 150 | [DataRow("TestData.zip")] 151 | public async Task EnsureDisposedWithExtractToDirectoryAsync(string fileName) 152 | { 153 | var directory = TestPathHelpers.GetFreshTestDirectory(); 154 | var copyDirectory = TestPathHelpers.GetFreshTestDirectory(); 155 | Directory.CreateDirectory(copyDirectory); 156 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 157 | // Make a copy of the archive so it can be deleted later to confirm properly disposed 158 | var copyPath = Path.Combine(copyDirectory, fileName); 159 | File.Copy(path, copyPath); 160 | var extractor = new Extractor(); 161 | await extractor.ExtractToDirectoryAsync(directory, copyPath); 162 | File.Delete(copyPath); 163 | if (Directory.Exists(directory)) 164 | { 165 | Directory.Delete(directory, true); 166 | } 167 | if (Directory.Exists(copyDirectory)) 168 | { 169 | Directory.Delete(copyDirectory, true); 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/ExtractorTests/EncryptedArchiveTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CST.RecursiveExtractor; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | 9 | namespace RecursiveExtractor.Tests.ExtractorTests; 10 | 11 | [TestClass] 12 | public class EncryptedArchiveTests : BaseExtractorTestClass 13 | { 14 | [DataTestMethod] 15 | [DataRow("TestDataEncryptedZipCrypto.zip")] 16 | [DataRow("TestDataEncryptedAes.zip")] 17 | [DataRow("TestDataEncrypted.7z")] 18 | [DataRow("TestDataEncrypted.rar4")] 19 | [DataRow("TestDataEncrypted.rar")] 20 | public void FileTypeSetCorrectlyForEncryptedArchives(string fileName, int expectedNumFiles = 1) 21 | { 22 | var extractor = new Extractor(); 23 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 24 | var results = extractor.Extract(path, new ExtractorOptions() { Parallel = false }).ToList(); 25 | Assert.AreEqual(expectedNumFiles, results.Count()); 26 | Assert.AreEqual(FileEntryStatus.EncryptedArchive, results.First().EntryStatus); 27 | } 28 | 29 | [DataTestMethod] 30 | [DataRow("TestDataEncryptedZipCrypto.zip")] 31 | [DataRow("TestDataEncryptedAes.zip")] 32 | [DataRow("TestDataEncrypted.7z")] 33 | [DataRow("TestDataEncrypted.rar4")] 34 | [DataRow("TestDataEncrypted.rar")] 35 | public async Task FileTypeSetCorrectlyForEncryptedArchivesAsync(string fileName, int expectedNumFiles = 1) 36 | { 37 | var extractor = new Extractor(); 38 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 39 | var results = new List(); 40 | await foreach (var entry in extractor.ExtractAsync(path, new ExtractorOptions())) 41 | { 42 | results.Add(entry); 43 | } 44 | 45 | Assert.AreEqual(expectedNumFiles, results.Count); 46 | Assert.AreEqual(FileEntryStatus.EncryptedArchive, results.First().EntryStatus); 47 | } 48 | 49 | [DataTestMethod] 50 | [DataRow("TestDataEncryptedZipCrypto.zip")] 51 | [DataRow("TestDataEncryptedAes.zip")] 52 | [DataRow("TestDataEncrypted.7z")] 53 | [DataRow("TestDataEncrypted.rar4")] 54 | [DataRow("EncryptedWithPlainNames.7z")] 55 | [DataRow("EncryptedWithPlainNames.rar4")] 56 | //[DataRow("TestDataEncrypted.rar")] // RAR5 is not yet supported by SharpCompress: https://github.com/adamhathcock/sharpcompress/issues/517 57 | public void ExtractEncryptedArchive(string fileName, int expectedNumFiles = 3) 58 | { 59 | var extractor = new Extractor(); 60 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 61 | var results = extractor.Extract(path, new ExtractorOptions() { Passwords = TestArchivePasswords, ExtractSelfOnFail = false }) 62 | .ToList(); // Make this a list so it fully populates 63 | Assert.AreEqual(expectedNumFiles, results.Count); 64 | Assert.AreEqual(0, results.Count(x => x.EntryStatus == FileEntryStatus.EncryptedArchive || x.EntryStatus == FileEntryStatus.FailedArchive)); 65 | } 66 | 67 | [DataTestMethod] 68 | [DataRow("TestDataEncryptedZipCrypto.zip")] 69 | [DataRow("TestDataEncryptedAes.zip")] 70 | [DataRow("TestDataEncrypted.7z")] 71 | [DataRow("TestDataEncrypted.rar4")] 72 | [DataRow("EncryptedWithPlainNames.7z")] 73 | [DataRow("EncryptedWithPlainNames.rar4")] 74 | //[DataRow("TestDataEncrypted.rar")] // RAR5 is not yet supported by SharpCompress: https://github.com/adamhathcock/sharpcompress/issues/517 75 | public async Task ExtractEncryptedArchiveAsync(string fileName, int expectedNumFiles = 3) 76 | { 77 | var extractor = new Extractor(); 78 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 79 | var results = extractor.ExtractAsync(path, new ExtractorOptions() { Passwords = TestArchivePasswords, ExtractSelfOnFail = false }); 80 | var numEntries = 0; 81 | var numEntriesEncrypted = 0; 82 | await foreach (var entry in results) 83 | { 84 | numEntries++; 85 | if (entry.EntryStatus == FileEntryStatus.EncryptedArchive || entry.EntryStatus == FileEntryStatus.FailedArchive) 86 | { 87 | numEntriesEncrypted++; 88 | } 89 | } 90 | 91 | Assert.AreEqual(expectedNumFiles, numEntries); 92 | Assert.AreEqual(0, numEntriesEncrypted); 93 | } 94 | 95 | 96 | public static Dictionary> TestArchivePasswords { get; } = new Dictionary>() 97 | { 98 | { 99 | new Regex("EncryptedZipCrypto.zip"), new List() 100 | { 101 | "AnIncorrectPassword", "TestData", // ZipCrypto Encrypted 102 | } 103 | }, 104 | { 105 | new Regex("EncryptedAes.zip"), new List() 106 | { 107 | "AnIncorrectPassword", "TestData" // AES Encrypted 108 | } 109 | }, 110 | { 111 | new Regex("\\.7z"), new List() 112 | { 113 | "AnIncorrectPassword", 114 | "TestData", // TestDataEncrypted.7z, EncryptedWithPlainNames.7z, NestedEncrypted.7z 115 | } 116 | }, 117 | { new Regex("\\.rar"), new List() { "AnIncorrectPassword", "TestData" } } 118 | }; 119 | } -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/ExtractorTests/FilterTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CST.RecursiveExtractor; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace RecursiveExtractor.Tests.ExtractorTests; 9 | 10 | [TestClass] 11 | public class FilterTests : BaseExtractorTestClass 12 | { 13 | [DataTestMethod] 14 | [DataRow("TestData.zip")] 15 | [DataRow("TestData.7z")] 16 | [DataRow("TestData.tar")] 17 | [DataRow("TestData.rar")] 18 | [DataRow("TestData.rar4")] 19 | [DataRow("TestData.tar.bz2")] 20 | [DataRow("TestData.tar.gz")] 21 | [DataRow("TestData.tar.xz")] 22 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 0)] 23 | [DataRow("TestData.a", 0)] 24 | [DataRow("TestData.bsd.ar", 0)] 25 | [DataRow("TestData.iso")] 26 | [DataRow("TestData.vhdx")] 27 | [DataRow("TestData.wim")] 28 | [DataRow("EmptyFile.txt", 0)] 29 | [DataRow("TestDataArchivesNested.Zip", 9)] 30 | public async Task ExtractArchiveAsyncAllowFiltered(string fileName, int expectedNumFiles = 1) 31 | { 32 | var extractor = new Extractor(); 33 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 34 | var results = extractor.ExtractAsync(path, 35 | new ExtractorOptions() { AllowFilters = new string[] { "**/Bar/**", "**/TestData.tar" } }); 36 | var numResults = 0; 37 | await foreach (var result in results) 38 | { 39 | numResults++; 40 | } 41 | 42 | Assert.AreEqual(expectedNumFiles, numResults); 43 | } 44 | 45 | [DataTestMethod] 46 | [DataRow("TestData.zip")] 47 | [DataRow("TestData.7z")] 48 | [DataRow("TestData.tar")] 49 | [DataRow("TestData.rar")] 50 | [DataRow("TestData.rar4")] 51 | [DataRow("TestData.tar.bz2")] 52 | [DataRow("TestData.tar.gz")] 53 | [DataRow("TestData.tar.xz")] 54 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 0)] 55 | [DataRow("TestData.a", 0)] 56 | [DataRow("TestData.bsd.ar", 0)] 57 | [DataRow("TestData.iso")] 58 | [DataRow("TestData.vhdx")] 59 | [DataRow("TestData.wim")] 60 | [DataRow("EmptyFile.txt", 0)] 61 | [DataRow("TestDataArchivesNested.Zip", 9)] 62 | public void ExtractArchiveAllowFiltered(string fileName, int expectedNumFiles = 1) 63 | { 64 | var extractor = new Extractor(); 65 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 66 | var results = extractor.Extract(path, 67 | new ExtractorOptions() { AllowFilters = new string[] { "**/Bar/**", "**/TestData.tar" } }); 68 | Assert.AreEqual(expectedNumFiles, results.Count()); 69 | } 70 | 71 | [DataTestMethod] 72 | [DataRow("TestData.zip")] 73 | [DataRow("TestData.7z")] 74 | [DataRow("TestData.tar")] 75 | [DataRow("TestData.rar")] 76 | [DataRow("TestData.rar4")] 77 | [DataRow("TestData.tar.bz2")] 78 | [DataRow("TestData.tar.gz")] 79 | [DataRow("TestData.tar.xz")] 80 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 0)] 81 | [DataRow("TestData.a", 0)] 82 | [DataRow("TestData.bsd.ar", 0)] 83 | [DataRow("TestData.iso")] 84 | [DataRow("TestData.vhdx")] 85 | [DataRow("TestData.wim")] 86 | [DataRow("EmptyFile.txt", 0)] 87 | [DataRow("TestDataArchivesNested.Zip", 9)] 88 | public void ExtractArchiveParallelAllowFiltered(string fileName, int expectedNumFiles = 1) 89 | { 90 | var extractor = new Extractor(); 91 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 92 | var results = extractor.Extract(path, 93 | new ExtractorOptions() { Parallel = true, AllowFilters = new string[] { "**/Bar/**", "**/TestData.tar" } }); 94 | Assert.AreEqual(expectedNumFiles, results.Count()); 95 | } 96 | 97 | [DataTestMethod] 98 | [DataRow("TestData.zip", 4)] 99 | [DataRow("TestData.7z")] 100 | [DataRow("TestData.tar", 5)] 101 | [DataRow("TestData.rar")] 102 | [DataRow("TestData.rar4")] 103 | [DataRow("TestData.tar.bz2", 5)] 104 | [DataRow("TestData.tar.gz", 5)] 105 | [DataRow("TestData.tar.xz")] 106 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8)] 107 | [DataRow("TestData.a", 3)] 108 | [DataRow("TestData.bsd.ar", 3)] 109 | [DataRow("TestData.iso")] 110 | [DataRow("TestData.vhdx")] 111 | [DataRow("TestData.wim")] 112 | [DataRow("EmptyFile.txt", 1)] 113 | [DataRow("TestDataArchivesNested.Zip", 45)] 114 | public void ExtractArchiveDenyFiltered(string fileName, int expectedNumFiles = 2) 115 | { 116 | var extractor = new Extractor(); 117 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 118 | var results = extractor.Extract(path, new ExtractorOptions() { DenyFilters = new string[] { "**/Bar/**" } }); 119 | Assert.AreEqual(expectedNumFiles, results.Count()); 120 | } 121 | 122 | [DataTestMethod] 123 | [DataRow("TestData.zip", 4)] 124 | [DataRow("TestData.7z")] 125 | [DataRow("TestData.tar", 5)] 126 | [DataRow("TestData.rar")] 127 | [DataRow("TestData.rar4")] 128 | [DataRow("TestData.tar.bz2", 5)] 129 | [DataRow("TestData.tar.gz", 5)] 130 | [DataRow("TestData.tar.xz")] 131 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8)] 132 | [DataRow("TestData.a", 3)] 133 | [DataRow("TestData.bsd.ar", 3)] 134 | [DataRow("TestData.iso")] 135 | [DataRow("TestData.vhdx")] 136 | [DataRow("TestData.wim")] 137 | [DataRow("EmptyFile.txt", 1)] 138 | [DataRow("TestDataArchivesNested.Zip", 45)] 139 | public void ExtractArchiveParallelDenyFiltered(string fileName, int expectedNumFiles = 2) 140 | { 141 | var extractor = new Extractor(); 142 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 143 | var results = extractor.Extract(path, 144 | new ExtractorOptions() { Parallel = true, DenyFilters = new string[] { "**/Bar/**" } }); 145 | Assert.AreEqual(expectedNumFiles, results.Count()); 146 | } 147 | 148 | [DataTestMethod] 149 | [DataRow("TestData.zip", 4)] 150 | [DataRow("TestData.7z")] 151 | [DataRow("TestData.tar", 5)] 152 | [DataRow("TestData.rar")] 153 | [DataRow("TestData.rar4")] 154 | [DataRow("TestData.tar.bz2", 5)] 155 | [DataRow("TestData.tar.gz", 5)] 156 | [DataRow("TestData.tar.xz")] 157 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8)] 158 | [DataRow("TestData.a", 3)] 159 | [DataRow("TestData.bsd.ar", 3)] 160 | [DataRow("TestData.iso")] 161 | [DataRow("TestData.vhdx")] 162 | [DataRow("TestData.wim")] 163 | [DataRow("EmptyFile.txt", 1)] 164 | [DataRow("TestDataArchivesNested.Zip", 45)] 165 | public async Task ExtractArchiveAsyncDenyFiltered(string fileName, int expectedNumFiles = 2) 166 | { 167 | var extractor = new Extractor(); 168 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 169 | var results = 170 | extractor.ExtractAsync(path, new ExtractorOptions() { DenyFilters = new string[] { "**/Bar/**" } }); 171 | var numResults = 0; 172 | await foreach (var result in results) 173 | { 174 | numResults++; 175 | } 176 | 177 | Assert.AreEqual(expectedNumFiles, numResults); 178 | } 179 | 180 | [DataTestMethod] 181 | [DataRow(ArchiveFileType.ZIP, new[] { ArchiveFileType.ZIP }, new ArchiveFileType[] { }, false)] 182 | [DataRow(ArchiveFileType.ZIP, new[] { ArchiveFileType.TAR }, new ArchiveFileType[] { }, true)] 183 | [DataRow(ArchiveFileType.ZIP, new ArchiveFileType[] { }, new[] { ArchiveFileType.ZIP }, true)] 184 | [DataRow(ArchiveFileType.TAR, new ArchiveFileType[] { }, new[] { ArchiveFileType.ZIP }, false)] 185 | [DataRow(ArchiveFileType.ZIP, new[] { ArchiveFileType.ZIP }, new[] { ArchiveFileType.ZIP }, false)] 186 | public void TestArchiveTypeFilters(ArchiveFileType typeToCheck, IEnumerable denyTypes, 187 | IEnumerable allowTypes, bool expected) 188 | { 189 | ExtractorOptions opts = new() { AllowTypes = allowTypes, DenyTypes = denyTypes }; 190 | Assert.AreEqual(expected, opts.IsAcceptableType(typeToCheck)); 191 | } 192 | } -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/ExtractorTests/MiniMagicTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CST.RecursiveExtractor; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.IO; 4 | 5 | namespace RecursiveExtractor.Tests.ExtractorTests; 6 | 7 | [TestClass] 8 | public class MiniMagicTests : BaseExtractorTestClass 9 | { 10 | [DataTestMethod] 11 | [DataRow("TestData.zip", ArchiveFileType.ZIP)] 12 | [DataRow("TestData.7z", ArchiveFileType.P7ZIP)] 13 | [DataRow("TestData.Tar", ArchiveFileType.TAR)] 14 | [DataRow("TestData.rar", ArchiveFileType.RAR5)] 15 | [DataRow("TestData.rar4", ArchiveFileType.RAR)] 16 | [DataRow("TestData.tar.bz2", ArchiveFileType.BZIP2)] 17 | [DataRow("TestData.tar.gz", ArchiveFileType.GZIP)] 18 | [DataRow("TestData.tar.xz", ArchiveFileType.XZ)] 19 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", ArchiveFileType.DEB)] 20 | [DataRow("TestData.a", ArchiveFileType.AR)] 21 | [DataRow("TestData.iso", ArchiveFileType.ISO_9660)] 22 | [DataRow("UdfTest.iso", ArchiveFileType.UDF)] 23 | [DataRow("TestData.vhdx", ArchiveFileType.VHDX)] 24 | [DataRow("TestData.wim", ArchiveFileType.WIM)] 25 | [DataRow("Empty.vmdk", ArchiveFileType.VMDK)] 26 | [DataRow("HfsSampleUDCO.dmg", ArchiveFileType.DMG)] 27 | [DataRow("EmptyFile.txt", ArchiveFileType.UNKNOWN)] 28 | public void TestMiniMagic(string fileName, ArchiveFileType expectedArchiveFileType) 29 | { 30 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 31 | using var fs = new FileStream(path, FileMode.Open); 32 | // Test just based on the content 33 | var fileEntry = new FileEntry("NoName", fs); 34 | 35 | // We make sure the expected type matches and we have reset the stream 36 | Assert.AreEqual(expectedArchiveFileType, fileEntry.ArchiveType); 37 | Assert.AreEqual(0, fileEntry.Content.Position); 38 | 39 | // Should also work if the stream doesn't start at 0 40 | fileEntry.Content.Position = 10; 41 | Assert.AreEqual(expectedArchiveFileType, fileEntry.ArchiveType); 42 | Assert.AreEqual(10, fileEntry.Content.Position); 43 | } 44 | } -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/ExtractorTests/MiscTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CST.RecursiveExtractor; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace RecursiveExtractor.Tests.ExtractorTests; 9 | 10 | [TestClass] 11 | public class MiscTests 12 | { 13 | [DataTestMethod] 14 | [DataRow("TestDataCorrupt.tar", false, 0, 1)] 15 | [DataRow("TestDataCorrupt.tar", true, 1, 1)] 16 | [DataRow("TestDataCorrupt.tar.zip", false, 0, 2)] 17 | [DataRow("TestDataCorrupt.tar.zip", true, 0, 2)] 18 | public async Task ExtractCorruptArchiveAsync(string fileName, bool requireTopLevelToBeArchive, int expectedNumFailures, int expectedNumFiles) 19 | { 20 | var extractor = new Extractor(); 21 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 22 | var results = await extractor.ExtractAsync(path, 23 | new ExtractorOptions() { RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToListAsync(); 24 | 25 | 26 | Assert.AreEqual(expectedNumFiles, results.Count); 27 | var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); 28 | Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); 29 | } 30 | 31 | [DataTestMethod] 32 | [DataRow("Lorem.txt", true, 1)] 33 | [DataRow("Lorem.txt", false, 0)] 34 | public async Task ExtractFlatFileAsync(string fileName, bool requireTopLevelToBeArchive, int expectedNumFailures) 35 | { 36 | var extractor = new Extractor(); 37 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestData", fileName); 38 | var results = await extractor.ExtractAsync(path, new ExtractorOptions(){ RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToListAsync(); 39 | 40 | Assert.AreEqual(1, results.Count); 41 | var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); 42 | Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); 43 | } 44 | 45 | 46 | 47 | [DataTestMethod] 48 | [DataRow("TestDataCorrupt.tar", false, 0, 1)] 49 | [DataRow("TestDataCorrupt.tar", true, 1, 1)] 50 | [DataRow("TestDataCorrupt.tar.zip", false, 0, 2)] 51 | [DataRow("TestDataCorrupt.tar.zip", true, 0, 2)] 52 | [DataRow("TestDataCorruptWim.zip", true, 0, 0)] 53 | public void ExtractCorruptArchive(string fileName, bool requireTopLevelToBeArchive, int expectedNumFailures, int expectedNumFiles) 54 | { 55 | var extractor = new Extractor(); 56 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 57 | var results = extractor.Extract(path, new ExtractorOptions(){ RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToList(); 58 | 59 | Assert.AreEqual(expectedNumFiles, results.Count); 60 | var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); 61 | Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); 62 | } 63 | 64 | [DataTestMethod] 65 | [DataRow("Lorem.txt", true, 1)] 66 | [DataRow("Lorem.txt", false, 0)] 67 | public void ExtractFlatFile(string fileName, bool requireTopLevelToBeArchive, int expectedNumFailures) 68 | { 69 | var extractor = new Extractor(); 70 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestData", fileName); 71 | var results = extractor.Extract(path, new ExtractorOptions(){ RequireTopLevelToBeArchive = requireTopLevelToBeArchive }).ToList(); 72 | 73 | Assert.AreEqual(1, results.Count); 74 | var actualNumberOfFailedArchives = results.Count(x => x.EntryStatus == FileEntryStatus.FailedArchive); 75 | Assert.AreEqual(expectedNumFailures, actualNumberOfFailedArchives); 76 | } 77 | 78 | [DataTestMethod] 79 | [DataRow("EmptyFile.txt")] 80 | [DataRow("TestData.zip", ".zip")] 81 | public void ExtractAsRaw(string fileName, string? RawExtension = null) 82 | { 83 | var extractor = new Extractor(); 84 | var options = new ExtractorOptions() 85 | { 86 | RawExtensions = RawExtension is null ? new List() : new List() { RawExtension } 87 | }; 88 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 89 | 90 | var results = extractor.Extract(path, options); 91 | Assert.AreEqual(1, results.Count()); 92 | } 93 | } -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/ExtractorTests/TestQuinesAndSlip.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CST.RecursiveExtractor; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using SharpCompress.Archives.Tar; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace RecursiveExtractor.Tests.ExtractorTests; 11 | 12 | [TestClass] 13 | public class TestQuinesAndSlip : BaseExtractorTestClass 14 | { 15 | public static IEnumerable ZipSlipNames 16 | { 17 | get 18 | { 19 | return new[] 20 | { 21 | new object [] { "zip-slip-win.zip" }, 22 | new object [] { "zip-slip-win.tar" }, 23 | new object [] { "zip-slip.zip" }, 24 | new object [] { "zip-slip.tar" } 25 | }; 26 | } 27 | } 28 | 29 | [TestMethod] 30 | [DynamicData(nameof(ZipSlipNames))] 31 | public void TestZipSlip(string fileName) 32 | { 33 | var extractor = new Extractor(); 34 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "Bombs", fileName); 35 | var results = extractor.Extract(path, new ExtractorOptions()).ToList(); 36 | Assert.IsTrue(results.All(x => !x.FullPath.Contains(".."))); 37 | } 38 | 39 | [TestMethod] 40 | [DynamicData(nameof(ZipSlipNames))] 41 | public async Task TestZipSlipAsync(string fileName) 42 | { 43 | var extractor = new Extractor(); 44 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "Bombs", fileName); 45 | var results = await extractor.ExtractAsync(path, new ExtractorOptions()).ToListAsync(); 46 | Assert.IsTrue(results.All(x => !x.FullPath.Contains(".."))); 47 | } 48 | 49 | public static IEnumerable QuineBombNames 50 | { 51 | get 52 | { 53 | return new[] 54 | { 55 | new object [] { "10GB.7z.bz2" }, 56 | new object [] { "10GB.gz.bz2" }, 57 | new object [] { "10GB.rar.bz2" }, 58 | new object [] { "10GB.xz.bz2" }, 59 | new object [] { "10GB.zip.bz2" }, 60 | new object [] { "zblg.zip" }, 61 | new object [] { "zbsm.zip" } 62 | }; 63 | } 64 | } 65 | 66 | [TestMethod] 67 | [DynamicData(nameof(QuineBombNames))] 68 | [ExpectedException(typeof(OverflowException))] 69 | public void TestQuineBombs(string fileName) 70 | { 71 | var extractor = new Extractor(); 72 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "Bombs", fileName); 73 | _ = extractor.Extract(path, new ExtractorOptions() { MemoryStreamCutoff = 1024 * 1024 * 1024 }).ToList(); 74 | } 75 | 76 | [TestMethod] 77 | [DynamicData(nameof(QuineBombNames))] 78 | [ExpectedException(typeof(OverflowException))] 79 | public async Task TestQuineBombsAsync(string fileName) 80 | { 81 | var extractor = new Extractor(); 82 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "Bombs", fileName); 83 | _ = await extractor.ExtractAsync(path, new ExtractorOptions() { MemoryStreamCutoff = 1024 * 1024 * 1024 }).ToListAsync(); 84 | } 85 | } -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/ExtractorTests/TimeOutTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CST.RecursiveExtractor; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | 7 | namespace RecursiveExtractor.Tests.ExtractorTests; 8 | 9 | [TestClass] 10 | public class TimeOutTests : BaseExtractorTestClass 11 | { 12 | [DataTestMethod] 13 | [DataRow("TestData.7z", 3, false)] 14 | [DataRow("TestData.tar", 6, false)] 15 | [DataRow("TestData.rar", 3, false)] 16 | [DataRow("TestData.rar4", 3, false)] 17 | [DataRow("TestData.tar.bz2", 6, false)] 18 | [DataRow("TestData.tar.gz", 6, false)] 19 | [DataRow("TestData.tar.xz", 3, false)] 20 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, false)] 21 | [DataRow("TestData.a", 3, false)] 22 | [DataRow("TestData.bsd.ar", 3, false)] 23 | [DataRow("TestData.iso", 3, false)] 24 | [DataRow("TestData.vhdx", 3, false)] 25 | [DataRow("TestData.wim", 3, false)] 26 | [DataRow("EmptyFile.txt", 1, false)] 27 | [DataRow("TestData.zip", 5, true)] 28 | [DataRow("TestData.7z", 3, true)] 29 | [DataRow("TestData.tar", 6, true)] 30 | [DataRow("TestData.rar", 3, true)] 31 | [DataRow("TestData.rar4", 3, true)] 32 | [DataRow("TestData.tar.bz2", 6, true)] 33 | [DataRow("TestData.tar.gz", 6, true)] 34 | [DataRow("TestData.tar.xz", 3, true)] 35 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, true)] 36 | [DataRow("TestData.a", 3, true)] 37 | [DataRow("TestData.bsd.ar", 3, true)] 38 | [DataRow("TestData.iso", 3, true)] 39 | [DataRow("TestData.vhdx", 3, true)] 40 | [DataRow("TestData.wim", 3, true)] 41 | [DataRow("EmptyFile.txt", 1, true)] 42 | [DataRow("TestDataArchivesNested.Zip", 54, true)] 43 | [DataRow("TestDataArchivesNested.Zip", 54, false)] 44 | public void TimeoutTest(string fileName, int expectedNumFiles = 3, bool parallel = false) 45 | { 46 | var extractor = new Extractor(); 47 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 48 | Assert.ThrowsException(() => 49 | { 50 | var results = extractor.Extract(path, 51 | new ExtractorOptions() 52 | { 53 | Parallel = parallel, EnableTiming = true, Timeout = new TimeSpan(0, 0, 0, 0, 0) 54 | }); 55 | int count = 0; 56 | foreach (var result in results) 57 | { 58 | count++; 59 | } 60 | 61 | // We should not be able to get to all the files 62 | Assert.Fail(); 63 | }); 64 | } 65 | 66 | [DataTestMethod] 67 | [DataRow("TestData.7z", 3, false)] 68 | [DataRow("TestData.tar", 6, false)] 69 | [DataRow("TestData.rar", 3, false)] 70 | [DataRow("TestData.rar4", 3, false)] 71 | [DataRow("TestData.tar.bz2", 6, false)] 72 | [DataRow("TestData.tar.gz", 6, false)] 73 | [DataRow("TestData.tar.xz", 3, false)] 74 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, false)] 75 | [DataRow("TestData.a", 3, false)] 76 | [DataRow("TestData.bsd.ar", 3, false)] 77 | [DataRow("TestData.iso", 3, false)] 78 | [DataRow("TestData.vhdx", 3, false)] 79 | [DataRow("TestData.wim", 3, false)] 80 | [DataRow("EmptyFile.txt", 1, false)] 81 | [DataRow("TestData.zip", 5, true)] 82 | [DataRow("TestData.7z", 3, true)] 83 | [DataRow("TestData.tar", 6, true)] 84 | [DataRow("TestData.rar", 3, true)] 85 | [DataRow("TestData.rar4", 3, true)] 86 | [DataRow("TestData.tar.bz2", 6, true)] 87 | [DataRow("TestData.tar.gz", 6, true)] 88 | [DataRow("TestData.tar.xz", 3, true)] 89 | [DataRow("sysvbanner_1.0-17fakesync1_amd64.deb", 8, true)] 90 | [DataRow("TestData.a", 3, true)] 91 | [DataRow("TestData.bsd.ar", 3, true)] 92 | [DataRow("TestData.iso", 3, true)] 93 | [DataRow("TestData.vhdx", 3, true)] 94 | [DataRow("TestData.wim", 3, true)] 95 | [DataRow("EmptyFile.txt", 1, true)] 96 | [DataRow("TestDataArchivesNested.Zip", 54, true)] 97 | [DataRow("TestDataArchivesNested.Zip", 54, false)] 98 | [DataRow("TestDataArchivesNested.Zip", 54, true)] 99 | [DataRow("TestDataArchivesNested.Zip", 54, false)] 100 | public async Task TimeoutTestAsync(string fileName, int expectedNumFiles = 3, bool parallel = false) 101 | { 102 | var extractor = new Extractor(); 103 | var path = Path.Combine(Directory.GetCurrentDirectory(), "TestData", "TestDataArchives", fileName); 104 | await Assert.ThrowsExceptionAsync(async () => 105 | { 106 | var results = extractor.ExtractAsync(path, 107 | new ExtractorOptions() 108 | { 109 | Parallel = parallel, EnableTiming = true, Timeout = new TimeSpan(0, 0, 0, 0, 0) 110 | }); 111 | int count = 0; 112 | await foreach (var result in results) 113 | { 114 | count++; 115 | } 116 | 117 | // We should not be able to get to all the files 118 | Assert.Fail(); 119 | }); 120 | } 121 | } -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/SanitizePathTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. Licensed under the MIT License. 2 | 3 | using Microsoft.CST.RecursiveExtractor; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using System.IO; 6 | using System.Runtime.InteropServices; 7 | 8 | namespace RecursiveExtractor.Tests 9 | { 10 | [TestClass] 11 | public class SanitizePathTests 12 | { 13 | [DataTestMethod] 14 | [DataRow("a\\file\\with:colon.name", "a\\file\\with_colon.name")] 15 | [DataRow("a\\folder:with\\colon.name", "a\\folder_with\\colon.name")] 16 | 17 | public void TestSanitizePathWindows(string windowsInputPath, string expectedWindowsPath) 18 | { 19 | var entry = new FileEntry(windowsInputPath, Stream.Null); 20 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 21 | { 22 | Assert.AreEqual(expectedWindowsPath, entry.GetSanitizedPath()); 23 | } 24 | } 25 | 26 | [DataTestMethod] 27 | [DataRow("a/file/with:colon.name", "a/file/with_colon.name")] 28 | [DataRow("a/folder:with/colon.name", "a/folder_with/colon.name")] 29 | 30 | public void TestSanitizePathLinux(string linuxInputPath, string expectedLinuxPath) 31 | { 32 | var entry = new FileEntry(linuxInputPath, Stream.Null); 33 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 34 | { 35 | Assert.AreEqual(expectedLinuxPath, entry.GetSanitizedPath()); 36 | } 37 | } 38 | 39 | protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 40 | } 41 | } -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/Bombs/10GB.7z.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/Bombs/10GB.7z.bz2 -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/Bombs/10GB.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/Bombs/10GB.bz2 -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/Bombs/10GB.gz.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/Bombs/10GB.gz.bz2 -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/Bombs/10GB.rar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/Bombs/10GB.rar.bz2 -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/Bombs/10GB.xz.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/Bombs/10GB.xz.bz2 -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/Bombs/10GB.zip.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/Bombs/10GB.zip.bz2 -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/Bombs/zblg.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/Bombs/zblg.zip -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/Bombs/zbsm.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/Bombs/zbsm.zip -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/Bombs/zip-slip-win.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/Bombs/zip-slip-win.zip -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/Bombs/zip-slip.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/Bombs/zip-slip.zip -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestData/Foo/Bar/Dolor.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Orci nulla pellentesque dignissim enim sit amet venenatis. Tincidunt augue interdum velit euismod in pellentesque. Tellus pellentesque eu tincidunt tortor aliquam nulla facilisi cras. Sit amet mauris commodo quis imperdiet massa tincidunt nunc pulvinar. Diam phasellus vestibulum lorem sed risus ultricies tristique. Dignissim cras tincidunt lobortis feugiat vivamus at augue eget. Consectetur purus ut faucibus pulvinar. Odio ut sem nulla pharetra diam sit amet nisl. Euismod elementum nisi quis eleifend quam adipiscing vitae. Odio pellentesque diam volutpat commodo sed. Egestas dui id ornare arcu. Adipiscing tristique risus nec feugiat in fermentum posuere urna nec. Nunc non blandit massa enim nec. 2 | 3 | Vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Tortor pretium viverra suspendisse potenti nullam. Tortor at auctor urna nunc. Phasellus vestibulum lorem sed risus ultricies tristique nulla aliquet. Lorem sed risus ultricies tristique nulla aliquet enim. Bibendum est ultricies integer quis. Est placerat in egestas erat. Nunc non blandit massa enim. Velit sed ullamcorper morbi tincidunt ornare massa eget egestas. Nec ullamcorper sit amet risus nullam eget felis eget nunc. Amet nisl suscipit adipiscing bibendum est ultricies. Donec et odio pellentesque diam volutpat commodo sed. Vel eros donec ac odio tempor. Donec pretium vulputate sapien nec sagittis aliquam. Vestibulum morbi blandit cursus risus at ultrices mi tempus. Elit sed vulputate mi sit amet mauris commodo. 4 | 5 | Euismod lacinia at quis risus sed vulputate odio. Nec feugiat nisl pretium fusce id velit ut tortor pretium. Praesent elementum facilisis leo vel fringilla. Vel risus commodo viverra maecenas accumsan lacus. In fermentum posuere urna nec tincidunt. Rutrum quisque non tellus orci ac auctor augue mauris augue. Fames ac turpis egestas integer eget aliquet nibh. Enim nec dui nunc mattis enim ut tellus elementum sagittis. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Netus et malesuada fames ac turpis egestas maecenas pharetra. In est ante in nibh mauris cursus. Nunc aliquet bibendum enim facilisis gravida neque convallis a. Massa tincidunt dui ut ornare lectus. Interdum posuere lorem ipsum dolor sit amet consectetur adipiscing elit. Commodo elit at imperdiet dui. Eu scelerisque felis imperdiet proin fermentum leo vel orci. Aliquet bibendum enim facilisis gravida neque convallis a cras. 6 | 7 | Vulputate odio ut enim blandit volutpat maecenas volutpat blandit aliquam. Egestas sed sed risus pretium quam vulputate dignissim suspendisse. Ipsum nunc aliquet bibendum enim facilisis gravida neque convallis. Urna cursus eget nunc scelerisque viverra mauris. Lacus luctus accumsan tortor posuere ac ut consequat. Urna molestie at elementum eu. Consequat semper viverra nam libero justo laoreet. Donec massa sapien faucibus et molestie ac feugiat sed lectus. Massa sed elementum tempus egestas. Neque ornare aenean euismod elementum nisi quis eleifend. Arcu cursus euismod quis viverra nibh. Diam vulputate ut pharetra sit amet aliquam. 8 | 9 | Egestas fringilla phasellus faucibus scelerisque eleifend. Neque convallis a cras semper auctor. Sollicitudin tempor id eu nisl. Dictum varius duis at consectetur lorem. Sodales neque sodales ut etiam sit amet nisl. Nibh nisl condimentum id venenatis a condimentum vitae sapien pellentesque. Cras sed felis eget velit aliquet. Ut ornare lectus sit amet est placerat in egestas erat. Mattis aliquam faucibus purus in. Gravida in fermentum et sollicitudin ac orci phasellus egestas. Eu ultrices vitae auctor eu augue ut. Orci nulla pellentesque dignissim enim sit amet venenatis. Eget magna fermentum iaculis eu non diam. Ac orci phasellus egestas tellus rutrum. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus. Sed lectus vestibulum mattis ullamcorper velit sed ullamcorper. Condimentum lacinia quis vel eros donec ac odio tempor orci. Senectus et netus et malesuada fames ac turpis egestas sed. Diam vulputate ut pharetra sit. -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestData/Foo/Ipsum.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Orci nulla pellentesque dignissim enim sit amet venenatis. Tincidunt augue interdum velit euismod in pellentesque. Tellus pellentesque eu tincidunt tortor aliquam nulla facilisi cras. Sit amet mauris commodo quis imperdiet massa tincidunt nunc pulvinar. Diam phasellus vestibulum lorem sed risus ultricies tristique. Dignissim cras tincidunt lobortis feugiat vivamus at augue eget. Consectetur purus ut faucibus pulvinar. Odio ut sem nulla pharetra diam sit amet nisl. Euismod elementum nisi quis eleifend quam adipiscing vitae. Odio pellentesque diam volutpat commodo sed. Egestas dui id ornare arcu. Adipiscing tristique risus nec feugiat in fermentum posuere urna nec. Nunc non blandit massa enim nec. 2 | 3 | Vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Tortor pretium viverra suspendisse potenti nullam. Tortor at auctor urna nunc. Phasellus vestibulum lorem sed risus ultricies tristique nulla aliquet. Lorem sed risus ultricies tristique nulla aliquet enim. Bibendum est ultricies integer quis. Est placerat in egestas erat. Nunc non blandit massa enim. Velit sed ullamcorper morbi tincidunt ornare massa eget egestas. Nec ullamcorper sit amet risus nullam eget felis eget nunc. Amet nisl suscipit adipiscing bibendum est ultricies. Donec et odio pellentesque diam volutpat commodo sed. Vel eros donec ac odio tempor. Donec pretium vulputate sapien nec sagittis aliquam. Vestibulum morbi blandit cursus risus at ultrices mi tempus. Elit sed vulputate mi sit amet mauris commodo. 4 | 5 | Euismod lacinia at quis risus sed vulputate odio. Nec feugiat nisl pretium fusce id velit ut tortor pretium. Praesent elementum facilisis leo vel fringilla. Vel risus commodo viverra maecenas accumsan lacus. In fermentum posuere urna nec tincidunt. Rutrum quisque non tellus orci ac auctor augue mauris augue. Fames ac turpis egestas integer eget aliquet nibh. Enim nec dui nunc mattis enim ut tellus elementum sagittis. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Netus et malesuada fames ac turpis egestas maecenas pharetra. In est ante in nibh mauris cursus. Nunc aliquet bibendum enim facilisis gravida neque convallis a. Massa tincidunt dui ut ornare lectus. Interdum posuere lorem ipsum dolor sit amet consectetur adipiscing elit. Commodo elit at imperdiet dui. Eu scelerisque felis imperdiet proin fermentum leo vel orci. Aliquet bibendum enim facilisis gravida neque convallis a cras. 6 | 7 | Vulputate odio ut enim blandit volutpat maecenas volutpat blandit aliquam. Egestas sed sed risus pretium quam vulputate dignissim suspendisse. Ipsum nunc aliquet bibendum enim facilisis gravida neque convallis. Urna cursus eget nunc scelerisque viverra mauris. Lacus luctus accumsan tortor posuere ac ut consequat. Urna molestie at elementum eu. Consequat semper viverra nam libero justo laoreet. Donec massa sapien faucibus et molestie ac feugiat sed lectus. Massa sed elementum tempus egestas. Neque ornare aenean euismod elementum nisi quis eleifend. Arcu cursus euismod quis viverra nibh. Diam vulputate ut pharetra sit amet aliquam. 8 | 9 | Egestas fringilla phasellus faucibus scelerisque eleifend. Neque convallis a cras semper auctor. Sollicitudin tempor id eu nisl. Dictum varius duis at consectetur lorem. Sodales neque sodales ut etiam sit amet nisl. Nibh nisl condimentum id venenatis a condimentum vitae sapien pellentesque. Cras sed felis eget velit aliquet. Ut ornare lectus sit amet est placerat in egestas erat. Mattis aliquam faucibus purus in. Gravida in fermentum et sollicitudin ac orci phasellus egestas. Eu ultrices vitae auctor eu augue ut. Orci nulla pellentesque dignissim enim sit amet venenatis. Eget magna fermentum iaculis eu non diam. Ac orci phasellus egestas tellus rutrum. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus. Sed lectus vestibulum mattis ullamcorper velit sed ullamcorper. Condimentum lacinia quis vel eros donec ac odio tempor orci. Senectus et netus et malesuada fames ac turpis egestas sed. Diam vulputate ut pharetra sit. -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestData/Lorem.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Orci nulla pellentesque dignissim enim sit amet venenatis. Tincidunt augue interdum velit euismod in pellentesque. Tellus pellentesque eu tincidunt tortor aliquam nulla facilisi cras. Sit amet mauris commodo quis imperdiet massa tincidunt nunc pulvinar. Diam phasellus vestibulum lorem sed risus ultricies tristique. Dignissim cras tincidunt lobortis feugiat vivamus at augue eget. Consectetur purus ut faucibus pulvinar. Odio ut sem nulla pharetra diam sit amet nisl. Euismod elementum nisi quis eleifend quam adipiscing vitae. Odio pellentesque diam volutpat commodo sed. Egestas dui id ornare arcu. Adipiscing tristique risus nec feugiat in fermentum posuere urna nec. Nunc non blandit massa enim nec. 2 | 3 | Vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Tortor pretium viverra suspendisse potenti nullam. Tortor at auctor urna nunc. Phasellus vestibulum lorem sed risus ultricies tristique nulla aliquet. Lorem sed risus ultricies tristique nulla aliquet enim. Bibendum est ultricies integer quis. Est placerat in egestas erat. Nunc non blandit massa enim. Velit sed ullamcorper morbi tincidunt ornare massa eget egestas. Nec ullamcorper sit amet risus nullam eget felis eget nunc. Amet nisl suscipit adipiscing bibendum est ultricies. Donec et odio pellentesque diam volutpat commodo sed. Vel eros donec ac odio tempor. Donec pretium vulputate sapien nec sagittis aliquam. Vestibulum morbi blandit cursus risus at ultrices mi tempus. Elit sed vulputate mi sit amet mauris commodo. 4 | 5 | Euismod lacinia at quis risus sed vulputate odio. Nec feugiat nisl pretium fusce id velit ut tortor pretium. Praesent elementum facilisis leo vel fringilla. Vel risus commodo viverra maecenas accumsan lacus. In fermentum posuere urna nec tincidunt. Rutrum quisque non tellus orci ac auctor augue mauris augue. Fames ac turpis egestas integer eget aliquet nibh. Enim nec dui nunc mattis enim ut tellus elementum sagittis. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Netus et malesuada fames ac turpis egestas maecenas pharetra. In est ante in nibh mauris cursus. Nunc aliquet bibendum enim facilisis gravida neque convallis a. Massa tincidunt dui ut ornare lectus. Interdum posuere lorem ipsum dolor sit amet consectetur adipiscing elit. Commodo elit at imperdiet dui. Eu scelerisque felis imperdiet proin fermentum leo vel orci. Aliquet bibendum enim facilisis gravida neque convallis a cras. 6 | 7 | Vulputate odio ut enim blandit volutpat maecenas volutpat blandit aliquam. Egestas sed sed risus pretium quam vulputate dignissim suspendisse. Ipsum nunc aliquet bibendum enim facilisis gravida neque convallis. Urna cursus eget nunc scelerisque viverra mauris. Lacus luctus accumsan tortor posuere ac ut consequat. Urna molestie at elementum eu. Consequat semper viverra nam libero justo laoreet. Donec massa sapien faucibus et molestie ac feugiat sed lectus. Massa sed elementum tempus egestas. Neque ornare aenean euismod elementum nisi quis eleifend. Arcu cursus euismod quis viverra nibh. Diam vulputate ut pharetra sit amet aliquam. 8 | 9 | Egestas fringilla phasellus faucibus scelerisque eleifend. Neque convallis a cras semper auctor. Sollicitudin tempor id eu nisl. Dictum varius duis at consectetur lorem. Sodales neque sodales ut etiam sit amet nisl. Nibh nisl condimentum id venenatis a condimentum vitae sapien pellentesque. Cras sed felis eget velit aliquet. Ut ornare lectus sit amet est placerat in egestas erat. Mattis aliquam faucibus purus in. Gravida in fermentum et sollicitudin ac orci phasellus egestas. Eu ultrices vitae auctor eu augue ut. Orci nulla pellentesque dignissim enim sit amet venenatis. Eget magna fermentum iaculis eu non diam. Ac orci phasellus egestas tellus rutrum. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus. Sed lectus vestibulum mattis ullamcorper velit sed ullamcorper. Condimentum lacinia quis vel eros donec ac odio tempor orci. Senectus et netus et malesuada fames ac turpis egestas sed. Diam vulputate ut pharetra sit. -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/.gitattributes: -------------------------------------------------------------------------------- 1 | * binary -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/100trees.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/100trees.7z -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/Empty.vmdk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/Empty.vmdk -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/EmptyFile.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/EmptyFile.txt -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/EncryptedWithPlainNames.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/EncryptedWithPlainNames.7z -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/EncryptedWithPlainNames.rar4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/EncryptedWithPlainNames.rar4 -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/HfsSampleUDCO.dmg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/HfsSampleUDCO.dmg -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/Lorem.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Orci nulla pellentesque dignissim enim sit amet venenatis. Tincidunt augue interdum velit euismod in pellentesque. Tellus pellentesque eu tincidunt tortor aliquam nulla facilisi cras. Sit amet mauris commodo quis imperdiet massa tincidunt nunc pulvinar. Diam phasellus vestibulum lorem sed risus ultricies tristique. Dignissim cras tincidunt lobortis feugiat vivamus at augue eget. Consectetur purus ut faucibus pulvinar. Odio ut sem nulla pharetra diam sit amet nisl. Euismod elementum nisi quis eleifend quam adipiscing vitae. Odio pellentesque diam volutpat commodo sed. Egestas dui id ornare arcu. Adipiscing tristique risus nec feugiat in fermentum posuere urna nec. Nunc non blandit massa enim nec. 2 | 3 | Vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Tortor pretium viverra suspendisse potenti nullam. Tortor at auctor urna nunc. Phasellus vestibulum lorem sed risus ultricies tristique nulla aliquet. Lorem sed risus ultricies tristique nulla aliquet enim. Bibendum est ultricies integer quis. Est placerat in egestas erat. Nunc non blandit massa enim. Velit sed ullamcorper morbi tincidunt ornare massa eget egestas. Nec ullamcorper sit amet risus nullam eget felis eget nunc. Amet nisl suscipit adipiscing bibendum est ultricies. Donec et odio pellentesque diam volutpat commodo sed. Vel eros donec ac odio tempor. Donec pretium vulputate sapien nec sagittis aliquam. Vestibulum morbi blandit cursus risus at ultrices mi tempus. Elit sed vulputate mi sit amet mauris commodo. 4 | 5 | Euismod lacinia at quis risus sed vulputate odio. Nec feugiat nisl pretium fusce id velit ut tortor pretium. Praesent elementum facilisis leo vel fringilla. Vel risus commodo viverra maecenas accumsan lacus. In fermentum posuere urna nec tincidunt. Rutrum quisque non tellus orci ac auctor augue mauris augue. Fames ac turpis egestas integer eget aliquet nibh. Enim nec dui nunc mattis enim ut tellus elementum sagittis. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Netus et malesuada fames ac turpis egestas maecenas pharetra. In est ante in nibh mauris cursus. Nunc aliquet bibendum enim facilisis gravida neque convallis a. Massa tincidunt dui ut ornare lectus. Interdum posuere lorem ipsum dolor sit amet consectetur adipiscing elit. Commodo elit at imperdiet dui. Eu scelerisque felis imperdiet proin fermentum leo vel orci. Aliquet bibendum enim facilisis gravida neque convallis a cras. 6 | 7 | Vulputate odio ut enim blandit volutpat maecenas volutpat blandit aliquam. Egestas sed sed risus pretium quam vulputate dignissim suspendisse. Ipsum nunc aliquet bibendum enim facilisis gravida neque convallis. Urna cursus eget nunc scelerisque viverra mauris. Lacus luctus accumsan tortor posuere ac ut consequat. Urna molestie at elementum eu. Consequat semper viverra nam libero justo laoreet. Donec massa sapien faucibus et molestie ac feugiat sed lectus. Massa sed elementum tempus egestas. Neque ornare aenean euismod elementum nisi quis eleifend. Arcu cursus euismod quis viverra nibh. Diam vulputate ut pharetra sit amet aliquam. 8 | 9 | Egestas fringilla phasellus faucibus scelerisque eleifend. Neque convallis a cras semper auctor. Sollicitudin tempor id eu nisl. Dictum varius duis at consectetur lorem. Sodales neque sodales ut etiam sit amet nisl. Nibh nisl condimentum id venenatis a condimentum vitae sapien pellentesque. Cras sed felis eget velit aliquet. Ut ornare lectus sit amet est placerat in egestas erat. Mattis aliquam faucibus purus in. Gravida in fermentum et sollicitudin ac orci phasellus egestas. Eu ultrices vitae auctor eu augue ut. Orci nulla pellentesque dignissim enim sit amet venenatis. Eget magna fermentum iaculis eu non diam. Ac orci phasellus egestas tellus rutrum. Dignissim diam quis enim lobortis scelerisque fermentum dui faucibus. Sed lectus vestibulum mattis ullamcorper velit sed ullamcorper. Condimentum lacinia quis vel eros donec ac odio tempor orci. Senectus et netus et malesuada fames ac turpis egestas sed. Diam vulputate ut pharetra sit. -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.7z -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.cdr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.cdr -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.iso -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.rar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.rar -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.rar4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.rar4 -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.tar -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.tar.bz2 -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.tar.gz -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.tar.xz -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.vhdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.vhdx -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.wim: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.wim -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestData.zip -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataArchivesNested.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataArchivesNested.zip -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataCorrupt.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataCorrupt.tar -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataCorrupt.tar.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataCorrupt.tar.zip -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataCorruptWim.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataCorruptWim.zip -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataEncrypted.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataEncrypted.7z -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataEncrypted.rar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataEncrypted.rar -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataEncrypted.rar4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataEncrypted.rar4 -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataEncryptedAES.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataEncryptedAES.zip -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataEncryptedZipCrypto.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataEncryptedZipCrypto.zip -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataForFilters.7z: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/TestDataForFilters.7z -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/UdfTest.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/UdfTest.iso -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/UdfTestWithMultiSystem.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/UdfTestWithMultiSystem.iso -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestData/TestDataArchives/sysvbanner_1.0-17fakesync1_amd64.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/RecursiveExtractor.Tests/TestData/TestDataArchives/sysvbanner_1.0-17fakesync1_amd64.deb -------------------------------------------------------------------------------- /RecursiveExtractor.Tests/TestPathHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace Microsoft.CST.RecursiveExtractor.Tests; 7 | 8 | public static class TestPathHelpers 9 | { 10 | public const string TestTempFolderName = "RE_Tests"; 11 | 12 | public static string TestDirectoryPath => Path.Combine(Path.GetTempPath(), TestTempFolderName); 13 | 14 | public static string GetFreshTestDirectory() 15 | { 16 | return Path.Combine(TestDirectoryPath, FileEntry.SanitizePath(Guid.NewGuid().ToString())); 17 | } 18 | 19 | public static void DeleteTestDirectory() 20 | { 21 | try 22 | { 23 | Directory.Delete(Path.Combine(TestDirectoryPath), true); 24 | } 25 | catch (DirectoryNotFoundException) 26 | { 27 | // Not an error. Not every test makes the folder. 28 | } 29 | catch (Exception e) 30 | { 31 | // Throwing the exception up may cause tests to fail due to file system oddness so just log 32 | Logger.Warn("Failed to delete Test Working Directory at {directory}", TestDirectoryPath); 33 | } 34 | } 35 | 36 | static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 37 | } -------------------------------------------------------------------------------- /RecursiveExtractor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34408.163 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RecursiveExtractor", "RecursiveExtractor\RecursiveExtractor.csproj", "{A7F7492B-60E0-468C-B267-BA60EC131E86}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RecursiveExtractor.Tests", "RecursiveExtractor.Tests\RecursiveExtractor.Tests.csproj", "{BB4A44C9-47E4-4BF5-A04A-D3A65E46D115}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7C009C0C-67E2-4A79-8D5E-C30ED68B2B02}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RecursiveExtractor.Cli", "RecursiveExtractor.Cli\RecursiveExtractor.Cli.csproj", "{443B4E50-9AAF-436E-B3DF-644F782AF9B6}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RecursiveExtractor.Cli.Tests", "RecursiveExtractor.Cli.Tests\RecursiveExtractor.Cli.Tests.csproj", "{F37B314B-F641-4336-BCD6-BC5B85BEC5DB}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {A7F7492B-60E0-468C-B267-BA60EC131E86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {A7F7492B-60E0-468C-B267-BA60EC131E86}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {A7F7492B-60E0-468C-B267-BA60EC131E86}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {A7F7492B-60E0-468C-B267-BA60EC131E86}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {BB4A44C9-47E4-4BF5-A04A-D3A65E46D115}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {BB4A44C9-47E4-4BF5-A04A-D3A65E46D115}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {BB4A44C9-47E4-4BF5-A04A-D3A65E46D115}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {BB4A44C9-47E4-4BF5-A04A-D3A65E46D115}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {443B4E50-9AAF-436E-B3DF-644F782AF9B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {443B4E50-9AAF-436E-B3DF-644F782AF9B6}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {443B4E50-9AAF-436E-B3DF-644F782AF9B6}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {443B4E50-9AAF-436E-B3DF-644F782AF9B6}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {F37B314B-F641-4336-BCD6-BC5B85BEC5DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {F37B314B-F641-4336-BCD6-BC5B85BEC5DB}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {F37B314B-F641-4336-BCD6-BC5B85BEC5DB}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {F37B314B-F641-4336-BCD6-BC5B85BEC5DB}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {38234B7F-8828-462C-8C2A-747A4A195D7F} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /RecursiveExtractor/DebArchiveFile.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace Microsoft.CST.RecursiveExtractor 6 | { 7 | /// 8 | /// Implementation of the Deb Archive format 9 | /// See: https://en.wikipedia.org/wiki/Deb_(file_format)#/media/File:Deb_File_Structure.svg 10 | /// 11 | public static class DebArchiveFile 12 | { 13 | /// 14 | /// Enumerate the FileEntries in the given Deb file 15 | /// 16 | /// The Deb file FileEntry 17 | /// The ExtractorOptions to use 18 | /// The ResourceGovernor to use 19 | /// The FileEntries found 20 | public static IEnumerable GetFileEntries(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor) 21 | { 22 | if (fileEntry == null) 23 | { 24 | yield break; 25 | } 26 | 27 | // First, cut out the file signature (8 bytes) and global header (64 bytes) 28 | fileEntry.Content.Position = 72; 29 | var headerBytes = new byte[60]; 30 | 31 | while (fileEntry.Content.Length - fileEntry.Content.Position >= 60) 32 | { 33 | fileEntry.Content.Read(headerBytes, 0, 60); 34 | var filename = Encoding.ASCII.GetString(headerBytes[0..16]).Trim(); // filename is 16 bytes 35 | var fileSizeBytes = headerBytes[48..58]; // File size is decimal-encoded, 10 bytes long 36 | if (int.TryParse(Encoding.ASCII.GetString(fileSizeBytes).Trim(), out var fileSize)) 37 | { 38 | governor.CheckResourceGovernor(fileSize); 39 | governor.AdjustRemainingBytes(-fileSize); 40 | 41 | var entryContent = new byte[fileSize]; 42 | fileEntry.Content.Read(entryContent, 0, fileSize); 43 | var stream = new MemoryStream(entryContent); 44 | yield return new FileEntry(filename, stream, fileEntry, true); 45 | } 46 | else 47 | { 48 | break; 49 | } 50 | } 51 | } 52 | 53 | /// 54 | /// Enumerate the FileEntries in the given Deb file asynchronously 55 | /// 56 | /// The Deb file FileEntry 57 | /// The ExtractorOptions to use 58 | /// The ResourceGovernor to use 59 | /// The FileEntries found 60 | public static async IAsyncEnumerable GetFileEntriesAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor) 61 | { 62 | if (fileEntry == null) 63 | { 64 | yield break; 65 | } 66 | 67 | // First, cut out the file signature (8 bytes) and global header (64 bytes) 68 | fileEntry.Content.Position = 72; 69 | var headerBytes = new byte[60]; 70 | 71 | while (fileEntry.Content.Length - fileEntry.Content.Position >= 60) 72 | { 73 | fileEntry.Content.Read(headerBytes, 0, 60); 74 | var filename = Encoding.ASCII.GetString(headerBytes[0..16]).Trim(); // filename is 16 bytes 75 | var fileSizeBytes = headerBytes[48..58]; // File size is decimal-encoded, 10 bytes long 76 | if (int.TryParse(Encoding.ASCII.GetString(fileSizeBytes).Trim(), out var fileSize)) 77 | { 78 | governor.CheckResourceGovernor(fileSize); 79 | governor.AdjustRemainingBytes(-fileSize); 80 | 81 | var entryContent = new byte[fileSize]; 82 | await fileEntry.Content.ReadAsync(entryContent, 0, fileSize).ConfigureAwait(false); 83 | var stream = new MemoryStream(entryContent); 84 | yield return new FileEntry(filename, stream, fileEntry, true); 85 | } 86 | else 87 | { 88 | break; 89 | } 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /RecursiveExtractor/ExtractionStatusCode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. Licensed under the MIT License. 2 | 3 | namespace Microsoft.CST.RecursiveExtractor 4 | { 5 | /// 6 | /// Status codes for the ExtractToDirectory functions. 7 | /// 8 | public enum ExtractionStatusCode 9 | { 10 | /// 11 | /// Extraction generally successful. 12 | /// 13 | Ok, 14 | /// 15 | /// One of the arguments provided was invalid. 16 | /// 17 | BadArgument, 18 | /// 19 | /// There was a critical error extracting. 20 | /// 21 | Failure 22 | } 23 | } -------------------------------------------------------------------------------- /RecursiveExtractor/ExtractorOptions.cs: -------------------------------------------------------------------------------- 1 | using GlobExpressions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace Microsoft.CST.RecursiveExtractor 8 | { 9 | /// 10 | /// Holder of options for the Extractor. 11 | /// 12 | public class ExtractorOptions 13 | { 14 | private IEnumerable _allowFilters = Array.Empty(); 15 | private IEnumerable _allowGlobs = Array.Empty(); 16 | private IEnumerable _denyFilters = Array.Empty(); 17 | private IEnumerable _denyGlobs = Array.Empty(); 18 | 19 | /// 20 | /// Maximum number of bytes before using a FileStream. Default 100MB 21 | /// 22 | public int MemoryStreamCutoff { get; set; } = 1024 * 1024 * 100; 23 | 24 | /// 25 | /// Enable timing limit for processing. 26 | /// 27 | public bool EnableTiming { get; set; } 28 | 29 | /// 30 | /// If an archive cannot be extracted return a single file entry for the archive itself. 31 | /// 32 | public bool ExtractSelfOnFail { get; set; } = true; 33 | 34 | /// 35 | /// The maximum number of bytes to extract from the archive and all embedded archives. Set to 0 to 36 | /// remove limit. Note that MaxExpansionRatio may also apply. Defaults to 0. 37 | /// 38 | public long MaxExtractedBytes { get; set; } = 0; 39 | 40 | /// 41 | /// By default, stop extracting if the total number of bytes seen is greater than this multiple of 42 | /// the original archive size. Used to avoid denial of service (zip bombs and the like). 43 | /// 44 | public double MaxExtractedBytesRatio { get; set; } = 200.0; 45 | 46 | /// 47 | /// If timing is enabled, stop processing after this time span. Used to avoid denial of service 48 | /// (zip bombs and the like). 49 | /// 50 | public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(300); 51 | 52 | /// 53 | /// Batch size to use when Parallel is set. 54 | /// 55 | public int BatchSize { get; set; } = 50; 56 | 57 | /// 58 | /// When extracting to directory, if extracted entries should be written to disk in parallel. 59 | /// 60 | public bool Parallel { get; set; } 61 | 62 | /// 63 | /// Parse these extensions as raw, don't extract any files from them. For example `.iso` to not extract from iso files. 64 | /// 65 | public IEnumerable RawExtensions { get; set; } = Array.Empty(); 66 | 67 | /// 68 | /// Dictionary of passwords to use. The Key is a Regex which matches the files to the associated Values against. 69 | /// 70 | public Dictionary> Passwords { get; set; } = new Dictionary>(); 71 | 72 | /// 73 | /// Should extraction recurse into archives, extracting archives contained in the top level archive. 74 | /// 75 | public bool Recurse { get; set; } = true; 76 | 77 | /// 78 | /// Buffer size to use for FileStream backed FileEntries. 79 | /// 80 | public int FileStreamBufferSize { get; set; } = 4096; 81 | 82 | /// 83 | /// If set, only return files that match these glob filters when checked against the full path of the file. 84 | /// 85 | public IEnumerable AllowFilters 86 | { 87 | get 88 | { 89 | return _allowFilters; 90 | } 91 | set 92 | { 93 | _allowFilters = value; 94 | _allowGlobs = value.Select(x => new Glob(x)); 95 | } 96 | } 97 | 98 | /// 99 | /// If set, don't return any files that match these glob filters when checked against the full path of the file. 100 | /// 101 | public IEnumerable DenyFilters 102 | { 103 | get 104 | { 105 | return _denyFilters; 106 | } 107 | set 108 | { 109 | _denyFilters = value; 110 | _denyGlobs = value.Select(x => new Glob(x)); 111 | } 112 | } 113 | 114 | /// 115 | /// Allow only the specified archive types to be extracted 116 | /// 117 | public IEnumerable AllowTypes { get; set; } = Array.Empty(); 118 | 119 | /// 120 | /// Prevent the specified archives types from being extracted 121 | /// 122 | public IEnumerable DenyTypes { get; set; } = Array.Empty(); 123 | 124 | /// 125 | /// Always expect the TopLevel to be an archive and mark as FailedArchive if it is not. When not set, extracting a flat file is not an error. 126 | /// 127 | public bool RequireTopLevelToBeArchive { get; set; } = false; 128 | 129 | /// 130 | /// If the file name provided should be extracted given the filter arguments in this ExtractorOptions instance 131 | /// 132 | /// 133 | /// 134 | public bool FileNamePasses(string filename) => (!_denyGlobs.Any() || (_denyGlobs.Any() && !_denyGlobs.Any(x => x.IsMatch(filename)))) && 135 | (!_allowGlobs.Any() || 136 | _allowGlobs.Any(x => x.IsMatch(filename))); 137 | 138 | /// 139 | /// Helper method to checks if the given is allowable by the set and . 140 | /// 141 | /// The to check. 142 | /// True if the options allow for extracting the specified type. 143 | public bool IsAcceptableType(ArchiveFileType type) => !DenyTypes.Contains(type) && (!AllowTypes.Any() || AllowTypes.Contains(type)); 144 | } 145 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/AsyncExtractorInterface.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Microsoft.CST.RecursiveExtractor.Extractors 4 | { 5 | /// 6 | /// An interface for an extractor that supports Asynchronous extraction. 7 | /// 8 | public interface AsyncExtractorInterface : ExtractorInterface 9 | { 10 | /// 11 | /// Extract from the provided 12 | /// 13 | /// The to extract. 14 | /// The to use for extraction. 15 | /// The to use for extraction. 16 | /// If this should be treated as the top level archive. 17 | /// of the files contained in the provided. 18 | public IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true); 19 | } 20 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/BZip2Extractor.cs: -------------------------------------------------------------------------------- 1 | using ICSharpCode.SharpZipLib.BZip2; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | 6 | namespace Microsoft.CST.RecursiveExtractor.Extractors 7 | { 8 | /// 9 | /// The implementation for BZip Archives 10 | /// 11 | public class BZip2Extractor : AsyncExtractorInterface 12 | { 13 | /// 14 | /// The constructor takes the Extractor context for recursion. 15 | /// 16 | /// The Extractor context. 17 | public BZip2Extractor(Extractor context) 18 | { 19 | Context = context; 20 | } 21 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 22 | 23 | // Uncompressed Size not exposed, so estimate a compression ratio of up to 20x for selecting between memory and file stream 24 | const int CompressionRatioEstimate = 20; 25 | 26 | internal Extractor Context { get; } 27 | 28 | /// 29 | /// Extracts an BZip2 file contained in fileEntry. 30 | /// 31 | /// FileEntry to extract 32 | /// The to use for extraction. 33 | /// The to use for extraction. 34 | /// If this should be treated as the top level archive. 35 | /// Extracted files 36 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 37 | { 38 | using var fs = StreamFactory.GenerateAppropriateBackingStream(options, fileEntry.Content.Length * CompressionRatioEstimate); 39 | var failed = false; 40 | try 41 | { 42 | BZip2.Decompress(fileEntry.Content, fs, false); 43 | } 44 | catch (Exception e) 45 | { 46 | Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, "BZip2", e.GetType(), e.Message, e.StackTrace); 47 | if (!options.ExtractSelfOnFail) 48 | { 49 | yield break; 50 | } 51 | else 52 | { 53 | failed = true; 54 | } 55 | } 56 | if (failed) 57 | { 58 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 59 | yield return fileEntry; 60 | yield break; 61 | } 62 | var newFilename = Path.GetFileNameWithoutExtension(fileEntry.Name); 63 | 64 | var entry = await FileEntry.FromStreamAsync(newFilename, fs, fileEntry).ConfigureAwait(false); 65 | 66 | if (entry != null) 67 | { 68 | if (options.Recurse || topLevel) 69 | { 70 | await foreach (var extractedFile in Context.ExtractAsync(entry, options, governor, false)) 71 | { 72 | yield return extractedFile; 73 | } 74 | } 75 | else 76 | { 77 | yield return entry; 78 | } 79 | } 80 | } 81 | 82 | /// 83 | /// Extracts a BZip2 file contained in fileEntry. 84 | /// 85 | /// FileEntry to extract 86 | /// The to use for extraction. 87 | /// The to use for extraction. 88 | /// If this should be treated as the top level archive. 89 | /// Extracted files 90 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 91 | { 92 | var newFilename = Path.GetFileNameWithoutExtension(fileEntry.Name); 93 | 94 | using var fs = StreamFactory.GenerateAppropriateBackingStream(options, fileEntry.Content.Length * CompressionRatioEstimate); 95 | 96 | var failed = false; 97 | 98 | try 99 | { 100 | BZip2.Decompress(fileEntry.Content, fs, false); 101 | } 102 | catch (Exception e) 103 | { 104 | Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, "BZip2", e.GetType(), e.Message, e.StackTrace); 105 | if (!options.ExtractSelfOnFail) 106 | { 107 | yield break; 108 | } 109 | else 110 | { 111 | failed = true; 112 | } 113 | } 114 | if (failed) 115 | { 116 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 117 | yield return fileEntry; 118 | yield break; 119 | } 120 | 121 | var entry = new FileEntry(newFilename, fs, fileEntry); 122 | 123 | if (entry != null) 124 | { 125 | if (options.Recurse || topLevel) 126 | { 127 | foreach (var extractedFile in Context.Extract(entry, options, governor, false)) 128 | { 129 | yield return extractedFile; 130 | } 131 | } 132 | else 133 | { 134 | yield return entry; 135 | } 136 | } 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/DebExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.CST.RecursiveExtractor.Extractors 9 | { 10 | /// 11 | /// The Deb Archive extractor implementation 12 | /// 13 | public class DebExtractor : AsyncExtractorInterface 14 | { 15 | /// 16 | /// The constructor takes the Extractor context for recursion. 17 | /// 18 | /// The Extractor context. 19 | public DebExtractor(Extractor context) 20 | { 21 | Context = context; 22 | } 23 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 24 | 25 | internal Extractor Context { get; } 26 | 27 | /// 28 | /// Extracts a .deb file contained in fileEntry. 29 | /// 30 | /// FileEntry to extract 31 | /// The to use for extraction. 32 | /// The to use for extraction. 33 | /// If this should be treated as the top level archive. 34 | /// Extracted files 35 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 36 | { 37 | await foreach (var entry in DebArchiveFile.GetFileEntriesAsync(fileEntry, options, governor)) 38 | { 39 | if (options.Recurse || topLevel) 40 | { 41 | await foreach (var extractedFile in Context.ExtractAsync(entry, options, governor, false)) 42 | { 43 | yield return extractedFile; 44 | } 45 | } 46 | else 47 | { 48 | yield return entry; 49 | } 50 | } 51 | } 52 | 53 | /// 54 | /// Extracts a .deb file contained in fileEntry. 55 | /// 56 | /// FileEntry to extract 57 | /// The to use for extraction. 58 | /// The to use for extraction. 59 | /// If this should be treated as the top level archive. 60 | /// Extracted files 61 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 62 | { 63 | var failed = false; 64 | IEnumerable? entries = null; 65 | try 66 | { 67 | entries = DebArchiveFile.GetFileEntries(fileEntry, options, governor); 68 | } 69 | catch (Exception e) when (e is not OverflowException) 70 | { 71 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 72 | Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.DEB, fileEntry.FullPath, string.Empty, e.GetType()); 73 | if (!options.ExtractSelfOnFail) 74 | { 75 | yield break; 76 | } 77 | else 78 | { 79 | failed = true; 80 | } 81 | } 82 | if (failed) 83 | { 84 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 85 | yield return fileEntry; 86 | yield break; 87 | } 88 | if (entries != null) 89 | { 90 | foreach (var entry in entries) 91 | { 92 | if (options.Recurse || topLevel) 93 | { 94 | foreach (var extractedFile in Context.Extract(entry, options, governor, false)) 95 | { 96 | yield return extractedFile; 97 | } 98 | } 99 | else 100 | { 101 | yield return entry; 102 | } 103 | } 104 | } 105 | else 106 | { 107 | if (options.ExtractSelfOnFail) 108 | { 109 | yield return fileEntry; 110 | } 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/DiscCommon.cs: -------------------------------------------------------------------------------- 1 | using DiscUtils; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | namespace Microsoft.CST.RecursiveExtractor.Extractors 9 | { 10 | /// 11 | /// Common crawler for some disc formats 12 | /// 13 | public static class DiscCommon 14 | { 15 | private static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 16 | 17 | /// 18 | /// Dump the FileEntries from a Logical Volume asynchronously 19 | /// 20 | /// The Volume to dump 21 | /// The Path to the parent Disc 22 | /// Extractor Options to use 23 | /// Resource Governor to use 24 | /// Extractor context to use 25 | /// The Parent FileEntry 26 | /// If this should be treated as the top level archive. 27 | /// 28 | public static async IAsyncEnumerable DumpLogicalVolumeAsync(LogicalVolumeInfo volume, string parentPath, ExtractorOptions options, ResourceGovernor governor, Extractor Context, FileEntry? parent = null, bool topLevel = true) 29 | { 30 | ReadOnlyCollection? fsInfos = null; 31 | try 32 | { 33 | fsInfos = FileSystemManager.DetectFileSystems(volume); 34 | } 35 | catch (Exception e) 36 | { 37 | Logger.Debug("Failed to get file systems from logical volume {0} Image {1} ({2}:{3})", volume.Identity, parentPath, e.GetType(), e.Message); 38 | } 39 | 40 | foreach (var fsInfo in fsInfos ?? Enumerable.Empty()) 41 | { 42 | using var fs = fsInfo.Open(volume); 43 | var diskFiles = fs.GetFiles(fs.Root.FullName, "*.*", SearchOption.AllDirectories).ToList(); 44 | 45 | foreach (var file in diskFiles) 46 | { 47 | Stream? fileStream = null; 48 | DiscFileInfo? fi = null; 49 | try 50 | { 51 | fi = fs.GetFileInfo(file); 52 | governor.CheckResourceGovernor(fi.Length); 53 | fileStream = fi.OpenRead(); 54 | } 55 | catch (Exception e) 56 | { 57 | Logger.Debug(e, "Failed to open {0} in volume {1}", file, volume.Identity); 58 | } 59 | if (fileStream != null && fi != null) 60 | { 61 | var newFileEntry = await FileEntry.FromStreamAsync($"{volume.Identity}{Path.DirectorySeparatorChar}{fi.FullName}", fileStream, parent, fi.CreationTime, fi.LastWriteTime, fi.LastAccessTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); 62 | if (options.Recurse || topLevel) 63 | { 64 | await foreach (var entry in Context.ExtractAsync(newFileEntry, options, governor, false)) 65 | { 66 | yield return entry; 67 | } 68 | } 69 | else 70 | { 71 | yield return newFileEntry; 72 | } 73 | 74 | } 75 | } 76 | } 77 | } 78 | 79 | /// 80 | /// Dump the FileEntries from a Logical Volume 81 | /// 82 | /// The Volume to dump 83 | /// The Path to the parent Disc 84 | /// Extractor Options to use 85 | /// Resource Governor to use 86 | /// Extractor context to use 87 | /// The Parent FilEntry 88 | /// If this should be treated as the top level archive. 89 | /// An enumerable of the contained File Entries. 90 | public static IEnumerable DumpLogicalVolume(LogicalVolumeInfo volume, string parentPath, ExtractorOptions options, ResourceGovernor governor, Extractor Context, FileEntry? parent = null, bool topLevel = true) 91 | { 92 | ReadOnlyCollection? fsInfos = null; 93 | try 94 | { 95 | fsInfos = FileSystemManager.DetectFileSystems(volume); 96 | } 97 | catch (Exception e) 98 | { 99 | Logger.Debug("Failed to get file systems from logical volume {0} Image {1} ({2}:{3})", volume.Identity, parentPath, e.GetType(), e.Message); 100 | } 101 | 102 | foreach (var fsInfo in fsInfos ?? Enumerable.Empty()) 103 | { 104 | using var fs = fsInfo.Open(volume); 105 | var diskFiles = fs.GetFiles(fs.Root.FullName, "*.*", SearchOption.AllDirectories).ToList(); 106 | 107 | foreach (var file in diskFiles) 108 | { 109 | Stream? fileStream = null; 110 | (DateTime? creation, DateTime? modification, DateTime? access) = (null, null, null); 111 | try 112 | { 113 | var fi = fs.GetFileInfo(file); 114 | governor.CheckResourceGovernor(fi.Length); 115 | fileStream = fi.OpenRead(); 116 | creation = fi.CreationTime; 117 | modification = fi.LastWriteTime; 118 | access = fi.LastAccessTime; 119 | } 120 | catch (Exception e) 121 | { 122 | Logger.Debug(e, "Failed to open {0} in volume {1}", file, volume.Identity); 123 | } 124 | if (fileStream != null) 125 | { 126 | var newFileEntry = new FileEntry($"{volume.Identity}{Path.DirectorySeparatorChar}{file}", fileStream, parent, false, creation, modification, access, memoryStreamCutoff: options.MemoryStreamCutoff); 127 | if (options.Recurse || topLevel) 128 | { 129 | foreach (var extractedFile in Context.Extract(newFileEntry, options, governor, false)) 130 | { 131 | yield return extractedFile; 132 | } 133 | } 134 | else 135 | { 136 | yield return newFileEntry; 137 | } 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/ExtractorInterface.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Microsoft.CST.RecursiveExtractor.Extractors 4 | { 5 | /// 6 | /// The interface for an extractor that supports Synchronous Extraction. 7 | /// 8 | public interface ExtractorInterface 9 | { 10 | /// 11 | /// Extract from the provided 12 | /// 13 | /// The to extract. 14 | /// The to use for extraction. 15 | /// The to use for extraction. 16 | /// If this should be treated as the top level archive. 17 | /// of the files contained in the provided. 18 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true); 19 | } 20 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/GnuArExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.InteropServices.ComTypes; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.CST.RecursiveExtractor.Extractors 9 | { 10 | /// 11 | /// The Ar file extractor. 12 | /// 13 | public class GnuArExtractor : AsyncExtractorInterface 14 | { 15 | /// 16 | /// The constructor takes the Extractor context for recursion. 17 | /// 18 | /// The Extractor context. 19 | public GnuArExtractor(Extractor context) 20 | { 21 | Context = context; 22 | } 23 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 24 | 25 | internal Extractor Context { get; } 26 | 27 | /// 28 | /// Extracts an archive file created with GNU ar 29 | /// 30 | /// 31 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 32 | { 33 | await foreach (var entry in ArFile.GetFileEntriesAsync(fileEntry, options, governor)) 34 | { 35 | if (options.Recurse || topLevel) 36 | { 37 | await foreach (var extractedFile in Context.ExtractAsync(entry, options, governor, false)) 38 | { 39 | yield return extractedFile; 40 | } 41 | } 42 | else 43 | { 44 | yield return entry; 45 | } 46 | } 47 | } 48 | 49 | /// 50 | /// Extracts an archive file created with GNU ar 51 | /// 52 | /// 53 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 54 | { 55 | IEnumerable? fileEntries = null; 56 | try 57 | { 58 | fileEntries = ArFile.GetFileEntries(fileEntry, options, governor); 59 | } 60 | catch (Exception e) 61 | { 62 | Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.AR, fileEntry.FullPath, string.Empty, e.GetType()); 63 | if (e is OverflowException) 64 | { 65 | throw; 66 | } 67 | } 68 | if (fileEntries != null) 69 | { 70 | foreach (var entry in fileEntries) 71 | { 72 | if (options.Recurse || topLevel) 73 | { 74 | foreach (var extractedFile in Context.Extract(entry, options, governor, false)) 75 | { 76 | yield return extractedFile; 77 | } 78 | } 79 | else 80 | { 81 | yield return entry; 82 | } 83 | } 84 | } 85 | else 86 | { 87 | if (options.ExtractSelfOnFail) 88 | { 89 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 90 | yield return fileEntry; 91 | } 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/GzipExtractor.cs: -------------------------------------------------------------------------------- 1 | using ICSharpCode.SharpZipLib.GZip; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | 6 | namespace Microsoft.CST.RecursiveExtractor.Extractors 7 | { 8 | /// 9 | /// The Gzip extractor implementation 10 | /// 11 | public class GzipExtractor : AsyncExtractorInterface 12 | { 13 | /// 14 | /// The constructor takes the Extractor context for recursion. 15 | /// 16 | /// The Extractor context. 17 | public GzipExtractor(Extractor context) 18 | { 19 | Context = context; 20 | } 21 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 22 | 23 | // Uncompressed Size not exposed, so estimate a compression ratio of up to 20x for selecting between memory and file stream 24 | const int CompressionRatioEstimate = 20; 25 | 26 | internal Extractor Context { get; } 27 | 28 | /// 29 | /// Extracts an Gzip file contained in fileEntry. Since this function is recursive, even though 30 | /// Gzip only supports a single compressed file, that inner file could itself contain multiple others. 31 | /// 32 | /// FileEntry to extract 33 | /// The to use for extraction. 34 | /// The to use for extraction. 35 | /// If this should be treated as the top level archive. 36 | /// Extracted files 37 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 38 | { 39 | using var fs = StreamFactory.GenerateAppropriateBackingStream(options, fileEntry.Content.Length * CompressionRatioEstimate); 40 | var failed = false; 41 | try 42 | { 43 | GZip.Decompress(fileEntry.Content, fs, false); 44 | } 45 | catch (Exception e) 46 | { 47 | Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, "GZip", e.GetType(), e.Message, e.StackTrace); 48 | if (!options.ExtractSelfOnFail) 49 | { 50 | yield break; 51 | } 52 | else 53 | { 54 | failed = true; 55 | } 56 | } 57 | if (failed) 58 | { 59 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 60 | yield return fileEntry; 61 | yield break; 62 | } 63 | var newFilename = Path.GetFileNameWithoutExtension(fileEntry.Name); 64 | if (fileEntry.Name.EndsWith(".tgz", StringComparison.InvariantCultureIgnoreCase)) 65 | { 66 | newFilename = newFilename[0..^4] + ".tar"; 67 | } 68 | 69 | var entry = await FileEntry.FromStreamAsync(newFilename, fs, fileEntry).ConfigureAwait(false); 70 | 71 | if (entry != null) 72 | { 73 | if (options.Recurse || topLevel) 74 | { 75 | await foreach (var newFileEntry in Context.ExtractAsync(entry, options, governor, false)) 76 | { 77 | yield return newFileEntry; 78 | } 79 | } 80 | else 81 | { 82 | yield return entry; 83 | } 84 | } 85 | } 86 | 87 | /// 88 | /// Extracts a Gzip file contained in fileEntry. Since this function is recursive, even though 89 | /// Gzip only supports a single compressed file, that inner file could itself contain multiple others. 90 | /// 91 | /// FileEntry to extract 92 | /// The to use for extraction. 93 | /// The to use for extraction. 94 | /// If this should be treated as the top level archive. 95 | /// Extracted files 96 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 97 | { 98 | using var fs = StreamFactory.GenerateAppropriateBackingStream(options, fileEntry.Content.Length * CompressionRatioEstimate); 99 | var failed = false; 100 | try 101 | { 102 | GZip.Decompress(fileEntry.Content, fs, false); 103 | } 104 | catch (Exception e) 105 | { 106 | Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, "GZip", e.GetType(), e.Message, e.StackTrace); 107 | if (!options.ExtractSelfOnFail) 108 | { 109 | yield break; 110 | } 111 | else 112 | { 113 | failed = true; 114 | } 115 | } 116 | if (failed) 117 | { 118 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 119 | yield return fileEntry; 120 | yield break; 121 | } 122 | 123 | var newFilename = Path.GetFileNameWithoutExtension(fileEntry.Name); 124 | if (fileEntry.Name.EndsWith(".tgz", StringComparison.InvariantCultureIgnoreCase)) 125 | { 126 | newFilename = newFilename[0..^4] + ".tar"; 127 | } 128 | 129 | var entry = new FileEntry(newFilename, fs, fileEntry); 130 | 131 | if (entry != null) 132 | { 133 | if (options.Recurse || topLevel) 134 | { 135 | foreach (var extractedFile in Context.Extract(entry, options, governor, false)) 136 | { 137 | yield return extractedFile; 138 | } 139 | } 140 | else 141 | { 142 | yield return entry; 143 | } 144 | } 145 | } 146 | } 147 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/IsoExtractor.cs: -------------------------------------------------------------------------------- 1 | using DiscUtils.Iso9660; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | namespace Microsoft.CST.RecursiveExtractor.Extractors 9 | { 10 | /// 11 | /// The ISO disc image extractor implementation. 12 | /// 13 | public class IsoExtractor : AsyncExtractorInterface 14 | { 15 | /// 16 | /// The constructor takes the Extractor context for recursion. 17 | /// 18 | /// The Extractor context. 19 | public IsoExtractor(Extractor context) 20 | { 21 | Context = context; 22 | } 23 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 24 | 25 | internal Extractor Context { get; } 26 | 27 | /// 28 | /// Extracts an ISO file 29 | /// 30 | /// 31 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 32 | { 33 | DiscUtils.DiscFileInfo[]? entries = null; 34 | var failed = false; 35 | try 36 | { 37 | using var cd = new CDReader(fileEntry.Content, true); 38 | entries = cd.Root.GetFiles("*.*", SearchOption.AllDirectories).ToArray(); 39 | } 40 | catch (Exception e) 41 | { 42 | Logger.Debug("Failed to open ISO {0}. ({1}:{2})", fileEntry.FullPath, e.GetType(), e.Message); 43 | failed = true; 44 | } 45 | if (failed) 46 | { 47 | if (options.ExtractSelfOnFail) 48 | { 49 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 50 | yield return fileEntry; 51 | } 52 | } 53 | else if (entries != null) 54 | { 55 | foreach (var file in entries) 56 | { 57 | var fileInfo = file; 58 | governor.CheckResourceGovernor(fileInfo.Length); 59 | Stream? stream = null; 60 | try 61 | { 62 | stream = fileInfo.OpenRead(); 63 | } 64 | catch (Exception e) 65 | { 66 | Logger.Debug("Failed to extract {0} from ISO {1}. ({2}:{3})", fileInfo.FullName, fileEntry.FullPath, e.GetType(), e.Message); 67 | } 68 | if (stream != null) 69 | { 70 | var name = fileInfo.FullName.Replace('/', Path.DirectorySeparatorChar); 71 | var newFileEntry = await FileEntry.FromStreamAsync(name, stream, fileEntry, fileInfo.CreationTime, fileInfo.LastWriteTime, fileInfo.LastAccessTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); 72 | if (options.Recurse || topLevel) 73 | { 74 | await foreach (var entry in Context.ExtractAsync(newFileEntry, options, governor, false)) 75 | { 76 | yield return entry; 77 | } 78 | } 79 | else 80 | { 81 | yield return newFileEntry; 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | /// 89 | /// Extracts an ISO file 90 | /// 91 | /// 92 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 93 | { 94 | DiscUtils.DiscFileInfo[]? entries = null; 95 | var failed = false; 96 | try 97 | { 98 | using var cd = new CDReader(fileEntry.Content, true); 99 | entries = cd.Root.GetFiles("*.*", SearchOption.AllDirectories).ToArray(); 100 | } 101 | catch(Exception e) 102 | { 103 | Logger.Debug("Failed to open ISO {0}. ({1}:{2})", fileEntry.FullPath, e.GetType(), e.Message); 104 | failed = true; 105 | } 106 | if (failed) 107 | { 108 | if (options.ExtractSelfOnFail) 109 | { 110 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 111 | yield return fileEntry; 112 | } 113 | } 114 | else if (entries != null) 115 | { 116 | foreach (var file in entries) 117 | { 118 | var fileInfo = file; 119 | governor.CheckResourceGovernor(fileInfo.Length); 120 | Stream? stream = null; 121 | try 122 | { 123 | stream = fileInfo.OpenRead(); 124 | } 125 | catch (Exception e) 126 | { 127 | Logger.Debug("Failed to extract {0} from ISO {1}. ({2}:{3})", fileInfo.FullName, fileEntry.FullPath, e.GetType(), e.Message); 128 | } 129 | if (stream != null) 130 | { 131 | var name = fileInfo.FullName.Replace('/', Path.DirectorySeparatorChar); 132 | var newFileEntry = new FileEntry(name, stream, fileEntry, createTime: file.CreationTime, modifyTime: file.LastWriteTime, accessTime: file.LastAccessTime, memoryStreamCutoff: options.MemoryStreamCutoff); 133 | if (options.Recurse || topLevel) 134 | { 135 | foreach (var entry in Context.Extract(newFileEntry, options, governor, false)) 136 | { 137 | yield return entry; 138 | } 139 | } 140 | else 141 | { 142 | yield return newFileEntry; 143 | } 144 | } 145 | } 146 | } 147 | else 148 | { 149 | if (options.ExtractSelfOnFail) 150 | { 151 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 152 | yield return fileEntry; 153 | } 154 | } 155 | } 156 | } 157 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/RarExtractor.cs: -------------------------------------------------------------------------------- 1 | using SharpCompress.Archives.Rar; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | namespace Microsoft.CST.RecursiveExtractor.Extractors 9 | { 10 | /// 11 | /// The RAR Archive extractor implementation 12 | /// 13 | public class RarExtractor : AsyncExtractorInterface 14 | { 15 | /// 16 | /// The constructor takes the Extractor context for recursion. 17 | /// 18 | /// The Extractor context. 19 | public RarExtractor(Extractor context) 20 | { 21 | Context = context; 22 | } 23 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 24 | 25 | internal Extractor Context { get; } 26 | 27 | private (RarArchive? archive, FileEntryStatus archiveStatus) GetRarArchive(FileEntry fileEntry, ExtractorOptions options) 28 | { 29 | RarArchive? rarArchive = null; 30 | var needsPassword = false; 31 | 32 | try 33 | { 34 | rarArchive = RarArchive.Open(fileEntry.Content); 35 | // Test for invalid archives. This will throw invalidformatexception 36 | var t = rarArchive.IsSolid; 37 | if (rarArchive.Entries.Any(x => x.IsEncrypted)) 38 | { 39 | needsPassword = true; 40 | } 41 | } 42 | catch (SharpCompress.Common.CryptographicException) 43 | { 44 | needsPassword = true; 45 | } 46 | catch (Exception e) 47 | { 48 | Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, fileEntry.ArchiveType, fileEntry.FullPath, string.Empty, e.GetType()); 49 | return (null, FileEntryStatus.FailedArchive); 50 | } 51 | 52 | // RAR5 encryption is not supported by SharpCompress 53 | if (needsPassword && fileEntry.ArchiveType == ArchiveFileType.RAR5) 54 | { 55 | return (null, FileEntryStatus.EncryptedArchive); 56 | } 57 | 58 | if (needsPassword) 59 | { 60 | var passwordFound = false; 61 | foreach (var passwords in options.Passwords.Where(x => x.Key.IsMatch(fileEntry.Name))) 62 | { 63 | if (passwordFound) { break; } 64 | foreach (var password in passwords.Value) 65 | { 66 | try 67 | { 68 | fileEntry.Content.Position = 0; 69 | rarArchive = RarArchive.Open(fileEntry.Content, new SharpCompress.Readers.ReaderOptions() { Password = password, LookForHeader = true }); 70 | var byt = new byte[1]; 71 | var encryptedEntry = rarArchive.Entries.FirstOrDefault(x => x is { IsEncrypted: true, Size: > 0 }); 72 | // Justification for !: Because we use FirstOrDefault encryptedEntry may be null, but we have a catch below for it 73 | using var entryStream = encryptedEntry!.OpenEntryStream(); 74 | if (entryStream.Read(byt, 0, 1) > 0) 75 | { 76 | passwordFound = true; 77 | break; 78 | } 79 | } 80 | catch (Exception e) 81 | { 82 | Logger.Trace(Extractor.FAILED_PASSWORD_ERROR_MESSAGE_STRING, fileEntry.FullPath, ArchiveFileType.RAR, e.GetType(), e.Message); 83 | } 84 | } 85 | } 86 | if (!passwordFound) 87 | { 88 | return (null, FileEntryStatus.EncryptedArchive); 89 | } 90 | } 91 | return (rarArchive, FileEntryStatus.Default); 92 | } 93 | 94 | /// 95 | /// Extracts a RAR archive 96 | /// 97 | /// 98 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 99 | { 100 | (var rarArchive, var archiveType) = GetRarArchive(fileEntry, options); 101 | fileEntry.EntryStatus = archiveType; 102 | if (rarArchive != null && fileEntry.EntryStatus == FileEntryStatus.Default) 103 | { 104 | foreach (var entry in rarArchive.Entries.Where(x => x.IsComplete && !x.IsDirectory)) 105 | { 106 | governor.CheckResourceGovernor(entry.Size); 107 | var name = entry.Key.Replace('/', Path.DirectorySeparatorChar); 108 | var newFileEntry = await FileEntry.FromStreamAsync(name, entry.OpenEntryStream(), fileEntry, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); 109 | if (newFileEntry != null) 110 | { 111 | if (options.Recurse || topLevel) 112 | { 113 | await foreach (var innerEntry in Context.ExtractAsync(newFileEntry, options, governor, false)) 114 | { 115 | yield return innerEntry; 116 | } 117 | } 118 | else 119 | { 120 | yield return newFileEntry; 121 | } 122 | } 123 | } 124 | } 125 | else 126 | { 127 | if (options.ExtractSelfOnFail) 128 | { 129 | yield return fileEntry; 130 | } 131 | } 132 | } 133 | 134 | /// 135 | /// Extracts a RAR archive 136 | /// 137 | /// 138 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 139 | { 140 | (var rarArchive, var archiveType) = GetRarArchive(fileEntry, options); 141 | fileEntry.EntryStatus = archiveType; 142 | if (rarArchive != null && fileEntry.EntryStatus == FileEntryStatus.Default) 143 | { 144 | var entries = rarArchive.Entries.Where(x => x.IsComplete && !x.IsDirectory); 145 | foreach (var entry in entries) 146 | { 147 | governor.CheckResourceGovernor(entry.Size); 148 | FileEntry? newFileEntry = null; 149 | try 150 | { 151 | var stream = entry.OpenEntryStream(); 152 | var name = entry.Key.Replace('/', Path.DirectorySeparatorChar); 153 | newFileEntry = new FileEntry(name, stream, fileEntry, false, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff); 154 | } 155 | catch (Exception e) 156 | { 157 | Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.RAR, fileEntry.FullPath, entry.Key, e.GetType()); 158 | } 159 | if (newFileEntry != null) 160 | { 161 | if (options.Recurse || topLevel) 162 | { 163 | foreach (var innerEntry in Context.Extract(newFileEntry, options, governor, false)) 164 | { 165 | yield return innerEntry; 166 | } 167 | } 168 | else 169 | { 170 | yield return newFileEntry; 171 | } 172 | } 173 | } 174 | } 175 | else 176 | { 177 | if (options.ExtractSelfOnFail) 178 | { 179 | yield return fileEntry; 180 | } 181 | } 182 | } 183 | } 184 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/SevenZipExtractor.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using SharpCompress.Archives.SevenZip; 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Security.Cryptography; 9 | using System.Threading.Tasks; 10 | 11 | namespace Microsoft.CST.RecursiveExtractor.Extractors 12 | { 13 | /// 14 | /// The 7Zip extractor implementation 15 | /// 16 | public class SevenZipExtractor : AsyncExtractorInterface 17 | { 18 | /// 19 | /// The constructor takes the Extractor context for recursion. 20 | /// 21 | /// The Extractor context. 22 | public SevenZipExtractor(Extractor context) 23 | { 24 | Context = context; 25 | } 26 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 27 | 28 | internal Extractor Context { get; } 29 | 30 | /// 31 | /// Extracts a 7-Zip file contained in fileEntry. 32 | /// 33 | /// 34 | /// Extracted files 35 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 36 | { 37 | (var sevenZipArchive, var archiveStatus) = GetSevenZipArchive(fileEntry, options); 38 | fileEntry.EntryStatus = archiveStatus; 39 | if (sevenZipArchive != null && archiveStatus == FileEntryStatus.Default) 40 | { 41 | foreach (var entry in sevenZipArchive.Entries.Where(x => !x.IsDirectory && x.IsComplete).ToList()) 42 | { 43 | governor.CheckResourceGovernor(entry.Size); 44 | var name = entry.Key.Replace('/', Path.DirectorySeparatorChar); 45 | var newFileEntry = await FileEntry.FromStreamAsync(name, entry.OpenEntryStream(), fileEntry, entry.CreatedTime, entry.LastModifiedTime, entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); 46 | 47 | if (newFileEntry != null) 48 | { 49 | if (options.Recurse || topLevel) 50 | { 51 | await foreach (var innerEntry in Context.ExtractAsync(newFileEntry, options, governor, false)) 52 | { 53 | yield return innerEntry; 54 | } 55 | } 56 | else 57 | { 58 | yield return newFileEntry; 59 | } 60 | } 61 | } 62 | } 63 | else 64 | { 65 | if (options.ExtractSelfOnFail) 66 | { 67 | yield return fileEntry; 68 | } 69 | } 70 | } 71 | 72 | private (SevenZipArchive? archive, FileEntryStatus archiveStatus) GetSevenZipArchive(FileEntry fileEntry, ExtractorOptions options) 73 | { 74 | SevenZipArchive? sevenZipArchive = null; 75 | var needsPassword = false; 76 | try 77 | { 78 | sevenZipArchive = SevenZipArchive.Open(fileEntry.Content); 79 | if (sevenZipArchive.Entries.Where(x => !x.IsDirectory).Any(x => x.IsEncrypted)) 80 | { 81 | needsPassword = true; 82 | } 83 | } 84 | catch (Exception e) when (e is SharpCompress.Common.CryptographicException) 85 | { 86 | needsPassword = true; 87 | } 88 | // Unencrypted archives throw null reference exception on the .IsEncrypted property 89 | catch (NullReferenceException) 90 | { 91 | return (sevenZipArchive, FileEntryStatus.Default); 92 | } 93 | catch (Exception e) 94 | { 95 | Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.P7ZIP, fileEntry.FullPath, string.Empty, e.GetType()); 96 | return (sevenZipArchive, FileEntryStatus.FailedArchive); 97 | } 98 | if (needsPassword) 99 | { 100 | var passwordFound = false; 101 | foreach (var passwords in options.Passwords.Where(x => x.Key.IsMatch(fileEntry.Name))) 102 | { 103 | if (passwordFound) { break; } 104 | foreach (var password in passwords.Value) 105 | { 106 | try 107 | { 108 | sevenZipArchive = SevenZipArchive.Open(fileEntry.Content, new SharpCompress.Readers.ReaderOptions() { Password = password }); 109 | // When filenames are encrypted we can't access the size of individual files 110 | // But if we can accesss the total uncompressed size we have the right password 111 | try 112 | { 113 | var entry = sevenZipArchive.Entries.Where(x => !x.IsDirectory).FirstOrDefault(x => x.IsEncrypted && x.Size > 0); 114 | // When filenames are plain, we can access the properties, so the previous statement won't throw 115 | // Instead we need to check that we can actually read from the stream 116 | using var entryStream = entry?.OpenEntryStream(); 117 | var bytes = new byte[1]; 118 | if ((entryStream?.Read(bytes, 0, 1) ?? 0) == 0) 119 | { 120 | Logger.Trace(Extractor.FAILED_PASSWORD_ERROR_MESSAGE_STRING, fileEntry.FullPath, ArchiveFileType.P7ZIP); 121 | continue; 122 | } 123 | return (sevenZipArchive, FileEntryStatus.Default); 124 | } 125 | catch (Exception) 126 | { 127 | continue; 128 | } 129 | 130 | } 131 | catch (Exception e) 132 | { 133 | Logger.Trace(Extractor.FAILED_PASSWORD_ERROR_MESSAGE_STRING, fileEntry.FullPath, ArchiveFileType.P7ZIP, e.GetType(), e.Message); 134 | } 135 | } 136 | } 137 | return (null, FileEntryStatus.EncryptedArchive); 138 | } 139 | return (sevenZipArchive, FileEntryStatus.Default); 140 | } 141 | 142 | /// 143 | /// Extracts a 7-Zip file contained in fileEntry. 144 | /// 145 | /// 146 | /// Extracted files 147 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 148 | { 149 | (var sevenZipArchive, var archiveStatus) = GetSevenZipArchive(fileEntry, options); 150 | fileEntry.EntryStatus = archiveStatus; 151 | if (sevenZipArchive != null && archiveStatus == FileEntryStatus.Default) 152 | { 153 | var entries = sevenZipArchive.Entries.Where(x => !x.IsDirectory && x.IsComplete).ToList(); 154 | foreach (var entry in entries) 155 | { 156 | governor.CheckResourceGovernor(entry.Size); 157 | var name = entry.Key.Replace('/', Path.DirectorySeparatorChar); 158 | var newFileEntry = new FileEntry(name, entry.OpenEntryStream(), fileEntry, createTime: entry.CreatedTime, modifyTime: entry.LastModifiedTime, accessTime: entry.LastAccessedTime, memoryStreamCutoff: options.MemoryStreamCutoff); 159 | 160 | if (options.Recurse || topLevel) 161 | { 162 | foreach (var innerEntry in Context.Extract(newFileEntry, options, governor, false)) 163 | { 164 | yield return innerEntry; 165 | } 166 | } 167 | else 168 | { 169 | yield return newFileEntry; 170 | } 171 | } 172 | } 173 | else 174 | { 175 | if (options.ExtractSelfOnFail) 176 | { 177 | yield return fileEntry; 178 | } 179 | } 180 | } 181 | } 182 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/UdfExtractor.cs: -------------------------------------------------------------------------------- 1 | using DiscUtils.Udf; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using System.Linq; 7 | 8 | namespace Microsoft.CST.RecursiveExtractor.Extractors 9 | { 10 | /// 11 | /// The UDF disc image extractor implementation. 12 | /// 13 | public class UdfExtractor : AsyncExtractorInterface 14 | { 15 | /// 16 | /// The constructor takes the Extractor context for recursion. 17 | /// 18 | /// The Extractor context. 19 | public UdfExtractor(Extractor context) 20 | { 21 | Context = context; 22 | } 23 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 24 | 25 | internal Extractor Context { get; } 26 | 27 | /// 28 | /// Extracts an UDF file 29 | /// 30 | /// 31 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 32 | { 33 | DiscUtils.DiscFileInfo[]? entries = null; 34 | var failed = false; 35 | try 36 | { 37 | using var cd = new UdfReader(fileEntry.Content); 38 | entries = cd.Root.GetFiles("*.*", SearchOption.AllDirectories).ToArray(); 39 | } 40 | catch (Exception e) 41 | { 42 | Logger.Debug("Failed to open UDF {0}. ({1}:{2})", fileEntry.FullPath, e.GetType(), e.Message); 43 | failed = true; 44 | } 45 | if (failed) 46 | { 47 | if (options.ExtractSelfOnFail) 48 | { 49 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 50 | yield return fileEntry; 51 | } 52 | } 53 | else if (entries != null) 54 | { 55 | foreach (var file in entries) 56 | { 57 | var fileInfo = file; 58 | governor.CheckResourceGovernor(fileInfo.Length); 59 | Stream? stream = null; 60 | try 61 | { 62 | stream = fileInfo.OpenRead(); 63 | } 64 | catch (Exception e) 65 | { 66 | Logger.Debug("Failed to extract {0} from UDF {1}. ({2}:{3})", fileInfo.FullName, fileEntry.FullPath, e.GetType(), e.Message); 67 | } 68 | if (stream != null) 69 | { 70 | var name = fileInfo.FullName.Replace('/', Path.DirectorySeparatorChar); 71 | var newFileEntry = await FileEntry.FromStreamAsync(name, stream, fileEntry, fileInfo.CreationTime, fileInfo.LastWriteTime, fileInfo.LastAccessTime, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); 72 | if (options.Recurse || topLevel) 73 | { 74 | await foreach (var entry in Context.ExtractAsync(newFileEntry, options, governor, false)) 75 | { 76 | yield return entry; 77 | } 78 | } 79 | else 80 | { 81 | yield return newFileEntry; 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | /// 89 | /// Extracts an UDF file 90 | /// 91 | /// 92 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 93 | { 94 | DiscUtils.DiscFileInfo[]? entries = null; 95 | var failed = false; 96 | try 97 | { 98 | using var cd = new UdfReader(fileEntry.Content); 99 | entries = cd.Root.GetFiles("*.*", SearchOption.AllDirectories).ToArray(); 100 | } 101 | catch(Exception e) 102 | { 103 | Logger.Debug("Failed to open UDF {0}. ({1}:{2})", fileEntry.FullPath, e.GetType(), e.Message); 104 | failed = true; 105 | } 106 | if (failed) 107 | { 108 | if (options.ExtractSelfOnFail) 109 | { 110 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 111 | yield return fileEntry; 112 | } 113 | } 114 | else if (entries != null) 115 | { 116 | foreach (var file in entries) 117 | { 118 | var fileInfo = file; 119 | governor.CheckResourceGovernor(fileInfo.Length); 120 | Stream? stream = null; 121 | try 122 | { 123 | stream = fileInfo.OpenRead(); 124 | } 125 | catch (Exception e) 126 | { 127 | Logger.Debug("Failed to extract {0} from UDF {1}. ({2}:{3})", fileInfo.FullName, fileEntry.FullPath, e.GetType(), e.Message); 128 | } 129 | if (stream != null) 130 | { 131 | var name = fileInfo.FullName.Replace('/', Path.DirectorySeparatorChar); 132 | var newFileEntry = new FileEntry(name, stream, fileEntry, createTime: file.CreationTime, modifyTime: file.LastWriteTime, accessTime: file.LastAccessTime, memoryStreamCutoff: options.MemoryStreamCutoff); 133 | if (options.Recurse || topLevel) 134 | { 135 | foreach (var entry in Context.Extract(newFileEntry, options, governor, false)) 136 | { 137 | yield return entry; 138 | } 139 | } 140 | else 141 | { 142 | yield return newFileEntry; 143 | } 144 | } 145 | } 146 | } 147 | else 148 | { 149 | if (options.ExtractSelfOnFail) 150 | { 151 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 152 | yield return fileEntry; 153 | } 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/VhdExtractor.cs: -------------------------------------------------------------------------------- 1 | using DiscUtils; 2 | using DiscUtils.Streams; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.CST.RecursiveExtractor.Extractors 7 | { 8 | /// 9 | /// The VHD Extractor implementation 10 | /// 11 | public class VhdExtractor : AsyncExtractorInterface 12 | { 13 | /// 14 | /// The constructor takes the Extractor context for recursion. 15 | /// 16 | /// The Extractor context. 17 | public VhdExtractor(Extractor context) 18 | { 19 | Context = context; 20 | } 21 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 22 | 23 | internal Extractor Context { get; } 24 | 25 | /// 26 | /// Extracts an a VHD file 27 | /// 28 | /// 29 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 30 | { 31 | LogicalVolumeInfo[]? logicalVolumes = null; 32 | DiscUtils.Vhd.Disk? disk = null; 33 | 34 | try 35 | { 36 | disk = new DiscUtils.Vhd.Disk(fileEntry.Content, Ownership.None); 37 | var manager = new VolumeManager(disk); 38 | logicalVolumes = manager.GetLogicalVolumes(); 39 | } 40 | catch (Exception e) 41 | { 42 | Logger.Debug("Error reading {0} disk at {1} ({2}:{3})", fileEntry.ArchiveType, fileEntry.FullPath, e.GetType(), e.Message); 43 | } 44 | 45 | if (logicalVolumes != null) 46 | { 47 | foreach (var volume in logicalVolumes) 48 | { 49 | await foreach (var entry in DiscCommon.DumpLogicalVolumeAsync(volume, fileEntry.FullPath, options, governor, Context, fileEntry, topLevel)) 50 | { 51 | yield return entry; 52 | } 53 | } 54 | } 55 | else 56 | { 57 | if (options.ExtractSelfOnFail) 58 | { 59 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 60 | yield return fileEntry; 61 | } 62 | } 63 | disk?.Dispose(); 64 | } 65 | 66 | /// 67 | /// Extracts an a VHD file 68 | /// 69 | /// 70 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 71 | { 72 | LogicalVolumeInfo[]? logicalVolumes = null; 73 | DiscUtils.Vhd.Disk? disk = null; 74 | 75 | try 76 | { 77 | disk = new DiscUtils.Vhd.Disk(fileEntry.Content, Ownership.None); 78 | var manager = new VolumeManager(disk); 79 | logicalVolumes = manager.GetLogicalVolumes(); 80 | } 81 | catch (Exception e) 82 | { 83 | Logger.Debug("Error reading {0} disk at {1} ({2}:{3})", fileEntry.ArchiveType, fileEntry.FullPath, e.GetType(), e.Message); 84 | } 85 | 86 | if (logicalVolumes != null) 87 | { 88 | foreach (var volume in logicalVolumes) 89 | { 90 | foreach (var entry in DiscCommon.DumpLogicalVolume(volume, fileEntry.FullPath, options, governor, Context, fileEntry, topLevel)) 91 | { 92 | yield return entry; 93 | } 94 | } 95 | } 96 | else 97 | { 98 | if (options.ExtractSelfOnFail) 99 | { 100 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 101 | yield return fileEntry; 102 | } 103 | } 104 | disk?.Dispose(); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/VhdxExtractor.cs: -------------------------------------------------------------------------------- 1 | using DiscUtils; 2 | using DiscUtils.Streams; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.CST.RecursiveExtractor.Extractors 7 | { 8 | /// 9 | /// The VHDX Extractor Implementation 10 | /// 11 | public class VhdxExtractor : AsyncExtractorInterface 12 | { 13 | /// 14 | /// The constructor takes the Extractor context for recursion. 15 | /// 16 | /// The Extractor context. 17 | public VhdxExtractor(Extractor context) 18 | { 19 | Context = context; 20 | } 21 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 22 | 23 | internal Extractor Context { get; } 24 | 25 | /// 26 | /// Extracts an a VHDX file 27 | /// 28 | /// 29 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 30 | { 31 | LogicalVolumeInfo[]? logicalVolumes = null; 32 | DiscUtils.Vhdx.Disk? disk = null; 33 | try 34 | { 35 | disk = new DiscUtils.Vhdx.Disk(fileEntry.Content, Ownership.None); 36 | var manager = new VolumeManager(disk); 37 | logicalVolumes = manager.GetLogicalVolumes(); 38 | } 39 | catch (Exception e) 40 | { 41 | Logger.Debug("Error reading {0} disk at {1} ({2}:{3})", fileEntry.ArchiveType, fileEntry.FullPath, e.GetType(), e.Message); 42 | } 43 | 44 | if (logicalVolumes != null) 45 | { 46 | foreach (var volume in logicalVolumes) 47 | { 48 | var fsInfos = FileSystemManager.DetectFileSystems(volume); 49 | 50 | await foreach (var entry in DiscCommon.DumpLogicalVolumeAsync(volume, fileEntry.FullPath, options, governor, Context, fileEntry, topLevel)) 51 | { 52 | yield return entry; 53 | } 54 | } 55 | } 56 | else 57 | { 58 | if (options.ExtractSelfOnFail) 59 | { 60 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 61 | yield return fileEntry; 62 | } 63 | } 64 | disk?.Dispose(); 65 | } 66 | 67 | /// 68 | /// Extracts an a VHDX file 69 | /// 70 | /// 71 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 72 | { 73 | LogicalVolumeInfo[]? logicalVolumes = null; 74 | DiscUtils.Vhdx.Disk? disk = null; 75 | try 76 | { 77 | disk = new DiscUtils.Vhdx.Disk(fileEntry.Content, Ownership.None); 78 | var manager = new VolumeManager(disk); 79 | logicalVolumes = manager.GetLogicalVolumes(); 80 | } 81 | catch (Exception e) 82 | { 83 | Logger.Debug("Error reading {0} disk at {1} ({2}:{3})", fileEntry.ArchiveType, fileEntry.FullPath, e.GetType(), e.Message); 84 | } 85 | 86 | if (logicalVolumes != null) 87 | { 88 | foreach (var volume in logicalVolumes) 89 | { 90 | var fsInfos = FileSystemManager.DetectFileSystems(volume); 91 | 92 | foreach (var entry in DiscCommon.DumpLogicalVolume(volume, fileEntry.FullPath, options, governor, Context, fileEntry, topLevel)) 93 | { 94 | yield return entry; 95 | } 96 | } 97 | } 98 | else 99 | { 100 | if (options.ExtractSelfOnFail) 101 | { 102 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 103 | yield return fileEntry; 104 | } 105 | } 106 | disk?.Dispose(); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/VmdkExtractor.cs: -------------------------------------------------------------------------------- 1 | using DiscUtils; 2 | using DiscUtils.Streams; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.CST.RecursiveExtractor.Extractors 7 | { 8 | /// 9 | /// The VMDK Extractor Implementation 10 | /// 11 | public class VmdkExtractor : AsyncExtractorInterface 12 | { 13 | /// 14 | /// The constructor takes the Extractor context for recursion. 15 | /// 16 | /// The Extractor context. 17 | public VmdkExtractor(Extractor context) 18 | { 19 | Context = context; 20 | } 21 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 22 | 23 | internal Extractor Context { get; } 24 | 25 | /// 26 | /// Extracts an a VMDK file 27 | /// 28 | /// 29 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 30 | { 31 | LogicalVolumeInfo[]? logicalVolumes = null; 32 | 33 | try 34 | { 35 | using var disk = new DiscUtils.Vmdk.Disk(fileEntry.Content, Ownership.None); 36 | var manager = new VolumeManager(disk); 37 | logicalVolumes = manager.GetLogicalVolumes(); 38 | } 39 | catch (Exception e) 40 | { 41 | Logger.Debug("Error reading {0} disk at {1} ({2}:{3})", fileEntry.ArchiveType, fileEntry.FullPath, e.GetType(), e.Message); 42 | } 43 | 44 | if (logicalVolumes != null) 45 | { 46 | foreach (var volume in logicalVolumes) 47 | { 48 | await foreach (var entry in DiscCommon.DumpLogicalVolumeAsync(volume, fileEntry.FullPath, options, governor, Context, fileEntry, topLevel)) 49 | { 50 | yield return entry; 51 | } 52 | } 53 | } 54 | else 55 | { 56 | if (options.ExtractSelfOnFail) 57 | { 58 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 59 | yield return fileEntry; 60 | } 61 | } 62 | } 63 | 64 | /// 65 | /// Extracts an a VMDK file 66 | /// 67 | /// 68 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 69 | { 70 | LogicalVolumeInfo[]? logicalVolumes = null; 71 | 72 | try 73 | { 74 | using var disk = new DiscUtils.Vmdk.Disk(fileEntry.Content, Ownership.None); 75 | var manager = new VolumeManager(disk); 76 | logicalVolumes = manager.GetLogicalVolumes(); 77 | } 78 | catch (Exception e) 79 | { 80 | Logger.Debug("Error reading {0} disk at {1} ({2}:{3})", fileEntry.ArchiveType, fileEntry.FullPath, e.GetType(), e.Message); 81 | } 82 | 83 | if (logicalVolumes != null) 84 | { 85 | foreach (var volume in logicalVolumes) 86 | { 87 | foreach (var entry in DiscCommon.DumpLogicalVolume(volume, fileEntry.FullPath, options, governor, Context, fileEntry, topLevel)) 88 | { 89 | yield return entry; 90 | } 91 | } 92 | } 93 | else 94 | { 95 | if (options.ExtractSelfOnFail) 96 | { 97 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 98 | yield return fileEntry; 99 | } 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/WimExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace Microsoft.CST.RecursiveExtractor.Extractors 6 | { 7 | /// 8 | /// The WIM Image Extractor Implementation 9 | /// 10 | public class WimExtractor : AsyncExtractorInterface 11 | { 12 | /// 13 | /// The constructor takes the Extractor context for recursion. 14 | /// 15 | /// The Extractor context. 16 | public WimExtractor(Extractor context) 17 | { 18 | Context = context; 19 | } 20 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 21 | 22 | internal Extractor Context { get; } 23 | 24 | /// 25 | /// Extracts a WIM file contained in fileEntry. 26 | /// 27 | /// 28 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 29 | { 30 | DiscUtils.Wim.WimFile? baseFile = null; 31 | try 32 | { 33 | baseFile = new DiscUtils.Wim.WimFile(fileEntry.Content); 34 | } 35 | catch (Exception e) 36 | { 37 | Logger.Debug(e, "Failed to init WIM image."); 38 | } 39 | if (baseFile != null) 40 | { 41 | for (var i = 0; i < baseFile.ImageCount; i++) 42 | { 43 | var image = baseFile.GetImage(i); 44 | foreach (var file in image.GetFiles(image.Root.FullName, "*.*", SearchOption.AllDirectories)) 45 | { 46 | Stream? stream = null; 47 | try 48 | { 49 | var info = image.GetFileInfo(file); 50 | stream = info.OpenRead(); 51 | governor.CheckResourceGovernor(info.Length); 52 | } 53 | catch (Exception e) 54 | { 55 | Logger.Debug("Error reading {0} from WIM {1} ({2}:{3})", file, image.FriendlyName, e.GetType(), e.Message); 56 | } 57 | if (stream != null) 58 | { 59 | var name = file.Replace('\\', Path.DirectorySeparatorChar); 60 | var newFileEntry = await FileEntry.FromStreamAsync($"{image.FriendlyName}{Path.DirectorySeparatorChar}{name}", stream, fileEntry, memoryStreamCutoff: options.MemoryStreamCutoff).ConfigureAwait(false); 61 | 62 | if (options.Recurse || topLevel) 63 | { 64 | await foreach (var entry in Context.ExtractAsync(newFileEntry, options, governor, false)) 65 | { 66 | yield return entry; 67 | } 68 | } 69 | else 70 | { 71 | yield return newFileEntry; 72 | } 73 | stream.Dispose(); 74 | } 75 | } 76 | } 77 | } 78 | else 79 | { 80 | if (options.ExtractSelfOnFail) 81 | { 82 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 83 | yield return fileEntry; 84 | } 85 | } 86 | } 87 | 88 | /// 89 | /// Extracts a WIM file contained in fileEntry. 90 | /// 91 | /// 92 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 93 | { 94 | DiscUtils.Wim.WimFile? baseFile = null; 95 | try 96 | { 97 | baseFile = new DiscUtils.Wim.WimFile(fileEntry.Content); 98 | } 99 | catch (Exception e) 100 | { 101 | Logger.Debug(e, "Failed to init WIM image from {0}.", fileEntry.FullPath); 102 | } 103 | if (baseFile != null) 104 | { 105 | for (var i = 0; i < baseFile.ImageCount; i++) 106 | { 107 | if (!TryGetImage(baseFile, i, out var image)) 108 | { 109 | Logger.Debug("Error reading image {0} from WIM {1}. Potentially malformed?", i, fileEntry.FullPath); 110 | continue; 111 | } 112 | 113 | foreach (var file in image!.GetFiles(image.Root.FullName, "*.*", SearchOption.AllDirectories)) 114 | { 115 | Stream? stream = null; 116 | try 117 | { 118 | var info = image.GetFileInfo(file); 119 | stream = info.OpenRead(); 120 | governor.CheckResourceGovernor(info.Length); 121 | } 122 | catch (Exception e) 123 | { 124 | Logger.Debug("Error reading {0} from WIM image {1} in {2} ({3}:{4})", file, i, fileEntry.FullPath, e.GetType(), e.Message); 125 | } 126 | if (stream != null) 127 | { 128 | var name = file.Replace('\\', Path.DirectorySeparatorChar); 129 | 130 | var newFileEntry = new FileEntry($"{image.FriendlyName}{Path.DirectorySeparatorChar}{name}", stream, fileEntry, memoryStreamCutoff: options.MemoryStreamCutoff); 131 | if (options.Recurse || topLevel) 132 | { 133 | foreach (var extractedFile in Context.Extract(newFileEntry, options, governor, false)) 134 | { 135 | yield return extractedFile; 136 | } 137 | } 138 | else 139 | { 140 | yield return newFileEntry; 141 | } 142 | stream.Dispose(); 143 | } 144 | } 145 | } 146 | } 147 | else 148 | { 149 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 150 | if (options.ExtractSelfOnFail) 151 | { 152 | yield return fileEntry; 153 | } 154 | } 155 | } 156 | 157 | private bool TryGetImage(DiscUtils.Wim.WimFile wimFile, int index, out DiscUtils.Wim.WimFileSystem? image) 158 | { 159 | image = null; 160 | 161 | try 162 | { 163 | image = wimFile.GetImage(index); 164 | } 165 | catch (Exception e) 166 | { 167 | // Image may be corrupt or invalid 168 | Logger.Debug(e, "Failed to retrieve WIM image with index {index}.", index); 169 | } 170 | 171 | return image is not null; 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /RecursiveExtractor/Extractors/XzExtractor.cs: -------------------------------------------------------------------------------- 1 | using SharpCompress.Compressors.Xz; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | 7 | namespace Microsoft.CST.RecursiveExtractor.Extractors 8 | { 9 | /// 10 | /// The XZ Extractor Implementation 11 | /// 12 | public class XzExtractor : AsyncExtractorInterface 13 | { 14 | /// 15 | /// The constructor takes the Extractor context for recursion. 16 | /// 17 | /// The Extractor context. 18 | public XzExtractor(Extractor context) 19 | { 20 | Context = context; 21 | } 22 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 23 | 24 | internal Extractor Context { get; } 25 | 26 | /// 27 | /// Extracts an zip file contained in fileEntry. 28 | /// 29 | /// 30 | public async IAsyncEnumerable ExtractAsync(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 31 | { 32 | XZStream? xzStream = null; 33 | try 34 | { 35 | xzStream = new XZStream(fileEntry.Content); 36 | } 37 | catch (Exception e) 38 | { 39 | Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.XZ, fileEntry.FullPath, string.Empty, e.GetType()); 40 | } 41 | if (xzStream != null) 42 | { 43 | var newFilename = Path.GetFileNameWithoutExtension(fileEntry.Name); 44 | var newFileEntry = new FileEntry(newFilename, xzStream, fileEntry, memoryStreamCutoff: options.MemoryStreamCutoff); 45 | 46 | // SharpCompress does not expose metadata without a full read, so we need to decompress first, 47 | // and then abort if the bytes exceeded the governor's capacity. 48 | 49 | var streamLength = xzStream.Index?.Records?.Select(r => r.UncompressedSize) 50 | .Aggregate((ulong?)0, (a, b) => a + b); 51 | 52 | // BUG: Technically, we're casting a ulong to a long, but we don't expect 9 exabyte steams, so 53 | // low risk. 54 | if (streamLength.HasValue) 55 | { 56 | governor.CheckResourceGovernor((long)streamLength.Value); 57 | } 58 | 59 | if (newFileEntry != null) 60 | { 61 | if (options.Recurse || topLevel) 62 | { 63 | await foreach (var innerEntry in Context.ExtractAsync(newFileEntry, options, governor, false)) 64 | { 65 | yield return innerEntry; 66 | } 67 | } 68 | else 69 | { 70 | yield return newFileEntry; 71 | } 72 | } 73 | xzStream.Dispose(); 74 | } 75 | else 76 | { 77 | if (options.ExtractSelfOnFail) 78 | { 79 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 80 | yield return fileEntry; 81 | } 82 | } 83 | } 84 | 85 | /// 86 | /// Extracts an zip file contained in fileEntry. 87 | /// 88 | /// 89 | public IEnumerable Extract(FileEntry fileEntry, ExtractorOptions options, ResourceGovernor governor, bool topLevel = true) 90 | { 91 | XZStream? xzStream = null; 92 | try 93 | { 94 | xzStream = new XZStream(fileEntry.Content); 95 | } 96 | catch (Exception e) 97 | { 98 | Logger.Debug(Extractor.FAILED_PARSING_ERROR_MESSAGE_STRING, ArchiveFileType.XZ, fileEntry.FullPath, string.Empty, e.GetType()); 99 | } 100 | if (xzStream != null) 101 | { 102 | var newFilename = Path.GetFileNameWithoutExtension(fileEntry.Name); 103 | var newFileEntry = new FileEntry(newFilename, xzStream, fileEntry, memoryStreamCutoff: options.MemoryStreamCutoff); 104 | 105 | // SharpCompress does not expose metadata without a full read, so we need to decompress first, 106 | // and then abort if the bytes exceeded the governor's capacity. 107 | 108 | var streamLength = xzStream.Index?.Records?.Select(r => r.UncompressedSize) 109 | .Aggregate((ulong?)0, (a, b) => a + b); 110 | 111 | // BUG: Technically, we're casting a ulong to a long, but we don't expect 9 exabyte steams, so 112 | // low risk. 113 | if (streamLength.HasValue) 114 | { 115 | governor.CheckResourceGovernor((long)streamLength.Value); 116 | } 117 | 118 | if (options.Recurse || topLevel) 119 | { 120 | foreach (var innerEntry in Context.Extract(newFileEntry, options, governor, false)) 121 | { 122 | yield return innerEntry; 123 | } 124 | } 125 | else 126 | { 127 | yield return newFileEntry; 128 | } 129 | xzStream.Dispose(); 130 | } 131 | else 132 | { 133 | if (options.ExtractSelfOnFail) 134 | { 135 | fileEntry.EntryStatus = FileEntryStatus.FailedArchive; 136 | yield return fileEntry; 137 | } 138 | } 139 | } 140 | } 141 | } -------------------------------------------------------------------------------- /RecursiveExtractor/FileEntryInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.CST.RecursiveExtractor 2 | { 3 | /// 4 | /// Bag of File data used for Pass Filtering 5 | /// 6 | public class FileEntryInfo 7 | { 8 | /// 9 | /// Construct a FileEntryInfo 10 | /// 11 | /// 12 | /// 13 | /// 14 | public FileEntryInfo(string name, string parentPath, long size) 15 | { 16 | Name = name; 17 | ParentPath = parentPath; 18 | Size = size; 19 | } 20 | /// 21 | /// The Relative Path in the Parent 22 | /// 23 | public string Name { get; } 24 | /// 25 | /// The Parent Path 26 | /// 27 | public string ParentPath { get; } 28 | /// 29 | /// The Size of the File 30 | /// 31 | public long Size { get; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /RecursiveExtractor/README.md: -------------------------------------------------------------------------------- 1 | ## RecursiveExtractor 2 | 3 | RecursiveExtractor is a general-purpose file extractor. 4 | 5 | ### Format Support 6 | 7 | RecursiveExtractor supports extracting the following types of archives: 8 | 9 | * GNU AR 10 | * BZip2 11 | * [deb](https://en.wikipedia.org/wiki/Deb_(file_format)) 12 | * ISO 13 | * tar 14 | * VHD 15 | * VHDX 16 | * VMDK 17 | * WIM 18 | * XZip 19 | * zip 20 | 21 | ## Using RecursiveExtractor 22 | 23 | To use RecursiveExtractor, just instantiate an `Extractor` object and call the `ExtractFile` 24 | method with either a filename or a byte array. This method will return an IEnumerable 25 | of FileEntry objects, each one of which will contain the name of the file and its 26 | contents, plus some additional metadata. 27 | 28 | ``` 29 | using Microsoft.CST.RecursiveExtractor; 30 | 31 | ... 32 | 33 | // Initialize the RecursiveExtractor extractor 34 | var extractor = new Extractor(); 35 | 36 | // Extract from an existing file 37 | foreach (var fileEntry in extractor.ExtractFile("test.zip")) 38 | { 39 | Console.WriteLine(fileEntry.FullPath); 40 | } 41 | 42 | // Extract from a byte array 43 | byte[] bytes = ...; 44 | // The "nonexistent.zip" name doesn't really matter, but is used as part of the 45 | // FileEntry.FullPath string. 46 | foreach (var fileEntry in extractor.ExtractFile("nonexistent.zip", bytes)) 47 | { 48 | Console.WriteLine(fileEntry.FullPath); 49 | } 50 | ``` 51 | 52 | ## Issues 53 | 54 | If you find any issues with RecursiveExtractor, please [open an issue](https://github.com/Microsoft/OSSGadget/issues/new) 55 | in the [Microsoft/OSSGadget](https://github.com/Microsoft/OSSGadget) repository. 56 | 57 | 58 | -------------------------------------------------------------------------------- /RecursiveExtractor/RecursiveExtractor.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;netstandard2.1;net8.0;net9.0 5 | Microsoft.CST.RecursiveExtractor 6 | 0.0.0-placeholder 7 | Microsoft 8 | Microsoft 9 | © Microsoft Corporation. All rights reserved. 10 | true 11 | Debug;Release 12 | 10.0 13 | Enable 14 | false 15 | true 16 | RecursiveExtractor is able to process the following formats: ar, bzip2, deb, gzip, iso, tar, vhd, vhdx, vmdk, wim, xzip, and zip. RecursiveExtractor automatically detects the archive type and fails gracefully when attempting to process malformed content. 17 | Microsoft.CST.RecursiveExtractor 18 | unzip extract extractor 19 | 0.0.0-placeholder 20 | https://github.com/microsoft/RecursiveExtractor 21 | icon-128.png 22 | MIT 23 | true 24 | snupkg 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /RecursiveExtractor/ResourceGovernor.cs: -------------------------------------------------------------------------------- 1 | using NLog; 2 | using System; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Threading; 6 | 7 | namespace Microsoft.CST.RecursiveExtractor 8 | { 9 | /// 10 | /// Class that keeps track of bytes processed and time spent processing. Used to Detect ZipBombs etc. 11 | /// 12 | public class ResourceGovernor 13 | { 14 | private readonly ExtractorOptions options; 15 | private readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); 16 | 17 | /// 18 | /// Create a governor with the given options. 19 | /// 20 | /// 21 | public ResourceGovernor(ExtractorOptions opts) 22 | { 23 | options = opts ?? new ExtractorOptions(); 24 | GovernorStopwatch = new Stopwatch(); 25 | } 26 | 27 | internal void ResetResourceGovernor(Stream stream) 28 | { 29 | Logger.Trace("ResetResourceGovernor()"); 30 | if (stream == null) 31 | { 32 | throw new ArgumentNullException(nameof(stream), "Stream must not be null."); 33 | } 34 | 35 | GovernorStopwatch = Stopwatch.StartNew(); 36 | 37 | // Default value is we take MaxExtractedBytes (meaning, ratio is not defined) 38 | CurrentOperationProcessedBytesLeft = options.MaxExtractedBytes; 39 | if (options.MaxExtractedBytesRatio > 0) 40 | { 41 | long streamLength; 42 | try 43 | { 44 | streamLength = stream.Length; 45 | } 46 | catch (Exception) 47 | { 48 | throw new ArgumentException("Unable to get length of stream."); 49 | } 50 | 51 | // Ratio *is* defined, so the max value would be based on the stream length 52 | var maxViaRatio = (long)(options.MaxExtractedBytesRatio * streamLength); 53 | // Assign the samller of the two, accounting for MaxExtractedBytes == 0 means, 'no limit'. 54 | CurrentOperationProcessedBytesLeft = Math.Min(maxViaRatio, options.MaxExtractedBytes > 0 ? options.MaxExtractedBytes : long.MaxValue); 55 | } 56 | } 57 | 58 | /// 59 | /// Stores the number of bytes left before we abort (denial of service). 60 | /// 61 | private long CurrentOperationProcessedBytesLeft = -1; 62 | 63 | /// 64 | /// Adjust the amount of bytes remaining. 65 | /// 66 | /// 67 | internal void AdjustRemainingBytes(long ChangeAmount) 68 | { 69 | lock (this) 70 | { 71 | CurrentOperationProcessedBytesLeft += ChangeAmount; 72 | } 73 | } 74 | 75 | /// 76 | /// Times extraction operations to avoid denial of service. 77 | /// 78 | internal Stopwatch GovernorStopwatch; 79 | 80 | /// 81 | /// Checks to ensure we haven't extracted too many bytes, or taken too long. This exists primarily 82 | /// to mitigate the risks of quines (archives that contain themselves) and zip bombs (specially 83 | /// constructed to expand to huge sizes). 84 | /// Ref: https://alf.nu/ZipQuine 85 | /// 86 | /// 87 | internal void CheckResourceGovernor(long additionalBytes = 0) 88 | { 89 | Logger.ConditionalTrace("CheckResourceGovernor(duration={0}, bytes={1})", GovernorStopwatch.Elapsed.TotalMilliseconds, CurrentOperationProcessedBytesLeft); 90 | 91 | if (options.EnableTiming && GovernorStopwatch.Elapsed > options.Timeout) 92 | { 93 | throw new TimeoutException(string.Format($"Processing timeout exceeded: {GovernorStopwatch.Elapsed.TotalMilliseconds} ms.")); 94 | } 95 | 96 | if (CurrentOperationProcessedBytesLeft - additionalBytes < 0) 97 | { 98 | throw new OverflowException("Too many bytes extracted, exceeding limit."); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /RecursiveExtractor/StreamFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Microsoft.CST.RecursiveExtractor; 5 | 6 | /// 7 | /// Static class with static Methods to generate the correct type of backing stream based on the length of hte target stream and the . 8 | /// 9 | public static class StreamFactory 10 | { 11 | /// 12 | /// Generate the appropriate backing Stream to copy a target Stream to. 13 | /// 14 | /// Extractor Options 15 | /// The target stream to be backed 16 | /// 17 | public static Stream GenerateAppropriateBackingStream(ExtractorOptions opts, Stream targetStream) 18 | { 19 | return GenerateAppropriateBackingStream(opts.MemoryStreamCutoff, targetStream, opts.FileStreamBufferSize); 20 | } 21 | 22 | /// 23 | /// Generate the appropriate backing Stream to copy a target Stream to. 24 | /// 25 | /// Extractor Options 26 | /// The length of stream to be backed 27 | /// 28 | public static Stream GenerateAppropriateBackingStream(ExtractorOptions opts, long targetStreamLength) 29 | { 30 | return GenerateAppropriateBackingStream(opts.MemoryStreamCutoff, targetStreamLength, opts.FileStreamBufferSize); 31 | } 32 | 33 | /// 34 | /// Generate the appropriate backing Stream to copy a target Stream to. 35 | /// 36 | /// Largest size in bytes to use a memory stream for backing 37 | /// Stream to be copied to the new backer 38 | /// Size of FileStream's buffers 39 | /// 40 | internal static Stream GenerateAppropriateBackingStream(int? memoryStreamCutoff, Stream targetStream, int fileStreamBufferSize) 41 | { 42 | try 43 | { 44 | if (targetStream.Length > memoryStreamCutoff) 45 | { 46 | return GenerateDeleteOnCloseFileStream(fileStreamBufferSize); 47 | } 48 | return new MemoryStream(); 49 | } 50 | catch (Exception) 51 | { 52 | return GenerateDeleteOnCloseFileStream(fileStreamBufferSize); 53 | } 54 | } 55 | 56 | /// 57 | /// 58 | /// 59 | /// 60 | /// 61 | /// 62 | /// 63 | internal static Stream GenerateAppropriateBackingStream(int? memoryStreamCutoff, long targetStreamLength, int fileStreamBufferSize) 64 | { 65 | try 66 | { 67 | if (targetStreamLength > memoryStreamCutoff) 68 | { 69 | return GenerateDeleteOnCloseFileStream(fileStreamBufferSize); 70 | } 71 | return new MemoryStream(); 72 | } 73 | catch (Exception) 74 | { 75 | return GenerateDeleteOnCloseFileStream(fileStreamBufferSize); 76 | } 77 | } 78 | 79 | internal static Stream GenerateDeleteOnCloseFileStream(int fileStreamBufferSize) 80 | { 81 | return new FileStream(TempPath.GetTempFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite, fileStreamBufferSize, FileOptions.Asynchronous | FileOptions.DeleteOnClose); 82 | } 83 | } -------------------------------------------------------------------------------- /RecursiveExtractor/TempPath.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. Licensed under the MIT License. 2 | 3 | using System.IO; 4 | 5 | namespace Microsoft.CST.RecursiveExtractor; 6 | 7 | /// 8 | /// Helper class to get temporary file path 9 | /// 10 | public static class TempPath 11 | { 12 | /// 13 | /// Use this instead of Path.GetTempFileName() 14 | /// Path.GetTempFileName() generates predictable file names and is inherently unreliable and insecure. 15 | /// Additionally, the method will raise an IOException if it is used to create more than 65535 files 16 | /// without deleting previous temporary files. 17 | /// 18 | /// 19 | public static string GetTempFilePath() 20 | { 21 | return Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); 22 | } 23 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/RecursiveExtractor/6acbbc8b248b3fcec7de177a4382a08468c4f06a/icon-128.png -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "1.2", 4 | "publicReleaseRefSpec": [ 5 | "^refs/heads/main$", 6 | "^refs/heads/v\\d+(?:\\.\\d+)?$" 7 | ], 8 | "cloudBuild": { 9 | "buildNumber": { 10 | "enabled": true 11 | } 12 | } 13 | } 14 | --------------------------------------------------------------------------------