├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .markdownlint.json ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.ps1 ├── docs └── en-US │ ├── Compress-GzipArchive.md │ ├── Compress-ZipArchive.md │ ├── ConvertFrom-GzipString.md │ ├── ConvertTo-GzipString.md │ ├── Expand-GzipArchive.md │ ├── Expand-ZipEntry.md │ ├── Get-ZipEntry.md │ ├── Get-ZipEntryContent.md │ ├── New-ZipEntry.md │ ├── Remove-ZipEntry.md │ ├── Rename-ZipEntry.md │ └── Set-ZipEntryContent.md ├── module ├── PSCompression.Format.ps1xml └── PSCompression.psd1 ├── src └── PSCompression │ ├── CommandWithPathBase.cs │ ├── Commands │ ├── CompressGzipArchiveCommand.cs │ ├── CompressZipArchiveCommand.cs │ ├── ConvertFromGzipStringCommand.cs │ ├── ConvertToGzipStringCommand.cs │ ├── ExpandGzipArchiveCommand.cs │ ├── ExpandZipEntryCommand.cs │ ├── GetZipEntryCommand.cs │ ├── GetZipEntryContentCommand.cs │ ├── NewZipEntryCommand.cs │ ├── RemoveZipEntryCommand.cs │ ├── RenameZipEntryCommand.cs │ └── SetZipEntryContentCommand.cs │ ├── Dbg │ ├── Dbg.cs │ └── Nullable.cs │ ├── EncodingCompleter.cs │ ├── EncodingTransformation.cs │ ├── Exceptions │ ├── DuplicatedEntryException.cs │ ├── EntryNotFoundException.cs │ ├── ExceptionHelpers.cs │ └── InvalidNameException.cs │ ├── Extensions │ ├── DictionaryExtensions.cs │ ├── PathExtensions.cs │ └── ZipEntryExtensions.cs │ ├── GzipReaderOps.cs │ ├── PSCompression.csproj │ ├── PSCompression.sln │ ├── PSVersionHelper.cs │ ├── Records.cs │ ├── SortingOps.cs │ ├── ZipArchiveCache.cs │ ├── ZipContentOpsBase.cs │ ├── ZipContentReader.cs │ ├── ZipContentWriter.cs │ ├── ZipEntryBase.cs │ ├── ZipEntryCache.cs │ ├── ZipEntryDirectory.cs │ ├── ZipEntryFile.cs │ ├── ZipEntryMoveCache.cs │ ├── ZipEntryType.cs │ └── internal │ └── _Format.cs ├── tests ├── CompressZipArchive.tests.ps1 ├── EncodingCompleter.tests.ps1 ├── EncodingTransformation.tests.ps1 ├── FormattingInternals.tests.ps1 ├── GzipCmdlets.tests.ps1 ├── PSVersionHelper.tests.ps1 ├── ZipEntryBase.tests.ps1 ├── ZipEntryCmdlets.tests.ps1 ├── ZipEntryDirectory.tests.ps1 ├── ZipEntryFile.tests.ps1 └── shared.psm1 └── tools ├── InvokeBuild.ps1 ├── PesterTest.ps1 ├── ProjectBuilder ├── Documentation.cs ├── Extensions.cs ├── Module.cs ├── Pester.cs ├── Project.cs ├── ProjectBuilder.csproj ├── ProjectInfo.cs └── Types.cs ├── prompt.ps1 └── requiredModules.psd1 /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: PSCompression Workflow 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | release: 12 | types: 13 | - published 14 | 15 | env: 16 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 17 | POWERSHELL_TELEMETRY_OPTOUT: 1 18 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 19 | DOTNET_NOLOGO: true 20 | BUILD_CONFIGURATION: ${{ fromJSON('["Debug", "Release"]')[startsWith(github.ref, 'refs/tags/v')] }} 21 | 22 | jobs: 23 | build: 24 | name: build 25 | runs-on: windows-latest 26 | steps: 27 | - name: Check out repository 28 | uses: actions/checkout@v4 29 | 30 | - name: Build module - Debug 31 | shell: pwsh 32 | run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Build 33 | if: ${{ env.BUILD_CONFIGURATION == 'Debug' }} 34 | 35 | - name: Build module - Publish 36 | shell: pwsh 37 | run: ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Build 38 | if: ${{ env.BUILD_CONFIGURATION == 'Release' }} 39 | 40 | - name: Capture PowerShell Module 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: PSModule 44 | path: output/*.nupkg 45 | 46 | test: 47 | name: test 48 | needs: 49 | - build 50 | runs-on: ${{ matrix.info.os }} 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | info: 55 | - name: PS_5.1 56 | psversion: '5.1' 57 | os: windows-latest 58 | - name: PS_7_Windows 59 | psversion: '7' 60 | os: windows-latest 61 | - name: PS_7_Linux 62 | psversion: '7' 63 | os: ubuntu-latest 64 | 65 | steps: 66 | - uses: actions/checkout@v4 67 | 68 | - name: Restore Built PowerShell Module 69 | uses: actions/download-artifact@v4 70 | with: 71 | name: PSModule 72 | path: output 73 | 74 | - name: Install Built PowerShell Module 75 | shell: pwsh 76 | run: | 77 | $manifestItem = Get-Item ([IO.Path]::Combine('module', '*.psd1')) 78 | $moduleName = $manifestItem.BaseName 79 | $manifest = Test-ModuleManifest -Path $manifestItem.FullName -ErrorAction SilentlyContinue -WarningAction Ignore 80 | 81 | $destPath = [IO.Path]::Combine('output', $moduleName, $manifest.Version) 82 | if (-not (Test-Path -LiteralPath $destPath)) { 83 | New-Item -Path $destPath -ItemType Directory | Out-Null 84 | } 85 | 86 | Get-ChildItem output/*.nupkg | Rename-Item -NewName { $_.Name -replace '.nupkg', '.zip' } 87 | 88 | Expand-Archive -Path output/*.zip -DestinationPath $destPath -Force -ErrorAction Stop 89 | 90 | - name: Run Tests - Windows PowerShell 91 | if: ${{ matrix.info.psversion == '5.1' }} 92 | shell: pwsh 93 | run: | 94 | powershell.exe -NoProfile -File ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test 95 | exit $LASTEXITCODE 96 | 97 | - name: Run Tests - PowerShell 98 | if: ${{ matrix.info.psversion != '5.1' }} 99 | shell: pwsh 100 | run: | 101 | pwsh -NoProfile -File ./build.ps1 -Configuration $env:BUILD_CONFIGURATION -Task Test 102 | exit $LASTEXITCODE 103 | 104 | - name: Upload Test Results 105 | if: always() 106 | uses: actions/upload-artifact@v4 107 | with: 108 | name: Unit Test Results (${{ matrix.info.name }}) 109 | path: ./output/TestResults/Pester.xml 110 | 111 | - name: Upload Coverage Results 112 | if: always() && !startsWith(github.ref, 'refs/tags/v') 113 | uses: actions/upload-artifact@v4 114 | with: 115 | name: Coverage Results (${{ matrix.info.name }}) 116 | path: ./output/TestResults/Coverage.xml 117 | 118 | - name: Upload Coverage to codecov 119 | if: always() && !startsWith(github.ref, 'refs/tags/v') 120 | uses: codecov/codecov-action@v4 121 | with: 122 | files: ./output/TestResults/Coverage.xml 123 | flags: ${{ matrix.info.name }} 124 | token: ${{ secrets.CODECOV_TOKEN }} 125 | 126 | publish: 127 | name: publish 128 | if: startsWith(github.ref, 'refs/tags/v') 129 | needs: 130 | - build 131 | - test 132 | runs-on: windows-latest 133 | steps: 134 | - name: Restore Built PowerShell Module 135 | uses: actions/download-artifact@v4 136 | with: 137 | name: PSModule 138 | path: ./ 139 | 140 | - name: Publish to Gallery 141 | if: github.event_name == 'release' 142 | shell: pwsh 143 | run: >- 144 | dotnet nuget push '*.nupkg' 145 | --api-key $env:PSGALLERY_TOKEN 146 | --source 'https://www.powershellgallery.com/api/v2/package' 147 | --no-symbols 148 | env: 149 | PSGALLERY_TOKEN: ${{ secrets.PSGALLERY_TOKEN }} 150 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | benchmarks/ 5 | BenchmarkDotNet.Artifacts/ 6 | tools/dotnet 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | *.zip 14 | 15 | # User-specific files (MonoDevelop/Xamarin Studio) 16 | *.userprefs 17 | 18 | # Build results 19 | [Dd]ebug/ 20 | [Dd]ebugPublic/ 21 | [Rr]elease/ 22 | [Rr]eleases/ 23 | x64/ 24 | x86/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # DNX 49 | project.lock.json 50 | project.fragment.lock.json 51 | artifacts/ 52 | 53 | *_i.c 54 | *_p.c 55 | *_i.h 56 | *.ilk 57 | *.meta 58 | *.obj 59 | *.pch 60 | *.pdb 61 | *.pgc 62 | *.pgd 63 | *.rsp 64 | *.sbr 65 | *.tlb 66 | *.tli 67 | *.tlh 68 | *.tmp 69 | *.tmp_proj 70 | *.log 71 | *.vspscc 72 | *.vssscc 73 | .builds 74 | *.pidb 75 | *.svclog 76 | *.scc 77 | 78 | # Chutzpah Test files 79 | _Chutzpah* 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opendb 86 | *.opensdf 87 | *.sdf 88 | *.cachefile 89 | *.VC.db 90 | *.VC.VC.opendb 91 | 92 | # Visual Studio profiler 93 | *.psess 94 | *.vsp 95 | *.vspx 96 | *.sap 97 | 98 | # TFS 2012 Local Workspace 99 | $tf/ 100 | 101 | # Guidance Automation Toolkit 102 | *.gpState 103 | 104 | # ReSharper is a .NET coding add-in 105 | _ReSharper*/ 106 | *.[Rr]e[Ss]harper 107 | *.DotSettings.user 108 | 109 | # JustCode is a .NET coding add-in 110 | .JustCode 111 | 112 | # TeamCity is a build add-in 113 | _TeamCity* 114 | 115 | # DotCover is a Code Coverage Tool 116 | *.dotCover 117 | 118 | # NCrunch 119 | _NCrunch_* 120 | .*crunch*.local.xml 121 | nCrunchTemp_* 122 | 123 | # MightyMoose 124 | *.mm.* 125 | AutoTest.Net/ 126 | 127 | # Web workbench (sass) 128 | .sass-cache/ 129 | 130 | # Installshield output folder 131 | [Ee]xpress/ 132 | 133 | # DocProject is a documentation generator add-in 134 | DocProject/buildhelp/ 135 | DocProject/Help/*.HxT 136 | DocProject/Help/*.HxC 137 | DocProject/Help/*.hhc 138 | DocProject/Help/*.hhk 139 | DocProject/Help/*.hhp 140 | DocProject/Help/Html2 141 | DocProject/Help/html 142 | 143 | # Click-Once directory 144 | publish/ 145 | 146 | # Publish Web Output 147 | *.[Pp]ublish.xml 148 | *.azurePubxml 149 | # TODO: Comment the next line if you want to checkin your web deploy settings 150 | # but database connection strings (with potential passwords) will be unencrypted 151 | #*.pubxml 152 | *.publishproj 153 | 154 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 155 | # checkin your Azure Web App publish settings, but sensitive information contained 156 | # in these scripts will be unencrypted 157 | PublishScripts/ 158 | 159 | # NuGet Packages 160 | *.nupkg 161 | # The packages folder can be ignored because of Package Restore 162 | **/packages/* 163 | # except build/, which is used as an MSBuild target. 164 | !**/packages/build/ 165 | # Uncomment if necessary however generally it will be regenerated when needed 166 | #!**/packages/repositories.config 167 | # NuGet v3's project.json files produces more ignoreable files 168 | *.nuget.props 169 | *.nuget.targets 170 | 171 | # Microsoft Azure Build Output 172 | csx/ 173 | *.build.csdef 174 | 175 | # Microsoft Azure Emulator 176 | ecf/ 177 | rcf/ 178 | 179 | # Windows Store app package directories and files 180 | AppPackages/ 181 | BundleArtifacts/ 182 | Package.StoreAssociation.xml 183 | _pkginfo.txt 184 | 185 | # Visual Studio cache files 186 | # files ending in .cache can be ignored 187 | *.[Cc]ache 188 | # but keep track of directories ending in .cache 189 | !*.[Cc]ache/ 190 | 191 | # Others 192 | ClientBin/ 193 | ~$* 194 | *~ 195 | *.dbmdl 196 | *.dbproj.schemaview 197 | *.jfm 198 | *.pfx 199 | *.publishsettings 200 | node_modules/ 201 | orleans.codegen.cs 202 | 203 | # Since there are multiple workflows, uncomment next line to ignore bower_components 204 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 205 | #bower_components/ 206 | 207 | # RIA/Silverlight projects 208 | Generated_Code/ 209 | 210 | # Backup & report files from converting an old project file 211 | # to a newer Visual Studio version. Backup files are not needed, 212 | # because we have git ;-) 213 | _UpgradeReport_Files/ 214 | Backup*/ 215 | UpgradeLog*.XML 216 | UpgradeLog*.htm 217 | 218 | # SQL Server files 219 | *.mdf 220 | *.ldf 221 | 222 | # Business Intelligence projects 223 | *.rdl.data 224 | *.bim.layout 225 | *.bim_*.settings 226 | 227 | # Microsoft Fakes 228 | FakesAssemblies/ 229 | 230 | # GhostDoc plugin setting file 231 | *.GhostDoc.xml 232 | 233 | # Node.js Tools for Visual Studio 234 | .ntvs_analysis.dat 235 | 236 | # Visual Studio 6 build log 237 | *.plg 238 | 239 | # Visual Studio 6 workspace options file 240 | *.opt 241 | 242 | # Visual Studio LightSwitch build output 243 | **/*.HTMLClient/GeneratedArtifacts 244 | **/*.DesktopClient/GeneratedArtifacts 245 | **/*.DesktopClient/ModelManifest.xml 246 | **/*.Server/GeneratedArtifacts 247 | **/*.Server/ModelManifest.xml 248 | _Pvt_Extensions 249 | 250 | # Paket dependency manager 251 | .paket/paket.exe 252 | paket-files/ 253 | 254 | # FAKE - F# Make 255 | .fake/ 256 | 257 | # JetBrains Rider 258 | .idea/ 259 | *.sln.iml 260 | 261 | # CodeRush 262 | .cr/ 263 | 264 | # Python Tools for Visual Studio (PTVS) 265 | __pycache__/ 266 | *.pyc 267 | 268 | ### Custom entries ### 269 | output/ 270 | tools/Modules 271 | test.settings.json 272 | tests/integration/.vagrant 273 | tests/integration/cert_setup 274 | !assets/*.zip 275 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "no-hard-tabs": true, 4 | "no-duplicate-heading": false, 5 | "line-length": false, 6 | "no-inline-html": false, 7 | "ul-indent": false 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "formulahendry.dotnet-test-explorer", 6 | "ms-dotnettools.csharp", 7 | "ms-vscode.powershell", 8 | ], 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "PowerShell launch", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "program": "pwsh", 12 | "args": [ 13 | "-NoExit", 14 | "-NoProfile", 15 | "-Command", 16 | ". ./tools/prompt.ps1;", 17 | "Import-Module ./output/PSCompression" 18 | ], 19 | "cwd": "${workspaceFolder}", 20 | "stopAtEntry": false, 21 | "console": "externalTerminal", 22 | }, 23 | { 24 | "name": "PowerShell Launch Current File", 25 | "type": "PowerShell", 26 | "request": "launch", 27 | "script": "${file}", 28 | "cwd": "${workspaceFolder}" 29 | }, 30 | { 31 | "name": ".NET FullCLR Attach", 32 | "type": "clr", 33 | "request": "attach", 34 | "processId": "${command:pickProcess}", 35 | "justMyCode": true, 36 | }, 37 | { 38 | "name": ".NET CoreCLR Attach", 39 | "type": "coreclr", 40 | "request": "attach", 41 | "processId": "${command:pickProcess}", 42 | "justMyCode": true, 43 | }, 44 | ], 45 | } 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.enableFiletypes": [ 3 | "!powershell" 4 | ], 5 | //-------- Files configuration -------- 6 | // When enabled, will trim trailing whitespace when you save a file. 7 | "files.trimTrailingWhitespace": true, 8 | // When enabled, insert a final new line at the end of the file when saving it. 9 | "files.insertFinalNewline": true, 10 | "search.exclude": { 11 | "Release": true, 12 | "tools/ResGen": true, 13 | "tools/dotnet": true, 14 | }, 15 | "json.schemas": [ 16 | { 17 | "fileMatch": [ 18 | "/test.settings.json" 19 | ], 20 | "url": "./tests/settings.schema.json" 21 | } 22 | ], 23 | "dotnet-test-explorer.testProjectPath": "tests/units/*.csproj", 24 | "editor.rulers": [ 25 | 120, 26 | ], 27 | //-------- PowerShell configuration -------- 28 | // Binary modules cannot be unloaded so running in separate processes solves that problem 29 | //"powershell.debugging.createTemporaryIntegratedConsole": true, 30 | // We use Pester v5 so we don't need the legacy code lens 31 | "powershell.pester.useLegacyCodeLens": false, 32 | "cSpell.words": [ 33 | "pwsh" 34 | ], 35 | "dotnet.defaultSolution": "src\\PSCompression\\PSCompression.sln", 36 | "pester.autoRunOnSave": false, 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "pwsh", 9 | "type": "shell", 10 | "args": [ 11 | "-File", 12 | "${workspaceFolder}/build.ps1" 13 | ], 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | }, 18 | "problemMatcher": "$msCompile" 19 | }, 20 | { 21 | "label": "update docs", 22 | "command": "pwsh", 23 | "type": "shell", 24 | "args": [ 25 | "-Command", 26 | "Import-Module ${workspaceFolder}/output/PSCompression; Import-Module ${workspaceFolder}/tools/Modules/platyPS; Update-MarkdownHelpModule ${workspaceFolder}/docs/en-US -AlphabeticParamsOrder -RefreshModulePage -UpdateInputOutput" 27 | ], 28 | "problemMatcher": [], 29 | "dependsOn": [ 30 | "build" 31 | ] 32 | }, 33 | { 34 | "label": "test", 35 | "command": "pwsh", 36 | "type": "shell", 37 | "args": [ 38 | "-File", 39 | "${workspaceFolder}/build.ps1", 40 | "-Task", 41 | "Test" 42 | ], 43 | "problemMatcher": [], 44 | "dependsOn": [ 45 | "build" 46 | ] 47 | } 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 01/10/2025 4 | 5 | - Code improvements. 6 | - Instance methods `.OpenRead()` and `.OpenWrite()` moved from `ZipEntryFile` to `ZipEntryBase`. 7 | - Adds support to list, read and extract zip archive entries from Stream. 8 | 9 | ## 06/24/2024 10 | 11 | - Update build process. 12 | 13 | ## 06/05/2024 14 | 15 | - Update `ci.yml` to use `codecov-action@v4`. 16 | - Fixed parameter names in `Compress-ZipArchive` documentation. Thanks to @martincostello. 17 | - Fixed coverlet.console support for Linux runner tests. 18 | 19 | ## 02/26/2024 20 | 21 | - Fixed a bug with `CompressionRatio` property showing always in InvariantCulture format. 22 | 23 | ## 02/25/2024 24 | 25 | - `ZipEntryBase` Type: 26 | - Renamed Property `EntryName` to `Name`. 27 | - Renamed Property `EntryRelativePath` to `RelativePath`. 28 | - Renamed Property `EntryType` to `Type`. 29 | - Renamed Method `RemoveEntry()` to `Remove()`. 30 | - Added Property `CompressionRatio`. 31 | - `ZipEntryFile` Type: 32 | - Added Property `Extension`. 33 | - Added Property `BaseName`. 34 | - `ZipEntryDirectory` Type: 35 | - `.Name` Property now reflects the directory entries name instead of an empty string. 36 | - Added command `Rename-ZipEntry`. 37 | - `NormalizePath` Method: 38 | - Moved from `[PSCompression.ZipEntryExtensions]::NormalizePath` to `[PSCompression.Extensions.PathExtensions]::NormalizePath`. 39 | - `Get-ZipEntry` command: 40 | - Renamed Parameter `-EntryType` to `-Type`. 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Santiago Squarzon 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

PSCompression

2 |
3 | Zip and GZip utilities for PowerShell 4 |

5 | 6 | [![build](https://github.com/santisq/PSCompression/actions/workflows/ci.yml/badge.svg)](https://github.com/santisq/PSCompression/actions/workflows/ci.yml) 7 | [![codecov](https://codecov.io/gh/santisq/PSCompression/branch/main/graph/badge.svg)](https://codecov.io/gh/santisq/PSCompression) 8 | [![PowerShell Gallery](https://img.shields.io/powershellgallery/dt/PSCompression?color=%23008FC7 9 | )](https://www.powershellgallery.com/packages/PSCompression) 10 | [![LICENSE](https://img.shields.io/github/license/santisq/PSCompression)](https://github.com/santisq/PSCompression/blob/main/LICENSE) 11 | 12 |
13 | 14 | PSCompression is a PowerShell Module that provides Zip and Gzip utilities for compression, expansion and management. It also solves a few issues with Zip compression existing in _built-in PowerShell_. 15 | 16 | ## What does this Module offer? 17 | 18 | ### Zip Cmdlets 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 37 | 38 | 39 | 44 | 49 | 50 | 51 | 56 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 76 | 77 | 78 | 79 | 84 | 85 | 86 | 87 | 92 | 93 | 94 | 95 | 100 | 107 | 108 |
CmdletDescription
28 | 29 | [`Get-ZipEntry`](docs/en-US/Get-ZipEntry.md) 30 | 31 | 33 | 34 | Main entry point for the `*-ZipEntry` cmdlets in this module. It can list zip archive entries from specified paths or input stream. 35 | 36 |
40 | 41 | [`Expand-ZipEntry`](docs/en-US/Expand-ZipEntry.md) 42 | 43 | 45 | 46 | Expands zip entries to a destination directory. 47 | 48 |
52 | 53 | [`Get-ZipEntryContent`](docs/en-US/Get-ZipEntryContent.md) 54 | 55 | 57 | 58 | Gets the content of one or more zip entries. 59 | 60 |
64 | 65 | [`New-ZipEntry`](docs/en-US/New-ZipEntry.md) 66 | 67 | Creates zip entries from specified path or paths.
72 | 73 | [`Remove-ZipEntry`](docs/en-US/Remove-ZipEntry.md) 74 | 75 | Removes zip entries from one or more zip archives.
80 | 81 | [`Rename-ZipEntry`](docs/en-US/Rename-ZipEntry.md) 82 | 83 | Renames zip entries from one or more zip archives.
88 | 89 | [`Set-ZipEntryContent`](docs/en-US/Set-ZipEntryContent.md) 90 | 91 | Sets or appends content to a zip entry.
96 | 97 | [`Compress-ZipArchive`](docs/en-US/Compress-ZipArchive.md) 98 | 99 | 101 | 102 | Similar capabilities as 103 | [`Compress-Archive`](docs/en-US/https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.archive/compress-archive?view=powershell-7.2) 104 | and overcomes a few issues with the built-in cmdlet (2 GB limit and more). 105 | 106 |
109 |
110 | 111 | ### Gzip Cmdlets 112 | 113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 125 | 128 | 129 | 130 | 135 | 138 | 139 | 140 | 141 | 146 | 149 | 150 | 151 | 152 | 157 | 162 | 163 |
CmdletDescription
121 | 122 | [`Compress-GzipArchive`](docs/en-US/Compress-GzipArchive.md) 123 | 124 | 126 | Can compress one or more specified file paths into a Gzip file. 127 |
131 | 132 | [`ConvertFrom-GzipString`](docs/en-US/ConvertFrom-GzipString.md) 133 | 134 | 136 | Expands Gzip Base64 input strings. 137 |
142 | 143 | [`ConvertTo-GzipString`](docs/en-US/ConvertTo-GzipString.md) 144 | 145 | 147 | Can compress input strings into Gzip Base64 strings or raw bytes. 148 |
153 | 154 | [`Expand-GzipArchive`](docs/en-US/Expand-GzipArchive.md) 155 | 156 | 158 | 159 | Expands Gzip compressed files to a destination path or to the [success stream](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_output_streams?view=powershell-7.3#success-stream). 160 | 161 |
164 |
165 | 166 | ## Documentation 167 | 168 | Check out [__the docs__](docs/en-US) for information about how to use this Module. 169 | 170 | ## Installation 171 | 172 | ### Gallery 173 | 174 | The module is available through the [PowerShell Gallery](https://www.powershellgallery.com/): 175 | 176 | ```powershell 177 | Install-Module PSCompression -Scope CurrentUser 178 | ``` 179 | 180 | ### Source 181 | 182 | ```powershell 183 | git clone 'https://github.com/santisq/PSCompression.git' 184 | Set-Location ./PSCompression 185 | ./build.ps1 186 | ``` 187 | 188 | ## Requirements 189 | 190 | This module has no external requirements and is compatible with __Windows PowerShell 5.1__ and [__PowerShell 7+__](https://github.com/PowerShell/PowerShell). 191 | 192 | ## Contributing 193 | 194 | Contributions are more than welcome, if you wish to contribute, fork this repository and submit a pull request with the changes. 195 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [Parameter()] 4 | [ValidateSet('Debug', 'Release')] 5 | [string] $Configuration = 'Debug', 6 | 7 | [Parameter()] 8 | [ValidateSet('Build', 'Test')] 9 | [string[]] $Task = 'Build' 10 | ) 11 | 12 | $prev = $ErrorActionPreference 13 | $ErrorActionPreference = 'Stop' 14 | 15 | if (-not ('ProjectBuilder.ProjectInfo' -as [type])) { 16 | try { 17 | $builderPath = [IO.Path]::Combine($PSScriptRoot, 'tools', 'ProjectBuilder') 18 | Push-Location $builderPath 19 | 20 | dotnet @( 21 | 'publish' 22 | '--configuration', 'Release' 23 | '-o', 'output' 24 | '--framework', 'netstandard2.0' 25 | '--verbosity', 'q' 26 | '-nologo' 27 | ) 28 | 29 | if ($LASTEXITCODE) { 30 | throw "Failed to compiled 'ProjectBuilder'" 31 | } 32 | 33 | $dll = [IO.Path]::Combine($builderPath, 'output', 'ProjectBuilder.dll') 34 | Add-Type -Path $dll 35 | } 36 | finally { 37 | Pop-Location 38 | } 39 | } 40 | 41 | $projectInfo = [ProjectBuilder.ProjectInfo]::Create($PSScriptRoot, $Configuration) 42 | $projectInfo.GetRequirements() | Import-Module -DisableNameChecking -Force 43 | 44 | $ErrorActionPreference = $prev 45 | 46 | $invokeBuildSplat = @{ 47 | Task = $Task 48 | File = Convert-Path ([IO.Path]::Combine($PSScriptRoot, 'tools', 'InvokeBuild.ps1')) 49 | ProjectInfo = $projectInfo 50 | } 51 | Invoke-Build @invokeBuildSplat 52 | -------------------------------------------------------------------------------- /docs/en-US/ConvertFrom-GzipString.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression-help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # ConvertFrom-GzipString 9 | 10 | ## SYNOPSIS 11 | 12 | Expands Gzip Base64 compressed input strings. 13 | 14 | ## SYNTAX 15 | 16 | ```powershell 17 | ConvertFrom-GzipString 18 | -InputObject 19 | [-Encoding ] 20 | [-Raw] 21 | [] 22 | ``` 23 | 24 | ## DESCRIPTION 25 | 26 | The `ConvertFrom-GzipString` cmdlet can expand Base64 encoded Gzip compressed strings using the [`GzipStream` Class](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.gzipstream). This cmdlet is the counterpart of [`ConvertTo-GzipString`](ConvertTo-GzipString.md). 27 | 28 | ## EXAMPLES 29 | 30 | ### Example 1: Expanding a Gzip compressed string 31 | 32 | ```powershell 33 | PS ..\pwsh> ConvertFrom-GzipString H4sIAAAAAAAACstIzcnJ5+Uqzy/KSeHlUuTlAgBLr/K2EQAAAA== 34 | 35 | hello 36 | world 37 | ! 38 | ``` 39 | 40 | ### Example 2: Demonstrates how `-NoNewLine` works 41 | 42 | ```powershell 43 | PS ..\pwsh> $strings = 'hello', 'world', '!' 44 | 45 | # New lines are preserved when the cmdlet receives an array of strings. 46 | PS ..\pwsh> $strings | ConvertTo-GzipString | ConvertFrom-GzipString 47 | 48 | hello 49 | world 50 | ! 51 | 52 | # When using the `-NoNewLine` switch, all strings are concatenated 53 | PS ..\pwsh> $strings | ConvertTo-GzipString -NoNewLine | ConvertFrom-GzipString 54 | 55 | helloworld! 56 | ``` 57 | 58 | ## PARAMETERS 59 | 60 | ### -Encoding 61 | 62 | Determines the character encoding used when expanding the input strings. 63 | 64 | > [!NOTE] 65 | > The default encoding is __`utf8NoBOM`__. 66 | 67 | ```yaml 68 | Type: Encoding 69 | Parameter Sets: (All) 70 | Aliases: 71 | 72 | Required: False 73 | Position: Named 74 | Default value: Utf8 75 | Accept pipeline input: False 76 | Accept wildcard characters: False 77 | ``` 78 | 79 | ### -InputObject 80 | 81 | Specifies the input string or strings to expand. 82 | 83 | ```yaml 84 | Type: String[] 85 | Parameter Sets: (All) 86 | Aliases: 87 | 88 | Required: True 89 | Position: 0 90 | Default value: None 91 | Accept pipeline input: True (ByValue) 92 | Accept wildcard characters: False 93 | ``` 94 | 95 | ### -Raw 96 | 97 | Outputs the expanded string as a single string with newlines preserved. 98 | By default, newline characters in the expanded string are used as delimiters to separate the input into an array of strings. 99 | 100 | ```yaml 101 | Type: SwitchParameter 102 | Parameter Sets: (All) 103 | Aliases: 104 | 105 | Required: False 106 | Position: Named 107 | Default value: False 108 | Accept pipeline input: False 109 | Accept wildcard characters: False 110 | ``` 111 | 112 | ### CommonParameters 113 | 114 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 115 | 116 | ## INPUTS 117 | 118 | ### String 119 | 120 | You can pipe Gzip Base64 strings to this cmdlet. 121 | 122 | ## OUTPUTS 123 | 124 | ### String 125 | 126 | By default, this cmdlet streams strings. When the `-Raw` switch is used, it returns a single multi-line string. 127 | -------------------------------------------------------------------------------- /docs/en-US/ConvertTo-GzipString.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression-help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # ConvertTo-GzipString 9 | 10 | ## SYNOPSIS 11 | 12 | Creates a Gzip Base64 compressed string from a specified input string or strings. 13 | 14 | ## SYNTAX 15 | 16 | ```powershell 17 | ConvertTo-GzipString 18 | -InputObject 19 | [-Encoding ] 20 | [-CompressionLevel ] 21 | [-AsByteStream] 22 | [-NoNewLine] 23 | [] 24 | ``` 25 | 26 | ## DESCRIPTION 27 | 28 | The `ConvertTo-GzipString` cmdlet can compress input strings into Gzip Base64 encoded strings or raw bytes using the [`GzipStream` Class](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.gzipstream). For expansion of Base64 Gzip strings, see [`ConvertFrom-GzipString`](ConvertFrom-GzipString.md). 29 | 30 | ## EXAMPLES 31 | 32 | ### Example 1: Compress strings to Gzip compressed Base64 encoded string 33 | 34 | ```powershell 35 | PS ..\pwsh> $strings = 'hello', 'world', '!' 36 | 37 | # With positional binding 38 | PS ..\pwsh> ConvertTo-GzipString $strings 39 | 40 | H4sIAAAAAAAEAMtIzcnJ5+Uqzy/KSeHlUuTlAgBLr/K2EQAAAA== 41 | 42 | # Or pipeline input, both work 43 | PS ..\pwsh> $strings | ConvertTo-GzipString 44 | 45 | H4sIAAAAAAAEAMtIzcnJ5+Uqzy/KSeHlUuTlAgBLr/K2EQAAAA== 46 | ``` 47 | 48 | ### Example 2: Create a Gzip compressed file from a string 49 | 50 | ```powershell 51 | PS ..\pwsh> 'hello world!' | ConvertTo-GzipString -AsByteStream | 52 | Compress-GzipArchive -DestinationPath .\files\file.gz 53 | ``` 54 | 55 | Demonstrates how `-AsByteStream` works on `ConvertTo-GzipString`, the cmdlet outputs a byte array that is received by `Compress-GzipArchive` and stored in a file. __Note that the byte array is not enumerated__. 56 | 57 | ### Example 3: Compress strings using a specific Encoding 58 | 59 | ```powershell 60 | PS ..\pwsh> 'ñ' | ConvertTo-GzipString -Encoding ansi | ConvertFrom-GzipString 61 | � 62 | 63 | PS ..\pwsh> 'ñ' | ConvertTo-GzipString -Encoding utf8BOM | ConvertFrom-GzipString 64 | ñ 65 | ``` 66 | 67 | The default Encoding is `utf8NoBom`. 68 | 69 | ### Example 4: Compressing multiple files into one Gzip Base64 string 70 | 71 | ```powershell 72 | PS ..\pwsh> 0..10 | ForEach-Object { 73 | Invoke-RestMethod loripsum.net/api/10/long/plaintext -OutFile .\files\lorem$_.txt 74 | } 75 | 76 | # Check the total Length of the downloaded files 77 | PS ..\pwsh> (Get-Content .\files\lorem*.txt | Measure-Object Length -Sum).Sum / 1kb 78 | 87.216796875 79 | 80 | # Check the total Length after compression 81 | PS ..\pwsh> (Get-Content .\files\lorem*.txt | ConvertTo-GzipString).Length / 1kb 82 | 36.94921875 83 | ``` 84 | 85 | ## PARAMETERS 86 | 87 | ### -AsByteStream 88 | 89 | Outputs the compressed byte array to the Success Stream. 90 | 91 | > [!NOTE] 92 | > This parameter is meant to be used in combination with [`Compress-GzipArchive`](./Compress-GzipArchive.md). 93 | 94 | ```yaml 95 | Type: SwitchParameter 96 | Parameter Sets: (All) 97 | Aliases: Raw 98 | 99 | Required: False 100 | Position: Named 101 | Default value: False 102 | Accept pipeline input: False 103 | Accept wildcard characters: False 104 | ``` 105 | 106 | ### -CompressionLevel 107 | 108 | Define the compression level that should be used. 109 | __See [`CompressionLevel` Enum](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.compressionlevel) for details__. 110 | 111 | ```yaml 112 | Type: CompressionLevel 113 | Parameter Sets: (All) 114 | Aliases: 115 | Accepted values: Optimal, Fastest, NoCompression, SmallestSize 116 | 117 | Required: False 118 | Position: Named 119 | Default value: Optimal 120 | Accept pipeline input: False 121 | Accept wildcard characters: False 122 | ``` 123 | 124 | ### -Encoding 125 | 126 | Determines the character encoding used when compressing the input strings. 127 | 128 | > [!NOTE] 129 | > The default encoding is __`utf8NoBOM`__. 130 | 131 | ```yaml 132 | Type: Encoding 133 | Parameter Sets: (All) 134 | Aliases: 135 | 136 | Required: False 137 | Position: Named 138 | Default value: Utf8 139 | Accept pipeline input: False 140 | Accept wildcard characters: False 141 | ``` 142 | 143 | ### -InputObject 144 | 145 | Specifies the input string or strings to compress. 146 | 147 | ```yaml 148 | Type: String[] 149 | Parameter Sets: (All) 150 | Aliases: 151 | 152 | Required: True 153 | Position: 0 154 | Default value: None 155 | Accept pipeline input: True (ByValue) 156 | Accept wildcard characters: False 157 | ``` 158 | 159 | ### -NoNewLine 160 | 161 | The encoded string representation of the input objects are concatenated to form the output. 162 | No new line character is added after each output string when this switch is used. 163 | 164 | ```yaml 165 | Type: SwitchParameter 166 | Parameter Sets: (All) 167 | Aliases: 168 | 169 | Required: False 170 | Position: Named 171 | Default value: False 172 | Accept pipeline input: False 173 | Accept wildcard characters: False 174 | ``` 175 | 176 | ### CommonParameters 177 | 178 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 179 | 180 | ## INPUTS 181 | 182 | ### String 183 | 184 | You can pipe strings to this cmdlet. 185 | 186 | ## OUTPUTS 187 | 188 | ### String 189 | 190 | By default, this cmdlet outputs a single string. 191 | 192 | ### Byte[] 193 | 194 | When the `-AsByteStream` switch is used this cmdlet outputs a byte array down the pipeline. 195 | -------------------------------------------------------------------------------- /docs/en-US/Expand-GzipArchive.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression-help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # Expand-GzipArchive 9 | 10 | ## SYNOPSIS 11 | 12 | Expands a Gzip compressed file from a specified File Path or Paths. 13 | 14 | ## SYNTAX 15 | 16 | ### Path 17 | 18 | ```powershell 19 | Expand-GzipArchive 20 | -Path 21 | [-Raw] 22 | [] 23 | ``` 24 | 25 | ### PathDestination 26 | 27 | ```powershell 28 | Expand-GzipArchive 29 | -Path 30 | -Destination 31 | [-Encoding ] 32 | [-PassThru] 33 | [-Force] 34 | [-Update] 35 | [] 36 | ``` 37 | 38 | ### LiteralPath 39 | 40 | ```powershell 41 | Expand-GzipArchive 42 | -LiteralPath 43 | [-Raw] 44 | [] 45 | ``` 46 | 47 | ### LiteralPathDestination 48 | 49 | ```powershell 50 | Expand-GzipArchive 51 | -LiteralPath 52 | -Destination 53 | [-Encoding ] 54 | [-PassThru] 55 | [-Force] 56 | [-Update] 57 | [] 58 | ``` 59 | 60 | ## DESCRIPTION 61 | 62 | The `Expand-GzipArchive` cmdlet aims to expand Gzip compressed files to a destination path or to the success stream using the [`GzipStream` Class](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.gzipstream). This cmdlet is the counterpart of [`Compress-GzipArchive`](Compress-GzipArchive.md). 63 | 64 | ## EXAMPLES 65 | 66 | ### Example 1: Expanding a Gzip archive to the success stream 67 | 68 | ```powershell 69 | PS ..\pwsh> Expand-GzipArchive .\files\file.gz 70 | 71 | hello world! 72 | ``` 73 | 74 | Output goes to the Success Stream when `-Destination` is not used. 75 | 76 | ### Example 2: Expanding a Gzip archive to a new file 77 | 78 | ```powershell 79 | PS ..\pwsh> Expand-GzipArchive .\files\file.gz -Destination .\files\file.txt 80 | 81 | # Checking Length Difference 82 | PS ..\pwsh> Get-Item -Path .\files\file.gz, .\files\file.txt | 83 | Select-Object Name, Length 84 | 85 | Name Length 86 | ---- ------ 87 | file.gz 3168 88 | file.txt 6857 89 | ``` 90 | 91 | ### Example 3: Appending content to an existing file 92 | 93 | ```powershell 94 | PS ..\pwsh> Expand-GzipArchive *.gz -Destination .\files\file.txt -Update 95 | ``` 96 | 97 | ### Example 4: Expanding a Gzip archive overwritting an existing file 98 | 99 | ```powershell 100 | PS ..\pwsh> Expand-GzipArchive *.gz -Destination .\files\file.txt -Force 101 | ``` 102 | 103 | ## PARAMETERS 104 | 105 | ### -Path 106 | 107 | Specifies the path or paths to the Gzip files to expand. 108 | To specify multiple paths, and include files in multiple locations, use commas to separate the paths. 109 | This Parameter accepts wildcard characters. 110 | Wildcard characters allow you to add all files in a directory to your archive file. 111 | 112 | ```yaml 113 | Type: String[] 114 | Parameter Sets: PathDestination, Path 115 | Aliases: 116 | 117 | Required: True 118 | Position: 0 119 | Default value: None 120 | Accept pipeline input: True (ByValue) 121 | Accept wildcard characters: False 122 | ``` 123 | 124 | ### -LiteralPath 125 | 126 | Specifies the path or paths to the Gzip files to expand. 127 | Unlike the `-Path` Parameter, the value of `-LiteralPath` is used exactly as it's typed. 128 | No characters are interpreted as wildcards 129 | 130 | ```yaml 131 | Type: String[] 132 | Parameter Sets: LiteralPathDestination, LiteralPath 133 | Aliases: PSPath 134 | 135 | Required: True 136 | Position: Named 137 | Default value: None 138 | Accept pipeline input: True (ByPropertyName) 139 | Accept wildcard characters: False 140 | ``` 141 | 142 | ### -Destination 143 | 144 | The destination path where to expand the Gzip file. 145 | The target folder is created if it does not exist. 146 | 147 | > [!NOTE] 148 | > This parameter is Optional, if not used, this cmdlet outputs to the Success Stream. 149 | 150 | ```yaml 151 | Type: String 152 | Parameter Sets: PathDestination, LiteralPathDestination 153 | Aliases: DestinationPath 154 | 155 | Required: True 156 | Position: Named 157 | Default value: None 158 | Accept pipeline input: False 159 | Accept wildcard characters: False 160 | ``` 161 | 162 | ### -Encoding 163 | 164 | Character encoding used when expanding the Gzip content. This parameter is only available when expanding to the Success Stream. 165 | 166 | > [!NOTE] 167 | > The default encoding is __`utf8NoBOM`__. 168 | 169 | ```yaml 170 | Type: Encoding 171 | Parameter Sets: Path, LiteralPath 172 | Aliases: 173 | 174 | Required: False 175 | Position: Named 176 | Default value: utf8NoBOM 177 | Accept pipeline input: False 178 | Accept wildcard characters: False 179 | ``` 180 | 181 | ### -Raw 182 | 183 | Outputs the expanded file as a single string with newlines preserved. 184 | By default, newline characters in the expanded string are used as delimiters to separate the input into an array of strings. 185 | 186 | > [!NOTE] 187 | > This parameter is only available when expanding to the Success Stream. 188 | 189 | ```yaml 190 | Type: SwitchParameter 191 | Parameter Sets: Path, LiteralPath 192 | Aliases: 193 | 194 | Required: False 195 | Position: Named 196 | Default value: False 197 | Accept pipeline input: False 198 | Accept wildcard characters: False 199 | ``` 200 | 201 | ### -PassThru 202 | 203 | Outputs the object representing the expanded file. 204 | 205 | ```yaml 206 | Type: SwitchParameter 207 | Parameter Sets: PathDestination, LiteralPathDestination 208 | Aliases: 209 | 210 | Required: False 211 | Position: Named 212 | Default value: False 213 | Accept pipeline input: False 214 | Accept wildcard characters: False 215 | ``` 216 | 217 | ### -Force 218 | 219 | The destination file gets overwritten if exists, otherwise created when this switch is used. 220 | 221 | > [!NOTE] 222 | > If `-Force` and `-Update` are used together this cmdlet will append content to the destination file. 223 | 224 | ```yaml 225 | Type: SwitchParameter 226 | Parameter Sets: PathDestination, LiteralPathDestination 227 | Aliases: 228 | 229 | Required: False 230 | Position: Named 231 | Default value: None 232 | Accept pipeline input: False 233 | Accept wildcard characters: False 234 | ``` 235 | 236 | ### -Update 237 | 238 | Contents of the expanded file or files are appended to the destination path if exists, otherwise the destination is created. 239 | 240 | > [!NOTE] 241 | > If `-Force` and `-Update` are used together this cmdlet will append content to the destination file. 242 | 243 | ```yaml 244 | Type: SwitchParameter 245 | Parameter Sets: PathDestination, LiteralPathDestination 246 | Aliases: 247 | 248 | Required: False 249 | Position: Named 250 | Default value: None 251 | Accept pipeline input: False 252 | Accept wildcard characters: False 253 | ``` 254 | 255 | ### CommonParameters 256 | 257 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 258 | 259 | ## INPUTS 260 | 261 | ### String 262 | 263 | You can pipe paths to this cmdlet. Output from `Get-ChildItem` or `Get-Item` can be piped to this cmdlet. 264 | 265 | ## OUTPUTS 266 | 267 | ### String 268 | 269 | This cmdlet outputs an array of string to the success stream when `-Destination` is not used and a single multi-line string when used with the `-Raw` switch. 270 | 271 | ### None 272 | 273 | This cmdlet produces no output when expanding to a file and `-PassThru` is not used. 274 | 275 | ### FileInfo 276 | 277 | When the `-PassThru` switch is used this cmdlet outputs the `FileInfo` instance representing the expanded file. 278 | -------------------------------------------------------------------------------- /docs/en-US/Expand-ZipEntry.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression.dll-Help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # Expand-ZipEntry 9 | 10 | ## SYNOPSIS 11 | 12 | Expands Zip Archive Entries to a destination directory. 13 | 14 | ## SYNTAX 15 | 16 | ```powershell 17 | Expand-ZipEntry 18 | -InputObject 19 | [-Destination ] 20 | [-Force] 21 | [-PassThru] 22 | [] 23 | ``` 24 | 25 | ## DESCRIPTION 26 | 27 | The `Expand-ZipEntry` cmdlet can expand zip entries outputted by the [`Get-ZipEntry`](./Get-ZipEntry.md) command to a destination directory. Expanded entries maintain their original folder structure based on their relative path. 28 | 29 | ## EXAMPLES 30 | 31 | ### Example 1: Extract all `.txt` files from a Zip Archive to the current directory 32 | 33 | ```powershell 34 | PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Include *.txt | Expand-ZipEntry 35 | ``` 36 | 37 | ### Example 2: Extract all `.txt` files from a Zip Archive to the a desired directory 38 | 39 | ```powershell 40 | PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Include *.txt | Expand-ZipEntry -Destination path\to\myfolder 41 | ``` 42 | 43 | ### Example 3: Extract all entries excluding `.txt` files to the current directory 44 | 45 | ```powershell 46 | PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Exclude *.txt | Expand-ZipEntry 47 | ``` 48 | 49 | ### Example 4: Extract all entries excluding `.txt` files to the current directory overwritting existing files 50 | 51 | ```powershell 52 | PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Exclude *.txt | Expand-ZipEntry -Force 53 | ``` 54 | 55 | Demonstrates how `-Force` switch works. 56 | 57 | ### Example 5: Extract all entries excluding `.txt` files to the current directory outputting the expanded entries 58 | 59 | ```powershell 60 | PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Exclude *.txt | Expand-ZipEntry -PassThru 61 | ``` 62 | 63 | By default this cmdlet produces no output. When `-PassThru` is used, this cmdlet outputs the `FileInfo` and `DirectoryInfo` instances representing the expanded entries. 64 | 65 | ### Example 6: Extract an entry from input Stream 66 | 67 | ```powershell 68 | PS ..\pwsh> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression 69 | PS ..\pwsh> $file = $package | Get-ZipEntry -Include *.psd1 | Expand-ZipEntry -PassThru -Force 70 | PS ..\pwsh> Get-Content $file.FullName -Raw | Invoke-Expression 71 | 72 | Name Value 73 | ---- ----- 74 | PowerShellVersion 5.1 75 | Description Zip and GZip utilities for PowerShell! 76 | RootModule bin/netstandard2.0/PSCompression.dll 77 | FormatsToProcess {PSCompression.Format.ps1xml} 78 | VariablesToExport {} 79 | PrivateData {[PSData, System.Collections.Hashtable]} 80 | CmdletsToExport {Get-ZipEntry, Get-ZipEntryContent, Set-ZipEntryContent, Remove-ZipEntry…} 81 | ModuleVersion 2.0.10 82 | Author Santiago Squarzon 83 | CompanyName Unknown 84 | GUID c63aa90e-ae64-4ae1-b1c8-456e0d13967e 85 | FunctionsToExport {} 86 | RequiredAssemblies {System.IO.Compression, System.IO.Compression.FileSystem} 87 | Copyright (c) Santiago Squarzon. All rights reserved. 88 | AliasesToExport {gziptofile, gzipfromfile, gziptostring, gzipfromstring…} 89 | ``` 90 | 91 | ## PARAMETERS 92 | 93 | ### -Destination 94 | 95 | The destination directory where to extract the Zip Entries. 96 | 97 | > [!NOTE] 98 | > This parameter is optional, when not used, the entries are extracted to the their relative zip path in the current directory. 99 | 100 | ```yaml 101 | Type: String 102 | Parameter Sets: (All) 103 | Aliases: 104 | 105 | Required: False 106 | Position: 0 107 | Default value: $PWD 108 | Accept pipeline input: False 109 | Accept wildcard characters: False 110 | ``` 111 | 112 | ### -Force 113 | 114 | Existing files in the destination directory are overwritten when this switch is used. 115 | 116 | ```yaml 117 | Type: SwitchParameter 118 | Parameter Sets: (All) 119 | Aliases: 120 | 121 | Required: False 122 | Position: Named 123 | Default value: None 124 | Accept pipeline input: False 125 | Accept wildcard characters: False 126 | ``` 127 | 128 | ### -PassThru 129 | 130 | The cmdlet outputs the `FileInfo` and `DirectoryInfo` instances representing the extracted entries when this switch is used. 131 | 132 | ```yaml 133 | Type: SwitchParameter 134 | Parameter Sets: (All) 135 | Aliases: 136 | 137 | Required: False 138 | Position: Named 139 | Default value: None 140 | Accept pipeline input: False 141 | Accept wildcard characters: False 142 | ``` 143 | 144 | ### -InputObject 145 | 146 | The zip entries to expand. 147 | 148 | > [!NOTE] 149 | > 150 | > - This parameter takes input from pipeline, however binding by name is also possible. 151 | > - The input are instances inheriting from `ZipEntryBase` (`ZipEntryFile` or `ZipEntryDirectory`) outputted by [`Get-ZipEntry`](Get-ZipEntry.md) and [`New-ZipEntry`](New-ZipEntry.md) cmdlets. 152 | 153 | ```yaml 154 | Type: ZipEntryBase[] 155 | Parameter Sets: (All) 156 | Aliases: 157 | 158 | Required: True 159 | Position: Named 160 | Default value: None 161 | Accept pipeline input: True (ByValue) 162 | Accept wildcard characters: False 163 | ``` 164 | 165 | ### CommonParameters 166 | 167 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 168 | 169 | ## INPUTS 170 | 171 | ### ZipEntryBase 172 | 173 | You can pipe instances of `ZipEntryFile` or `ZipEntryDirectory` to this cmdlet. These instances are produced by [`Get-ZipEntry`](Get-ZipEntry.md) and [`New-ZipEntry`](New-ZipEntry.md) cmdlets. 174 | 175 | ## OUTPUTS 176 | 177 | ### None 178 | 179 | By default, this cmdlet produces no output. 180 | 181 | ### FileSystemInfo 182 | 183 | The cmdlet outputs the `FileInfo` and `DirectoryInfo` instances of the extracted entries when `-PassThru` switch is used. 184 | -------------------------------------------------------------------------------- /docs/en-US/Get-ZipEntryContent.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression.dll-Help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # Get-ZipEntryContent 9 | 10 | ## SYNOPSIS 11 | 12 | Gets the content of a zip entry. 13 | 14 | ## SYNTAX 15 | 16 | ### Stream (Default) 17 | 18 | ```powershell 19 | Get-ZipEntryContent 20 | -ZipEntry 21 | [-Encoding ] 22 | [-Raw] 23 | [] 24 | ``` 25 | 26 | ### Bytes 27 | 28 | ```powershell 29 | Get-ZipEntryContent 30 | -ZipEntry 31 | [-Raw] 32 | [-AsByteStream] 33 | [-BufferSize ] 34 | [] 35 | ``` 36 | 37 | ## DESCRIPTION 38 | 39 | The `Get-ZipEntryContent` cmdlet gets the content of one or more `ZipEntryFile` instances. 40 | This cmdlet is meant to be used with [`Get-ZipEntry`](./Get-ZipEntry.md) as your entry point. 41 | 42 | > [!TIP] 43 | > Entries outputted by `Get-ZipEntry` can be piped to this cmdlet. 44 | 45 | ## EXAMPLES 46 | 47 | ### Example 1: Get the content of a Zip Archive Entry 48 | 49 | ```powershell 50 | PS ..pwsh\> Get-ZipEntry .\myZip.zip -Include myrelative/entry.txt | Get-ZipEntryContent 51 | ``` 52 | 53 | `-Include` parameter from `Get-ZipEntry` can be used to target a specific entry by passing the entry's relative path, from there the output can be piped directly to `Get-ZipEntryContent`. 54 | By default, the cmdlet streams line-by-line . 55 | 56 | ### Example 2: Get raw content of a Zip Archive Entry 57 | 58 | ```powershell 59 | PS ..pwsh\> Get-ZipEntry .\myZip.zip -Include myrelative/entry.txt | Get-ZipEntryContent -Raw 60 | ``` 61 | 62 | The cmdlet outputs a single multi-line string when the `-Raw` switch is used instead of line-by-line streaming. 63 | 64 | ### Example 3: Get the bytes of a Zip Archive Entry as a Stream 65 | 66 | ```powershell 67 | PS ..pwsh\> $bytes = Get-ZipEntry .\test.zip -Include test/helloworld.txt | Get-ZipEntryContent -AsByteStream 68 | PS ..pwsh\> $bytes 69 | 104 70 | 101 71 | 108 72 | 108 73 | 111 74 | 32 75 | 119 76 | 111 77 | 114 78 | 108 79 | 100 80 | 33 81 | 13 82 | 10 83 | 84 | PS ..pwsh\> [System.Text.Encoding]::UTF8.GetString($bytes) 85 | hello world! 86 | ``` 87 | 88 | The `-AsByteStream` switch can be useful to read non-text zip entries. 89 | 90 | ### Example 4: Get contents of all `.md` files as byte arrays 91 | 92 | ```powershell 93 | PS ..pwsh\> $bytes = Get-ZipEntry .\test.zip -Include *.md | Get-ZipEntryContent -AsByteStream -Raw 94 | PS ..pwsh\> $bytes[0].GetType() 95 | 96 | IsPublic IsSerial Name BaseType 97 | -------- -------- ---- -------- 98 | True True Byte[] System.Array 99 | 100 | PS ..pwsh\> $bytes[1].Length 101 | 7767 102 | ``` 103 | 104 | When the `-Raw` and `-AsByteStream` switches are used together the cmdlet outputs `byte[]` as single objects for each zip entry. 105 | 106 | ### Example 5: Get content from input Stream 107 | 108 | ```powershell 109 | PS ..\pwsh> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression 110 | PS ..\pwsh> $package | Get-ZipEntry -Include *.psd1 | Get-ZipEntryContent -Raw | Invoke-Expression 111 | 112 | Name Value 113 | ---- ----- 114 | PowerShellVersion 5.1 115 | Description Zip and GZip utilities for PowerShell! 116 | RootModule bin/netstandard2.0/PSCompression.dll 117 | FormatsToProcess {PSCompression.Format.ps1xml} 118 | VariablesToExport {} 119 | PrivateData {[PSData, System.Collections.Hashtable]} 120 | CmdletsToExport {Get-ZipEntry, Get-ZipEntryContent, Set-ZipEntryContent, Remove-ZipEntry…} 121 | ModuleVersion 2.0.10 122 | Author Santiago Squarzon 123 | CompanyName Unknown 124 | GUID c63aa90e-ae64-4ae1-b1c8-456e0d13967e 125 | FunctionsToExport {} 126 | RequiredAssemblies {System.IO.Compression, System.IO.Compression.FileSystem} 127 | Copyright (c) Santiago Squarzon. All rights reserved. 128 | AliasesToExport {gziptofile, gzipfromfile, gziptostring, gzipfromstring…} 129 | ``` 130 | 131 | ## PARAMETERS 132 | 133 | ### -BufferSize 134 | 135 | This parameter determines the total number of bytes read into the buffer before outputting the stream of bytes. __This parameter is applicable only when `-Raw` is not used.__ The buffer default value is __128 KiB.__ 136 | 137 | ```yaml 138 | Type: Int32 139 | Parameter Sets: Bytes 140 | Aliases: 141 | 142 | Required: False 143 | Position: Named 144 | Default value: 128000 145 | Accept pipeline input: False 146 | Accept wildcard characters: False 147 | ``` 148 | 149 | ### -Encoding 150 | 151 | The character encoding used to read the entry content. 152 | 153 | > [!NOTE] 154 | > 155 | > - __This parameter is applicable only when `-AsByteStream` is not used.__ 156 | > - The default encoding is __`utf8NoBOM`__. 157 | 158 | ```yaml 159 | Type: Encoding 160 | Parameter Sets: Stream 161 | Aliases: 162 | 163 | Required: False 164 | Position: Named 165 | Default value: utf8NoBOM 166 | Accept pipeline input: False 167 | Accept wildcard characters: False 168 | ``` 169 | 170 | ### -Raw 171 | 172 | Ignores newline characters and returns the entire contents of an entry in one string with the newlines preserved. By default, newline characters in a file are used as delimiters to separate the input into an array of strings. 173 | 174 | ```yaml 175 | Type: SwitchParameter 176 | Parameter Sets: (All) 177 | Aliases: 178 | 179 | Required: False 180 | Position: Named 181 | Default value: False 182 | Accept pipeline input: False 183 | Accept wildcard characters: False 184 | ``` 185 | 186 | ### -ZipEntry 187 | 188 | The entry or entries to get the content from. This parameter can be and is meant to be bound from pipeline however can be also used as a named parameter. 189 | 190 | ```yaml 191 | Type: ZipEntryFile[] 192 | Parameter Sets: (All) 193 | Aliases: 194 | 195 | Required: True 196 | Position: Named 197 | Default value: None 198 | Accept pipeline input: True (ByValue) 199 | Accept wildcard characters: False 200 | ``` 201 | 202 | ### -AsByteStream 203 | 204 | Specifies that the content should be read as a stream of bytes. 205 | 206 | ```yaml 207 | Type: SwitchParameter 208 | Parameter Sets: Bytes 209 | Aliases: 210 | 211 | Required: False 212 | Position: Named 213 | Default value: False 214 | Accept pipeline input: False 215 | Accept wildcard characters: False 216 | ``` 217 | 218 | ### CommonParameters 219 | 220 | This cmdlet supports the common parameters. See [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 221 | 222 | ## INPUTS 223 | 224 | ### ZipEntryFile 225 | 226 | You can pipe instances of `ZipEntryFile` to this cmdlet. These instances are produced by [`Get-ZipEntry`](Get-ZipEntry.md) and [`New-ZipEntry`](New-ZipEntry.md) cmdlets. 227 | 228 | ## OUTPUTS 229 | 230 | ### String 231 | 232 | By default, this cmdlet returns the content as an array of strings, one per line. When the `-Raw` parameter is used, it returns a single string. 233 | 234 | ### Byte 235 | 236 | This cmdlet returns the content as bytes when the `-AsByteStream` parameter is used. 237 | -------------------------------------------------------------------------------- /docs/en-US/Remove-ZipEntry.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression.dll-Help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # Remove-ZipEntry 9 | 10 | ## SYNOPSIS 11 | 12 | Removes zip entries from one or more zip archives. 13 | 14 | ## SYNTAX 15 | 16 | ```powershell 17 | Remove-ZipEntry 18 | -InputObject 19 | [-WhatIf] 20 | [-Confirm] 21 | [] 22 | ``` 23 | 24 | ## DESCRIPTION 25 | 26 | The `Remove-ZipEntry` cmdlet can remove Zip Archive Entries from one or more Zip Archives. This cmdlet takes input from and is intended to be used in combination with the [`Get-ZipEntry`](./Get-ZipEntry.md) cmdlet. 27 | 28 | ## EXAMPLES 29 | 30 | ### Example 1: Remove all Zip Archive Entries from a Zip Archive 31 | 32 | ```powershell 33 | PS ..pwsh\> Get-ZipEntry .\myZip.zip | Remove-ZipEntry 34 | ``` 35 | 36 | ### Example 2: Remove all `.txt` Entries from a Zip Archive 37 | 38 | ```powershell 39 | PS ..pwsh\> Get-ZipEntry .\myZip.zip -Include *.txt | Remove-ZipEntry 40 | ``` 41 | 42 | ### Example 3: Prompt for confirmation before removing entries 43 | 44 | ```powershell 45 | PS ..pwsh\> Get-ZipEntry .\myZip.zip -Include *.txt | Remove-ZipEntry -Confirm 46 | 47 | Confirm 48 | Are you sure you want to perform this action? 49 | Performing the operation "Remove" on target "test/helloworld.txt". 50 | [Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): 51 | ``` 52 | 53 | This cmdlet supports [`ShouldProcess`](https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-shouldprocess?view=powershell-7.3), you can prompt for confirmation before removing entries with `-Confirm` or check what the cmdlet would do without performing any action with `-WhatIf`. 54 | 55 | ## PARAMETERS 56 | 57 | ### -InputObject 58 | 59 | The entries that should be removed. This parameter can be and is meant to be bound from pipeline however can be also used as a named parameter. 60 | 61 | ```yaml 62 | Type: ZipEntryBase[] 63 | Parameter Sets: (All) 64 | Aliases: 65 | 66 | Required: True 67 | Position: Named 68 | Default value: None 69 | Accept pipeline input: True (ByValue) 70 | Accept wildcard characters: False 71 | ``` 72 | 73 | ### -Confirm 74 | 75 | Prompts you for confirmation before running the cmdlet. 76 | 77 | ```yaml 78 | Type: SwitchParameter 79 | Parameter Sets: (All) 80 | Aliases: cf 81 | 82 | Required: False 83 | Position: Named 84 | Default value: None 85 | Accept pipeline input: False 86 | Accept wildcard characters: False 87 | ``` 88 | 89 | ### -WhatIf 90 | 91 | Shows what would happen if the cmdlet runs. 92 | The cmdlet is not run. 93 | 94 | ```yaml 95 | Type: SwitchParameter 96 | Parameter Sets: (All) 97 | Aliases: wi 98 | 99 | Required: False 100 | Position: Named 101 | Default value: None 102 | Accept pipeline input: False 103 | Accept wildcard characters: False 104 | ``` 105 | 106 | ### CommonParameters 107 | 108 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 109 | 110 | ## INPUTS 111 | 112 | ### ZipEntryBase 113 | 114 | You can pipe instances of `ZipEntryFile` and `ZipEntryDirectory` to this cmdlet. These instances are produced by [`Get-ZipEntry`](Get-ZipEntry.md) and [`New-ZipEntry`](New-ZipEntry.md) cmdlets. 115 | 116 | ## OUTPUTS 117 | 118 | ### None 119 | 120 | This cmdlet produces no output. 121 | -------------------------------------------------------------------------------- /docs/en-US/Set-ZipEntryContent.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSCompression.dll-Help.xml 3 | Module Name: PSCompression 4 | online version: https://github.com/santisq/PSCompression 5 | schema: 2.0.0 6 | --- 7 | 8 | # Set-ZipEntryContent 9 | 10 | ## SYNOPSIS 11 | 12 | Sets or appends content to an existing zip entry. 13 | 14 | ## SYNTAX 15 | 16 | ### StringValue (Default) 17 | 18 | ```powershell 19 | Set-ZipEntryContent 20 | -Value 21 | -SourceEntry 22 | [-Encoding ] 23 | [-Append] 24 | [-PassThru] 25 | [] 26 | ``` 27 | 28 | ### ByteStream 29 | 30 | ```powershell 31 | Set-ZipEntryContent 32 | -Value 33 | -SourceEntry 34 | [-AsByteStream] 35 | [-Append] 36 | [-BufferSize ] 37 | [-PassThru] 38 | [] 39 | ``` 40 | 41 | ## DESCRIPTION 42 | 43 | The `Set-ZipEntryContent` cmdlet can write or append content to a Zip Archive Entry. By default, this cmdlet replaces the existing content of a Zip Archive Entry, if you need to append content you can use the `-Append` switch. This cmdlet also supports writing or appending raw bytes while using the `-AsByteStream` switch. To send content to `Set-ZipEntryContent` you can use the `-Value` parameter on the command line or send content through the pipeline. 44 | 45 | If you need to create a new Zip Archive Entry you can use the [`New-ZipEntry` cmdlet](./New-ZipEntry.md). 46 | 47 | ## EXAMPLES 48 | 49 | ### Example 1: Write new content to a Zip Archive Entry 50 | 51 | ```powershell 52 | PS ..pwsh\> $entry = New-ZipEntry .\test.zip -EntryPath test\helloworld.txt 53 | PS ..pwsh\> 'hello', 'world', '!' | Set-ZipEntryContent $entry 54 | PS ..pwsh\> $entry | Get-ZipEntryContent 55 | hello 56 | world 57 | ! 58 | ``` 59 | 60 | You can send content through the pipeline or using the `-Value` parameter as shown in the next example. 61 | 62 | ### Example 2: Append content to a Zip Archive Entry 63 | 64 | ```powershell 65 | PS ..pwsh\> Set-ZipEntryContent $entry -Value 'hello', 'world', '!' -Append 66 | PS ..pwsh\> $entry | Get-ZipEntryContent 67 | hello 68 | world 69 | ! 70 | hello 71 | world 72 | ! 73 | ``` 74 | 75 | ### Example 3: Write raw bytes to a Zip Archive Entry 76 | 77 | ```powershell 78 | PS ..pwsh\> $entry = Get-ZipEntry .\test.zip -Include test/helloworld.txt 79 | PS ..pwsh\> $bytes = [System.Text.Encoding]::UTF8.GetBytes('hello world!') 80 | PS ..pwsh\> $bytes | Set-ZipEntryContent $entry -AsByteStream 81 | PS ..pwsh\> $entry | Get-ZipEntryContent 82 | hello world! 83 | ``` 84 | 85 | The cmdlet supports writing and appending raw bytes while using the `-AsByteStream` switch. 86 | 87 | ### Example 4: Append raw bytes to a Zip Archive Entry 88 | 89 | ```powershell 90 | PS ..pwsh\> $bytes | Set-ZipEntryContent $entry -AsByteStream -Append 91 | PS ..pwsh\> $entry | Get-ZipEntryContent 92 | hello world!hello world! 93 | ``` 94 | 95 | Using the same byte array in the previous example, we can append bytes to the entry stream. 96 | 97 | ## PARAMETERS 98 | 99 | ### -Append 100 | 101 | Appends the content to the zip entry instead of overwriting it. 102 | 103 | ```yaml 104 | Type: SwitchParameter 105 | Parameter Sets: (All) 106 | Aliases: 107 | 108 | Required: False 109 | Position: Named 110 | Default value: None 111 | Accept pipeline input: False 112 | Accept wildcard characters: False 113 | ``` 114 | 115 | ### -AsByteStream 116 | 117 | Specifies that the content should be written as a stream of bytes. 118 | 119 | ```yaml 120 | Type: SwitchParameter 121 | Parameter Sets: ByteStream 122 | Aliases: 123 | 124 | Required: False 125 | Position: Named 126 | Default value: None 127 | Accept pipeline input: False 128 | Accept wildcard characters: False 129 | ``` 130 | 131 | ### -BufferSize 132 | 133 | For efficiency purposes this cmdlet buffers bytes before writing them to the Zip Archive Entry. This parameter determines how many bytes are buffered before being written to the stream. 134 | 135 | > [!NOTE] 136 | > 137 | > - __This parameter is applicable only when `-AsByteStream` is used.__ 138 | > The buffer default value is __128 KiB.__ 139 | 140 | ```yaml 141 | Type: Int32 142 | Parameter Sets: ByteStream 143 | Aliases: 144 | 145 | Required: False 146 | Position: Named 147 | Default value: 128000 148 | Accept pipeline input: False 149 | Accept wildcard characters: False 150 | ``` 151 | 152 | ### -Encoding 153 | 154 | The character encoding used to read the entry content. 155 | 156 | > [!NOTE] 157 | > 158 | > - __This parameter is applicable only when `-AsByteStream` is not used.__ 159 | > - The default encoding is __`utf8NoBOM`__. 160 | 161 | ```yaml 162 | Type: Encoding 163 | Parameter Sets: StringValue 164 | Aliases: 165 | 166 | Required: False 167 | Position: Named 168 | Default value: utf8NoBOM 169 | Accept pipeline input: False 170 | Accept wildcard characters: False 171 | ``` 172 | 173 | ### -PassThru 174 | 175 | Outputs the object representing the updated zip archive entry. By default, this cmdlet does not generate any output. 176 | 177 | ```yaml 178 | Type: SwitchParameter 179 | Parameter Sets: (All) 180 | Aliases: 181 | 182 | Required: False 183 | Position: Named 184 | Default value: None 185 | Accept pipeline input: False 186 | Accept wildcard characters: False 187 | ``` 188 | 189 | ### -SourceEntry 190 | 191 | Specifies the zip archive entry that receives the content. `ZipEntryFile` instances can be obtained using `Get-ZipEntry` or `New-ZipEntry` cmdlets. 192 | 193 | ```yaml 194 | Type: ZipEntryFile 195 | Parameter Sets: (All) 196 | Aliases: 197 | 198 | Required: True 199 | Position: 0 200 | Default value: None 201 | Accept pipeline input: False 202 | Accept wildcard characters: False 203 | ``` 204 | 205 | ### -Value 206 | 207 | Specifies the new content for the zip entry. 208 | 209 | ```yaml 210 | Type: Object[] 211 | Parameter Sets: (All) 212 | Aliases: 213 | 214 | Required: True 215 | Position: Named 216 | Default value: None 217 | Accept pipeline input: True (ByValue) 218 | Accept wildcard characters: False 219 | ``` 220 | 221 | ### CommonParameters 222 | 223 | This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 224 | 225 | ## INPUTS 226 | 227 | ### Object 228 | 229 | You can pipe strings or bytes to this cmdlet. 230 | 231 | ## OUTPUTS 232 | 233 | ### None 234 | 235 | This cmdlet produces no output by default . 236 | 237 | ### ZipEntryFile 238 | 239 | This cmdlet outputs the updated entry when the `-PassThru` switch is used. 240 | -------------------------------------------------------------------------------- /module/PSCompression.Format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | zipentryview 6 | 7 | PSCompression.ZipEntryBase 8 | 9 | 10 | [PSCompression.Internal._Format]::GetDirectoryPath($_) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 10 18 | Left 19 | 20 | 21 | 22 | 26 23 | Right 24 | 25 | 26 | 27 | 15 28 | Right 29 | 30 | 31 | 32 | 15 33 | Right 34 | 35 | 36 | 37 | Left 38 | 39 | 40 | 41 | 42 | 43 | 44 | Type 45 | 46 | 47 | [PSCompression.Internal._Format]::GetFormattedDate($_.LastWriteTime) 48 | 49 | 50 | 51 | if ($_ -is [PSCompression.ZipEntryFile]) { 52 | [PSCompression.Internal._Format]::GetFormattedLength($_.CompressedLength) 53 | } 54 | 55 | 56 | 57 | 58 | if ($_ -is [PSCompression.ZipEntryFile]) { 59 | [PSCompression.Internal._Format]::GetFormattedLength($_.Length) 60 | } 61 | 62 | 63 | 64 | Name 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /module/PSCompression.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'PSCompression' 3 | # 4 | # Generated by: Santiago Squarzon 5 | # 6 | # Generated on: 11/06/2022 7 | # 8 | 9 | @{ 10 | # Script module or binary module file associated with this manifest. 11 | RootModule = 'bin/netstandard2.0/PSCompression.dll' 12 | 13 | # Version number of this module. 14 | ModuleVersion = '2.1.0' 15 | 16 | # Supported PSEditions 17 | # CompatiblePSEditions = @() 18 | 19 | # ID used to uniquely identify this module 20 | GUID = 'c63aa90e-ae64-4ae1-b1c8-456e0d13967e' 21 | 22 | # Author of this module 23 | Author = 'Santiago Squarzon' 24 | 25 | # Company or vendor of this module 26 | CompanyName = 'Unknown' 27 | 28 | # Copyright statement for this module 29 | Copyright = '(c) Santiago Squarzon. All rights reserved.' 30 | 31 | # Description of the functionality provided by this module 32 | Description = 'Zip and GZip utilities for PowerShell!' 33 | 34 | # Minimum version of the PowerShell engine required by this module 35 | PowerShellVersion = '5.1' 36 | 37 | # Name of the PowerShell host required by this module 38 | # PowerShellHostName = '' 39 | 40 | # Minimum version of the PowerShell host required by this module 41 | # PowerShellHostVersion = '' 42 | 43 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 44 | # DotNetFrameworkVersion = '' 45 | 46 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 47 | # ClrVersion = '' 48 | 49 | # Processor architecture (None, X86, Amd64) required by this module 50 | # ProcessorArchitecture = '' 51 | 52 | # Modules that must be imported into the global environment prior to importing this module 53 | # RequiredModules = @() 54 | 55 | # Assemblies that must be loaded prior to importing this module 56 | RequiredAssemblies = @( 57 | 'System.IO.Compression' 58 | 'System.IO.Compression.FileSystem' 59 | ) 60 | 61 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 62 | # ScriptsToProcess = @() 63 | 64 | # Type files (.ps1xml) to be loaded when importing this module 65 | # TypesToProcess = @() 66 | 67 | # Format files (.ps1xml) to be loaded when importing this module 68 | FormatsToProcess = @('PSCompression.Format.ps1xml') 69 | 70 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 71 | # NestedModules = @() 72 | 73 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 74 | FunctionsToExport = @() 75 | 76 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 77 | CmdletsToExport = @( 78 | 'Get-ZipEntry' 79 | 'Get-ZipEntryContent' 80 | 'Set-ZipEntryContent' 81 | 'Remove-ZipEntry' 82 | 'New-ZipEntry' 83 | 'Expand-ZipEntry' 84 | 'ConvertTo-GzipString' 85 | 'ConvertFrom-GzipString' 86 | 'Expand-GzipArchive' 87 | 'Compress-GzipArchive' 88 | 'Compress-ZipArchive' 89 | 'Rename-ZipEntry' 90 | ) 91 | 92 | # Variables to export from this module 93 | VariablesToExport = @() 94 | 95 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 96 | AliasesToExport = @( 97 | 'gziptofile' 98 | 'gzipfromfile' 99 | 'gziptostring' 100 | 'gzipfromstring' 101 | 'zip' 102 | 'ziparchive' 103 | 'gze' 104 | ) 105 | 106 | # DSC resources to export from this module 107 | # DscResourcesToExport = @() 108 | 109 | # List of all modules packaged with this module 110 | # ModuleList = @() 111 | 112 | # List of all files packaged with this module 113 | # FileList = @() 114 | 115 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 116 | PrivateData = @{ 117 | PSData = @{ 118 | # Tags applied to this module. These help with module discovery in online galleries. 119 | Tags = @( 120 | 'powershell' 121 | 'zip' 122 | 'zip-compression' 123 | 'gzip' 124 | 'gzip-compression' 125 | 'compression' 126 | 'csharp' 127 | ) 128 | 129 | # A URL to the license for this module. 130 | LicenseUri = 'https://github.com/santisq/PSCompression/blob/main/LICENSE' 131 | 132 | # A URL to the main website for this project. 133 | ProjectUri = 'https://github.com/santisq/PSCompression' 134 | 135 | # A URL to an icon representing this module. 136 | # IconUri = '' 137 | 138 | # ReleaseNotes of this module 139 | # ReleaseNotes = '' 140 | 141 | # Prerelease string of this module 142 | # Prerelease = '' 143 | 144 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 145 | # RequireLicenseAcceptance = $false 146 | 147 | # External dependent modules of this module 148 | # ExternalModuleDependencies = @() 149 | 150 | } # End of PSData hashtable 151 | 152 | } # End of PrivateData hashtable 153 | 154 | # HelpInfo URI of this module 155 | # HelpInfoURI = '' 156 | 157 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 158 | # DefaultCommandPrefix = '' 159 | } 160 | -------------------------------------------------------------------------------- /src/PSCompression/CommandWithPathBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.ComponentModel; 5 | using System.Management.Automation; 6 | using PSCompression.Exceptions; 7 | using PSCompression.Extensions; 8 | 9 | namespace PSCompression; 10 | 11 | [EditorBrowsable(EditorBrowsableState.Never)] 12 | public abstract class CommandWithPathBase : PSCmdlet 13 | { 14 | protected string[] _paths = []; 15 | 16 | protected bool IsLiteral 17 | { 18 | get => MyInvocation.BoundParameters.ContainsKey("LiteralPath"); 19 | } 20 | 21 | [Parameter( 22 | ParameterSetName = "Path", 23 | Position = 0, 24 | Mandatory = true, 25 | ValueFromPipeline = true)] 26 | [SupportsWildcards] 27 | public virtual string[] Path 28 | { 29 | get => _paths; 30 | set => _paths = value; 31 | } 32 | 33 | [Parameter( 34 | ParameterSetName = "LiteralPath", 35 | Mandatory = true, 36 | ValueFromPipelineByPropertyName = true)] 37 | [Alias("PSPath")] 38 | public virtual string[] LiteralPath 39 | { 40 | get => _paths; 41 | set => _paths = value; 42 | } 43 | 44 | protected IEnumerable EnumerateResolvedPaths() 45 | { 46 | Collection resolvedPaths; 47 | ProviderInfo provider; 48 | 49 | foreach (string path in _paths) 50 | { 51 | if (IsLiteral) 52 | { 53 | string resolved = SessionState.Path.GetUnresolvedProviderPathFromPSPath( 54 | path: path, 55 | provider: out provider, 56 | drive: out _); 57 | 58 | if (provider.Validate(resolved, throwOnInvalidProvider: false, this)) 59 | { 60 | yield return resolved; 61 | } 62 | 63 | continue; 64 | } 65 | 66 | try 67 | { 68 | resolvedPaths = GetResolvedProviderPathFromPSPath(path, out provider); 69 | } 70 | catch (Exception exception) 71 | { 72 | WriteError(exception.ToResolvePathError(path)); 73 | continue; 74 | } 75 | 76 | 77 | foreach (string resolvedPath in resolvedPaths) 78 | { 79 | if (provider.Validate(resolvedPath, throwOnInvalidProvider: true, this)) 80 | { 81 | yield return resolvedPath; 82 | } 83 | } 84 | } 85 | } 86 | 87 | protected string ResolvePath(string path) => path.ResolvePath(this); 88 | } 89 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/CompressGzipArchiveCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Management.Automation; 5 | using PSCompression.Extensions; 6 | using PSCompression.Exceptions; 7 | 8 | namespace PSCompression.Commands; 9 | 10 | [Cmdlet(VerbsData.Compress, "GzipArchive", DefaultParameterSetName = "Path")] 11 | [OutputType(typeof(FileInfo))] 12 | [Alias("gziptofile")] 13 | public sealed class CompressGzipArchive : CommandWithPathBase, IDisposable 14 | { 15 | private FileStream? _destination; 16 | 17 | private GZipStream? _gzip; 18 | 19 | private FileMode Mode 20 | { 21 | get => (Update.IsPresent, Force.IsPresent) switch 22 | { 23 | (true, _) => FileMode.Append, 24 | (_, true) => FileMode.Create, 25 | _ => FileMode.CreateNew 26 | }; 27 | } 28 | 29 | [Parameter( 30 | ParameterSetName = "InputBytes", 31 | Mandatory = true, 32 | ValueFromPipeline = true)] 33 | public byte[]? InputBytes { get; set; } 34 | 35 | [Parameter(Mandatory = true, Position = 1)] 36 | [Alias("DestinationPath")] 37 | public string Destination { get; set; } = null!; 38 | 39 | [Parameter] 40 | public CompressionLevel CompressionLevel { get; set; } = CompressionLevel.Optimal; 41 | 42 | [Parameter] 43 | public SwitchParameter Update { get; set; } 44 | 45 | [Parameter] 46 | public SwitchParameter Force { get; set; } 47 | 48 | [Parameter] 49 | public SwitchParameter PassThru { get; set; } 50 | 51 | protected override void BeginProcessing() 52 | { 53 | Destination = ResolvePath(Destination).AddExtensionIfMissing(".gz"); 54 | 55 | try 56 | { 57 | string parent = Destination.GetParent(); 58 | 59 | if (!Directory.Exists(parent)) 60 | { 61 | Directory.CreateDirectory(parent); 62 | } 63 | 64 | _destination = File.Open(Destination, Mode); 65 | } 66 | catch (Exception exception) 67 | { 68 | ThrowTerminatingError(exception.ToStreamOpenError(Destination)); 69 | } 70 | } 71 | 72 | protected override void ProcessRecord() 73 | { 74 | Dbg.Assert(_destination is not null); 75 | 76 | if (InputBytes is not null) 77 | { 78 | try 79 | { 80 | _destination.Write(InputBytes, 0, InputBytes.Length); 81 | } 82 | catch (Exception exception) 83 | { 84 | WriteError(exception.ToWriteError(InputBytes)); 85 | } 86 | 87 | return; 88 | } 89 | 90 | _gzip ??= new GZipStream(_destination, CompressionLevel); 91 | 92 | foreach (string path in EnumerateResolvedPaths()) 93 | { 94 | if (!path.IsArchive()) 95 | { 96 | WriteError(ExceptionHelper.NotArchivePath( 97 | path, 98 | IsLiteral ? nameof(LiteralPath) : nameof(Path))); 99 | 100 | continue; 101 | } 102 | 103 | try 104 | { 105 | using FileStream stream = File.OpenRead(path); 106 | stream.CopyTo(_gzip); 107 | } 108 | catch (Exception exception) 109 | { 110 | WriteError(exception.ToWriteError(path)); 111 | } 112 | } 113 | } 114 | 115 | protected override void EndProcessing() 116 | { 117 | _gzip?.Dispose(); 118 | _destination?.Dispose(); 119 | 120 | if (PassThru.IsPresent && _destination is not null) 121 | { 122 | WriteObject(new FileInfo(_destination.Name)); 123 | } 124 | } 125 | 126 | public void Dispose() 127 | { 128 | _gzip?.Dispose(); 129 | _destination?.Dispose(); 130 | GC.SuppressFinalize(this); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ConvertFromGzipStringCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Management.Automation; 5 | using System.Text; 6 | using PSCompression.Exceptions; 7 | 8 | namespace PSCompression.Commands; 9 | 10 | [Cmdlet(VerbsData.ConvertFrom, "GzipString")] 11 | [OutputType(typeof(string))] 12 | [Alias("gzipfromstring")] 13 | public sealed class ConvertFromGzipStringCommand : PSCmdlet 14 | { 15 | [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] 16 | public string[] InputObject { get; set; } = null!; 17 | 18 | [Parameter] 19 | [ArgumentCompleter(typeof(EncodingCompleter))] 20 | [EncodingTransformation] 21 | [ValidateNotNullOrEmpty] 22 | public Encoding Encoding { get; set; } = new UTF8Encoding(); 23 | 24 | [Parameter] 25 | public SwitchParameter Raw { get; set; } 26 | 27 | protected override void ProcessRecord() 28 | { 29 | foreach (string line in InputObject) 30 | { 31 | try 32 | { 33 | using MemoryStream inStream = new(Convert.FromBase64String(line)); 34 | using GZipStream gzip = new(inStream, CompressionMode.Decompress); 35 | using StreamReader reader = new(gzip, Encoding); 36 | 37 | if (Raw.IsPresent) 38 | { 39 | WriteObject(reader.ReadToEnd()); 40 | return; 41 | } 42 | 43 | while (!reader.EndOfStream) 44 | { 45 | WriteObject(reader.ReadLine()); 46 | } 47 | } 48 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 49 | { 50 | throw; 51 | } 52 | catch (Exception exception) 53 | { 54 | WriteError(exception.ToEnumerationError(line)); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ConvertToGzipStringCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Management.Automation; 5 | using System.Text; 6 | using PSCompression.Exceptions; 7 | 8 | namespace PSCompression.Commands; 9 | 10 | [Cmdlet(VerbsData.ConvertTo, "GzipString")] 11 | [OutputType(typeof(byte[]), typeof(string))] 12 | [Alias("gziptostring")] 13 | public sealed class ConvertToGzipStringCommand : PSCmdlet, IDisposable 14 | { 15 | private StreamWriter? _writer; 16 | 17 | private GZipStream? _gzip; 18 | 19 | private readonly MemoryStream _outstream = new(); 20 | 21 | [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0)] 22 | [AllowEmptyString] 23 | public string[] InputObject { get; set; } = null!; 24 | 25 | [Parameter] 26 | [ArgumentCompleter(typeof(EncodingCompleter))] 27 | [EncodingTransformation] 28 | [ValidateNotNullOrEmpty] 29 | public Encoding Encoding { get; set; } = new UTF8Encoding(); 30 | 31 | [Parameter] 32 | public CompressionLevel CompressionLevel { get; set; } = CompressionLevel.Optimal; 33 | 34 | [Parameter] 35 | [Alias("Raw")] 36 | public SwitchParameter AsByteStream { get; set; } 37 | 38 | [Parameter] 39 | public SwitchParameter NoNewLine { get; set; } 40 | 41 | protected override void ProcessRecord() 42 | { 43 | try 44 | { 45 | _gzip ??= new(_outstream, CompressionLevel); 46 | _writer ??= new(_gzip, Encoding); 47 | 48 | if (NoNewLine.IsPresent) 49 | { 50 | Write(_writer, InputObject); 51 | return; 52 | } 53 | 54 | WriteLines(_writer, InputObject); 55 | } 56 | catch (Exception exception) 57 | { 58 | WriteError(exception.ToWriteError(InputObject)); 59 | } 60 | } 61 | 62 | protected override void EndProcessing() 63 | { 64 | _writer?.Dispose(); 65 | _gzip?.Dispose(); 66 | _outstream?.Dispose(); 67 | 68 | try 69 | { 70 | if (_writer is null || _gzip is null || _outstream is null) 71 | { 72 | return; 73 | } 74 | 75 | if (AsByteStream.IsPresent) 76 | { 77 | WriteObject(_outstream.ToArray(), enumerateCollection: false); 78 | return; 79 | } 80 | 81 | WriteObject(Convert.ToBase64String(_outstream.ToArray())); 82 | } 83 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 84 | { 85 | throw; 86 | } 87 | catch (Exception exception) 88 | { 89 | WriteError(exception.ToWriteError(_outstream)); 90 | } 91 | } 92 | 93 | private void WriteLines(StreamWriter writer, string[] lines) 94 | { 95 | foreach (string line in lines) 96 | { 97 | writer.WriteLine(line); 98 | } 99 | } 100 | 101 | private void Write(StreamWriter writer, string[] lines) 102 | { 103 | foreach (string line in lines) 104 | { 105 | writer.Write(line); 106 | } 107 | } 108 | 109 | public void Dispose() 110 | { 111 | _writer?.Dispose(); 112 | _gzip?.Dispose(); 113 | _outstream?.Dispose(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ExpandGzipArchiveCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Management.Automation; 4 | using System.Text; 5 | using PSCompression.Extensions; 6 | using PSCompression.Exceptions; 7 | 8 | namespace PSCompression.Commands; 9 | 10 | [Cmdlet(VerbsData.Expand, "GzipArchive")] 11 | [OutputType( 12 | typeof(string), 13 | ParameterSetName = ["Path", "LiteralPath"])] 14 | [OutputType( 15 | typeof(FileInfo), 16 | ParameterSetName = ["PathDestination", "LiteralPathDestination"])] 17 | [Alias("gzipfromfile")] 18 | public sealed class ExpandGzipArchiveCommand : CommandWithPathBase, IDisposable 19 | { 20 | private FileStream? _destination; 21 | 22 | private FileMode FileMode 23 | { 24 | get => (Update.IsPresent, Force.IsPresent) switch 25 | { 26 | (true, _) => FileMode.Append, 27 | (_, true) => FileMode.Create, 28 | _ => FileMode.CreateNew 29 | }; 30 | } 31 | 32 | [Parameter( 33 | ParameterSetName = "Path", 34 | Position = 0, 35 | Mandatory = true, 36 | ValueFromPipeline = true)] 37 | [Parameter( 38 | ParameterSetName = "PathDestination", 39 | Position = 0, 40 | Mandatory = true, 41 | ValueFromPipeline = true)] 42 | [SupportsWildcards] 43 | public override string[] Path 44 | { 45 | get => _paths; 46 | set => _paths = value; 47 | } 48 | 49 | [Parameter( 50 | ParameterSetName = "LiteralPath", 51 | Mandatory = true, 52 | ValueFromPipelineByPropertyName = true)] 53 | [Parameter( 54 | ParameterSetName = "LiteralPathDestination", 55 | Mandatory = true, 56 | ValueFromPipelineByPropertyName = true)] 57 | [Alias("PSPath")] 58 | public override string[] LiteralPath 59 | { 60 | get => _paths; 61 | set => _paths = value; 62 | } 63 | 64 | [Parameter( 65 | Mandatory = true, 66 | Position = 1, 67 | ParameterSetName = "PathDestination")] 68 | [Parameter( 69 | Mandatory = true, 70 | Position = 1, 71 | ParameterSetName = "LiteralPathDestination")] 72 | [Alias("DestinationPath")] 73 | public string Destination { get; set; } = null!; 74 | 75 | [Parameter(ParameterSetName = "PathDestination")] 76 | [Parameter(ParameterSetName = "LiteralPathDestination")] 77 | [ArgumentCompleter(typeof(EncodingCompleter))] 78 | [EncodingTransformation] 79 | [ValidateNotNullOrEmpty] 80 | public Encoding Encoding { get; set; } = new UTF8Encoding(); 81 | 82 | [Parameter(ParameterSetName = "Path")] 83 | [Parameter(ParameterSetName = "LiteralPath")] 84 | public SwitchParameter Raw { get; set; } 85 | 86 | [Parameter(ParameterSetName = "PathDestination")] 87 | [Parameter(ParameterSetName = "LiteralPathDestination")] 88 | public SwitchParameter PassThru { get; set; } 89 | 90 | [Parameter(ParameterSetName = "PathDestination")] 91 | [Parameter(ParameterSetName = "LiteralPathDestination")] 92 | public SwitchParameter Force { get; set; } 93 | 94 | [Parameter(ParameterSetName = "PathDestination")] 95 | [Parameter(ParameterSetName = "LiteralPathDestination")] 96 | public SwitchParameter Update { get; set; } 97 | 98 | protected override void BeginProcessing() 99 | { 100 | if (Destination is not null && _destination is null) 101 | { 102 | try 103 | { 104 | Destination = ResolvePath(Destination); 105 | string parent = Destination.GetParent(); 106 | 107 | if (!Directory.Exists(parent)) 108 | { 109 | Directory.CreateDirectory(parent); 110 | } 111 | 112 | _destination = File.Open(Destination, FileMode); 113 | } 114 | catch (Exception exception) 115 | { 116 | ThrowTerminatingError(exception.ToStreamOpenError(Destination)); 117 | } 118 | } 119 | } 120 | 121 | protected override void ProcessRecord() 122 | { 123 | foreach (string path in EnumerateResolvedPaths()) 124 | { 125 | if (!path.IsArchive()) 126 | { 127 | WriteError(ExceptionHelper.NotArchivePath( 128 | path, 129 | IsLiteral ? nameof(LiteralPath) : nameof(Path))); 130 | 131 | continue; 132 | } 133 | 134 | try 135 | { 136 | if (_destination is not null) 137 | { 138 | GzipReaderOps.CopyTo( 139 | path: path, 140 | isCoreCLR: PSVersionHelper.IsCoreCLR, 141 | destination: _destination); 142 | 143 | continue; 144 | } 145 | 146 | GzipReaderOps.GetContent( 147 | path: path, 148 | isCoreCLR: PSVersionHelper.IsCoreCLR, 149 | raw: Raw.IsPresent, 150 | encoding: Encoding, 151 | cmdlet: this); 152 | } 153 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 154 | { 155 | throw; 156 | } 157 | catch (Exception exception) 158 | { 159 | WriteError(exception.ToOpenError(path)); 160 | } 161 | } 162 | } 163 | 164 | protected override void EndProcessing() 165 | { 166 | _destination?.Dispose(); 167 | 168 | if (PassThru.IsPresent && _destination is not null) 169 | { 170 | WriteObject(new FileInfo(_destination.Name)); 171 | } 172 | } 173 | 174 | public void Dispose() 175 | { 176 | _destination?.Dispose(); 177 | GC.SuppressFinalize(this); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/ExpandZipEntryCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Management.Automation; 4 | using PSCompression.Extensions; 5 | using PSCompression.Exceptions; 6 | 7 | namespace PSCompression.Commands; 8 | 9 | [Cmdlet(VerbsData.Expand, "ZipEntry")] 10 | [OutputType(typeof(FileSystemInfo))] 11 | public sealed class ExpandZipEntryCommand : PSCmdlet, IDisposable 12 | { 13 | private readonly ZipArchiveCache _cache = new(); 14 | 15 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 16 | public ZipEntryBase[] InputObject { get; set; } = null!; 17 | 18 | [Parameter(Position = 0)] 19 | [ValidateNotNullOrEmpty] 20 | public string? Destination { get; set; } 21 | 22 | [Parameter] 23 | public SwitchParameter Force { get; set; } 24 | 25 | [Parameter] 26 | public SwitchParameter PassThru { get; set; } 27 | 28 | protected override void BeginProcessing() 29 | { 30 | Destination = Destination is null 31 | ? SessionState.Path.CurrentFileSystemLocation.Path 32 | : Destination.ResolvePath(this); 33 | 34 | if (Destination.IsArchive()) 35 | { 36 | ThrowTerminatingError( 37 | ExceptionHelper.NotDirectoryPath( 38 | Destination, 39 | nameof(Destination))); 40 | } 41 | 42 | if (!Directory.Exists(Destination)) 43 | { 44 | Directory.CreateDirectory(Destination); 45 | } 46 | } 47 | 48 | protected override void ProcessRecord() 49 | { 50 | Dbg.Assert(Destination is not null); 51 | 52 | foreach (ZipEntryBase entry in InputObject) 53 | { 54 | try 55 | { 56 | (string path, bool isfile) = entry.ExtractTo( 57 | _cache.GetOrAdd(entry), 58 | Destination, 59 | Force.IsPresent); 60 | 61 | if (PassThru.IsPresent) 62 | { 63 | if (isfile) 64 | { 65 | WriteObject(new FileInfo(path)); 66 | continue; 67 | } 68 | 69 | WriteObject(new DirectoryInfo(path)); 70 | } 71 | } 72 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 73 | { 74 | throw; 75 | } 76 | catch (Exception exception) 77 | { 78 | WriteError(exception.ToExtractEntryError(entry)); 79 | } 80 | } 81 | } 82 | 83 | public void Dispose() 84 | { 85 | _cache?.Dispose(); 86 | GC.SuppressFinalize(this); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/GetZipEntryCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Compression; 4 | using System.Linq; 5 | using System.Management.Automation; 6 | using PSCompression.Extensions; 7 | using PSCompression.Exceptions; 8 | using System.IO; 9 | 10 | namespace PSCompression.Commands; 11 | 12 | [Cmdlet(VerbsCommon.Get, "ZipEntry", DefaultParameterSetName = "Path")] 13 | [OutputType(typeof(ZipEntryDirectory), typeof(ZipEntryFile))] 14 | [Alias("gezip")] 15 | public sealed class GetZipEntryCommand : CommandWithPathBase 16 | { 17 | [Parameter( 18 | ParameterSetName = "Stream", 19 | Position = 0, 20 | Mandatory = true, 21 | ValueFromPipeline = true, 22 | ValueFromPipelineByPropertyName = true)] 23 | [Alias("RawContentStream")] 24 | public Stream? InputStream { get; set; } 25 | 26 | private readonly List _output = []; 27 | 28 | private WildcardPattern[]? _includePatterns; 29 | 30 | private WildcardPattern[]? _excludePatterns; 31 | 32 | [Parameter] 33 | public ZipEntryType? Type { get; set; } 34 | 35 | [Parameter] 36 | [SupportsWildcards] 37 | public string[]? Include { get; set; } 38 | 39 | [Parameter] 40 | [SupportsWildcards] 41 | public string[]? Exclude { get; set; } 42 | 43 | protected override void BeginProcessing() 44 | { 45 | if (Exclude is null && Include is null) 46 | { 47 | return; 48 | } 49 | 50 | const WildcardOptions options = WildcardOptions.Compiled 51 | | WildcardOptions.CultureInvariant 52 | | WildcardOptions.IgnoreCase; 53 | 54 | if (Exclude is not null) 55 | { 56 | _excludePatterns = [.. Exclude.Select(e => new WildcardPattern(e, options))]; 57 | } 58 | 59 | if (Include is not null) 60 | { 61 | _includePatterns = [.. Include.Select(e => new WildcardPattern(e, options))]; 62 | } 63 | } 64 | 65 | protected override void ProcessRecord() 66 | { 67 | IEnumerable entries; 68 | if (InputStream is not null) 69 | { 70 | ZipEntryBase CreateFromStream(ZipArchiveEntry entry, bool isDirectory) => 71 | isDirectory 72 | ? new ZipEntryDirectory(entry, InputStream) 73 | : new ZipEntryFile(entry, InputStream); 74 | 75 | try 76 | { 77 | using (ZipArchive zip = new(InputStream, ZipArchiveMode.Read, true)) 78 | { 79 | entries = GetEntries(zip, CreateFromStream); 80 | } 81 | 82 | WriteObject(entries, enumerateCollection: true); 83 | return; 84 | } 85 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 86 | { 87 | throw; 88 | } 89 | catch (InvalidDataException exception) 90 | { 91 | ThrowTerminatingError(exception.ToInvalidZipArchive()); 92 | } 93 | catch (Exception exception) 94 | { 95 | WriteError(exception.ToOpenError("InputStream")); 96 | } 97 | } 98 | 99 | foreach (string path in EnumerateResolvedPaths()) 100 | { 101 | ZipEntryBase CreateFromFile(ZipArchiveEntry entry, bool isDirectory) => 102 | isDirectory 103 | ? new ZipEntryDirectory(entry, path) 104 | : new ZipEntryFile(entry, path); 105 | 106 | if (!path.IsArchive()) 107 | { 108 | WriteError( 109 | ExceptionHelper.NotArchivePath( 110 | path, 111 | IsLiteral ? nameof(LiteralPath) : nameof(Path))); 112 | 113 | continue; 114 | } 115 | 116 | try 117 | { 118 | using (ZipArchive zip = ZipFile.OpenRead(path)) 119 | { 120 | entries = GetEntries(zip, CreateFromFile); 121 | } 122 | 123 | WriteObject(entries, enumerateCollection: true); 124 | } 125 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 126 | { 127 | throw; 128 | } 129 | catch (InvalidDataException exception) 130 | { 131 | ThrowTerminatingError(exception.ToInvalidZipArchive()); 132 | } 133 | catch (Exception exception) 134 | { 135 | WriteError(exception.ToOpenError(path)); 136 | } 137 | } 138 | } 139 | 140 | private IEnumerable GetEntries( 141 | ZipArchive zip, 142 | Func createMethod) 143 | { 144 | _output.Clear(); 145 | foreach (ZipArchiveEntry entry in zip.Entries) 146 | { 147 | bool isDirectory = string.IsNullOrEmpty(entry.Name); 148 | 149 | if (ShouldSkipEntry(isDirectory)) 150 | { 151 | continue; 152 | } 153 | 154 | if (!ShouldInclude(entry) || ShouldExclude(entry)) 155 | { 156 | continue; 157 | } 158 | 159 | _output.Add(createMethod(entry, isDirectory)); 160 | } 161 | 162 | return _output.ZipEntrySort(); 163 | } 164 | 165 | private static bool MatchAny( 166 | ZipArchiveEntry entry, 167 | WildcardPattern[] patterns) 168 | { 169 | foreach (WildcardPattern pattern in patterns) 170 | { 171 | if (pattern.IsMatch(entry.FullName)) 172 | { 173 | return true; 174 | } 175 | } 176 | 177 | return false; 178 | } 179 | 180 | private bool ShouldInclude(ZipArchiveEntry entry) 181 | { 182 | if (_includePatterns is null) 183 | { 184 | return true; 185 | } 186 | 187 | return MatchAny(entry, _includePatterns); 188 | } 189 | 190 | private bool ShouldExclude(ZipArchiveEntry entry) 191 | { 192 | if (_excludePatterns is null) 193 | { 194 | return false; 195 | } 196 | 197 | return MatchAny(entry, _excludePatterns); 198 | } 199 | 200 | private bool ShouldSkipEntry(bool isDirectory) => 201 | isDirectory && Type is ZipEntryType.Archive 202 | || !isDirectory && Type is ZipEntryType.Directory; 203 | } 204 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/GetZipEntryContentCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using System.Text; 5 | using PSCompression.Exceptions; 6 | 7 | namespace PSCompression.Commands; 8 | 9 | [Cmdlet(VerbsCommon.Get, "ZipEntryContent", DefaultParameterSetName = "Stream")] 10 | [OutputType(typeof(string), ParameterSetName = ["Stream"])] 11 | [OutputType(typeof(byte), ParameterSetName = ["Bytes"])] 12 | [Alias("gczip")] 13 | public sealed class GetZipEntryContentCommand : PSCmdlet, IDisposable 14 | { 15 | private readonly ZipArchiveCache _cache = new(); 16 | 17 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 18 | public ZipEntryFile[] ZipEntry { get; set; } = null!; 19 | 20 | [Parameter(ParameterSetName = "Stream")] 21 | [ArgumentCompleter(typeof(EncodingCompleter))] 22 | [EncodingTransformation] 23 | [ValidateNotNullOrEmpty] 24 | public Encoding Encoding { get; set; } = new UTF8Encoding(); 25 | 26 | [Parameter] 27 | public SwitchParameter Raw { get; set; } 28 | 29 | [Parameter(ParameterSetName = "Bytes")] 30 | public SwitchParameter AsByteStream { get; set; } 31 | 32 | [Parameter(ParameterSetName = "Bytes")] 33 | [ValidateNotNullOrEmpty] 34 | public int BufferSize { get; set; } = 128_000; 35 | 36 | protected override void ProcessRecord() 37 | { 38 | foreach (ZipEntryFile entry in ZipEntry) 39 | { 40 | try 41 | { 42 | ZipContentReader reader = new(GetOrAdd(entry)); 43 | ReadEntry(entry, reader); 44 | } 45 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 46 | { 47 | throw; 48 | } 49 | catch (Exception exception) 50 | { 51 | WriteError(exception.ToOpenError(entry.Source)); 52 | } 53 | } 54 | } 55 | 56 | private void ReadEntry(ZipEntryFile entry, ZipContentReader reader) 57 | { 58 | if (AsByteStream.IsPresent) 59 | { 60 | if (Raw.IsPresent) 61 | { 62 | WriteObject(reader.ReadAllBytes(entry)); 63 | return; 64 | } 65 | 66 | WriteObject( 67 | reader.StreamBytes(entry, BufferSize), 68 | enumerateCollection: true); 69 | return; 70 | } 71 | 72 | if (Raw.IsPresent) 73 | { 74 | WriteObject(reader.ReadToEnd(entry, Encoding)); 75 | return; 76 | } 77 | 78 | WriteObject( 79 | reader.StreamLines(entry, Encoding), 80 | enumerateCollection: true); 81 | } 82 | 83 | private ZipArchive GetOrAdd(ZipEntryFile entry) => _cache.GetOrAdd(entry); 84 | 85 | public void Dispose() 86 | { 87 | _cache?.Dispose(); 88 | GC.SuppressFinalize(this); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/NewZipEntryCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using System.Management.Automation; 7 | using System.Text; 8 | using PSCompression.Extensions; 9 | using PSCompression.Exceptions; 10 | 11 | namespace PSCompression.Commands; 12 | 13 | [Cmdlet(VerbsCommon.New, "ZipEntry", DefaultParameterSetName = "Value")] 14 | [OutputType(typeof(ZipEntryDirectory), typeof(ZipEntryFile))] 15 | public sealed class NewZipEntryCommand : PSCmdlet, IDisposable 16 | { 17 | private readonly List _entries = []; 18 | 19 | private ZipArchive? _zip; 20 | 21 | private ZipContentWriter[]? _writers; 22 | 23 | private string[]? _entryPath; 24 | 25 | [Parameter(ValueFromPipeline = true, ParameterSetName = "Value")] 26 | [ValidateNotNull] 27 | public string[]? Value { get; set; } 28 | 29 | [Parameter(Mandatory = true, Position = 0)] 30 | public string Destination { get; set; } = null!; 31 | 32 | [Parameter(ParameterSetName = "File", Position = 1)] 33 | [ValidateNotNullOrEmpty] 34 | public string? SourcePath { get; set; } 35 | 36 | [Parameter(ParameterSetName = "Value", Mandatory = true, Position = 2)] 37 | [Parameter(ParameterSetName = "File", Position = 2)] 38 | public string[]? EntryPath 39 | { 40 | get => _entryPath; 41 | set => _entryPath = [.. value.Select(e => e.NormalizePath())]; 42 | } 43 | 44 | [Parameter] 45 | public CompressionLevel CompressionLevel { get; set; } = CompressionLevel.Optimal; 46 | 47 | [Parameter(ParameterSetName = "Value")] 48 | [ArgumentCompleter(typeof(EncodingCompleter))] 49 | [EncodingTransformation] 50 | public Encoding Encoding { get; set; } = new UTF8Encoding(); 51 | 52 | [Parameter] 53 | public SwitchParameter Force { get; set; } 54 | 55 | protected override void BeginProcessing() 56 | { 57 | Destination = Destination.ResolvePath(this); 58 | if (!Destination.IsArchive()) 59 | { 60 | ThrowTerminatingError( 61 | ExceptionHelper.NotArchivePath( 62 | Destination, 63 | nameof(Destination))); 64 | } 65 | 66 | try 67 | { 68 | _zip = ZipFile.Open(Destination, ZipArchiveMode.Update); 69 | 70 | if (ParameterSetName == "Value") 71 | { 72 | // Mandatory Parameter on this ParameterSet 73 | Dbg.Assert(EntryPath is not null); 74 | // We can create the entries here and go the process block 75 | foreach (string entry in EntryPath) 76 | { 77 | if (_zip.TryGetEntry(entry, out ZipArchiveEntry? zipentry)) 78 | { 79 | if (!Force.IsPresent) 80 | { 81 | WriteError( 82 | DuplicatedEntryException 83 | .Create(entry, Destination) 84 | .ToDuplicatedEntryError()); 85 | 86 | continue; 87 | } 88 | 89 | zipentry.Delete(); 90 | } 91 | 92 | _entries.Add(_zip.CreateEntry(entry, CompressionLevel)); 93 | } 94 | 95 | return; 96 | } 97 | 98 | // else, we're on File ParameterSet, this can't be null 99 | Dbg.Assert(SourcePath is not null); 100 | // Create Entries from file here 101 | SourcePath = SourcePath.ResolvePath(this); 102 | 103 | if (!SourcePath.IsArchive()) 104 | { 105 | ThrowTerminatingError( 106 | ExceptionHelper.NotArchivePath( 107 | SourcePath, 108 | nameof(SourcePath))); 109 | } 110 | 111 | using FileStream fileStream = File.Open( 112 | path: SourcePath, 113 | mode: FileMode.Open, 114 | access: FileAccess.Read, 115 | share: FileShare.ReadWrite); 116 | 117 | EntryPath ??= [SourcePath.NormalizePath()]; 118 | foreach (string entry in EntryPath) 119 | { 120 | if (_zip.TryGetEntry(entry, out ZipArchiveEntry? zipentry)) 121 | { 122 | if (!Force.IsPresent) 123 | { 124 | WriteError( 125 | DuplicatedEntryException 126 | .Create(entry, Destination) 127 | .ToDuplicatedEntryError()); 128 | 129 | continue; 130 | } 131 | 132 | zipentry.Delete(); 133 | } 134 | 135 | _entries.Add(_zip.CreateEntryFromFile( 136 | entry: entry, 137 | fileStream: fileStream, 138 | compressionLevel: CompressionLevel)); 139 | } 140 | } 141 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 142 | { 143 | throw; 144 | } 145 | catch (Exception exception) 146 | { 147 | ThrowTerminatingError(exception.ToOpenError(Destination)); 148 | } 149 | } 150 | 151 | protected override void ProcessRecord() 152 | { 153 | Dbg.Assert(_zip is not null); 154 | 155 | // no input from pipeline, go to end block 156 | if (Value is null) 157 | { 158 | return; 159 | } 160 | 161 | try 162 | { 163 | _writers ??= [.. _entries 164 | .Where(e => !string.IsNullOrEmpty(e.Name)) 165 | .Select(e => new ZipContentWriter(_zip, e, Encoding))]; 166 | 167 | foreach (ZipContentWriter writer in _writers) 168 | { 169 | writer.WriteLines(Value); 170 | } 171 | } 172 | catch (Exception exception) 173 | { 174 | ThrowTerminatingError(exception.ToWriteError(_zip)); 175 | } 176 | } 177 | 178 | protected override void EndProcessing() 179 | { 180 | try 181 | { 182 | if (_writers is not null) 183 | { 184 | foreach (ZipContentWriter writer in _writers) 185 | { 186 | writer.Close(); 187 | } 188 | } 189 | 190 | _zip?.Dispose(); 191 | WriteObject(GetResult(), enumerateCollection: true); 192 | } 193 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 194 | { 195 | throw; 196 | } 197 | catch (Exception exception) 198 | { 199 | ThrowTerminatingError(exception.ToOpenError(Destination)); 200 | } 201 | } 202 | 203 | private IEnumerable GetResult() 204 | { 205 | using ZipArchive zip = ZipFile.OpenRead(Destination); 206 | List _result = new(_entries.Count); 207 | 208 | foreach (ZipArchiveEntry entry in _entries) 209 | { 210 | if (string.IsNullOrEmpty(entry.Name)) 211 | { 212 | _result.Add(new ZipEntryDirectory( 213 | zip.GetEntry(entry.FullName), 214 | Destination)); 215 | continue; 216 | } 217 | 218 | _result.Add(new ZipEntryFile( 219 | zip.GetEntry(entry.FullName), 220 | Destination)); 221 | } 222 | 223 | return _result.ZipEntrySort(); 224 | } 225 | 226 | public void Dispose() 227 | { 228 | if (_writers is not null) 229 | { 230 | foreach (ZipContentWriter writer in _writers) 231 | { 232 | writer?.Dispose(); 233 | } 234 | } 235 | 236 | _zip?.Dispose(); 237 | GC.SuppressFinalize(this); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/RemoveZipEntryCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using PSCompression.Exceptions; 5 | 6 | namespace PSCompression.Commands; 7 | 8 | [Cmdlet(VerbsCommon.Remove, "ZipEntry", SupportsShouldProcess = true)] 9 | [OutputType(typeof(void))] 10 | public sealed class RemoveZipEntryCommand : PSCmdlet, IDisposable 11 | { 12 | private readonly ZipArchiveCache _cache = new(ZipArchiveMode.Update); 13 | 14 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 15 | public ZipEntryBase[] InputObject { get; set; } = null!; 16 | 17 | protected override void ProcessRecord() 18 | { 19 | foreach (ZipEntryBase entry in InputObject) 20 | { 21 | try 22 | { 23 | if (ShouldProcess(target: entry.ToString(), action: "Remove")) 24 | { 25 | entry.Remove(_cache.GetOrAdd(entry)); 26 | } 27 | } 28 | catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) 29 | { 30 | throw; 31 | } 32 | catch (NotSupportedException exception) 33 | { 34 | ThrowTerminatingError(exception.ToStreamOpenError(entry)); 35 | } 36 | catch (Exception exception) 37 | { 38 | WriteError(exception.ToOpenError(entry.Source)); 39 | } 40 | } 41 | } 42 | 43 | public void Dispose() 44 | { 45 | _cache?.Dispose(); 46 | GC.SuppressFinalize(this); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/RenameZipEntryCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Compression; 4 | using System.Management.Automation; 5 | using PSCompression.Exceptions; 6 | using PSCompression.Extensions; 7 | 8 | namespace PSCompression.Commands; 9 | 10 | [Cmdlet(VerbsCommon.Rename, "ZipEntry", SupportsShouldProcess = true)] 11 | [OutputType(typeof(ZipEntryFile), typeof(ZipEntryDirectory))] 12 | public sealed class RenameZipEntryCommand : PSCmdlet, IDisposable 13 | { 14 | private readonly ZipArchiveCache _zipArchiveCache = new(ZipArchiveMode.Update); 15 | 16 | private ZipEntryCache? _zipEntryCache; 17 | 18 | private readonly ZipEntryMoveCache _moveCache = new(); 19 | 20 | [Parameter( 21 | Mandatory = true, 22 | Position = 0, 23 | ValueFromPipeline = true)] 24 | public ZipEntryBase ZipEntry { get; set; } = null!; 25 | 26 | [Parameter( 27 | Mandatory = true, 28 | Position = 1, 29 | ValueFromPipeline = true)] 30 | public string NewName { get; set; } = null!; 31 | 32 | [Parameter] 33 | public SwitchParameter PassThru { get; set; } 34 | 35 | protected override void BeginProcessing() 36 | { 37 | if (PassThru.IsPresent) 38 | { 39 | _zipEntryCache = new(); 40 | } 41 | } 42 | 43 | protected override void ProcessRecord() 44 | { 45 | if (!ShouldProcess(target: ZipEntry.ToString(), action: "Rename")) 46 | { 47 | return; 48 | } 49 | 50 | try 51 | { 52 | ZipEntry.ThrowIfFromStream(); 53 | NewName.ThrowIfInvalidNameChar(); 54 | _zipArchiveCache.TryAdd(ZipEntry); 55 | _moveCache.AddEntry(ZipEntry, NewName); 56 | } 57 | catch (NotSupportedException exception) 58 | { 59 | ThrowTerminatingError(exception.ToStreamOpenError(ZipEntry)); 60 | } 61 | catch (InvalidNameException exception) 62 | { 63 | WriteError(exception.ToInvalidNameError(NewName)); 64 | } 65 | catch (Exception exception) 66 | { 67 | WriteError(exception.ToOpenError(ZipEntry.Source)); 68 | } 69 | } 70 | 71 | protected override void EndProcessing() 72 | { 73 | foreach (var mapping in _moveCache.GetMappings(_zipArchiveCache)) 74 | { 75 | Rename(mapping); 76 | } 77 | 78 | _zipArchiveCache?.Dispose(); 79 | if (!PassThru.IsPresent || _zipEntryCache is null) 80 | { 81 | return; 82 | } 83 | 84 | WriteObject( 85 | _zipEntryCache 86 | .AddRange(_moveCache.GetPassThruMappings()) 87 | .GetEntries() 88 | .ZipEntrySort(), 89 | enumerateCollection: true); 90 | } 91 | 92 | private void Rename(KeyValuePair> mapping) 93 | { 94 | foreach ((string source, string destination) in mapping.Value) 95 | { 96 | try 97 | { 98 | ZipEntryBase.Move( 99 | sourceRelativePath: source, 100 | destination: destination, 101 | sourceZipPath: mapping.Key, 102 | _zipArchiveCache[mapping.Key]); 103 | } 104 | catch (DuplicatedEntryException exception) 105 | { 106 | if (_moveCache.IsDirectoryEntry(mapping.Key, source)) 107 | { 108 | ThrowTerminatingError(exception.ToDuplicatedEntryError()); 109 | } 110 | 111 | WriteError(exception.ToDuplicatedEntryError()); 112 | } 113 | catch (EntryNotFoundException exception) 114 | { 115 | WriteError(exception.ToEntryNotFoundError()); 116 | } 117 | catch (Exception exception) 118 | { 119 | WriteError(exception.ToWriteError(ZipEntry)); 120 | } 121 | } 122 | } 123 | 124 | public void Dispose() 125 | { 126 | _zipArchiveCache?.Dispose(); 127 | GC.SuppressFinalize(this); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/PSCompression/Commands/SetZipEntryContentCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | using System.Text; 4 | using PSCompression.Exceptions; 5 | 6 | namespace PSCompression.Commands; 7 | 8 | [Cmdlet(VerbsCommon.Set, "ZipEntryContent", DefaultParameterSetName = "StringValue")] 9 | [OutputType(typeof(ZipEntryFile))] 10 | public sealed class SetZipEntryContentCommand : PSCmdlet, IDisposable 11 | { 12 | private ZipContentWriter? _zipWriter; 13 | 14 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 15 | public object[] Value { get; set; } = null!; 16 | 17 | [Parameter(Mandatory = true, Position = 0)] 18 | public ZipEntryFile SourceEntry { get; set; } = null!; 19 | 20 | [Parameter(ParameterSetName = "StringValue")] 21 | [ArgumentCompleter(typeof(EncodingCompleter))] 22 | [EncodingTransformation] 23 | public Encoding Encoding { get; set; } = new UTF8Encoding(); 24 | 25 | [Parameter(ParameterSetName = "ByteStream")] 26 | public SwitchParameter AsByteStream { get; set; } 27 | 28 | [Parameter(ParameterSetName = "StringValue")] 29 | [Parameter(ParameterSetName = "ByteStream")] 30 | public SwitchParameter Append { get; set; } 31 | 32 | [Parameter(ParameterSetName = "ByteStream")] 33 | public int BufferSize { get; set; } = 128_000; 34 | 35 | [Parameter] 36 | public SwitchParameter PassThru { get; set; } 37 | 38 | protected override void BeginProcessing() 39 | { 40 | try 41 | { 42 | if (AsByteStream.IsPresent) 43 | { 44 | _zipWriter = new ZipContentWriter( 45 | entry: SourceEntry, 46 | append: Append.IsPresent, 47 | bufferSize: BufferSize); 48 | return; 49 | } 50 | 51 | _zipWriter = new ZipContentWriter( 52 | entry: SourceEntry, 53 | append: Append.IsPresent, 54 | encoding: Encoding); 55 | } 56 | catch (Exception exception) 57 | { 58 | ThrowTerminatingError(exception.ToStreamOpenError(SourceEntry)); 59 | } 60 | } 61 | 62 | protected override void ProcessRecord() 63 | { 64 | try 65 | { 66 | Dbg.Assert(_zipWriter is not null); 67 | 68 | if (AsByteStream.IsPresent) 69 | { 70 | _zipWriter.WriteBytes(LanguagePrimitives.ConvertTo(Value)); 71 | return; 72 | } 73 | 74 | _zipWriter.WriteLines( 75 | LanguagePrimitives.ConvertTo(Value)); 76 | } 77 | catch (Exception exception) 78 | { 79 | ThrowTerminatingError(exception.ToWriteError(SourceEntry)); 80 | } 81 | } 82 | 83 | protected override void EndProcessing() 84 | { 85 | Dbg.Assert(_zipWriter is not null); 86 | 87 | if (!PassThru.IsPresent) 88 | { 89 | return; 90 | } 91 | 92 | try 93 | { 94 | _zipWriter.Dispose(); 95 | SourceEntry.Refresh(); 96 | WriteObject(SourceEntry); 97 | } 98 | catch (Exception exception) 99 | { 100 | ThrowTerminatingError(exception.ToStreamOpenError(SourceEntry)); 101 | } 102 | } 103 | 104 | public void Dispose() 105 | { 106 | _zipWriter?.Dispose(); 107 | GC.SuppressFinalize(this); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/PSCompression/Dbg/Dbg.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace PSCompression; 5 | 6 | internal static class Dbg 7 | { 8 | [Conditional("DEBUG")] 9 | public static void Assert([DoesNotReturnIf(false)] bool condition) => 10 | Debug.Assert(condition); 11 | } 12 | -------------------------------------------------------------------------------- /src/PSCompression/Dbg/Nullable.cs: -------------------------------------------------------------------------------- 1 | #if !NETCOREAPP 2 | 3 | namespace System.Diagnostics.CodeAnalysis; 4 | 5 | /// Specifies that null is allowed as an input even if the corresponding type disallows it. 6 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] 7 | [ExcludeFromCodeCoverage] 8 | internal sealed class AllowNullAttribute : Attribute { } 9 | 10 | /// Specifies that null is disallowed as an input even if the corresponding type allows it. 11 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] 12 | [ExcludeFromCodeCoverage] 13 | internal sealed class DisallowNullAttribute : Attribute { } 14 | 15 | /// Specifies that an output may be null even if the corresponding type disallows it. 16 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] 17 | [ExcludeFromCodeCoverage] 18 | internal sealed class MaybeNullAttribute : Attribute { } 19 | 20 | /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns. 21 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] 22 | [ExcludeFromCodeCoverage] 23 | internal sealed class NotNullAttribute : Attribute { } 24 | 25 | /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. 26 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 27 | [ExcludeFromCodeCoverage] 28 | internal sealed class MaybeNullWhenAttribute : Attribute 29 | { 30 | /// Initializes the attribute with the specified return value condition. 31 | /// 32 | /// The return value condition. If the method returns this value, the associated parameter may be null. 33 | /// 34 | public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; 35 | 36 | /// Gets the return value condition. 37 | public bool ReturnValue { get; } 38 | } 39 | 40 | /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. 41 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 42 | [ExcludeFromCodeCoverage] 43 | internal sealed class NotNullWhenAttribute : Attribute 44 | { 45 | /// Initializes the attribute with the specified return value condition. 46 | /// 47 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 48 | /// 49 | public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; 50 | 51 | /// Gets the return value condition. 52 | public bool ReturnValue { get; } 53 | } 54 | 55 | /// Specifies that the output will be non-null if the named parameter is non-null. 56 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] 57 | [ExcludeFromCodeCoverage] 58 | internal sealed class NotNullIfNotNullAttribute : Attribute 59 | { 60 | /// Initializes the attribute with the associated parameter name. 61 | /// 62 | /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. 63 | /// 64 | public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; 65 | 66 | /// Gets the associated parameter name. 67 | public string ParameterName { get; } 68 | } 69 | 70 | /// Applied to a method that will never return under any circumstance. 71 | [AttributeUsage(AttributeTargets.Method, Inherited = false)] 72 | [ExcludeFromCodeCoverage] 73 | internal sealed class DoesNotReturnAttribute : Attribute { } 74 | 75 | /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. 76 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] 77 | [ExcludeFromCodeCoverage] 78 | internal sealed class DoesNotReturnIfAttribute : Attribute 79 | { 80 | /// Initializes the attribute with the specified parameter value. 81 | /// 82 | /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to 83 | /// the associated parameter matches this value. 84 | /// 85 | public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; 86 | 87 | /// Gets the condition parameter value. 88 | public bool ParameterValue { get; } 89 | } 90 | 91 | /// Specifies that the method or property will ensure that the listed field and property members have not-null values. 92 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] 93 | [ExcludeFromCodeCoverage] 94 | internal sealed class MemberNotNullAttribute : Attribute 95 | { 96 | /// Initializes the attribute with a field or property member. 97 | /// 98 | /// The field or property member that is promised to be not-null. 99 | /// 100 | public MemberNotNullAttribute(string member) => Members = new[] { member }; 101 | 102 | /// Initializes the attribute with the list of field and property members. 103 | /// 104 | /// The list of field and property members that are promised to be not-null. 105 | /// 106 | public MemberNotNullAttribute(params string[] members) => Members = members; 107 | 108 | /// Gets field or property member names. 109 | public string[] Members { get; } 110 | } 111 | 112 | /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition. 113 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)] 114 | [ExcludeFromCodeCoverage] 115 | internal sealed class MemberNotNullWhenAttribute : Attribute 116 | { 117 | /// Initializes the attribute with the specified return value condition and a field or property member. 118 | /// 119 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 120 | /// 121 | /// 122 | /// The field or property member that is promised to be not-null. 123 | /// 124 | public MemberNotNullWhenAttribute(bool returnValue, string member) 125 | { 126 | ReturnValue = returnValue; 127 | Members = new[] { member }; 128 | } 129 | 130 | /// Initializes the attribute with the specified return value condition and list of field and property members. 131 | /// 132 | /// The return value condition. If the method returns this value, the associated parameter will not be null. 133 | /// 134 | /// 135 | /// The list of field and property members that are promised to be not-null. 136 | /// 137 | public MemberNotNullWhenAttribute(bool returnValue, params string[] members) 138 | { 139 | ReturnValue = returnValue; 140 | Members = members; 141 | } 142 | 143 | /// Gets the return value condition. 144 | public bool ReturnValue { get; } 145 | 146 | /// Gets field or property member names. 147 | public string[] Members { get; } 148 | } 149 | 150 | #endif 151 | -------------------------------------------------------------------------------- /src/PSCompression/EncodingCompleter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Management.Automation; 4 | using System.Management.Automation.Language; 5 | using System; 6 | using System.Runtime.InteropServices; 7 | 8 | namespace PSCompression; 9 | 10 | public sealed class EncodingCompleter : IArgumentCompleter 11 | { 12 | private static readonly string[] s_encodingSet; 13 | 14 | static EncodingCompleter() 15 | { 16 | List set = new( 17 | [ 18 | "ascii", 19 | "bigendianUtf32", 20 | "unicode", 21 | "utf8", 22 | "utf8NoBOM", 23 | "bigendianUnicode", 24 | "oem", 25 | "utf8BOM", 26 | "utf32" 27 | ]); 28 | 29 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 30 | { 31 | set.Add("ansi"); 32 | } 33 | 34 | s_encodingSet = [.. set]; 35 | } 36 | 37 | public IEnumerable CompleteArgument( 38 | string commandName, 39 | string parameterName, 40 | string wordToComplete, 41 | CommandAst commandAst, 42 | IDictionary fakeBoundParameters) 43 | { 44 | foreach (string encoding in s_encodingSet) 45 | { 46 | if (encoding.StartsWith(wordToComplete, StringComparison.InvariantCultureIgnoreCase)) 47 | { 48 | yield return new CompletionResult(encoding); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/PSCompression/EncodingTransformation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | namespace PSCompression; 7 | 8 | public sealed class EncodingTransformation : ArgumentTransformationAttribute 9 | { 10 | public override object Transform( 11 | EngineIntrinsics engineIntrinsics, 12 | object inputData) 13 | { 14 | inputData = inputData is PSObject pso 15 | ? pso.BaseObject 16 | : inputData; 17 | 18 | return inputData switch 19 | { 20 | Encoding enc => enc, 21 | int num => Encoding.GetEncoding(num), 22 | string str => ParseStringEncoding(str), 23 | _ => throw new ArgumentTransformationMetadataException( 24 | $"Could not convert input '{inputData}' to a valid Encoding object."), 25 | }; 26 | } 27 | 28 | private Encoding ParseStringEncoding(string str) => 29 | str.ToLowerInvariant() switch 30 | { 31 | "ascii" => new ASCIIEncoding(), 32 | "bigendianunicode" => new UnicodeEncoding(true, true), 33 | "bigendianutf32" => new UTF32Encoding(true, true), 34 | "oem" => Console.OutputEncoding, 35 | "unicode" => new UnicodeEncoding(), 36 | "utf8" => new UTF8Encoding(false), 37 | "utf8bom" => new UTF8Encoding(true), 38 | "utf8nobom" => new UTF8Encoding(false), 39 | "utf32" => new UTF32Encoding(), 40 | "ansi" => Encoding.GetEncoding(GetACP()), 41 | _ => Encoding.GetEncoding(str), 42 | }; 43 | 44 | [DllImport("Kernel32.dll")] 45 | private static extern int GetACP(); 46 | } 47 | -------------------------------------------------------------------------------- /src/PSCompression/Exceptions/DuplicatedEntryException.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PSCompression.Exceptions; 4 | 5 | public sealed class DuplicatedEntryException : IOException 6 | { 7 | internal string _path; 8 | 9 | private DuplicatedEntryException(string message, string path) 10 | : base(message: message) 11 | { 12 | _path = path; 13 | } 14 | 15 | internal static DuplicatedEntryException Create(string path, string source) => 16 | new($"An entry with path '{path}' already exists in '{source}'.", path); 17 | } 18 | -------------------------------------------------------------------------------- /src/PSCompression/Exceptions/EntryNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PSCompression.Exceptions; 4 | 5 | public sealed class EntryNotFoundException : IOException 6 | { 7 | internal string _path; 8 | 9 | private EntryNotFoundException(string message, string path) 10 | : base(message: message) 11 | { 12 | _path = path; 13 | } 14 | 15 | internal static EntryNotFoundException Create(string path, string source) => 16 | new($"Cannot find an entry with path: '{path}' in '{source}'.", path); 17 | } 18 | -------------------------------------------------------------------------------- /src/PSCompression/Exceptions/ExceptionHelpers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Management.Automation; 6 | using PSCompression.Extensions; 7 | 8 | namespace PSCompression.Exceptions; 9 | 10 | internal static class ExceptionHelper 11 | { 12 | private static readonly char[] s_InvalidFileNameChar = Path.GetInvalidFileNameChars(); 13 | 14 | private static readonly char[] s_InvalidPathChar = Path.GetInvalidPathChars(); 15 | 16 | internal static ErrorRecord NotArchivePath(string path, string paramname) => 17 | new( 18 | new ArgumentException( 19 | $"The specified path '{path}' does not exist or is a Directory.", 20 | paramname), 21 | "NotArchivePath", ErrorCategory.InvalidArgument, path); 22 | 23 | internal static ErrorRecord NotDirectoryPath(string path, string paramname) => 24 | new( 25 | new ArgumentException( 26 | $"Destination path is an existing File: '{path}'.", paramname), 27 | "NotDirectoryPath", ErrorCategory.InvalidArgument, path); 28 | 29 | internal static ErrorRecord ToInvalidProviderError(this ProviderInfo provider, string path) => 30 | new( 31 | new NotSupportedException( 32 | $"The resolved path '{path}' is not a FileSystem path but '{provider.Name}'."), 33 | "NotFileSystemPath", ErrorCategory.InvalidArgument, path); 34 | 35 | internal static ErrorRecord ToOpenError(this Exception exception, string path) => 36 | new(exception, "ZipOpen", ErrorCategory.OpenError, path); 37 | 38 | internal static ErrorRecord ToResolvePathError(this Exception exception, string path) => 39 | new(exception, "ResolvePath", ErrorCategory.NotSpecified, path); 40 | 41 | internal static ErrorRecord ToExtractEntryError(this Exception exception, ZipEntryBase entry) => 42 | new(exception, "ExtractEntry", ErrorCategory.NotSpecified, entry); 43 | 44 | internal static ErrorRecord ToStreamOpenError(this Exception exception, ZipEntryBase entry) => 45 | new(exception, "StreamOpen", ErrorCategory.NotSpecified, entry); 46 | 47 | internal static ErrorRecord ToStreamOpenError(this Exception exception, string path) => 48 | new(exception, "StreamOpen", ErrorCategory.NotSpecified, path); 49 | 50 | internal static ErrorRecord ToWriteError(this Exception exception, object? item) => 51 | new(exception, "WriteError", ErrorCategory.WriteError, item); 52 | 53 | internal static ErrorRecord ToDuplicatedEntryError(this DuplicatedEntryException exception) => 54 | new(exception, "DuplicatedEntry", ErrorCategory.WriteError, exception._path); 55 | 56 | internal static ErrorRecord ToInvalidNameError(this InvalidNameException exception, string name) => 57 | new(exception, "InvalidName", ErrorCategory.InvalidArgument, name); 58 | 59 | internal static ErrorRecord ToEntryNotFoundError(this EntryNotFoundException exception) => 60 | new(exception, "EntryNotFound", ErrorCategory.ObjectNotFound, exception._path); 61 | 62 | internal static ErrorRecord ToEnumerationError(this Exception exception, object item) => 63 | new(exception, "EnumerationError", ErrorCategory.ReadError, item); 64 | 65 | internal static ErrorRecord ToInvalidZipArchive(this InvalidDataException exception) => 66 | new( 67 | new InvalidDataException( 68 | "Specified path or stream is not a valid zip archive, " + 69 | "might be compressed using an unsupported method, " + 70 | "or could be corrupted.", 71 | exception), 72 | "InvalidZipArchive", 73 | ErrorCategory.InvalidData, 74 | null); 75 | 76 | 77 | internal static void ThrowIfNotFound( 78 | this ZipArchive zip, 79 | string path, 80 | string source, 81 | [NotNull] out ZipArchiveEntry? entry) 82 | { 83 | if (!zip.TryGetEntry(path, out entry)) 84 | { 85 | throw EntryNotFoundException.Create(path, source); 86 | } 87 | } 88 | 89 | internal static void ThrowIfDuplicate( 90 | this ZipArchive zip, 91 | string path, 92 | string source) 93 | { 94 | if (zip.TryGetEntry(path, out ZipArchiveEntry? _)) 95 | { 96 | throw DuplicatedEntryException.Create(path, source); 97 | } 98 | } 99 | 100 | internal static void ThrowIfInvalidNameChar(this string newname) 101 | { 102 | if (newname.IndexOfAny(s_InvalidFileNameChar) != -1) 103 | { 104 | throw InvalidNameException.Create(newname); 105 | } 106 | } 107 | 108 | internal static void ThrowIfInvalidPathChar(this string path) 109 | { 110 | if (path.IndexOfAny(s_InvalidPathChar) != -1) 111 | { 112 | throw new ArgumentException( 113 | $"Path: '{path}' contains invalid path characters."); 114 | } 115 | } 116 | 117 | internal static void ThrowIfFromStream(this ZipEntryBase entry) 118 | { 119 | if (entry.FromStream) 120 | { 121 | throw new NotSupportedException( 122 | "The operation is not supported for entries created from input Stream."); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/PSCompression/Exceptions/InvalidNameException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PSCompression.Exceptions; 4 | 5 | public sealed class InvalidNameException : ArgumentException 6 | { 7 | internal string _name; 8 | 9 | private InvalidNameException(string message, string name) 10 | : base(message: message) 11 | { 12 | _name = name; 13 | } 14 | 15 | internal static InvalidNameException Create(string name) => 16 | new("Cannot rename the specified target, because it represents a path, " 17 | + "device name or contains invalid File Name characters.", name); 18 | } 19 | -------------------------------------------------------------------------------- /src/PSCompression/Extensions/DictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PSCompression.Extensions; 4 | 5 | internal static class DictionaryExtensions 6 | { 7 | internal static void Deconstruct( 8 | this KeyValuePair keyv, 9 | out TKey key, 10 | out TValue value) 11 | { 12 | key = keyv.Key; 13 | value = keyv.Value; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/PSCompression/Extensions/PathExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Management.Automation; 4 | using System.Text.RegularExpressions; 5 | using Microsoft.PowerShell.Commands; 6 | using PSCompression.Exceptions; 7 | 8 | namespace PSCompression.Extensions; 9 | 10 | public static class PathExtensions 11 | { 12 | private static readonly Regex s_reNormalize = new( 13 | @"(?:^[a-z]:)?[\\/]+|(? File.Exists(path); 56 | 57 | internal static string GetParent(this string path) => Path.GetDirectoryName(path); 58 | 59 | internal static string AddExtensionIfMissing(this string path, string extension) 60 | { 61 | if (!path.EndsWith(extension, StringComparison.InvariantCultureIgnoreCase)) 62 | { 63 | path += extension; 64 | } 65 | 66 | return path; 67 | } 68 | 69 | internal static string NormalizeEntryPath(this string path) => 70 | s_reNormalize.Replace(path, _directorySeparator).TrimStart('/'); 71 | 72 | internal static string NormalizeFileEntryPath(this string path) => 73 | NormalizeEntryPath(path).TrimEnd('/'); 74 | 75 | internal static bool IsDirectoryPath(this string path) => 76 | s_reEntryDir.IsMatch(path); 77 | 78 | public static string NormalizePath(this string path) => 79 | s_reEntryDir.IsMatch(path) 80 | ? NormalizeEntryPath(path) 81 | : NormalizeFileEntryPath(path); 82 | } 83 | -------------------------------------------------------------------------------- /src/PSCompression/Extensions/ZipEntryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace PSCompression.Extensions; 7 | 8 | internal static class ZipEntryExtensions 9 | { 10 | private static readonly Regex s_reGetDirName = new( 11 | @"[^/]+(?=/$)", 12 | RegexOptions.Compiled | RegexOptions.RightToLeft); 13 | 14 | private const string _directorySeparator = "/"; 15 | 16 | internal static string RelativeTo(this DirectoryInfo directory, int length) => 17 | (directory.FullName.Substring(length) + _directorySeparator).NormalizeEntryPath(); 18 | 19 | internal static string RelativeTo(this FileInfo directory, int length) => 20 | directory.FullName.Substring(length).NormalizeFileEntryPath(); 21 | 22 | internal static ZipArchiveEntry CreateEntryFromFile( 23 | this ZipArchive zip, 24 | string entry, 25 | FileStream fileStream, 26 | CompressionLevel compressionLevel) 27 | { 28 | if (entry.IsDirectoryPath()) 29 | { 30 | return zip.CreateEntry(entry); 31 | } 32 | 33 | fileStream.Seek(0, SeekOrigin.Begin); 34 | ZipArchiveEntry newentry = zip.CreateEntry(entry, compressionLevel); 35 | 36 | using (Stream stream = newentry.Open()) 37 | { 38 | fileStream.CopyTo(stream); 39 | } 40 | 41 | return newentry; 42 | } 43 | 44 | internal static bool TryGetEntry( 45 | this ZipArchive zip, 46 | string path, 47 | [NotNullWhen(true)] out ZipArchiveEntry? entry) => 48 | (entry = zip.GetEntry(path)) is not null; 49 | 50 | internal static (string, bool) ExtractTo( 51 | this ZipEntryBase entryBase, 52 | ZipArchive zip, 53 | string destination, 54 | bool overwrite) 55 | { 56 | destination = Path.GetFullPath( 57 | Path.Combine(destination, entryBase.RelativePath)); 58 | 59 | if (entryBase.Type is ZipEntryType.Directory) 60 | { 61 | Directory.CreateDirectory(destination); 62 | return (destination, false); 63 | } 64 | 65 | string parent = Path.GetDirectoryName(destination); 66 | 67 | if (!Directory.Exists(parent)) 68 | { 69 | Directory.CreateDirectory(parent); 70 | } 71 | 72 | ZipArchiveEntry entry = zip.GetEntry(entryBase.RelativePath); 73 | entry.ExtractToFile(destination, overwrite); 74 | return (destination, true); 75 | } 76 | 77 | internal static string ChangeName( 78 | this ZipEntryFile file, 79 | string newname) 80 | { 81 | string normalized = file.RelativePath.NormalizePath(); 82 | 83 | if (normalized.IndexOf(_directorySeparator) == -1) 84 | { 85 | return newname; 86 | } 87 | 88 | return string.Join( 89 | _directorySeparator, 90 | normalized.Substring(0, normalized.Length - file.Name.Length - 1), 91 | newname); 92 | } 93 | 94 | internal static string ChangeName( 95 | this ZipEntryDirectory directory, 96 | string newname) => 97 | s_reGetDirName.Replace( 98 | directory.RelativePath.NormalizePath(), 99 | newname); 100 | 101 | internal static string ChangePath( 102 | this ZipArchiveEntry entry, 103 | string oldPath, 104 | string newPath) => 105 | string.Concat(newPath, entry.FullName.Remove(0, oldPath.Length)); 106 | 107 | internal static string GetDirectoryName(this ZipArchiveEntry entry) => 108 | s_reGetDirName.Match(entry.FullName).Value; 109 | } 110 | -------------------------------------------------------------------------------- /src/PSCompression/GzipReaderOps.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Management.Automation; 4 | using System.Runtime.Serialization; 5 | using System.Text; 6 | 7 | namespace PSCompression; 8 | 9 | internal static class GzipReaderOps 10 | { 11 | private static readonly byte[] gzipPreamble = [0x1f, 0x8b, 0x08]; 12 | 13 | internal static void CopyTo( 14 | string path, 15 | bool isCoreCLR, 16 | FileStream destination) 17 | { 18 | if (isCoreCLR) 19 | { 20 | using FileStream fs = File.OpenRead(path); 21 | using GZipStream gzip = new(fs, CompressionMode.Decompress); 22 | gzip.CopyTo(destination); 23 | return; 24 | } 25 | 26 | using MemoryStream mem = GetFrameworkStream(path); 27 | mem.CopyTo(destination); 28 | } 29 | 30 | internal static void GetContent( 31 | string path, 32 | bool isCoreCLR, 33 | bool raw, 34 | Encoding encoding, 35 | PSCmdlet cmdlet) 36 | { 37 | if (isCoreCLR) 38 | { 39 | using FileStream fs = File.OpenRead(path); 40 | using GZipStream gzip = new(fs, CompressionMode.Decompress); 41 | 42 | if (raw) 43 | { 44 | ReadToEnd(gzip, encoding, cmdlet); 45 | return; 46 | } 47 | 48 | ReadLines(gzip, encoding, cmdlet); 49 | return; 50 | } 51 | 52 | using MemoryStream stream = GetFrameworkStream(path); 53 | 54 | if (raw) 55 | { 56 | ReadToEnd(stream, encoding, cmdlet); 57 | return; 58 | } 59 | 60 | ReadLines(stream, encoding, cmdlet); 61 | } 62 | 63 | private static void ReadLines( 64 | Stream stream, 65 | Encoding encoding, 66 | PSCmdlet cmdlet) 67 | { 68 | using StreamReader reader = new(stream, encoding); 69 | 70 | while (!reader.EndOfStream) 71 | { 72 | cmdlet.WriteObject(reader.ReadLine()); 73 | } 74 | } 75 | 76 | private static void ReadToEnd( 77 | Stream stream, 78 | Encoding encoding, 79 | PSCmdlet cmdlet) 80 | { 81 | using StreamReader reader = new(stream, encoding); 82 | cmdlet.WriteObject(reader.ReadToEnd()); 83 | } 84 | 85 | // this stuff is to make this work reading appended gzip streams in .net framework 86 | // i hate it :( 87 | private static MemoryStream GetFrameworkStream(string path) 88 | { 89 | int marker = 0; 90 | int b; 91 | using FileStream fs = File.OpenRead(path); 92 | 93 | byte[] preamble = new byte[3]; 94 | fs.Read(preamble, 0, 3); 95 | 96 | for (int i = 0; i < 3; i++) 97 | { 98 | if(preamble[i] != gzipPreamble[i]) 99 | { 100 | throw new InvalidDataContractException( 101 | "The archive entry was compressed using an unsupported compression method."); 102 | } 103 | } 104 | 105 | fs.Seek(0, SeekOrigin.Begin); 106 | 107 | MemoryStream outmem = new(); 108 | 109 | while ((b = fs.ReadByte()) != -1) 110 | { 111 | if (marker == 0 && (byte)b == gzipPreamble[marker]) 112 | { 113 | marker++; 114 | continue; 115 | } 116 | 117 | if (marker == 1) 118 | { 119 | if ((byte)b == gzipPreamble[marker]) 120 | { 121 | marker++; 122 | continue; 123 | } 124 | 125 | marker = 0; 126 | } 127 | 128 | if (marker == 2) 129 | { 130 | if ((byte)b == gzipPreamble[marker]) 131 | { 132 | CopyTo(path, outmem, fs.Position - 3); 133 | } 134 | 135 | marker = 0; 136 | } 137 | } 138 | 139 | outmem.Seek(0, SeekOrigin.Begin); 140 | return outmem; 141 | } 142 | 143 | private static void CopyTo(string path, MemoryStream outmem, long pos) 144 | { 145 | using FileStream substream = File.OpenRead(path); 146 | substream.Seek(pos, SeekOrigin.Begin); 147 | using GZipStream gzip = new(substream, CompressionMode.Decompress); 148 | gzip.CopyTo(outmem); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/PSCompression/PSCompression.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | enable 6 | true 7 | PSCompression 8 | latest 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | true 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/PSCompression/PSCompression.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.001.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSCompression", "PSCompression.csproj", "{04592EEF-913E-46C1-A7C2-20143FA08138}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {04592EEF-913E-46C1-A7C2-20143FA08138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {04592EEF-913E-46C1-A7C2-20143FA08138}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {04592EEF-913E-46C1-A7C2-20143FA08138}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {04592EEF-913E-46C1-A7C2-20143FA08138}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {761C4EBF-BC3E-404E-974F-5DC394531F3C} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/PSCompression/PSVersionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | using System.Reflection; 4 | 5 | namespace PSCompression; 6 | 7 | internal static class PSVersionHelper 8 | { 9 | private static bool? _isCore; 10 | 11 | internal static bool IsCoreCLR => _isCore ??= IsCore(); 12 | 13 | private static bool IsCore() 14 | { 15 | PropertyInfo property = typeof(PowerShell) 16 | .Assembly.GetType("System.Management.Automation.PSVersionInfo") 17 | .GetProperty( 18 | "PSVersion", 19 | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); 20 | 21 | return (Version)property.GetValue(property) is not { Major: 5, Minor: 1 }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/PSCompression/Records.cs: -------------------------------------------------------------------------------- 1 | namespace PSCompression; 2 | 3 | internal record struct EntryWithPath(ZipEntryBase ZipEntry, string Path); 4 | 5 | internal record struct PathWithType(string Path, ZipEntryType EntryType); 6 | -------------------------------------------------------------------------------- /src/PSCompression/SortingOps.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using PSCompression.Extensions; 5 | 6 | namespace PSCompression; 7 | 8 | internal static class SortingOps 9 | { 10 | private static string SortByParent(ZipEntryBase entry) => 11 | Path.GetDirectoryName(entry.RelativePath) 12 | .NormalizeEntryPath(); 13 | 14 | private static int SortByLength(ZipEntryBase entry) => 15 | entry.RelativePath.Count(e => e == '/'); 16 | 17 | private static string SortByName(ZipEntryBase entry) => 18 | entry.Name; 19 | 20 | private static ZipEntryType SortByType(ZipEntryBase entry) => 21 | entry.Type; 22 | 23 | internal static IEnumerable ZipEntrySort( 24 | this IEnumerable zip) => zip 25 | .OrderBy(SortByParent) 26 | .ThenBy(SortByType) 27 | .ThenBy(SortByLength) 28 | .ThenBy(SortByName); 29 | } 30 | -------------------------------------------------------------------------------- /src/PSCompression/ZipArchiveCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Compression; 4 | 5 | namespace PSCompression; 6 | 7 | internal sealed class ZipArchiveCache : IDisposable 8 | { 9 | private readonly Dictionary _cache; 10 | 11 | private readonly ZipArchiveMode _mode = ZipArchiveMode.Read; 12 | 13 | internal ZipArchiveCache() => _cache = []; 14 | 15 | internal ZipArchiveCache(ZipArchiveMode mode) 16 | { 17 | _cache = []; 18 | _mode = mode; 19 | } 20 | 21 | internal ZipArchive this[string source] => _cache[source]; 22 | 23 | internal void TryAdd(ZipEntryBase entry) 24 | { 25 | if (!_cache.ContainsKey(entry.Source)) 26 | { 27 | _cache[entry.Source] = entry.OpenZip(_mode); 28 | } 29 | } 30 | 31 | internal ZipArchive GetOrAdd(ZipEntryBase entry) 32 | { 33 | if (!_cache.ContainsKey(entry.Source)) 34 | { 35 | _cache[entry.Source] = entry.OpenZip(_mode); 36 | } 37 | 38 | return _cache[entry.Source]; 39 | } 40 | 41 | public void Dispose() 42 | { 43 | foreach (ZipArchive zip in _cache.Values) 44 | { 45 | zip?.Dispose(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/PSCompression/ZipContentOpsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Compression; 3 | 4 | namespace PSCompression; 5 | 6 | internal abstract class ZipContentOpsBase(ZipArchive zip) : IDisposable 7 | { 8 | protected ZipArchive _zip = zip; 9 | 10 | protected byte[]? _buffer; 11 | 12 | public bool Disposed { get; internal set; } 13 | 14 | protected virtual void Dispose(bool disposing) 15 | { 16 | if (disposing && !Disposed) 17 | { 18 | _zip?.Dispose(); 19 | Disposed = true; 20 | } 21 | } 22 | 23 | public void Dispose() 24 | { 25 | Dispose(disposing: true); 26 | GC.SuppressFinalize(this); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/PSCompression/ZipContentReader.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Text; 5 | 6 | namespace PSCompression; 7 | 8 | internal sealed class ZipContentReader : ZipContentOpsBase 9 | { 10 | internal ZipContentReader(ZipArchive zip) : base(zip) { } 11 | 12 | internal IEnumerable StreamBytes(ZipEntryFile entry, int bufferSize) 13 | { 14 | using Stream entryStream = entry.Open(_zip); 15 | _buffer ??= new byte[bufferSize]; 16 | int bytes; 17 | 18 | while ((bytes = entryStream.Read(_buffer, 0, bufferSize)) > 0) 19 | { 20 | for (int i = 0; i < bytes; i++) 21 | { 22 | yield return _buffer[i]; 23 | } 24 | } 25 | } 26 | 27 | internal byte[] ReadAllBytes(ZipEntryFile entry) 28 | { 29 | using Stream entryStream = entry.Open(_zip); 30 | using MemoryStream mem = new(); 31 | 32 | entryStream.CopyTo(mem); 33 | return mem.ToArray(); 34 | } 35 | 36 | internal IEnumerable StreamLines(ZipEntryFile entry, Encoding encoding) 37 | { 38 | using Stream entryStream = entry.Open(_zip); 39 | using StreamReader reader = new(entryStream, encoding); 40 | 41 | while (!reader.EndOfStream) 42 | { 43 | yield return reader.ReadLine(); 44 | } 45 | } 46 | 47 | internal string ReadToEnd(ZipEntryFile entry, Encoding encoding) 48 | { 49 | using Stream entryStream = entry.Open(_zip); 50 | using StreamReader reader = new(entryStream, encoding); 51 | return reader.ReadToEnd(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/PSCompression/ZipContentWriter.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using System.Text; 4 | 5 | namespace PSCompression; 6 | 7 | internal sealed class ZipContentWriter : ZipContentOpsBase 8 | { 9 | private int _index; 10 | 11 | private readonly StreamWriter? _writer; 12 | 13 | private readonly Stream _stream; 14 | 15 | private bool _disposed; 16 | 17 | internal ZipContentWriter(ZipEntryFile entry, bool append, int bufferSize) 18 | : base(entry.OpenWrite()) 19 | { 20 | _stream = entry.Open(_zip); 21 | _buffer = new byte[bufferSize]; 22 | 23 | if (append) 24 | { 25 | _stream.Seek(0, SeekOrigin.End); 26 | return; 27 | } 28 | 29 | _stream.SetLength(0); 30 | } 31 | 32 | internal ZipContentWriter(ZipArchive zip, ZipArchiveEntry entry, Encoding encoding) 33 | : base(zip) 34 | { 35 | _stream = entry.Open(); 36 | _writer = new StreamWriter(_stream, encoding); 37 | } 38 | 39 | internal ZipContentWriter(ZipEntryFile entry, bool append, Encoding encoding) 40 | : base(entry.OpenWrite()) 41 | { 42 | _stream = entry.Open(_zip); 43 | _writer = new StreamWriter(_stream, encoding); 44 | 45 | if (append) 46 | { 47 | _writer.BaseStream.Seek(0, SeekOrigin.End); 48 | return; 49 | } 50 | 51 | _writer.BaseStream.SetLength(0); 52 | } 53 | 54 | internal void WriteLines(string[] lines) 55 | { 56 | if (_writer is null) 57 | { 58 | return; 59 | } 60 | 61 | foreach (string line in lines) 62 | { 63 | _writer.WriteLine(line); 64 | } 65 | } 66 | 67 | internal void WriteBytes(byte[] bytes) 68 | { 69 | if (_buffer is null) 70 | { 71 | return; 72 | } 73 | 74 | foreach (byte b in bytes) 75 | { 76 | if (_index == _buffer.Length) 77 | { 78 | _stream.Write(_buffer, 0, _index); 79 | _index = 0; 80 | } 81 | 82 | _buffer[_index++] = b; 83 | } 84 | } 85 | 86 | public void Flush() 87 | { 88 | if (_index > 0 && _buffer is not null) 89 | { 90 | _stream.Write(_buffer, 0, _index); 91 | _index = 0; 92 | _stream.Flush(); 93 | } 94 | 95 | if (_writer is { BaseStream.CanWrite: true }) 96 | { 97 | _writer.Flush(); 98 | } 99 | } 100 | 101 | public void Close() 102 | { 103 | if (_writer is not null) 104 | { 105 | _writer.Close(); 106 | return; 107 | } 108 | 109 | _stream.Close(); 110 | } 111 | 112 | protected override void Dispose(bool disposing) 113 | { 114 | try 115 | { 116 | if (disposing && !_disposed) 117 | { 118 | Flush(); 119 | } 120 | } 121 | finally 122 | { 123 | _writer?.Dispose(); 124 | _stream.Dispose(); 125 | _disposed = true; 126 | base.Dispose(disposing); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using PSCompression.Extensions; 5 | using PSCompression.Exceptions; 6 | 7 | namespace PSCompression; 8 | 9 | public abstract class ZipEntryBase(ZipArchiveEntry entry, string source) 10 | { 11 | protected string? _formatDirectoryPath; 12 | 13 | protected Stream? _stream; 14 | 15 | internal bool FromStream { get => _stream is not null; } 16 | 17 | internal abstract string FormatDirectoryPath { get; } 18 | 19 | public string Source { get; } = source; 20 | 21 | public string Name { get; protected set; } = entry.Name; 22 | 23 | public string RelativePath { get; } = entry.FullName; 24 | 25 | public DateTime LastWriteTime { get; } = entry.LastWriteTime.LocalDateTime; 26 | 27 | public long Length { get; internal set; } = entry.Length; 28 | 29 | public long CompressedLength { get; internal set; } = entry.CompressedLength; 30 | 31 | public abstract ZipEntryType Type { get; } 32 | 33 | protected ZipEntryBase(ZipArchiveEntry entry, Stream? stream) 34 | : this(entry, $"InputStream.{Guid.NewGuid()}") 35 | { 36 | _stream = stream; 37 | } 38 | 39 | public ZipArchive OpenRead() => _stream is null 40 | ? ZipFile.OpenRead(Source) 41 | : new ZipArchive(_stream); 42 | 43 | public ZipArchive OpenWrite() 44 | { 45 | this.ThrowIfFromStream(); 46 | return ZipFile.Open(Source, ZipArchiveMode.Update); 47 | } 48 | 49 | public void Remove() 50 | { 51 | this.ThrowIfFromStream(); 52 | 53 | using ZipArchive zip = ZipFile.Open( 54 | Source, 55 | ZipArchiveMode.Update); 56 | 57 | zip.ThrowIfNotFound( 58 | path: RelativePath, 59 | source: Source, 60 | out ZipArchiveEntry entry); 61 | 62 | entry.Delete(); 63 | } 64 | 65 | internal void Remove(ZipArchive zip) 66 | { 67 | this.ThrowIfFromStream(); 68 | 69 | zip.ThrowIfNotFound( 70 | path: RelativePath, 71 | source: Source, 72 | out ZipArchiveEntry entry); 73 | 74 | entry.Delete(); 75 | } 76 | 77 | internal static string Move( 78 | string sourceRelativePath, 79 | string destination, 80 | string sourceZipPath, 81 | ZipArchive zip) 82 | { 83 | zip.ThrowIfNotFound( 84 | path: sourceRelativePath, 85 | source: sourceZipPath, 86 | entry: out ZipArchiveEntry sourceEntry); 87 | 88 | zip.ThrowIfDuplicate( 89 | path: destination, 90 | source: sourceZipPath); 91 | 92 | destination.ThrowIfInvalidPathChar(); 93 | 94 | ZipArchiveEntry destinationEntry = zip.CreateEntry(destination); 95 | using (Stream sourceStream = sourceEntry.Open()) 96 | using (Stream destinationStream = destinationEntry.Open()) 97 | { 98 | sourceStream.CopyTo(destinationStream); 99 | } 100 | sourceEntry.Delete(); 101 | 102 | return destination; 103 | } 104 | 105 | internal ZipArchive OpenZip(ZipArchiveMode mode) => 106 | _stream is null 107 | ? ZipFile.Open(Source, mode) 108 | : new ZipArchive(_stream, mode, true); 109 | 110 | public FileSystemInfo ExtractTo(string destination, bool overwrite) 111 | { 112 | using ZipArchive zip = _stream is null 113 | ? ZipFile.OpenRead(Source) 114 | : new ZipArchive(_stream); 115 | 116 | (string path, bool isArchive) = this.ExtractTo(zip, destination, overwrite); 117 | 118 | if (isArchive) 119 | { 120 | return new FileInfo(path); 121 | } 122 | 123 | return new DirectoryInfo(path); 124 | } 125 | 126 | public override string ToString() => RelativePath; 127 | } 128 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Compression; 4 | 5 | namespace PSCompression; 6 | 7 | public sealed class ZipEntryCache 8 | { 9 | private readonly Dictionary> _cache; 10 | 11 | internal ZipEntryCache() => _cache = new(StringComparer.InvariantCultureIgnoreCase); 12 | 13 | internal List WithSource(string source) 14 | { 15 | if (!_cache.ContainsKey(source)) 16 | { 17 | _cache[source] = []; 18 | } 19 | 20 | return _cache[source]; 21 | } 22 | 23 | internal void Add(string source, PathWithType pathWithType) => 24 | WithSource(source).Add(pathWithType); 25 | 26 | internal ZipEntryCache AddRange(IEnumerable<(string, PathWithType)> values) 27 | { 28 | foreach ((string source, PathWithType pathWithType) in values) 29 | { 30 | Add(source, pathWithType); 31 | } 32 | 33 | return this; 34 | } 35 | 36 | internal IEnumerable GetEntries() 37 | { 38 | foreach (var entry in _cache) 39 | { 40 | using ZipArchive zip = ZipFile.OpenRead(entry.Key); 41 | foreach ((string path, ZipEntryType type) in entry.Value) 42 | { 43 | if (type is ZipEntryType.Archive) 44 | { 45 | yield return new ZipEntryFile(zip.GetEntry(path), entry.Key); 46 | continue; 47 | } 48 | 49 | yield return new ZipEntryDirectory(zip.GetEntry(path), entry.Key); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryDirectory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Compression; 5 | using System.Linq; 6 | using PSCompression.Extensions; 7 | 8 | namespace PSCompression; 9 | 10 | public sealed class ZipEntryDirectory : ZipEntryBase 11 | { 12 | private const StringComparison _comparer = StringComparison.InvariantCultureIgnoreCase; 13 | 14 | internal override string FormatDirectoryPath 15 | { 16 | get => _formatDirectoryPath ??= $"/{RelativePath.NormalizeEntryPath()}"; 17 | } 18 | 19 | public override ZipEntryType Type => ZipEntryType.Directory; 20 | 21 | internal ZipEntryDirectory(ZipArchiveEntry entry, string source) 22 | : base(entry, source) 23 | { 24 | Name = entry.GetDirectoryName(); 25 | } 26 | 27 | internal ZipEntryDirectory(ZipArchiveEntry entry, Stream? stream) 28 | : base(entry, stream) 29 | { } 30 | 31 | internal IEnumerable GetChilds(ZipArchive zip) => 32 | zip.Entries.Where(e => 33 | !string.Equals(e.FullName, RelativePath, _comparer) 34 | && e.FullName.StartsWith(RelativePath, _comparer)); 35 | } 36 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.IO.Compression; 3 | using PSCompression.Exceptions; 4 | using PSCompression.Extensions; 5 | 6 | namespace PSCompression; 7 | 8 | public sealed class ZipEntryFile : ZipEntryBase 9 | { 10 | internal override string FormatDirectoryPath 11 | { 12 | get => _formatDirectoryPath ??= 13 | $"/{Path.GetDirectoryName(RelativePath).NormalizeEntryPath()}"; 14 | } 15 | 16 | public string CompressionRatio => GetRatio(Length, CompressedLength); 17 | 18 | public override ZipEntryType Type => ZipEntryType.Archive; 19 | 20 | public string BaseName => Path.GetFileNameWithoutExtension(Name); 21 | 22 | public string Extension => Path.GetExtension(RelativePath); 23 | 24 | internal ZipEntryFile(ZipArchiveEntry entry, string source) 25 | : base(entry, source) 26 | { } 27 | 28 | internal ZipEntryFile(ZipArchiveEntry entry, Stream? stream) 29 | : base(entry, stream) 30 | { } 31 | 32 | private static string GetRatio(long size, long compressedSize) 33 | { 34 | float compressedRatio = (float)compressedSize / size; 35 | 36 | if (float.IsNaN(compressedRatio)) 37 | { 38 | compressedRatio = 0; 39 | } 40 | 41 | return string.Format("{0:F2}%", 100 - (compressedRatio * 100)); 42 | } 43 | 44 | internal Stream Open(ZipArchive zip) 45 | { 46 | zip.ThrowIfNotFound( 47 | path: RelativePath, 48 | source: Source, 49 | out ZipArchiveEntry entry); 50 | 51 | return entry.Open(); 52 | } 53 | 54 | internal void Refresh() 55 | { 56 | this.ThrowIfFromStream(); 57 | using ZipArchive zip = OpenRead(); 58 | Refresh(zip); 59 | } 60 | 61 | internal void Refresh(ZipArchive zip) 62 | { 63 | ZipArchiveEntry entry = zip.GetEntry(RelativePath); 64 | Length = entry.Length; 65 | CompressedLength = entry.CompressedLength; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryMoveCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO.Compression; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using PSCompression.Extensions; 7 | 8 | namespace PSCompression; 9 | 10 | internal sealed class ZipEntryMoveCache 11 | { 12 | private readonly Dictionary> _cache; 13 | 14 | private readonly Dictionary> _mappings; 15 | 16 | internal ZipEntryMoveCache() 17 | { 18 | _cache = new(StringComparer.InvariantCultureIgnoreCase); 19 | _mappings = []; 20 | } 21 | 22 | private Dictionary WithSource(ZipEntryBase entry) 23 | { 24 | if (!_cache.ContainsKey(entry.Source)) 25 | { 26 | _cache[entry.Source] = []; 27 | } 28 | 29 | return _cache[entry.Source]; 30 | } 31 | 32 | internal bool IsDirectoryEntry(string source, string path) => 33 | _cache[source].TryGetValue(path, out EntryWithPath entryWithPath) 34 | && entryWithPath.ZipEntry.Type is ZipEntryType.Directory; 35 | 36 | internal void AddEntry(ZipEntryBase entry, string newname) => 37 | WithSource(entry).Add(entry.RelativePath, new(entry, newname)); 38 | 39 | internal IEnumerable<(string, PathWithType)> GetPassThruMappings() 40 | { 41 | foreach (var source in _cache) 42 | { 43 | foreach ((string path, EntryWithPath entryWithPath) in source.Value) 44 | { 45 | yield return ( 46 | source.Key, 47 | new PathWithType( 48 | _mappings[source.Key][path], 49 | entryWithPath.ZipEntry.Type)); 50 | } 51 | } 52 | } 53 | 54 | internal Dictionary> GetMappings( 55 | ZipArchiveCache cache) 56 | { 57 | foreach (var source in _cache) 58 | { 59 | _mappings[source.Key] = GetChildMappings(cache, source.Value); 60 | } 61 | 62 | return _mappings; 63 | } 64 | 65 | private Dictionary GetChildMappings( 66 | ZipArchiveCache cache, 67 | Dictionary pathChanges) 68 | { 69 | string newpath; 70 | Dictionary result = []; 71 | 72 | foreach (var pair in pathChanges.OrderByDescending(e => e.Key)) 73 | { 74 | (ZipEntryBase entry, string newname) = pair.Value; 75 | if (entry.Type is ZipEntryType.Archive) 76 | { 77 | newpath = ((ZipEntryFile)entry).ChangeName(newname); 78 | result[pair.Key] = newpath; 79 | continue; 80 | } 81 | 82 | ZipEntryDirectory dir = (ZipEntryDirectory)entry; 83 | newpath = dir.ChangeName(newname); 84 | result[pair.Key] = newpath; 85 | Regex re = new( 86 | Regex.Escape(dir.RelativePath), 87 | RegexOptions.Compiled | RegexOptions.IgnoreCase); 88 | 89 | foreach (ZipArchiveEntry key in dir.GetChilds(cache[dir.Source])) 90 | { 91 | string child = result.ContainsKey(key.FullName) 92 | ? result[key.FullName] 93 | : key.FullName; 94 | 95 | result[key.FullName] = re.Replace(child, newpath); 96 | } 97 | } 98 | 99 | return result; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/PSCompression/ZipEntryType.cs: -------------------------------------------------------------------------------- 1 | namespace PSCompression; 2 | 3 | public enum ZipEntryType 4 | { 5 | Directory = 0, 6 | Archive = 1 7 | } 8 | -------------------------------------------------------------------------------- /src/PSCompression/internal/_Format.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Globalization; 4 | using System.Management.Automation; 5 | 6 | namespace PSCompression.Internal; 7 | 8 | #pragma warning disable IDE1006 9 | 10 | [EditorBrowsable(EditorBrowsableState.Never)] 11 | public static class _Format 12 | { 13 | private static readonly CultureInfo _culture = CultureInfo.CurrentCulture; 14 | 15 | private readonly static string[] s_suffix = 16 | [ 17 | "B", 18 | "KB", 19 | "MB", 20 | "GB", 21 | "TB", 22 | "PB", 23 | "EB", 24 | "ZB", 25 | "YB" 26 | ]; 27 | 28 | [Hidden, EditorBrowsable(EditorBrowsableState.Never)] 29 | public static string GetDirectoryPath(ZipEntryBase entry) => entry.FormatDirectoryPath; 30 | 31 | [Hidden, EditorBrowsable(EditorBrowsableState.Never)] 32 | public static string GetFormattedDate(DateTime dateTime) => 33 | string.Format(_culture, "{0,10:d} {0,8:t}", dateTime); 34 | 35 | [Hidden, EditorBrowsable(EditorBrowsableState.Never)] 36 | public static string GetFormattedLength(long length) 37 | { 38 | int index = 0; 39 | double len = length; 40 | 41 | while (len >= 1024) 42 | { 43 | len /= 1024; 44 | index++; 45 | } 46 | 47 | return $"{Math.Round(len, 2):0.00} {s_suffix[index],2}"; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/CompressZipArchive.tests.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | $moduleName = (Get-Item ([IO.Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 4 | $manifestPath = [IO.Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 5 | 6 | Import-Module $manifestPath 7 | Import-Module ([System.IO.Path]::Combine($PSScriptRoot, 'shared.psm1')) 8 | 9 | Describe 'Compress-ZipArchive' -Tag 'Compress-ZipArchive' { 10 | BeforeAll { 11 | $sourceName = 'CompressZipArchiveTests' 12 | $destName = 'CompressZipArchiveExtract' 13 | $testpath = Join-Path $TestDrive $sourceName 14 | $extractpath = Join-Path $TestDrive $destName 15 | 16 | $structure = Get-Structure | ForEach-Object { 17 | $path = Join-Path $testpath $_ 18 | if ($_.EndsWith('.txt')) { 19 | New-Item $path -ItemType File -Value (Get-Random) -Force 20 | return 21 | } 22 | New-Item $path -ItemType Directory -Force 23 | } 24 | 25 | $structure, $extractpath | Out-Null 26 | } 27 | 28 | It 'Can compress a folder and all its child items' { 29 | Compress-ZipArchive $testpath $extractpath -PassThru | 30 | Should -BeOfType ([System.IO.FileInfo]) 31 | } 32 | 33 | It 'Should throw if the destination already exists' { 34 | { Compress-ZipArchive $testpath $extractpath } | 35 | Should -Throw 36 | } 37 | 38 | It 'Should overwrite if using -Force' { 39 | { Compress-ZipArchive $testpath $extractpath -Force } | 40 | Should -Not -Throw 41 | } 42 | 43 | It 'Extracted files should be exactly the same with the same structure' { 44 | BeforeAll { 45 | $map = @{} 46 | Get-ChildItem $testpath -Recurse | ForEach-Object { 47 | $relative = $_.FullName.Substring($testpath.Length) 48 | if ($_ -is [System.IO.FileInfo]) { 49 | $map[$relative] = ($_ | Get-FileHash -Algorithm MD5).Hash 50 | return 51 | } 52 | $map[$relative] = $null 53 | } 54 | 55 | Expand-Archive "$extractpath.zip" $extractpath 56 | 57 | $extractpath = Join-Path $extractpath $sourceName 58 | Get-ChildItem $extractpath -Recurse | ForEach-Object { 59 | $relative = $_.FullName.Substring($extractpath.Length) 60 | $map.ContainsKey($relative) | Should -BeTrue 61 | 62 | if ($_ -is [System.IO.FileInfo]) { 63 | $thishash = ($_ | Get-FileHash -Algorithm MD5).Hash 64 | $map[$relative] | Should -BeExactly $thishash 65 | } 66 | } 67 | } 68 | } 69 | 70 | It 'Can update entries if they exist' { 71 | $destination = [IO.Path]::Combine($TestDrive, 'UpdateTest', 'test.zip') 72 | $destinationExtract = [IO.Path]::Combine($TestDrive, 'UpdateTest') 73 | 74 | 0..10 | ForEach-Object { 75 | New-Item (Join-Path $TestDrive ('file{0:D2}.txt' -f $_)) -ItemType File -Value 'hello' 76 | } | Compress-ZipArchive -Destination $destination 77 | 78 | Get-ChildItem $TestDrive -Filter *.txt | ForEach-Object { 79 | 'world!' | Add-Content -LiteralPath $_.FullName 80 | $_ 81 | } | Compress-ZipArchive -Destination $destination -Update 82 | 83 | Expand-Archive $destination $destinationExtract 84 | Get-ChildItem $destinationExtract -Filter *.txt | ForEach-Object { 85 | $_ | Get-Content | Should -BeExactly 'helloworld!' 86 | } 87 | } 88 | 89 | It 'Should skip the entry if the source and destination are the same' { 90 | Push-Location $TestDrive 91 | $zipname = 'testskipitself.zip' 92 | 93 | { Compress-ZipArchive $pwd.Path $zipname } | 94 | Should -Not -Throw 95 | 96 | { Compress-ZipArchive $pwd.Path $zipname -Force } | 97 | Should -Not -Throw 98 | 99 | { Compress-ZipArchive $pwd.Path $zipname -Update } | 100 | Should -Not -Throw 101 | 102 | Expand-Archive $zipname -DestinationPath skipitself 103 | 104 | Get-ChildItem skipitself -Recurse | ForEach-Object Name | 105 | Should -Not -Contain $zipname 106 | } 107 | 108 | It 'Should skip items that match the exclusion patterns' { 109 | Remove-Item "$extractpath.zip" -Force 110 | Compress-ZipArchive $testpath $extractpath -Exclude *testfile00*, *testfolder05* 111 | Expand-Archive "$extractpath.zip" $extractpath 112 | Get-ChildItem $extractpath -Recurse | ForEach-Object { 113 | $_.FullName | Should -Not -BeLike *testfile00* 114 | $_.FullName | Should -Not -BeLike *testfolder05* 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/EncodingCompleter.tests.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | $moduleName = (Get-Item ([IO.Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 4 | $manifestPath = [IO.Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 5 | 6 | Import-Module $manifestPath 7 | Import-Module ([System.IO.Path]::Combine($PSScriptRoot, 'shared.psm1')) 8 | 9 | BeforeAll { 10 | $encodingSet = @( 11 | 'ascii' 12 | 'bigendianUtf32' 13 | 'unicode' 14 | 'utf8' 15 | 'utf8NoBOM' 16 | 'bigendianUnicode' 17 | 'oem' 18 | 'utf8BOM' 19 | 'utf32' 20 | 21 | if ($osIsWindows) { 22 | 'ansi' 23 | } 24 | ) 25 | 26 | $encodingSet | Out-Null 27 | } 28 | 29 | Describe 'EncodingCompleter Class' { 30 | It 'Completes results from a completion set' { 31 | (Complete 'Test-Completer ').CompletionText | 32 | Should -BeExactly $encodingSet 33 | } 34 | 35 | It 'Completes results from a word to complete' { 36 | (Complete 'Test-Completer utf').CompletionText | 37 | Should -BeExactly ($encodingSet -match '^utf') 38 | } 39 | 40 | It 'Should not offer ansi as a completion result if the OS is not Windows' { 41 | if ($osIsWindows) { 42 | return 43 | } 44 | 45 | (Complete 'Test-Completer ansi').CompletionText | 46 | Should -BeNullOrEmpty 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/EncodingTransformation.tests.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | $moduleName = (Get-Item ([IO.Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 4 | $manifestPath = [IO.Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 5 | 6 | Import-Module $manifestPath 7 | Import-Module ([System.IO.Path]::Combine($PSScriptRoot, 'shared.psm1')) 8 | 9 | Describe 'EncodingTransformation Class' { 10 | BeforeAll { 11 | Add-Type -TypeDefinition ' 12 | public static class Acp 13 | { 14 | [System.Runtime.InteropServices.DllImport("Kernel32.dll")] 15 | public static extern int GetACP(); 16 | } 17 | ' 18 | 19 | $encodings = @{ 20 | 'ascii' = [System.Text.ASCIIEncoding]::new() 21 | 'bigendianunicode' = [System.Text.UnicodeEncoding]::new($true, $true) 22 | 'bigendianutf32' = [System.Text.UTF32Encoding]::new($true, $true) 23 | 'oem' = [Console]::OutputEncoding 24 | 'unicode' = [System.Text.UnicodeEncoding]::new() 25 | 'utf8' = [System.Text.UTF8Encoding]::new($false) 26 | 'utf8bom' = [System.Text.UTF8Encoding]::new($true) 27 | 'utf8nobom' = [System.Text.UTF8Encoding]::new($false) 28 | 'utf32' = [System.Text.UTF32Encoding]::new() 29 | } 30 | 31 | if ($osIsWindows) { 32 | $encodings['ansi'] = [System.Text.Encoding]::GetEncoding([Acp]::GetACP()) 33 | } 34 | 35 | $transform = [PSCompression.EncodingTransformation]::new() 36 | $transform | Out-Null 37 | } 38 | 39 | It 'Transforms Encoding to Encoding' { 40 | $transform.Transform($ExecutionContext, [System.Text.Encoding]::UTF8) | 41 | Should -BeExactly ([System.Text.Encoding]::UTF8) 42 | } 43 | 44 | It 'Transforms a completion set to their Encoding Representations' { 45 | $encodings.GetEnumerator() | ForEach-Object { 46 | $transform.Transform($ExecutionContext, $_.Key) | 47 | Should -BeExactly $_.Value 48 | } 49 | } 50 | 51 | It 'Transforms CodePage to their Encoding Representations' { 52 | [System.Text.Encoding]::GetEncodings() | ForEach-Object { 53 | $transform.Transform($ExecutionContext, $_.CodePage) | 54 | Should -BeExactly $_.GetEncoding() 55 | } 56 | } 57 | 58 | It 'Throws if input value cannot be transformed' { 59 | { $transform.Transform($ExecutionContext, 'doesnotexist') } | 60 | Should -Throw 61 | } 62 | 63 | It 'Throws if the input value type is not Encoding, string or int' { 64 | { $transform.Transform($ExecutionContext, [type]) } | 65 | Should -Throw 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/FormattingInternals.tests.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | $moduleName = (Get-Item ([IO.Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 4 | $manifestPath = [IO.Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 5 | 6 | Import-Module $manifestPath 7 | Import-Module ([System.IO.Path]::Combine($PSScriptRoot, 'shared.psm1')) 8 | 9 | Describe 'Formatting internals' { 10 | BeforeAll { 11 | $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force 12 | 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt 13 | New-ZipEntry $zip.FullName -EntryPath afolder/ 14 | } 15 | 16 | It 'Converts Length to their friendly representation' { 17 | [PSCompression.Internal._Format]::GetFormattedLength(1mb) | 18 | Should -BeExactly '1.00 MB' 19 | } 20 | 21 | It 'Gets the directory of an entry' { 22 | $zip | Get-ZipEntry | ForEach-Object { 23 | [PSCompression.Internal._Format]::GetDirectoryPath($_) 24 | } | Should -BeOfType ([string]) 25 | } 26 | 27 | It 'Formats datetime instances' { 28 | [PSCompression.Internal._Format]::GetFormattedDate([datetime]::Now) | 29 | Should -BeExactly ([string]::Format([CultureInfo]::CurrentCulture,'{0,10:d} {0,8:t}', [datetime]::Now)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/PSVersionHelper.tests.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | $moduleName = (Get-Item ([IO.Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 4 | $manifestPath = [IO.Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 5 | 6 | Import-Module $manifestPath 7 | Import-Module ([System.IO.Path]::Combine($PSScriptRoot, 'shared.psm1')) 8 | 9 | Describe 'PSVersionHelper Class' { 10 | It 'Should have the same value as $IsCoreCLR' { 11 | $property = [PSCompression.ZipEntryBase].Assembly. 12 | GetType('PSCompression.PSVersionHelper'). 13 | GetProperty( 14 | 'IsCoreCLR', 15 | [System.Reflection.BindingFlags] 'NonPublic, Static') 16 | 17 | $property.GetValue($property) | Should -BeExactly ([bool] $IsCoreCLR) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/ZipEntryBase.tests.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | $moduleName = (Get-Item ([IO.Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 4 | $manifestPath = [IO.Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 5 | 6 | Import-Module $manifestPath 7 | Import-Module ([System.IO.Path]::Combine($PSScriptRoot, 'shared.psm1')) 8 | 9 | Describe 'ZipEntryBase Class' { 10 | BeforeAll { 11 | $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force 12 | 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt 13 | New-ZipEntry $zip.FullName -EntryPath somefolder/ 14 | } 15 | 16 | It 'Can extract an entry' { 17 | ($zip | Get-ZipEntry -Type Archive).ExtractTo($TestDrive, $false) | 18 | Should -BeOfType ([System.IO.FileInfo]) 19 | } 20 | 21 | It 'Can overwrite a file when extracting' { 22 | ($zip | Get-ZipEntry -Type Archive).ExtractTo($TestDrive, $true) | 23 | Should -BeOfType ([System.IO.FileInfo]) 24 | } 25 | 26 | It 'Can extract a file from entries created from input Stream' { 27 | Use-Object ($stream = $zip.OpenRead()) { 28 | ($stream | Get-ZipEntry -Type Archive).ExtractTo($TestDrive, $true) 29 | } | Should -BeOfType ([System.IO.FileInfo]) 30 | } 31 | 32 | It 'Can create a new folder in the destination path when extracting' { 33 | $entry = $zip | Get-ZipEntry -Type Archive 34 | $file = $entry.ExtractTo( 35 | [System.IO.Path]::Combine($TestDrive, 'myTestFolder'), 36 | $false) 37 | 38 | $file.FullName | Should -BeExactly ([System.IO.Path]::Combine($TestDrive, 'myTestFolder', $entry.Name)) 39 | } 40 | 41 | It 'Can extract folders' { 42 | ($zip | Get-ZipEntry -Type Directory).ExtractTo($TestDrive, $false) | 43 | Should -BeOfType ([System.IO.DirectoryInfo]) 44 | } 45 | 46 | It 'Can overwrite folders when extracting' { 47 | ($zip | Get-ZipEntry -Type Directory).ExtractTo($TestDrive, $true) | 48 | Should -BeOfType ([System.IO.DirectoryInfo]) 49 | } 50 | 51 | It 'Has a LastWriteTime Property' { 52 | ($zip | Get-ZipEntry).LastWriteTime | Should -BeOfType ([datetime]) 53 | } 54 | 55 | It 'Has a CompressionRatio Property' { 56 | New-ZipEntry $zip.FullName -EntryPath empty.txt 57 | ($zip | Get-ZipEntry).CompressionRatio | Should -BeOfType ([string]) 58 | } 59 | 60 | It 'Can remove an entry in the source zip' { 61 | { $zip | Get-ZipEntry | ForEach-Object Remove } | 62 | Should -Not -Throw 63 | 64 | $zip | Get-ZipEntry | Should -BeNullOrEmpty 65 | } 66 | 67 | It 'Should throw if Remove() is used on entries created from input Stream' { 68 | 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt 69 | 70 | { 71 | Use-Object ($stream = $zip.OpenRead()) { 72 | $stream | Get-ZipEntry -Type Archive | ForEach-Object Remove 73 | } 74 | } | Should -Throw 75 | } 76 | 77 | It 'Opens a ZipArchive on OpenRead() and OpenWrite()' { 78 | Use-Object ($archive = ($zip | Get-ZipEntry).OpenRead()) { 79 | $archive | Should -BeOfType ([System.IO.Compression.ZipArchive]) 80 | } 81 | 82 | Use-Object ($stream = $zip.OpenRead()) { 83 | Use-Object ($archive = ($stream | Get-ZipEntry).OpenRead()) { 84 | $archive | Should -BeOfType ([System.IO.Compression.ZipArchive]) 85 | } 86 | } 87 | 88 | Use-Object ($archive = ($zip | Get-ZipEntry).OpenWrite()) { 89 | $archive | Should -BeOfType ([System.IO.Compression.ZipArchive]) 90 | } 91 | } 92 | 93 | It 'Should throw if calling OpenWrite() on entries created from input Stream' { 94 | Use-Object ($stream = $zip.OpenRead()) { 95 | { 96 | Use-Object ($stream | Get-ZipEntry).OpenWrite() { } 97 | } | Should -Throw 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/ZipEntryDirectory.tests.ps1: -------------------------------------------------------------------------------- 1 | $moduleName = (Get-Item ([IO.Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 2 | $manifestPath = [IO.Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 3 | 4 | Import-Module $manifestPath 5 | Import-Module ([System.IO.Path]::Combine($PSScriptRoot, 'shared.psm1')) 6 | 7 | Describe 'ZipEntryDirectory Class' { 8 | BeforeAll { 9 | $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force 10 | New-ZipEntry $zip.FullName -EntryPath afolder/ 11 | } 12 | 13 | It 'Should be of type Directory' { 14 | ($zip | Get-ZipEntry).Type | Should -BeExactly ([PSCompression.ZipEntryType]::Directory) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/ZipEntryFile.tests.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop' 2 | 3 | $moduleName = (Get-Item ([IO.Path]::Combine($PSScriptRoot, '..', 'module', '*.psd1'))).BaseName 4 | $manifestPath = [IO.Path]::Combine($PSScriptRoot, '..', 'output', $moduleName) 5 | 6 | Import-Module $manifestPath 7 | Import-Module ([System.IO.Path]::Combine($PSScriptRoot, 'shared.psm1')) 8 | 9 | Describe 'ZipEntryFile Class' { 10 | BeforeAll { 11 | $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force 12 | 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt 13 | } 14 | 15 | It 'Should be of type Archive' { 16 | ($zip | Get-ZipEntry).Type | Should -BeExactly ([PSCompression.ZipEntryType]::Archive) 17 | } 18 | 19 | It 'Should Have a BaseName Property' { 20 | ($zip | Get-ZipEntry).BaseName | Should -BeOfType ([string]) 21 | ($zip | Get-ZipEntry).BaseName | Should -BeExactly helloworld 22 | } 23 | 24 | It 'Should Have an Extension Property' { 25 | ($zip | Get-ZipEntry).Extension | Should -BeOfType ([string]) 26 | ($zip | Get-ZipEntry).Extension | Should -BeExactly .txt 27 | } 28 | 29 | It 'Should Open the source zip' { 30 | Use-Object ($stream = ($zip | Get-ZipEntry).OpenRead()) { 31 | $stream | Should -BeOfType ([System.IO.Compression.ZipArchive]) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/shared.psm1: -------------------------------------------------------------------------------- 1 | function Complete { 2 | [OutputType([System.Management.Automation.CompletionResult])] 3 | param([string] $Expression) 4 | 5 | end { 6 | [System.Management.Automation.CommandCompletion]::CompleteInput( 7 | $Expression, 8 | $Expression.Length, 9 | $null).CompletionMatches 10 | } 11 | } 12 | 13 | function Decode { 14 | param([byte[]] $bytes) 15 | 16 | end { 17 | try { 18 | $gzip = [System.IO.Compression.GZipStream]::new( 19 | ($mem = [System.IO.MemoryStream]::new($bytes)), 20 | [System.IO.Compression.CompressionMode]::Decompress) 21 | 22 | $out = [System.IO.MemoryStream]::new() 23 | $gzip.CopyTo($out) 24 | } 25 | finally { 26 | if ($gzip -is [System.IDisposable]) { 27 | $gzip.Dispose() 28 | } 29 | 30 | if ($mem -is [System.IDisposable]) { 31 | $mem.Dispose() 32 | } 33 | 34 | if ($out -is [System.IDisposable]) { 35 | $out.Dispose() 36 | [System.Text.UTF8Encoding]::new().GetString($out.ToArray()) 37 | } 38 | } 39 | } 40 | } 41 | 42 | function Test-Completer { 43 | param( 44 | [ArgumentCompleter([PSCompression.EncodingCompleter])] 45 | [string] $Test 46 | ) 47 | } 48 | 49 | function Get-Structure { 50 | foreach ($folder in 0..5) { 51 | $folder = 'testfolder{0:D2}/' -f $folder 52 | $folder 53 | foreach ($file in 0..5) { 54 | [System.IO.Path]::Combine($folder, 'testfile{0:D2}.txt' -f $file) 55 | } 56 | } 57 | } 58 | 59 | $osIsWindows = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform( 60 | [System.Runtime.InteropServices.OSPlatform]::Windows) 61 | 62 | $osIsWindows | Out-Null 63 | 64 | $exportModuleMemberSplat = @{ 65 | Variable = 'moduleName', 'manifestPath', 'osIsWindows' 66 | Function = 'Decode', 'Complete', 'Test-Completer', 'Get-Structure' 67 | } 68 | 69 | Export-ModuleMember @exportModuleMemberSplat 70 | -------------------------------------------------------------------------------- /tools/InvokeBuild.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [Parameter(Mandatory)] 4 | [ProjectBuilder.ProjectInfo] $ProjectInfo 5 | ) 6 | 7 | task Clean { 8 | $ProjectInfo.CleanRelease() 9 | } 10 | 11 | task BuildDocs { 12 | $helpParams = $ProjectInfo.Documentation.GetParams() 13 | $null = New-ExternalHelp @helpParams 14 | } 15 | 16 | task BuildManaged { 17 | $arguments = $ProjectInfo.GetBuildArgs() 18 | Push-Location -LiteralPath $ProjectInfo.Project.Source.FullName 19 | 20 | try { 21 | foreach ($framework in $ProjectInfo.Project.TargetFrameworks) { 22 | Write-Host "Compiling for $framework" 23 | dotnet @arguments --framework $framework 24 | 25 | if ($LASTEXITCODE) { 26 | throw "Failed to compiled code for $framework" 27 | } 28 | } 29 | } 30 | finally { 31 | Pop-Location 32 | } 33 | } 34 | 35 | task CopyToRelease { 36 | $ProjectInfo.Module.CopyToRelease() 37 | $ProjectInfo.Project.CopyToRelease() 38 | } 39 | 40 | task Package { 41 | $ProjectInfo.Project.ClearNugetPackage() 42 | $repoParams = $ProjectInfo.Project.GetPSRepoParams() 43 | 44 | if (Get-PSRepository -Name $repoParams.Name -ErrorAction SilentlyContinue) { 45 | Unregister-PSRepository -Name $repoParams.Name 46 | } 47 | 48 | Register-PSRepository @repoParams 49 | try { 50 | $publishModuleSplat = @{ 51 | Path = $ProjectInfo.Project.Release 52 | Repository = $repoParams.Name 53 | } 54 | Publish-Module @publishModuleSplat 55 | } 56 | finally { 57 | Unregister-PSRepository -Name $repoParams.Name 58 | } 59 | } 60 | 61 | task Analyze { 62 | if (-not $ProjectInfo.AnalyzerPath) { 63 | Write-Host 'No Analyzer Settings found, skipping' 64 | return 65 | } 66 | 67 | $pssaSplat = $ProjectInfo.GetAnalyzerParams() 68 | $results = Invoke-ScriptAnalyzer @pssaSplat 69 | 70 | if ($results) { 71 | $results | Out-String 72 | throw 'Failed PsScriptAnalyzer tests, build failed' 73 | } 74 | } 75 | 76 | task PesterTests { 77 | if (-not $ProjectInfo.Pester.PesterScript) { 78 | Write-Host 'No Pester tests found, skipping' 79 | return 80 | } 81 | 82 | $ProjectInfo.Pester.ClearResultFile() 83 | 84 | if (-not (dotnet tool list --global | Select-String coverlet.console -SimpleMatch)) { 85 | Write-Host 'Installing dotnet tool coverlet.console' -ForegroundColor Yellow 86 | dotnet tool install --global coverlet.console 87 | } 88 | 89 | coverlet $ProjectInfo.Pester.GetTestArgs($PSVersionTable.PSVersion) 90 | 91 | if ($LASTEXITCODE) { 92 | throw 'Pester failed tests' 93 | } 94 | } 95 | 96 | task Build -Jobs Clean, BuildManaged, CopyToRelease, BuildDocs, Package 97 | task Test -Jobs BuildManaged, Analyze, PesterTests 98 | -------------------------------------------------------------------------------- /tools/PesterTest.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Parameter(Mandatory)] 4 | [String] $TestPath, 5 | 6 | [Parameter(Mandatory)] 7 | [String] $OutputFile 8 | ) 9 | 10 | $ErrorActionPreference = 'Stop' 11 | 12 | Get-ChildItem ([IO.Path]::Combine($PSScriptRoot, 'Modules')) -Directory | 13 | Import-Module -Name { $_.FullName } -Force -DisableNameChecking 14 | 15 | [PSCustomObject] $PSVersionTable | Select-Object *, @{ 16 | Name = 'Architecture' 17 | Expression = { 18 | switch ([IntPtr]::Size) { 19 | 4 { 'x86' } 20 | 8 { 'x64' } 21 | default { 'Unknown' } 22 | } 23 | } 24 | } | Format-List | Out-Host 25 | 26 | $configuration = [PesterConfiguration]::Default 27 | $configuration.Output.Verbosity = 'Detailed' 28 | $configuration.Run.Path = $TestPath 29 | $configuration.Run.Throw = $true 30 | $configuration.TestResult.Enabled = $true 31 | $configuration.TestResult.OutputPath = $OutputFile 32 | $configuration.TestResult.OutputFormat = 'NUnitXml' 33 | 34 | Invoke-Pester -Configuration $configuration -WarningAction Ignore 35 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Documentation.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace ProjectBuilder; 4 | 5 | public record struct Documentation(string Source, string Output) 6 | { 7 | public readonly Hashtable GetParams() => new() 8 | { 9 | ["Path"] = Source, 10 | ["OutputPath"] = Output 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace ProjectBuilder; 5 | 6 | internal static class Extensions 7 | { 8 | internal static void CopyRecursive(this DirectoryInfo source, string? destination) 9 | { 10 | if (destination is null) 11 | { 12 | throw new ArgumentNullException($"Destination path is null.", nameof(destination)); 13 | } 14 | 15 | if (!Directory.Exists(destination)) 16 | { 17 | Directory.CreateDirectory(destination); 18 | } 19 | 20 | foreach (DirectoryInfo dir in source.EnumerateDirectories("*", SearchOption.AllDirectories)) 21 | { 22 | Directory.CreateDirectory(dir.FullName.Replace(source.FullName, destination)); 23 | } 24 | 25 | foreach (FileInfo file in source.EnumerateFiles("*", SearchOption.AllDirectories)) 26 | { 27 | file.CopyTo(file.FullName.Replace(source.FullName, destination)); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Module.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.IO.Compression; 6 | using System.Linq; 7 | using System.Management.Automation; 8 | using System.Net.Http; 9 | using System.Threading.Tasks; 10 | 11 | namespace ProjectBuilder; 12 | 13 | public sealed class Module 14 | { 15 | public DirectoryInfo Root { get; } 16 | 17 | public FileInfo? Manifest { get; internal set; } 18 | 19 | public Version? Version { get; internal set; } 20 | 21 | public string Name { get; } 22 | 23 | public string PreRequisitePath { get; } 24 | 25 | private string? Release { get => _info.Project.Release; } 26 | 27 | private readonly UriBuilder _builder = new(_base); 28 | 29 | private const string _base = "https://www.powershellgallery.com"; 30 | 31 | private const string _path = "api/v2/package/{0}/{1}"; 32 | 33 | private readonly ProjectInfo _info; 34 | 35 | private Hashtable? _req; 36 | 37 | internal Module( 38 | DirectoryInfo directory, 39 | string name, 40 | ProjectInfo info) 41 | { 42 | Root = directory; 43 | Name = name; 44 | PreRequisitePath = InitPrerequisitePath(Root); 45 | _info = info; 46 | } 47 | 48 | public void CopyToRelease() => Root.CopyRecursive(Release); 49 | 50 | internal IEnumerable GetRequirements(string path) 51 | { 52 | _req ??= ImportRequirements(path); 53 | 54 | if (_req is { Count: 0 }) 55 | { 56 | return []; 57 | } 58 | 59 | List modules = new(_req.Count); 60 | foreach (DictionaryEntry entry in _req) 61 | { 62 | modules.Add(new ModuleDownload 63 | { 64 | Module = entry.Key.ToString(), 65 | Version = LanguagePrimitives.ConvertTo(entry.Value) 66 | }); 67 | } 68 | 69 | return DownloadModules([.. modules]); 70 | } 71 | 72 | private static Hashtable ImportRequirements(string path) 73 | { 74 | using PowerShell powerShell = PowerShell.Create(RunspaceMode.CurrentRunspace); 75 | return powerShell 76 | .AddCommand("Import-PowerShellDataFile") 77 | .AddArgument(path) 78 | .Invoke() 79 | .FirstOrDefault(); 80 | } 81 | 82 | private string[] DownloadModules(ModuleDownload[] modules) 83 | { 84 | List> tasks = new(modules.Length); 85 | List output = new(modules.Length); 86 | 87 | foreach ((string module, Version version) in modules) 88 | { 89 | string destination = GetDestination(module); 90 | string modulePath = GetModulePath(module); 91 | 92 | if (Directory.Exists(modulePath)) 93 | { 94 | output.Add(modulePath); 95 | continue; 96 | } 97 | 98 | Console.WriteLine($"Installing build pre-req '{module}'"); 99 | _builder.Path = string.Format(_path, module, version); 100 | Task task = GetModuleAsync( 101 | uri: _builder.Uri.ToString(), 102 | destination: destination, 103 | expandPath: modulePath); 104 | tasks.Add(task); 105 | } 106 | 107 | output.AddRange(WaitTask(tasks)); 108 | return [.. output]; 109 | } 110 | 111 | private static string[] WaitTask(List> tasks) => 112 | WaitTaskAsync(tasks).GetAwaiter().GetResult(); 113 | 114 | private static void ExpandArchive(string source, string destination) => 115 | ZipFile.ExtractToDirectory(source, destination); 116 | 117 | private static async Task WaitTaskAsync( 118 | List> tasks) 119 | { 120 | List completedTasks = new(tasks.Count); 121 | while (tasks.Count > 0) 122 | { 123 | Task awaiter = await Task.WhenAny(tasks); 124 | tasks.Remove(awaiter); 125 | string module = await awaiter; 126 | completedTasks.Add(module); 127 | } 128 | return [.. completedTasks]; 129 | } 130 | 131 | private string GetDestination(string module) => 132 | Path.Combine(PreRequisitePath, Path.ChangeExtension(module, "zip")); 133 | 134 | private string GetModulePath(string module) => 135 | Path.Combine(PreRequisitePath, module); 136 | 137 | private static async Task GetModuleAsync( 138 | string uri, 139 | string destination, 140 | string expandPath) 141 | { 142 | using (FileStream fs = File.Create(destination)) 143 | { 144 | using HttpClient client = new(); 145 | using Stream stream = await client.GetStreamAsync(uri); 146 | await stream.CopyToAsync(fs); 147 | } 148 | 149 | ExpandArchive(destination, expandPath); 150 | File.Delete(destination); 151 | return expandPath; 152 | } 153 | 154 | private static string InitPrerequisitePath(DirectoryInfo root) 155 | { 156 | string path = Path.Combine(root.Parent.FullName, "tools", "Modules"); 157 | if (!Directory.Exists(path)) 158 | { 159 | Directory.CreateDirectory(path); 160 | } 161 | return path; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Pester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Runtime.InteropServices; 6 | using System.Text.RegularExpressions; 7 | 8 | namespace ProjectBuilder; 9 | 10 | public sealed class Pester 11 | { 12 | public string? PesterScript 13 | { 14 | get 15 | { 16 | _pesterPath ??= Path.Combine( 17 | _info.Root.FullName, 18 | "tools", 19 | "PesterTest.ps1"); 20 | 21 | if (_testsExist = File.Exists(_pesterPath)) 22 | { 23 | return _pesterPath; 24 | } 25 | 26 | return null; 27 | } 28 | } 29 | 30 | public string? ResultPath { get => _testsExist ? Path.Combine(_info.Project.Build, "TestResults") : null; } 31 | 32 | public string? ResultFile { get => _testsExist ? Path.Combine(ResultPath, "Pester.xml") : null; } 33 | 34 | private readonly ProjectInfo _info; 35 | 36 | private string? _pesterPath; 37 | 38 | private bool _testsExist; 39 | 40 | internal Pester(ProjectInfo info) => _info = info; 41 | 42 | private void CreateResultPath() 43 | { 44 | if (!Directory.Exists(ResultPath)) 45 | { 46 | Directory.CreateDirectory(ResultPath); 47 | } 48 | } 49 | 50 | public void ClearResultFile() 51 | { 52 | if (File.Exists(ResultFile)) 53 | { 54 | File.Delete(ResultFile); 55 | } 56 | } 57 | 58 | public string[] GetTestArgs(Version version) 59 | { 60 | CreateResultPath(); 61 | 62 | List arguments = [ 63 | "-NoProfile", 64 | "-NonInteractive", 65 | ]; 66 | 67 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 68 | { 69 | arguments.AddRange([ "-ExecutionPolicy", "Bypass" ]); 70 | } 71 | 72 | arguments.AddRange([ 73 | "-File", PesterScript!, 74 | "-TestPath", Path.Combine(_info.Root.FullName, "tests"), 75 | "-OutputFile", ResultFile! 76 | ]); 77 | 78 | Regex re = new("^|$", RegexOptions.Compiled); 79 | string targetArgs = re.Replace(string.Join("\" \"", [.. arguments]), "\""); 80 | string pwsh = Regex.Replace(Environment.GetCommandLineArgs().First(), @"\.dll$", string.Empty); 81 | string unitCoveragePath = Path.Combine(ResultPath, "UnitCoverage.json"); 82 | string watchFolder = Path.Combine(_info.Project.Release, "bin", _info.Project.TestFramework); 83 | string sourceMappingFile = Path.Combine(ResultPath, "CoverageSourceMapping.txt"); 84 | 85 | if (version is not { Major: >= 7, Minor: > 0 }) 86 | { 87 | targetArgs = re.Replace(targetArgs, "\""); 88 | watchFolder = re.Replace(watchFolder, "\""); 89 | } 90 | 91 | arguments.Clear(); 92 | arguments.AddRange([ 93 | watchFolder, 94 | "--target", pwsh, 95 | "--targetargs", targetArgs, 96 | "--output", Path.Combine(ResultPath, "Coverage.xml"), 97 | "--format", "cobertura" 98 | ]); 99 | 100 | if (File.Exists(unitCoveragePath)) 101 | { 102 | arguments.AddRange([ "--merge-with", unitCoveragePath ]); 103 | } 104 | 105 | if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is "true") 106 | { 107 | arguments.AddRange([ "--source-mapping-file", sourceMappingFile ]); 108 | File.WriteAllText( 109 | sourceMappingFile, 110 | $"|{_info.Root.FullName}{Path.DirectorySeparatorChar}=/_/"); 111 | } 112 | 113 | return [.. arguments]; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Project.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace ProjectBuilder; 7 | 8 | public sealed class Project 9 | { 10 | public DirectoryInfo Source { get; } 11 | 12 | public string Build { get; } 13 | 14 | public string? Release { get; internal set; } 15 | 16 | public string[]? TargetFrameworks { get; internal set; } 17 | 18 | public string? TestFramework { get => TargetFrameworks.FirstOrDefault(); } 19 | 20 | private Configuration Configuration { get => _info.Configuration; } 21 | 22 | private readonly ProjectInfo _info; 23 | 24 | internal Project(DirectoryInfo source, string build, ProjectInfo info) 25 | { 26 | Source = source; 27 | Build = build; 28 | _info = info; 29 | } 30 | 31 | public void CopyToRelease() 32 | { 33 | if (TargetFrameworks is null) 34 | { 35 | throw new ArgumentNullException( 36 | "TargetFrameworks is null.", 37 | nameof(TargetFrameworks)); 38 | } 39 | 40 | foreach (string framework in TargetFrameworks) 41 | { 42 | DirectoryInfo buildFolder = new(Path.Combine( 43 | Source.FullName, 44 | "bin", 45 | Configuration.ToString(), 46 | framework, 47 | "publish")); 48 | 49 | string binFolder = Path.Combine(Release, "bin", framework); 50 | buildFolder.CopyRecursive(binFolder); 51 | } 52 | } 53 | 54 | public Hashtable GetPSRepoParams() => new() 55 | { 56 | ["Name"] = "LocalRepo", 57 | ["SourceLocation"] = Build, 58 | ["PublishLocation"] = Build, 59 | ["InstallationPolicy"] = "Trusted" 60 | }; 61 | 62 | public void ClearNugetPackage() 63 | { 64 | string nugetPath = Path.Combine( 65 | Build, 66 | $"{_info.Module.Name}.{_info.Module.Version}.nupkg"); 67 | 68 | if (File.Exists(nugetPath)) 69 | { 70 | File.Delete(nugetPath); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/ProjectBuilder.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | enable 6 | latest 7 | ProjectBuilder 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/ProjectInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Management.Automation; 7 | using System.Xml; 8 | 9 | namespace ProjectBuilder; 10 | 11 | public sealed class ProjectInfo 12 | { 13 | public DirectoryInfo Root { get; } 14 | 15 | public Module Module { get; } 16 | 17 | public Configuration Configuration { get; internal set; } 18 | 19 | public Documentation Documentation { get; internal set; } 20 | 21 | public Project Project { get; } 22 | 23 | public Pester Pester { get; } 24 | 25 | public string? AnalyzerPath 26 | { 27 | get 28 | { 29 | _analyzerPath ??= Path.Combine( 30 | Root.FullName, 31 | "ScriptAnalyzerSettings.psd1"); 32 | 33 | if (File.Exists(_analyzerPath)) 34 | { 35 | return _analyzerPath; 36 | } 37 | 38 | return null; 39 | } 40 | } 41 | 42 | private string? _analyzerPath; 43 | 44 | private ProjectInfo(string path) 45 | { 46 | Root = AssertDirectory(path); 47 | 48 | Module = new Module( 49 | directory: AssertDirectory(GetModulePath(path)), 50 | name: Path.GetFileNameWithoutExtension(path), 51 | info: this); 52 | 53 | Project = new Project( 54 | source: AssertDirectory(GetSourcePath(path, Module.Name)), 55 | build: GetBuildPath(path), 56 | info: this); 57 | 58 | Pester = new(this); 59 | } 60 | 61 | public static ProjectInfo Create( 62 | string path, 63 | Configuration configuration) 64 | { 65 | ProjectInfo builder = new(path) 66 | { 67 | Configuration = configuration 68 | }; 69 | builder.Module.Manifest = GetManifest(builder); 70 | builder.Module.Version = GetManifestVersion(builder); 71 | builder.Project.Release = GetReleasePath( 72 | builder.Project.Build, 73 | builder.Module.Name, 74 | builder.Module.Version!); 75 | builder.Project.TargetFrameworks = GetTargetFrameworks(GetProjectFile(builder)); 76 | builder.Documentation = new Documentation 77 | { 78 | Source = Path.Combine(builder.Root.FullName, "docs", "en-US"), 79 | Output = Path.Combine(builder.Project.Release, "en-US") 80 | }; 81 | 82 | return builder; 83 | } 84 | 85 | public IEnumerable GetRequirements() 86 | { 87 | string req = Path.Combine(Root.FullName, "tools", "requiredModules.psd1"); 88 | if (!File.Exists(req)) 89 | { 90 | return []; 91 | } 92 | return Module.GetRequirements(req); 93 | } 94 | 95 | public void CleanRelease() 96 | { 97 | if (Directory.Exists(Project.Release)) 98 | { 99 | Directory.Delete(Project.Release, recursive: true); 100 | } 101 | Directory.CreateDirectory(Project.Release); 102 | } 103 | 104 | public string[] GetBuildArgs() => 105 | [ 106 | "publish", 107 | "--configuration", Configuration.ToString(), 108 | "--verbosity", "q", 109 | "-nologo", 110 | $"-p:Version={Module.Version}" 111 | ]; 112 | 113 | public Hashtable GetAnalyzerParams() => new() 114 | { 115 | ["Path"] = Project.Release, 116 | ["Settings"] = AnalyzerPath, 117 | ["Recurse"] = true, 118 | ["ErrorAction"] = "SilentlyContinue" 119 | }; 120 | 121 | private static string[] GetTargetFrameworks(string path) 122 | { 123 | XmlDocument xmlDocument = new(); 124 | xmlDocument.Load(path); 125 | return xmlDocument 126 | .SelectSingleNode("Project/PropertyGroup/TargetFrameworks") 127 | .InnerText 128 | .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); 129 | } 130 | 131 | private static string GetBuildPath(string path) => 132 | Path.Combine(path, "output"); 133 | 134 | private static string GetSourcePath(string path, string moduleName) => 135 | Path.Combine(path, "src", moduleName); 136 | 137 | private static string GetModulePath(string path) => 138 | Path.Combine(path, "module"); 139 | 140 | private static string GetReleasePath( 141 | string buildPath, 142 | string moduleName, 143 | Version version) => Path.Combine( 144 | buildPath, 145 | moduleName, 146 | LanguagePrimitives.ConvertTo(version)); 147 | 148 | private static DirectoryInfo AssertDirectory(string path) 149 | { 150 | DirectoryInfo directory = new(path); 151 | return directory.Exists ? directory 152 | : throw new ArgumentException( 153 | $"Path '{path}' could not be found or is not a Directory.", 154 | nameof(path)); 155 | } 156 | 157 | private static FileInfo GetManifest(ProjectInfo builder) => 158 | builder.Module.Root.EnumerateFiles("*.psd1").FirstOrDefault() 159 | ?? throw new FileNotFoundException( 160 | $"Manifest file could not be found in '{builder.Root.FullName}'"); 161 | 162 | private static string GetProjectFile(ProjectInfo builder) => 163 | builder.Project.Source.EnumerateFiles("*.csproj").FirstOrDefault()?.FullName 164 | ?? throw new FileNotFoundException( 165 | $"Project file could not be found in ''{builder.Project.Source.FullName}'"); 166 | 167 | private static Version? GetManifestVersion(ProjectInfo builder) 168 | { 169 | using PowerShell powershell = PowerShell.Create(RunspaceMode.CurrentRunspace); 170 | Hashtable? moduleInfo = powershell 171 | .AddCommand("Import-PowerShellDataFile") 172 | .AddArgument(builder.Module.Manifest?.FullName) 173 | .Invoke() 174 | .FirstOrDefault(); 175 | 176 | return powershell.HadErrors 177 | ? throw powershell.Streams.Error.First().Exception 178 | : LanguagePrimitives.ConvertTo(moduleInfo?["ModuleVersion"]); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tools/ProjectBuilder/Types.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ProjectBuilder; 4 | 5 | public enum Configuration 6 | { 7 | Debug, 8 | Release 9 | } 10 | 11 | internal record struct ModuleDownload(string Module, Version Version); 12 | -------------------------------------------------------------------------------- /tools/prompt.ps1: -------------------------------------------------------------------------------- 1 | function prompt { "PS $($PWD.Path -replace '.+(?=\\)', '..')$('>' * ($nestedPromptLevel + 1)) " } 2 | -------------------------------------------------------------------------------- /tools/requiredModules.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | InvokeBuild = '5.12.1' 3 | platyPS = '0.14.2' 4 | PSScriptAnalyzer = '1.23.0' 5 | Pester = '5.7.1' 6 | PSUsing = '1.0.0' 7 | } 8 | --------------------------------------------------------------------------------