├── .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 | [](https://github.com/santisq/PSCompression/actions/workflows/ci.yml)
7 | [](https://codecov.io/gh/santisq/PSCompression)
8 | [](https://www.powershellgallery.com/packages/PSCompression)
10 | [](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 | Cmdlet
24 | Description
25 |
26 |
27 |
28 |
29 | [`Get-ZipEntry`](docs/en-US/Get-ZipEntry.md)
30 |
31 |
32 |
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 |
37 |
38 |
39 |
40 |
41 | [`Expand-ZipEntry`](docs/en-US/Expand-ZipEntry.md)
42 |
43 |
44 |
45 |
46 | Expands zip entries to a destination directory.
47 |
48 |
49 |
50 |
51 |
52 |
53 | [`Get-ZipEntryContent`](docs/en-US/Get-ZipEntryContent.md)
54 |
55 |
56 |
57 |
58 | Gets the content of one or more zip entries.
59 |
60 |
61 |
62 |
63 |
64 |
65 | [`New-ZipEntry`](docs/en-US/New-ZipEntry.md)
66 |
67 |
68 | Creates zip entries from specified path or paths.
69 |
70 |
71 |
72 |
73 | [`Remove-ZipEntry`](docs/en-US/Remove-ZipEntry.md)
74 |
75 |
76 | Removes zip entries from one or more zip archives.
77 |
78 |
79 |
80 |
81 | [`Rename-ZipEntry`](docs/en-US/Rename-ZipEntry.md)
82 |
83 |
84 | Renames zip entries from one or more zip archives.
85 |
86 |
87 |
88 |
89 | [`Set-ZipEntryContent`](docs/en-US/Set-ZipEntryContent.md)
90 |
91 |
92 | Sets or appends content to a zip entry.
93 |
94 |
95 |
96 |
97 | [`Compress-ZipArchive`](docs/en-US/Compress-ZipArchive.md)
98 |
99 |
100 |
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 |
107 |
108 |
109 |
110 |
111 | ### Gzip Cmdlets
112 |
113 |
114 |
115 |
116 | Cmdlet
117 | Description
118 |
119 |
120 |
121 |
122 | [`Compress-GzipArchive`](docs/en-US/Compress-GzipArchive.md)
123 |
124 |
125 |
126 | Can compress one or more specified file paths into a Gzip file.
127 |
128 |
129 |
130 |
131 |
132 | [`ConvertFrom-GzipString`](docs/en-US/ConvertFrom-GzipString.md)
133 |
134 |
135 |
136 | Expands Gzip Base64 input strings.
137 |
138 |
139 |
140 |
141 |
142 |
143 | [`ConvertTo-GzipString`](docs/en-US/ConvertTo-GzipString.md)
144 |
145 |
146 |
147 | Can compress input strings into Gzip Base64 strings or raw bytes.
148 |
149 |
150 |
151 |
152 |
153 |
154 | [`Expand-GzipArchive`](docs/en-US/Expand-GzipArchive.md)
155 |
156 |
157 |
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 |
162 |
163 |
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 | Directory
12 |
13 |
14 |
15 |
16 | Type
17 | 10
18 | Left
19 |
20 |
21 | LastWriteTime
22 | 26
23 | Right
24 |
25 |
26 | CompressedSize
27 | 15
28 | Right
29 |
30 |
31 | Size
32 | 15
33 | Right
34 |
35 |
36 | Name
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 |
--------------------------------------------------------------------------------