diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e51104..ce6dcae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: runs-on: windows-latest steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Build module - Debug shell: pwsh @@ -63,7 +63,7 @@ jobs: os: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Restore Built PowerShell Module uses: actions/download-artifact@v8 @@ -117,7 +117,7 @@ jobs: - name: Upload Coverage to codecov if: always() && !startsWith(github.ref, 'refs/tags/v') - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: files: ./output/TestResults/Coverage.xml flags: ${{ matrix.info.name }} diff --git a/.gitignore b/.gitignore index 03ede8b..7c5b799 100644 --- a/.gitignore +++ b/.gitignore @@ -267,6 +267,7 @@ __pycache__/ ### Custom entries ### output/ +rar/ tools/Modules test.settings.json tests/integration/.vagrant diff --git a/.vscode/launch.json b/.vscode/launch.json index fecef85..4956c30 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,7 +18,7 @@ ], "cwd": "${workspaceFolder}", "stopAtEntry": false, - "console": "externalTerminal", + "console": "integratedTerminal", }, { "name": "PowerShell Launch Current File", @@ -27,13 +27,6 @@ "script": "${file}", "cwd": "${workspaceFolder}" }, - { - "name": ".NET FullCLR Attach", - "type": "clr", - "request": "attach", - "processId": "${command:pickProcess}", - "justMyCode": true, - }, { "name": ".NET CoreCLR Attach", "type": "coreclr", diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d68d8..c456086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # CHANGELOG +## 09/29/2026 + +- **RAR Archive Support** + - Added new cmdlets targeting `SharpCompress` to inspect, extract, and read `.rar` archives: + - `Get-RarEntry` + - `Get-RarEntryContent` + - `Expand-RarEntry` + - `Expand-RarArchive` + - Added support for reading and extracting encrypted / password-protected RAR entries. + - Added support for processing RAR entries directly from input streams. + +- **Documentation Updates** + - Updated namespace references to reflect `PSCompression.FormatHandlers.`. + - Updated and clarified descriptions for `-AsByteStream`, `-Raw`, and `-Force` parameters across Tar, Zip, and RAR cmdlet documentation. + +- **CI & Build System** + - Updated GitHub Actions workflows in `.github/workflows/ci.yml`: + - Updated `actions/checkout` to `v7`. + - Updated `codecov/codecov-action` to `v7`. + ## 12/13/2025 - **Native Zip Entry Objects** diff --git a/README.md b/README.md index 9885c74..2615189 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

PSCompression

-Zip, tar, and string compression utilities for PowerShell! +Zip, tar, rar, and string compression utilities for PowerShell!

[![build](https://github.com/santisq/PSCompression/actions/workflows/ci.yml/badge.svg)](https://github.com/santisq/PSCompression/actions/workflows/ci.yml) @@ -11,14 +11,15 @@
-`PSCompression` is a PowerShell module that provides utilities for creating, managing, and extracting zip and tar archives, as well as compressing and decompressing strings. It overcomes limitations in built-in PowerShell archive cmdlets (e.g., 2 GB zip file limits) and supports multiple compression algorithms, including gzip, bzip2, Zstandard, lzip, Brotli, Deflate, and Zlib. Built for cross-platform use, it’s compatible with Windows, Linux, and macOS. +`PSCompression` is a PowerShell module that provides utilities for creating, managing, and extracting zip, tar, and rar archives, as well as compressing and decompressing strings. It overcomes limitations in built-in PowerShell archive cmdlets (e.g., 2 GB zip file limits) and supports multiple compression algorithms, including gzip, bzip2, Zstandard, lzip, Brotli, Deflate, and Zlib. Built for cross-platform use, it’s compatible with Windows, Linux, and macOS. -__New in recent updates:__ Full support for reading and extracting __password-protected (encrypted) zip entries__, including AES encryption. +__New in recent updates:__ Support for __RAR archives__ (listing, reading, and extracting encrypted/password-protected entries) and full support for reading and extracting __password-protected (encrypted) zip entries__, including AES encryption. ## Features - __Zip Archive Management__: Create, list, extract, retrieve content, modify, and remove entries in zip archives with pipeline support. Now includes __full support for reading and extracting password-protected entries__ (traditional ZipCrypto and AES encryption). +- __RAR Archive Management__: List, extract, and retrieve content from RAR entries (including encrypted/password-protected entries and streams). - __Tar Archive Management__: Compress and extract tar archives with support for `gz`, `bz2`, `zst`, `lz`, and uncompressed (`none`) formats. - __Tar Entry Management__: List, extract, and retrieve content from individual tar entries. - __String Compression__: Compress and decompress strings using Brotli, Deflate, Gzip, and Zlib algorithms. @@ -39,6 +40,13 @@ __New in recent updates:__ Full support for reading and extracting __password-pr > [!NOTE] > Due to a .NET limitation, cmdlets like `New-ZipEntry`, `Compress-ZipArchive` with `-Update`, and `Set-ZipEntryContent` may fail when handling files or content > 2 GB __in existing zip archives__. As a workaround, recreate the zip archive or use tools like 7-Zip, which support larger files. See [issue #19](https://github.com/santisq/PSCompression/issues/19) for details. +### RAR Archive + +- [__`Expand-RarArchive`__](docs/en-US/Expand-RarArchive.md) — Extracts a RAR archive to a destination directory. +- [__`Expand-RarEntry`__](docs/en-US/Expand-RarEntry.md) — Extracts individual RAR entries to a destination directory. +- [__`Get-RarEntry`__](docs/en-US/Get-RarEntry.md) — Lists RAR archive entries from paths or streams. +- [__`Get-RarEntryContent`__](docs/en-US/Get-RarEntryContent.md) — Retrieves the content of RAR entries as text or bytes. + ### Tar Archive - [__`Compress-TarArchive`__](docs/en-US/Compress-TarArchive.md) — Compresses files and folders into a tar archive with optional compression (gz, bz2, zst, lz, none). diff --git a/assets/test.rar b/assets/test.rar new file mode 100644 index 0000000..aae788b Binary files /dev/null and b/assets/test.rar differ diff --git a/assets/helloworld.zip b/assets/test.zip similarity index 100% rename from assets/helloworld.zip rename to assets/test.zip diff --git a/assets/testEncrypted.rar b/assets/testEncrypted.rar new file mode 100644 index 0000000..58fc126 Binary files /dev/null and b/assets/testEncrypted.rar differ diff --git a/docs/en-US/Compress-TarArchive.md b/docs/en-US/Compress-TarArchive.md index 076c60b..9944093 100644 --- a/docs/en-US/Compress-TarArchive.md +++ b/docs/en-US/Compress-TarArchive.md @@ -50,7 +50,7 @@ The `Compress-TarArchive` cmdlet creates a tar archive, optionally compressed wi ### Example 1: Compress all `.log` files in a directory ```powershell -Get-ChildItem C:\Logs -Recurse -Filter *.log | +PS /> Get-ChildItem C:\Logs -Recurse -Filter *.log | Compress-TarArchive -Destination C:\Archives\logs.tar.gz ``` @@ -62,7 +62,7 @@ This example demonstrates how to compress all `.log` files in the `C:\Logs` dire ### Example 2: Compress a folder using `Fastest` Compression Level ```powershell -Compress-TarArchive -Path . -Destination myPath.tar.gz -CompressionLevel Fastest +PS /> Compress-TarArchive -Path . -Destination myPath.tar.gz -CompressionLevel Fastest ``` This example shows how to compress the current directory (`.`) into a gzip-compressed tar archive named `myPath.tar.gz` using the `Fastest` compression level for quicker processing. @@ -70,7 +70,7 @@ This example shows how to compress the current directory (`.`) into a gzip-compr ### Example 3: Overwrite an existing tar archive ```powershell -Compress-TarArchive -Path .\Path -Destination dest.tar.gz -Force +PS /> Compress-TarArchive -Path .\Path -Destination dest.tar.gz -Force ``` This example illustrates how to create a new tar archive named `dest.tar.gz` from the path directory, overwriting any existing archive with the same name using the `-Force` parameter. @@ -78,7 +78,7 @@ This example illustrates how to create a new tar archive named `dest.tar.gz` fro ### Example 4: Exclude files and folders from source ```powershell -Compress-TarArchive -Path .\Path -Destination myPath.tar.gz -Exclude *.xyz, *\test\* +PS /> Compress-TarArchive -Path .\Path -Destination myPath.tar.gz -Exclude *.xyz, *\test\* ``` This example shows how to compress all items in `path` excluding all files having a `.xyz` extension, any folder named `test` and all its child items. @@ -90,7 +90,7 @@ This example shows how to compress all items in `path` excluding all files havin ### Example 5: Compress a directory using bzip2 algorithm ```powershell -Compress-TarArchive -Path .\data -Destination C:\Backups\data.tar.bz2 -Algorithm bz2 +PS /> Compress-TarArchive -Path .\data -Destination C:\Backups\data.tar.bz2 -Algorithm bz2 ``` This example demonstrates how to compress the `data` directory into a bzip2-compressed tar archive named `data.tar.bz2` in the `C:\Backups` directory using the `bz2` algorithm. diff --git a/docs/en-US/Compress-ZipArchive.md b/docs/en-US/Compress-ZipArchive.md index d605265..adb144e 100644 --- a/docs/en-US/Compress-ZipArchive.md +++ b/docs/en-US/Compress-ZipArchive.md @@ -65,39 +65,39 @@ This cmdlet handles compression like the `ZipFile.CreateFromDirectory` method bu ### Example 1: Compress all `.ext` files from a specific folder ```powershell -Get-ChildItem .\Path -Recurse -Filter *.ext | +PS /> Get-ChildItem .\Path -Recurse -Filter *.ext | Compress-ZipArchive -Destination dest.zip ``` ### Example 2: Compress all `.txt` files from subfolders in the current directory ```powershell -Compress-ZipArchive .\*\*.txt -Destination dest.zip +PS /> Compress-ZipArchive .\*\*.txt -Destination dest.zip ``` ### Example 3: Compress all `.ext` and `.ext2` from a specific folder ```powershell -Compress-ZipArchive .\*.ext, .\*.ext2 -Destination dest.zip +PS /> Compress-ZipArchive .\*.ext, .\*.ext2 -Destination dest.zip ``` ### Example 4: Compress a folder using `Fastest` Compression Level ```powershell -Compress-ZipArchive .\Path -Destination myPath.zip -CompressionLevel Fastest +PS /> Compress-ZipArchive .\Path -Destination myPath.zip -CompressionLevel Fastest ``` ### Example 5: Compressing all directories in `.\Path` ```powershell -Get-ChildItem .\Path -Recurse -Directory | +PS /> Get-ChildItem .\Path -Recurse -Directory | Compress-ZipArchive -Destination dest.zip ``` ### Example 6: Replacing an existing Zip Archive ```powershell -Compress-ZipArchive -Path .\Path -Destination dest.zip -Force +PS /> Compress-ZipArchive -Path .\Path -Destination dest.zip -Force ``` Demonstrates the use of `-Force` parameter switch. This overwrites any existing archive at the destination path. @@ -105,7 +105,7 @@ Demonstrates the use of `-Force` parameter switch. This overwrites any existing ### Example 7: Adding and updating new entries to an existing Zip Archive ```powershell -Get-ChildItem .\Path -Recurse -Directory | +PS /> Get-ChildItem .\Path -Recurse -Directory | Compress-ZipArchive -Destination dest.zip -Update ``` @@ -114,7 +114,7 @@ Demonstrates the use of `-Update` parameter switch. This adds the directories to ### Example 8: Exclude files and folders from source ```powershell -Compress-ZipArchive .\Path -Destination myPath.zip -Exclude *.xyz, *\test\* +PS /> Compress-ZipArchive .\Path -Destination myPath.zip -Exclude *.xyz, *\test\* ``` This example shows how to compress all items in `Path` excluding all files having a `.xyz` extension and excluding diff --git a/docs/en-US/ConvertFrom-BrotliString.md b/docs/en-US/ConvertFrom-BrotliString.md index c8cdac8..8b6ac6e 100644 --- a/docs/en-US/ConvertFrom-BrotliString.md +++ b/docs/en-US/ConvertFrom-BrotliString.md @@ -30,7 +30,7 @@ The `ConvertFrom-BrotliString` cmdlet decompresses Base64-encoded strings that w ### Example 1: Decompress a Brotli-compressed Base64 string ```powershell -PS ..\pwsh> ConvertFrom-BrotliString CwiAaGVsbG8NCndvcmxkDQohDQoD +PS /> ConvertFrom-BrotliString CwiAaGVsbG8NCndvcmxkDQohDQoD hello world @@ -42,11 +42,11 @@ This example decompresses a Brotli-compressed Base64 string, restoring the origi ### Example 2: Compare default behavior with the `-Raw` switch ```powershell -PS ..\pwsh> $strings = 'hello', 'world', '!' -PS ..\pwsh> $compressed = $strings | ConvertTo-BrotliString -PS ..\pwsh> $decompressed = $compressed | ConvertFrom-BrotliString -Raw -PS ..\pwsh> $decompressed.GetType() # System.String -PS ..\pwsh> $decompressed +PS /> $strings = 'hello', 'world', '!' +PS /> $compressed = $strings | ConvertTo-BrotliString +PS /> $decompressed = $compressed | ConvertFrom-BrotliString -Raw +PS /> $decompressed.GetType() # System.String +PS /> $decompressed hello world diff --git a/docs/en-US/ConvertFrom-DeflateString.md b/docs/en-US/ConvertFrom-DeflateString.md index 5a9e9b9..617bbbf 100644 --- a/docs/en-US/ConvertFrom-DeflateString.md +++ b/docs/en-US/ConvertFrom-DeflateString.md @@ -30,7 +30,7 @@ The ConvertFrom-DeflateString cmdlet decompresses Base64-encoded strings that we ### Example 1: Decompress a Deflate-compressed Base64 string ```powershell -PS ..\pwsh> ConvertFrom-DeflateString ykjNycnn5SrPL8pJ4eVS5OUCAAAA//8DAA== +PS /> ConvertFrom-DeflateString ykjNycnn5SrPL8pJ4eVS5OUCAAAA//8DAA== hello world @@ -42,11 +42,11 @@ This example decompresses a Deflate-compressed Base64 string, restoring the orig ### Example 2: Compare default behavior with the `-Raw` switch ```powershell -PS ..\pwsh> $strings = 'hello', 'world', '!' -PS ..\pwsh> $compressed = $strings | ConvertTo-DeflateString -PS ..\pwsh> $decompressed = $compressed | ConvertFrom-DeflateString -Raw -PS ..\pwsh> $decompressed.GetType() # System.String -PS ..\pwsh> $decompressed +PS /> $strings = 'hello', 'world', '!' +PS /> $compressed = $strings | ConvertTo-DeflateString +PS /> $decompressed = $compressed | ConvertFrom-DeflateString -Raw +PS /> $decompressed.GetType() # System.String +PS /> $decompressed hello world diff --git a/docs/en-US/ConvertFrom-GzipString.md b/docs/en-US/ConvertFrom-GzipString.md index 492d4b5..7c67a15 100644 --- a/docs/en-US/ConvertFrom-GzipString.md +++ b/docs/en-US/ConvertFrom-GzipString.md @@ -30,7 +30,7 @@ The `ConvertFrom-GzipString` cmdlet decompresses Base64-encoded strings that wer ### Example 1: Decompress a GZip-compressed Base64 string ```powershell -PS ..\pwsh> ConvertFrom-GzipString H4sIAAAAAAAACstIzcnJ5+Uqzy/KSeHlUuTlAgBLr/K2EQAAAA== +PS /> ConvertFrom-GzipString H4sIAAAAAAAACstIzcnJ5+Uqzy/KSeHlUuTlAgBLr/K2EQAAAA== hello world @@ -42,11 +42,11 @@ This example decompresses a GZip-compressed Base64 string, restoring the origina ### Example 2: Compare default behavior with the `-Raw` switch ```powershell -PS ..\pwsh> $strings = 'hello', 'world', '!' -PS ..\pwsh> $compressed = $strings | ConvertTo-GzipString -PS ..\pwsh> $decompressed = $compressed | ConvertFrom-GzipString -Raw -PS ..\pwsh> $decompressed.GetType() # System.String -PS ..\pwsh> $decompressed +PS /> $strings = 'hello', 'world', '!' +PS /> $compressed = $strings | ConvertTo-GzipString +PS /> $decompressed = $compressed | ConvertFrom-GzipString -Raw +PS /> $decompressed.GetType() # System.String +PS /> $decompressed hello world diff --git a/docs/en-US/ConvertFrom-ZLibString.md b/docs/en-US/ConvertFrom-ZLibString.md index 78184eb..90c63a3 100644 --- a/docs/en-US/ConvertFrom-ZLibString.md +++ b/docs/en-US/ConvertFrom-ZLibString.md @@ -31,7 +31,7 @@ This cmdlet is the counterpart of [`ConvertTo-ZLibString`](./ConvertTo-ZLibStrin ### Example 1: Decompress a ZLib-compressed Base64 string ```powershell -PS ..\pwsh> ConvertFrom-ZLibString eJzKSM3JyeflKs8vyknh5VLk5QIAAAD//wMAMosEow== +PS /> ConvertFrom-ZLibString eJzKSM3JyeflKs8vyknh5VLk5QIAAAD//wMAMosEow== hello world @@ -43,11 +43,11 @@ This example decompresses a ZLib-compressed Base64 string, restoring the origina ### Example 2: Compare default behavior with the `-Raw` switch ```powershell -PS ..\pwsh> $strings = 'hello', 'world', '!' -PS ..\pwsh> $compressed = $strings | ConvertTo-ZlibString -PS ..\pwsh> $decompressed = $compressed | ConvertFrom-ZlibString -Raw -PS ..\pwsh> $decompressed.GetType() # System.String -PS ..\pwsh> $decompressed +PS /> $strings = 'hello', 'world', '!' +PS /> $compressed = $strings | ConvertTo-ZlibString +PS /> $decompressed = $compressed | ConvertFrom-ZlibString -Raw +PS /> $decompressed.GetType() # System.String +PS /> $decompressed hello world diff --git a/docs/en-US/ConvertTo-BrotliString.md b/docs/en-US/ConvertTo-BrotliString.md index b7a6692..c747ea9 100644 --- a/docs/en-US/ConvertTo-BrotliString.md +++ b/docs/en-US/ConvertTo-BrotliString.md @@ -32,13 +32,13 @@ The `ConvertTo-BrotliString` cmdlet compresses input strings into Brotli-compres ### Example 1: Compress strings into a Brotli-compressed Base64 string ```powershell -PS ..\pwsh> $strings = 'hello', 'world', '!' -PS ..\pwsh> ConvertTo-BrotliString $strings +PS /> $strings = 'hello', 'world', '!' +PS /> ConvertTo-BrotliString $strings CwiAaGVsbG8NCndvcmxkDQohDQoD # Or using pipeline input -PS ..\pwsh> $strings | ConvertTo-BrotliString +PS /> $strings | ConvertTo-BrotliString CwiAaGVsbG8NCndvcmxkDQohDQoD ``` @@ -48,11 +48,11 @@ This example shows how to compress an array of strings into a single Brotli-comp ### Example 2: Save Brotli-compressed bytes to a file using `-AsByteStream` ```powershell -PS ..\pwsh> 'hello world!' | ConvertTo-BrotliString -AsByteStream | Set-Content -FilePath .\helloworld.br -AsByteStream +PS /> 'hello world!' | ConvertTo-BrotliString -AsByteStream | Set-Content -FilePath .\helloworld.br -AsByteStream # To read the file back you can use `ConvertFrom-BrotliString` following these steps: -PS ..\pwsh> $path = Convert-Path .\helloworld.br -PS ..\pwsh> [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path)) | ConvertFrom-BrotliString +PS /> $path = Convert-Path .\helloworld.br +PS /> [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path)) | ConvertFrom-BrotliString hello world! ``` @@ -65,10 +65,10 @@ This example shows how to use `-AsByteStream` to output raw compressed bytes tha ### Example 3: Compress strings using a specific Encoding ```powershell -PS ..\pwsh> 'ñ' | ConvertTo-BrotliString -Encoding ansi | ConvertFrom-BrotliString +PS /> 'ñ' | ConvertTo-BrotliString -Encoding ansi | ConvertFrom-BrotliString � -PS ..\pwsh> 'ñ' | ConvertTo-BrotliString -Encoding utf8BOM | ConvertFrom-BrotliString +PS /> 'ñ' | ConvertTo-BrotliString -Encoding utf8BOM | ConvertFrom-BrotliString ñ ``` @@ -78,11 +78,11 @@ This example shows how different encodings affect the compression and decompress ```powershell # Check the total length of the files -PS ..\pwsh> (Get-Content myLogs\*.txt | Measure-Object Length -Sum).Sum / 1kb +PS /> (Get-Content myLogs\*.txt | Measure-Object Length -Sum).Sum / 1kb 87.216796875 # Check the total length after compression -PS ..\pwsh> (Get-Content myLogs\*.txt | ConvertTo-BrotliString).Length / 1kb +PS /> (Get-Content myLogs\*.txt | ConvertTo-BrotliString).Length / 1kb 35.123456789 ``` diff --git a/docs/en-US/ConvertTo-DeflateString.md b/docs/en-US/ConvertTo-DeflateString.md index 0c29005..e84fa72 100644 --- a/docs/en-US/ConvertTo-DeflateString.md +++ b/docs/en-US/ConvertTo-DeflateString.md @@ -32,13 +32,13 @@ The `ConvertTo-DeflateString` cmdlet compresses input strings into Deflate-compr ### Example 1: Compress strings to Deflate compressed Base64 encoded string ```powershell -PS ..\pwsh> $strings = 'hello', 'world', '!' -PS ..\pwsh> ConvertTo-DeflateString $strings +PS /> $strings = 'hello', 'world', '!' +PS /> ConvertTo-DeflateString $strings ykjNycnn5SrPL8pJ4eVS5OUCAAAA//8DAA== # Or using pipeline input -PS ..\pwsh> $strings | ConvertTo-DeflateString +PS /> $strings | ConvertTo-DeflateString ykjNycnn5SrPL8pJ4eVS5OUCAAAA//8DAA== ``` @@ -48,11 +48,11 @@ This example shows how to compress an array of strings into a single Deflate-com ### Example 2: Save Deflate-compressed bytes to a file using `-AsByteStream` ```powershell -PS ..\pwsh> 'hello world!' | ConvertTo-DeflateString -AsByteStream | Set-Content -FilePath .\helloworld.deflate -AsByteStream +PS /> 'hello world!' | ConvertTo-DeflateString -AsByteStream | Set-Content -FilePath .\helloworld.deflate -AsByteStream # To read the file back you can use `ConvertFrom-DeflateString` following these steps: -PS ..\pwsh> $path = Convert-Path .\helloworld.deflate -PS ..\pwsh> [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path)) | ConvertFrom-DeflateString +PS /> $path = Convert-Path .\helloworld.deflate +PS /> [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path)) | ConvertFrom-DeflateString hello world! ``` @@ -65,10 +65,10 @@ This example shows how to use `-AsByteStream` to output raw compressed bytes tha ### Example 3: Compress strings using a specific Encoding ```powershell -PS ..\pwsh> 'ñ' | ConvertTo-DeflateString -Encoding ansi | ConvertFrom-DeflateString +PS /> 'ñ' | ConvertTo-DeflateString -Encoding ansi | ConvertFrom-DeflateString � -PS ..\pwsh> 'ñ' | ConvertTo-DeflateString -Encoding utf8BOM | ConvertFrom-DeflateString +PS /> 'ñ' | ConvertTo-DeflateString -Encoding utf8BOM | ConvertFrom-DeflateString ñ ``` @@ -78,11 +78,11 @@ This example shows how different encodings affect the compression and decompress ```powershell # Check the total length of the files -PS ..\pwsh> (Get-Content myLogs\*.txt | Measure-Object Length -Sum).Sum / 1kb +PS /> (Get-Content myLogs\*.txt | Measure-Object Length -Sum).Sum / 1kb 87.216796875 # Check the total length after compression -PS ..\pwsh> (Get-Content myLogs\*.txt | ConvertTo-DeflateString).Length / 1kb +PS /> (Get-Content myLogs\*.txt | ConvertTo-DeflateString).Length / 1kb 35.123456789 ``` diff --git a/docs/en-US/ConvertTo-GzipString.md b/docs/en-US/ConvertTo-GzipString.md index 37bc9f1..1f54cc4 100644 --- a/docs/en-US/ConvertTo-GzipString.md +++ b/docs/en-US/ConvertTo-GzipString.md @@ -32,15 +32,15 @@ The `ConvertTo-GzipString` cmdlet compresses input strings into Gzip-compressed ### Example 1: Compress strings into a GZip-compressed Base64 string ```powershell -PS ..\pwsh> $strings = 'hello', 'world', '!' +PS /> $strings = 'hello', 'world', '!' # With positional binding -PS ..\pwsh> ConvertTo-GzipString $strings +PS /> ConvertTo-GzipString $strings H4sIAAAAAAAEAMtIzcnJ5+Uqzy/KSeHlUuTlAgBLr/K2EQAAAA== # Or pipeline input, both work -PS ..\pwsh> $strings | ConvertTo-GzipString +PS /> $strings | ConvertTo-GzipString H4sIAAAAAAAEAMtIzcnJ5+Uqzy/KSeHlUuTlAgBLr/K2EQAAAA== ``` @@ -50,11 +50,11 @@ This example shows how to compress an array of strings into a single Gzip-compre ### Example 2: Save Gzip-compressed bytes to a file using `-AsByteStream` ```powershell -PS ..\pwsh> 'hello world!' | ConvertTo-GzipString -AsByteStream | Set-Content -FilePath .\helloworld.gz -AsByteStream +PS /> 'hello world!' | ConvertTo-GzipString -AsByteStream | Set-Content -FilePath .\helloworld.gz -AsByteStream # To read the file back you can use `ConvertFrom-GzipString` following these steps: -PS ..\pwsh> $path = Convert-Path .\helloworld.gz -PS ..\pwsh> [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path)) | ConvertFrom-GzipString +PS /> $path = Convert-Path .\helloworld.gz +PS /> [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path)) | ConvertFrom-GzipString hello world! ``` @@ -67,10 +67,10 @@ This example shows how to use `-AsByteStream` to output raw compressed bytes tha ### Example 3: Compress strings using a specific Encoding ```powershell -PS ..\pwsh> 'ñ' | ConvertTo-GzipString -Encoding ansi | ConvertFrom-GzipString +PS /> 'ñ' | ConvertTo-GzipString -Encoding ansi | ConvertFrom-GzipString � -PS ..\pwsh> 'ñ' | ConvertTo-GzipString -Encoding utf8BOM | ConvertFrom-GzipString +PS /> 'ñ' | ConvertTo-GzipString -Encoding utf8BOM | ConvertFrom-GzipString ñ ``` @@ -80,11 +80,11 @@ This example shows how different encodings affect the compression and decompress ```powershell # Check the total length of the files -PS ..\pwsh> (Get-Content myLogs\*.txt | Measure-Object Length -Sum).Sum / 1kb +PS /> (Get-Content myLogs\*.txt | Measure-Object Length -Sum).Sum / 1kb 87.216796875 # Check the total length after compression -PS ..\pwsh> (Get-Content myLogs\*.txt | ConvertTo-GzipString).Length / 1kb +PS /> (Get-Content myLogs\*.txt | ConvertTo-GzipString).Length / 1kb 35.123456789 ``` diff --git a/docs/en-US/ConvertTo-ZLibString.md b/docs/en-US/ConvertTo-ZLibString.md index e68665a..669b831 100644 --- a/docs/en-US/ConvertTo-ZLibString.md +++ b/docs/en-US/ConvertTo-ZLibString.md @@ -32,13 +32,13 @@ The `ConvertTo-ZLibString` cmdlet compresses input strings into ZLib-compressed ### Example 1: Compress strings into a ZLib-compressed Base64 string ```powershell -PS ..\pwsh> $strings = 'hello', 'world', '!' -PS ..\pwsh> ConvertTo-ZLibString $strings +PS /> $strings = 'hello', 'world', '!' +PS /> ConvertTo-ZLibString $strings eJzKSM3JyeflKs8vyknh5VLk5QIAAAD//wMAMosEow== # Or using pipeline input -PS ..\pwsh> $strings | ConvertTo-ZLibString +PS /> $strings | ConvertTo-ZLibString eJzKSM3JyeflKs8vyknh5VLk5QIAAAD//wMAMosEow== ``` @@ -48,11 +48,11 @@ This example shows how to compress an array of strings into a single ZLib-compre ### Example 2: Save ZLib-compressed bytes to a file using `-AsByteStream` ```powershell -PS ..\pwsh> 'hello world!' | ConvertTo-ZLibString -AsByteStream | Set-Content -FilePath .\helloworld.zlib -AsByteStream +PS /> 'hello world!' | ConvertTo-ZLibString -AsByteStream | Set-Content -FilePath .\helloworld.zlib -AsByteStream # To read the file back you can use `ConvertFrom-ZLibString` following these steps: -PS ..\pwsh> $path = Convert-Path .\helloworld.zlib -PS ..\pwsh> [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path)) | ConvertFrom-ZLibString +PS /> $path = Convert-Path .\helloworld.zlib +PS /> [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path)) | ConvertFrom-ZLibString hello world! ``` @@ -65,10 +65,10 @@ This example shows how to use `-AsByteStream` to output raw compressed bytes tha ### Example 3: Compress strings using a specific Encoding ```powershell -PS ..\pwsh> 'ñ' | ConvertTo-ZLibString -Encoding ansi | ConvertFrom-ZLibString +PS /> 'ñ' | ConvertTo-ZLibString -Encoding ansi | ConvertFrom-ZLibString � -PS ..\pwsh> 'ñ' | ConvertTo-ZLibString -Encoding utf8BOM | ConvertFrom-ZLibString +PS /> 'ñ' | ConvertTo-ZLibString -Encoding utf8BOM | ConvertFrom-ZLibString ñ ``` @@ -78,11 +78,11 @@ This example shows how different encodings affect the compression and decompress ```powershell # Check the total length of the files -PS ..\pwsh> (Get-Content myLogs\*.txt | Measure-Object Length -Sum).Sum / 1kb +PS /> (Get-Content myLogs\*.txt | Measure-Object Length -Sum).Sum / 1kb 87.216796875 # Check the total length after compression -PS ..\pwsh> (Get-Content myLogs\*.txt | ConvertTo-ZLibString).Length / 1kb +PS /> (Get-Content myLogs\*.txt | ConvertTo-ZLibString).Length / 1kb 35.123456789 ``` diff --git a/docs/en-US/Expand-RarArchive.md b/docs/en-US/Expand-RarArchive.md new file mode 100644 index 0000000..bf6454f --- /dev/null +++ b/docs/en-US/Expand-RarArchive.md @@ -0,0 +1,221 @@ +--- +external help file: PSCompression.dll-Help.xml +Module Name: PSCompression +online version: +schema: 2.0.0 +--- + +# Expand-RarArchive + +## SYNOPSIS + +Extracts files and directories from a RAR archive. + +## SYNTAX + +### Path + +```powershell +Expand-RarArchive + [-Path] + [[-Destination] ] + [-Password ] + [-Force] + [-PassThru] + [] +``` + +### LiteralPath + +```powershell +Expand-RarArchive + [-Password ] + [[-Destination] ] + [-Force] + [-PassThru] + -LiteralPath + [] +``` + +## DESCRIPTION + +The `Expand-RarArchive` cmdlet extracts files and directories from RAR archives. It utilizes the `SharpCompress` library under the hood for archive decompression. + +By default, contents are extracted to the current working location unless specified otherwise using the `-Destination` parameter. If an archive is password-protected and the `-Password` parameter is not supplied, the cmdlet automatically prompts the user for credentials. Use `-Force` to overwrite existing extracted files and `-PassThru` to output `FileInfo` and `DirectoryInfo` objects representing the extracted items. + +## EXAMPLES + +### Example 1: Extract a RAR archive to the current location + +```powershell +PS /> Expand-RarArchive -Path .\archive.rar +PS /> Get-ChildItem + + Directory: C:\ + +Mode LastWriteTime Length Name +---- ------------- ------ ---- +d---- 2026-09-29 2:00 PM folder1 +-a--- 2026-09-29 2:00 PM 1024 file1.txt +-a--- 2026-09-29 2:00 PM 2048 file2.txt +``` + +This example extracts the contents of `archive.rar` to the current working directory, recreating the archived directory structure and files. + +### Example 2: Extract an encrypted RAR archive to a specific destination + +```powershell +PS /> $SecurePassword = Read-Host -AsSecureString +PS /> Expand-RarArchive -Path .\encrypted.rar -Destination .\output -Password $SecurePassword +``` + +This example extracts a password-protected RAR archive to the `.\output` directory using a pre-constructed `SecureString` password. + +### Example 3: Pipeline input with PassThru + +```powershell +PS /> Get-ChildItem *.rar | Expand-RarArchive -Destination .\extracted -PassThru + + Directory: C:\extracted + +Mode LastWriteTime Length Name +---- ------------- ------ ---- +d---- 2026-09-29 2:00 PM folder1 +-a--- 2026-09-29 2:00 PM 1024 file1.txt +-a--- 2026-09-29 2:00 PM 2048 file2.txt +``` + +This example pipes multiple `.rar` files to `Expand-RarArchive` and uses `-PassThru` to return `FileInfo` and `DirectoryInfo` objects for all extracted files and directories. + +## PARAMETERS + +### -Destination + +Specifies the path to the directory where the archive contents are extracted. If omitted, extraction defaults to the current working directory. The target directory is automatically created if it does not already exist. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force + +Overwrites existing files in the destination path without prompting. Without this switch, existing files will generate an error during extraction. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -LiteralPath + +Specifies the exact path(s) to the RAR archive(s) to extract. Unlike `-Path`, `-LiteralPath` does not evaluate wildcard characters and treats string input literally. + +```yaml +Type: String[] +Parameter Sets: LiteralPath +Aliases: PSPath + +Required: True +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -PassThru + +Outputs `System.IO.FileInfo` and `System.IO.DirectoryInfo` decorated objects for each item extracted from the archive. By default, this cmdlet produces no output. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Password + +Specifies a password as a `SecureString` for extracting encrypted RAR archives. + +> [!NOTE] +> If the archive is encrypted and this parameter is omitted, the cmdlet prompts the host interface interactively for a password. + +```yaml +Type: SecureString +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Path + +Specifies the path(s) to the RAR archive(s) to extract. Supports wildcard characters, allowing multiple archives to be targeted. + +```yaml +Type: String[] +Parameter Sets: Path +Aliases: + +Required: True +Position: 0 +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: True +``` + +### CommonParameters + +This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.String[] + +You can pipe paths to RAR archives to this cmdlet via the `-Path` or `-LiteralPath` parameters. + +## OUTPUTS + +### None + +By default, this cmdlet returns no output. + +### System.IO.FileInfo + +### System.IO.DirectoryInfo + +When `-PassThru` is specified, the cmdlet outputs FileInfo and DirectoryInfo objects representing extracted files and folders. + +## NOTES + +## RELATED LINKS + +[__`Get-RarEntry`__](Get-RarEntry.md) + +[__`Expand-RarEntry`__](Expand-RarEntry.md) + +[__SharpCompress__](https://github.com/adamhathcock/sharpcompress) diff --git a/docs/en-US/Expand-RarEntry.md b/docs/en-US/Expand-RarEntry.md new file mode 100644 index 0000000..c19468a --- /dev/null +++ b/docs/en-US/Expand-RarEntry.md @@ -0,0 +1,208 @@ +--- +external help file: PSCompression.dll-Help.xml +Module Name: PSCompression +online version: https://github.com/santisq/PSCompression +schema: 2.0.0 +--- + +# Expand-RarEntry + +## SYNOPSIS + +Extracts selected RAR archive entries to a destination directory while preserving their relative paths. + +## SYNTAX + +```powershell +Expand-RarEntry + -InputObject + [[-Destination] ] + [-Password ] + [-Force] + [-PassThru] + [] +``` + +## DESCRIPTION + +The `Expand-RarEntry` cmdlet extracts RAR entries produced by [`Get-RarEntry`](Get-RarEntry.md) to a destination directory. Extracted entries preserve their original relative paths and directory structure from the archive. It supports extracting entries from RAR archives on disk as well as from streams, including password-protected entries. + +## EXAMPLES + +### Example 1: Extract all `.txt` files from a RAR archive to the current directory + +```powershell +PS /> Get-RarEntry .\archive.rar -Include *.txt | Expand-RarEntry +``` + +This example extracts only the `.txt` files from `archive.rar` to the current directory, preserving their relative paths within the archive. + +### Example 2: Extract all `.txt` files from a RAR archive to a specific directory + +```powershell +PS /> Get-RarEntry .\archive.rar -Include *.txt | Expand-RarEntry -Destination .\extracted +``` + +This example extracts only the `.txt` files from a RAR archive to the specified `.\extracted` directory (created automatically if needed). + +### Example 3: Extract all entries excluding `.txt` files from a RAR archive + +```powershell +PS /> Get-RarEntry .\archive.rar -Exclude *.txt | Expand-RarEntry +``` + +This example extracts everything except `.txt` files from `archive.rar` to the current directory, preserving the original structure. + +### Example 4: Extract entries overwriting existing files + +```powershell +PS /> Get-RarEntry .\archive.rar -Include *.txt | Expand-RarEntry -Force +``` + +This example extracts the `.txt` files and overwrites any existing files with the same name in the destination due to the `-Force` switch. + +### Example 5: Extract entries and output the expanded items + +```powershell +PS /> Get-RarEntry .\archive.rar -Exclude *.txt | Expand-RarEntry -PassThru + + Directory: C:\ + +Mode LastWriteTime Length Name +---- ------------- ------ ---- +d---- 2026-09-29 2:00 PM folder1 +-a--- 2026-09-29 2:00 PM 2048 image.png +``` + +This example extracts everything except `.txt` files and uses `-PassThru` to output `FileInfo` and `DirectoryInfo` objects for the extracted items. By default, the cmdlet produces no output. + +### Example 6: Extract a password-protected entry to the current directory + +```powershell +PS /> Get-RarEntry .\archive.rar -Include secret.txt | Expand-RarEntry -Password (Read-Host -AsSecureString) +``` + +This example demonstrates how to expand an encrypted RAR entry using `Read-Host -AsSecureString` to provide the password. + +> [!NOTE] +> If an entry is encrypted and no password is supplied, the cmdlet will prompt for one interactively. + +## PARAMETERS + +### -Destination + +The destination directory where RAR entries are extracted. If not specified, entries are extracted to their relative path in the current directory, creating any necessary subdirectories. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force + +Overwrites existing files in the destination directory. Without `-Force`, attempting to extract an entry over an existing file results in a non-terminating error. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InputObject + +The RAR entries to extract. These are instances of RarEntry output by the [`Get-RarEntry`](Get-RarEntry.md) cmdlet. + +> [!NOTE] +> This parameter accepts pipeline input from `Get-RarEntry`. Binding by property name is also supported. + +```yaml +Type: RarEntry[] +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -PassThru + +Outputs `System.IO.FileInfo` and `System.IO.DirectoryInfo` objects for the extracted entries. By default, the cmdlet produces no output. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Password + +Specifies the password as a `SecureString` to extract an encrypted RAR entry. + +> [!NOTE] +> If an entry is encrypted and no password is supplied, the cmdlet will prompt for one. + +```yaml +Type: SecureString +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### PSCompression.FormatHandlers.Rar.RarEntry[] + +You can pipe instances of RarEntry from [`Get-RarEntry`](Get-RarEntry.md) to this cmdlet. + +## OUTPUTS + +### None + +By default, this cmdlet produces no output. + +### System.IO.FileInfo + +### System.IO.DirectoryInfo + +When the `-PassThru` switch is used, the cmdlet outputs `FileInfo` and `DirectoryInfo` objects representing the extracted items. + +## NOTES + +## RELATED LINKS + +[__`Get-RarEntry`__](Get-RarEntry.md) + +[__`Expand-RarArchive`__](Expand-RarArchive.md) + +[__SharpCompress__](https://github.com/adamhathcock/sharpcompress) diff --git a/docs/en-US/Expand-TarArchive.md b/docs/en-US/Expand-TarArchive.md index 02e9af0..3a1dcb5 100644 --- a/docs/en-US/Expand-TarArchive.md +++ b/docs/en-US/Expand-TarArchive.md @@ -49,8 +49,9 @@ The `Expand-TarArchive` cmdlet extracts files and directories from a tar archive ### Example 1: Extract an uncompressed tar archive ```powershell -PS C:\> Expand-TarArchive -Path .\archive.tar -PS C:\> Get-ChildItem +PS /> Expand-TarArchive -Path .\archive.tar +PS /> Get-ChildItem + Directory: C:\ Mode LastWriteTime Length Name @@ -65,8 +66,9 @@ This example extracts the contents of an uncompressed `archive.tar` file to the ### Example 2: Extract a gzip-compressed tar archive to a specific destination ```powershell -PS C:\> Expand-TarArchive -Path .\archive.tar.gz -Destination .\extracted -PS C:\> Get-ChildItem .\extracted +PS /> Expand-TarArchive -Path .\archive.tar.gz -Destination .\extracted +PS /> Get-ChildItem .\extracted + Directory: C:\extracted Mode LastWriteTime Length Name @@ -81,7 +83,7 @@ This example extracts a gzip-compressed tar archive (`archive.tar.gz`) to the sp ### Example 3: Extract multiple Zstandard-compressed tar archives with PassThru ```powershell -PS C:\> Get-ChildItem *.tar.zst | Expand-TarArchive -PassThru +PS /> Get-ChildItem *.tar.zst | Expand-TarArchive -PassThru Directory: C:\ @@ -99,8 +101,9 @@ This example pipes multiple Zstandard-compressed tar archives (`.tar.zst`) to th ### Example 4: Overwrite existing files with `-Force` ```powershell -PS C:\> Expand-TarArchive -Path .\archive.tar -Destination .\extracted -Force -PS C:\> Get-ChildItem .\extracted +PS /> Expand-TarArchive -Path .\archive.tar -Destination .\extracted -Force +PS /> Get-ChildItem .\extracted + Directory: C:\extracted Mode LastWriteTime Length Name @@ -156,7 +159,7 @@ Accept wildcard characters: False ### -Force -Overwrites existing files in the destination directory without prompting. Without `-Force`, the cmdlet skips files that already exist. +Overwrites existing files in the destination directory. Without `-Force`, attempting to extract an entry over an existing file results in a non-terminating error. ```yaml Type: SwitchParameter @@ -234,7 +237,9 @@ You can pipe paths to tar archives to this cmdlet via the `-Path` parameter or p By default, this cmdlet returns no output. -### System.IO.FileSystemInfo +### System.IO.FileInfo + +### System.IO.DirectoryInfo When the `-PassThru` parameter is used, the cmdlet outputs `FileInfo` and `DirectoryInfo` objects representing the extracted items. diff --git a/docs/en-US/Expand-TarEntry.md b/docs/en-US/Expand-TarEntry.md index cd69fc5..beea008 100644 --- a/docs/en-US/Expand-TarEntry.md +++ b/docs/en-US/Expand-TarEntry.md @@ -31,7 +31,7 @@ The `Expand-TarEntry` cmdlet extracts tar entries produced by [`Get-TarEntry`](. ### Example 1: Extract all `.txt` files from a tar archive to the current directory ```powershell -PS C:\> Get-TarEntry .\archive.tar -Include *.txt | Expand-TarEntry +PS /> Get-TarEntry .\archive.tar -Include *.txt | Expand-TarEntry ``` This example extracts only the `.txt` files from `archive.tar` to the current directory, preserving their relative paths within the archive. @@ -39,7 +39,7 @@ This example extracts only the `.txt` files from `archive.tar` to the current di ### Example 2: Extract all `.txt` files from a tar archive to a specific directory ```powershell -PS C:\> Get-TarEntry .\archive.tar.gz -Include *.txt | Expand-TarEntry -Destination .\extracted +PS /> Get-TarEntry .\archive.tar.gz -Include *.txt | Expand-TarEntry -Destination .\extracted ``` This example extracts only the `.txt` files from a gzip-compressed tar archive to the specified `.\extracted` directory (created automatically if needed). @@ -47,7 +47,7 @@ This example extracts only the `.txt` files from a gzip-compressed tar archive t ### Example 3: Extract all entries excluding `.txt` files from a tar archive ```powershell -PS C:\> Get-TarEntry .\archive.tar -Exclude *.txt | Expand-TarEntry +PS /> Get-TarEntry .\archive.tar -Exclude *.txt | Expand-TarEntry ``` This example extracts everything except `.txt` files from `archive.tar` to the current directory, preserving the original structure. @@ -55,7 +55,7 @@ This example extracts everything except `.txt` files from `archive.tar` to the c ### Example 4: Extract entries overwriting existing files ```powershell -PS C:\> Get-TarEntry .\archive.tar -Include *.txt | Expand-TarEntry -Force +PS /> Get-TarEntry .\archive.tar -Include *.txt | Expand-TarEntry -Force ``` This example extracts the `.txt` files and overwrites any existing files with the same name in the destination due to the `-Force` switch. @@ -63,7 +63,7 @@ This example extracts the `.txt` files and overwrites any existing files with th ### Example 5: Extract entries and output the expanded items ```powershell -PS C:\> Get-TarEntry .\archive.tar -Exclude *.txt | Expand-TarEntry -PassThru +PS /> Get-TarEntry .\archive.tar -Exclude *.txt | Expand-TarEntry -PassThru Directory: C:\ @@ -78,8 +78,8 @@ This example extracts everything except `.txt` files and uses `-PassThru` to out ### Example 6: Extract a specific entry from a compressed tar archive ```powershell -PS C:\> $stream = Invoke-WebRequest https://example.com/archive.tar.gz -PS C:\> $stream | Get-TarEntry -Include readme.md -Algorithm gz | Expand-TarEntry -PassThru | Get-Content +PS /> $stream = Invoke-WebRequest https://example.com/archive.tar.gz +PS /> $stream | Get-TarEntry -Include readme.md -Algorithm gz | Expand-TarEntry -PassThru | Get-Content ``` This example extracts only the `readme.md` file from a gzip-compressed tar archive streamed from the web and immediately displays its contents. @@ -107,7 +107,7 @@ Accept wildcard characters: False ### -Force -Overwrites existing files in the destination directory. Without `-Force`, existing files are skipped. +Overwrites existing files in the destination directory. Without `-Force`, attempting to extract an entry over an existing file results in a non-terminating error. ```yaml Type: SwitchParameter @@ -172,7 +172,9 @@ You can pipe instances of `TarEntryFile` or `TarEntryDirectory` from [`Get-TarEn By default, this cmdlet produces no output. -### System.IO.FileSystemInfo +### System.IO.FileInfo + +### System.IO.DirectoryInfo When the `-PassThru` switch is used, the cmdlet outputs `FileInfo` and `DirectoryInfo` objects representing the extracted items. diff --git a/docs/en-US/Expand-ZipEntry.md b/docs/en-US/Expand-ZipEntry.md index 128b000..3a9e60a 100644 --- a/docs/en-US/Expand-ZipEntry.md +++ b/docs/en-US/Expand-ZipEntry.md @@ -32,7 +32,7 @@ The `Expand-ZipEntry` cmdlet extracts zip entries produced by [`Get-ZipEntry`](. ### Example 1: Extract all `.txt` files from a Zip Archive to the current directory ```powershell -PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Include *.txt | Expand-ZipEntry +PS /> Get-ZipEntry path\to\myZip.zip -Include *.txt | Expand-ZipEntry ``` This example extracts only the `.txt` files from a zip archive to the current directory, preserving their relative paths within the archive. @@ -40,7 +40,7 @@ This example extracts only the `.txt` files from a zip archive to the current di ### Example 2: Extract all `.txt` files from a Zip Archive to the a desired directory ```powershell -PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Include *.txt | Expand-ZipEntry -Destination path\to\myfolder +PS /> Get-ZipEntry path\to\myZip.zip -Include *.txt | Expand-ZipEntry -Destination path\to\myfolder ``` This example extracts only the `.txt` files from a zip archive to the specified destination directory (created automatically if needed). @@ -48,7 +48,7 @@ This example extracts only the `.txt` files from a zip archive to the specified ### Example 3: Extract all entries excluding `.txt` files to the current directory ```powershell -PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Exclude *.txt | Expand-ZipEntry +PS /> Get-ZipEntry path\to\myZip.zip -Exclude *.txt | Expand-ZipEntry ``` This example extracts everything except `.txt` files from a zip archive to the current directory, preserving the original structure. @@ -56,7 +56,7 @@ This example extracts everything except `.txt` files from a zip archive to the c ### Example 4: Extract all entries excluding `.txt` files to the current directory overwritting existing files ```powershell -PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Exclude *.txt | Expand-ZipEntry -Force +PS /> Get-ZipEntry path\to\myZip.zip -Exclude *.txt | Expand-ZipEntry -Force ``` This example extracts everything except `.txt` files and overwrites any existing files with the same name due to the `-Force` switch. @@ -64,7 +64,7 @@ This example extracts everything except `.txt` files and overwrites any existing ### Example 5: Extract all entries excluding `.txt` files to the current directory outputting the expanded entries ```powershell -PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Exclude *.txt | Expand-ZipEntry -PassThru +PS /> Get-ZipEntry path\to\myZip.zip -Exclude *.txt | Expand-ZipEntry -PassThru ``` This example extracts everything except `.txt` files and uses `-PassThru` to output `FileInfo` and `DirectoryInfo` objects for the extracted items. By default, the cmdlet produces no output. @@ -72,9 +72,9 @@ This example extracts everything except `.txt` files and uses `-PassThru` to out ### Example 6: Extract an entry from input Stream ```powershell -PS ..\pwsh> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression -PS ..\pwsh> $file = $package | Get-ZipEntry -Include *.psd1 | Expand-ZipEntry -PassThru -PS ..\pwsh> Get-Content $file.FullName -Raw | Invoke-Expression +PS /> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression +PS /> $file = $package | Get-ZipEntry -Include *.psd1 | Expand-ZipEntry -PassThru +PS /> Get-Content $file.FullName -Raw | Invoke-Expression Name Value ---- ----- @@ -100,12 +100,12 @@ This example downloads a NuGet package (which is a zip archive) from PowerShell ### Example 7: Expand a password protected entry to the current directory ```powershell -PS ..\pwsh> Get-ZipEntry .\myZip.zip -Include myEncryptedEntry.txt | Expand-ZipEntry -Password (Read-Host -AsSecureString) +PS /> Get-ZipEntry .\myZip.zip -Include myEncryptedEntry.txt | Expand-ZipEntry -Password (Read-Host -AsSecureString) ``` This example demonstrates how to expand an encrypted entry using `Read-Host -AsSecureString` to provide the password. -> [!TIP] +> [!NOTE] > If an entry is encrypted and no password is supplied, the cmdlet will prompt for one. ## PARAMETERS @@ -131,7 +131,7 @@ Accept wildcard characters: False ### -Force -Overwrites existing files in the destination directory. Without `-Force`, existing files are skipped. +Overwrites existing files in the destination directory. Without `-Force`, attempting to extract an entry over an existing file results in a non-terminating error. ```yaml Type: SwitchParameter @@ -165,7 +165,7 @@ Accept wildcard characters: False Specifies the password as a `SecureString` to extract the encrypted zip entry. -> [!TIP] +> [!NOTE] > If an entry is encrypted and no password is supplied, the cmdlet will prompt for one. ```yaml @@ -217,7 +217,9 @@ You can pipe instances of `ZipEntryFile` or `ZipEntryDirectory` to this cmdlet. By default, this cmdlet produces no output. -### System.IO.FileSystemInfo +### System.IO.FileInfo + +### System.IO.DirectoryInfo When the `-PassThru` switch is used, the cmdlet outputs `FileInfo` and `DirectoryInfo` objects representing the extracted items. diff --git a/docs/en-US/Get-RarEntry.md b/docs/en-US/Get-RarEntry.md new file mode 100644 index 0000000..a5e2015 --- /dev/null +++ b/docs/en-US/Get-RarEntry.md @@ -0,0 +1,265 @@ +--- +external help file: PSCompression.dll-Help.xml +Module Name: PSCompression +online version: https://github.com/santisq/PSCompression +schema: 2.0.0 +--- + +# Get-RarEntry + +## SYNOPSIS + +Lists entries from RAR archives, supporting file paths and input streams. + +## SYNTAX + +### Path (Default) + +```powershell +Get-RarEntry + [-Path] + [-Type ] + [-Include ] + [-Exclude ] + [] +``` + +### LiteralPath + +```powershell +Get-RarEntry + -LiteralPath + [-Type ] + [-Include ] + [-Exclude ] + [] +``` + +### Stream + +```powershell +Get-RarEntry + [-InputStream] + [-Type ] + [-Include ] + [-Exclude ] + [] +``` + +## DESCRIPTION + +The `Get-RarEntry` cmdlet lists entries in RAR archives. It accepts input from file paths and streams, and outputs `RarEntry` objects that can be piped to cmdlets like [`Expand-RarEntry`](Expand-RarEntry.md) and [`Get-RarEntryContent`](Get-RarEntryContent.md). The cmdlet uses `SharpCompress` for RAR archive processing. + +## EXAMPLES + +### Example 1: List entries for a specified RAR archive + +```powershell +PS /> Get-RarEntry .\archive.rar + + Directory: /folder1/ + +Type LastWriteTime CompressedSize Size Name +---- ------------- -------------- ---- ---- +Directory 2026-09-29 2:00 PM folder1 +Archive 2026-09-29 2:00 PM 450.00 B 1.00 KB file1.txt +Archive 2026-09-29 2:00 PM 900.00 B 2.00 KB file2.txt +``` + +This example lists all entries in `archive.rar`. + +### Example 2: List entries from all RAR archives in the current directory + +```powershell +PS /> Get-RarEntry *.rar +``` + +This example lists entries from all RAR archives in the current directory using wildcard matching. + +### Example 3: List all file entries from a RAR archive + +```powershell +PS /> Get-RarEntry .\archive.rar -Type Archive + + Directory: /folder1/ + +Type LastWriteTime CompressedSize Size Name +---- ------------- -------------- ---- ---- +Archive 2026-09-29 2:00 PM 450.00 B 1.00 KB file1.txt +Archive 2026-09-29 2:00 PM 900.00 B 2.00 KB file2.txt +``` + +This example lists only file entries (excluding directories) from `archive.rar` using `-Type Archive`. + +### Example 4: Filter entries with Include and Exclude parameters + +```powershell +PS /> Get-RarEntry .\archive.rar -Include folder1/* -Exclude *.txt + + Directory: /folder1/ + +Type LastWriteTime CompressedSize Size Name +---- ------------- -------------- ---- ---- +Directory 2026-09-29 2:00 PM folder1 +Archive 2026-09-29 2:00 PM 1.20 KB 3.00 KB image.png +``` + +This example lists entries under `folder1/` while excluding any `.txt` files. + +> [!NOTE] +> Inclusion and exclusion patterns are applied to the entries' relative paths. Exclusions are applied after inclusions. + +### Example 5: List entries from an input stream + +```powershell +PS /> $stream = Invoke-WebRequest https://example.com/archive.rar +PS /> $stream | Get-RarEntry | Select-Object -First 3 + + Directory: /docs/ + +Type LastWriteTime CompressedSize Size Name +---- ------------- -------------- ---- ---- +Directory 2026-09-29 2:00 PM docs +Archive 2026-09-29 2:00 PM 600.00 B 1.50 KB readme.md +Archive 2026-09-29 2:00 PM 1.00 KB 2.50 KB license.txt +``` + +This example lists the first three entries from a RAR archive stream retrieved via a web request. + +## PARAMETERS + +### -Exclude + +Specifies an array of string patterns to match as the cmdlet lists entries. Matching entries are excluded from the output. Wildcard characters are supported. + +> [!NOTE] +> Inclusion and exclusion patterns are applied to the entries' relative paths. Exclusions are applied after inclusions. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: True +``` + +### -Include + +Specifies an array of string patterns to match as the cmdlet lists entries. Matching entries are included in the output. Wildcard characters are supported. + +> [!NOTE] +> Inclusion and exclusion patterns are applied to the entries' relative paths. Exclusions are applied after inclusions. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: True +``` + +### -InputStream + +Specifies an input stream containing a RAR archive. + +> [!TIP] +> Output from `Invoke-WebRequest` is automatically bound to this parameter. + +```yaml +Type: Stream +Parameter Sets: Stream +Aliases: RawContentStream + +Required: True +Position: 0 +Default value: None +Accept pipeline input: True (ByPropertyName, ByValue) +Accept wildcard characters: False +``` + +### -LiteralPath + +Specifies one or more paths to RAR archives. The value is used exactly as typed, with no wildcard character interpretation. + +```yaml +Type: String[] +Parameter Sets: LiteralPath +Aliases: PSPath + +Required: True +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### -Path + +Specifies one or more paths to RAR archives. Wildcard characters are supported. + +```yaml +Type: String[] +Parameter Sets: Path +Aliases: + +Required: True +Position: 0 +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: True +``` + +### -Type + +Filters output to include only files (`Archive`) or only directories (`Directory`). + +```yaml +Type: EntryType +Parameter Sets: (All) +Aliases: +Accepted values: Directory, Archive + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.IO.Stream + +You can pipe a stream containing a RAR archive to this cmdlet, such as output from `Invoke-WebRequest`. + +### System.String[] + +You can pipe strings containing paths to RAR archives, such as output from `Get-ChildItem` or `Get-Item`. + +## OUTPUTS + +### PSCompression.FormatHandlers.Rar.RarEntry + +Outputs `RarEntry` objects representing files or directories in the RAR archive. + +## NOTES + +## RELATED LINKS + +[__`Expand-RarEntry`__](Expand-RarEntry.md) + +[__`Expand-RarArchive`__](Expand-RarArchive.md) + +[__SharpCompress__](https://github.com/adamhathcock/sharpcompress) diff --git a/docs/en-US/Get-RarEntryContent.md b/docs/en-US/Get-RarEntryContent.md new file mode 100644 index 0000000..3f69c0e --- /dev/null +++ b/docs/en-US/Get-RarEntryContent.md @@ -0,0 +1,250 @@ +--- +external help file: PSCompression.dll-Help.xml +Module Name: PSCompression +online version: https://github.com/santisq/PSCompression +schema: 2.0.0 +--- + +# Get-RarEntryContent + +## SYNOPSIS + +Retrieves the content of one or more file entries from a RAR archive. + +## SYNTAX + +### Stream (Default) + +```powershell +Get-RarEntryContent + -Entry + [-Encoding ] + [-Raw] + [-Password ] + [] +``` + +### Bytes + +```powershell +Get-RarEntryContent + -Entry + [-Raw] + [-AsByteStream] + [-BufferSize ] + [-Password ] + [] +``` + +## DESCRIPTION + +The `Get-RarEntryContent` cmdlet retrieves the content of `RarEntry` objects produced by `Get-RarEntry`. This cmdlet supports text output (line-by-line or raw string) and binary output (byte arrays or streams). It also supports reading password-protected entries. + +> [!TIP] +> Entries output by `Get-RarEntry` can be piped directly to this cmdlet. + +## EXAMPLES + +### Example 1: Get the content of a RAR archive entry + +```powershell +PS /> Get-RarEntry .\archive.rar -Include folder1/file1.txt | Get-RarEntryContent +``` + +This example retrieves the text content of a specific file entry from a RAR archive. By default, content is streamed line by line (as an array of strings). + +### Example 2: Get raw content of a RAR archive entry + +```powershell +PS /> Get-RarEntry .\archive.rar -Include folder1/file1.txt | Get-RarEntryContent -Raw +``` + +This example retrieves the entire text content as a single multi-line string using the `-Raw` switch. + +### Example 3: Get the bytes of a RAR archive entry as a stream + +```powershell +PS /> $bytes = Get-RarEntry .\archive.rar -Include folder1/helloworld.txt | Get-RarEntryContent -AsByteStream +PS /> [System.Text.Encoding]::UTF8.GetString($bytes) +hello world! +``` + +This example retrieves the raw bytes of a file entry as a byte array using `-AsByteStream`, then converts them to a string. + +### Example 4: Get contents of all .md files as byte arrays + +```powershell +PS /> $bytes = Get-RarEntry .\archive.rar -Include *.md | Get-RarEntryContent -AsByteStream -Raw +PS /> $bytes[0].GetType() + +IsPublic IsSerial Name BaseType +-------- -------- ---- -------- +True True Byte[] System.Array + +PS /> $bytes[1].Length +7767 +``` + +This example retrieves the raw bytes of all `.md` files as an array of `byte[]` objects (one per entry) using `-AsByteStream` and `-Raw`. + +### Example 5: Get content from an input stream + +```powershell +PS /> $stream = Invoke-WebRequest https://example.com/archive.rar +PS /> $stream | Get-RarEntry -Include readme.md | Get-RarEntryContent -Raw +``` + +This example retrieves the content of `readme.md` from a RAR archive streamed from the web. + +### Example 6: Get content from a password-protected entry + +```powershell +PS /> Get-RarEntry .\archive.rar -Include secret.txt | Get-RarEntryContent -Password (Read-Host -AsSecureString) +``` + +This example demonstrates how to read an encrypted entry using `Read-Host -AsSecureString` to provide the password. + +> [!TIP] +> If an entry is encrypted and no password is supplied, the cmdlet will prompt for one interactively. + +## PARAMETERS + +### -AsByteStream + +Specifies that the content should be read as a stream of bytes. + +> [!TIP] +> By default, `-AsByteStream` streams bytes through the pipeline one by one. To receive the entire un-enumerated byte array (`byte[]`) per entry without pipeline unrolling, combine it with `-Raw`. + +```yaml +Type: SwitchParameter +Parameter Sets: Bytes +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -BufferSize + +Determines the number of bytes read into the buffer before outputting the stream of bytes. This parameter applies only when `-Raw` is not used. The default buffer size is 128 KiB. + +```yaml +Type: Int32 +Parameter Sets: Bytes +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Encoding + +Specifies the character encoding used to read the entry content. The default encoding is `utf8NoBOM`. + +> [!NOTE] +> +> - This parameter applies only when `-AsByteStream` is not used. +> - The default encoding is UTF-8 without BOM. + +```yaml +Type: Encoding +Parameter Sets: Stream +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Entry + +The RAR entry or entries to get the content from. This parameter accepts pipeline input from `Get-RarEntry` but can also be used as a named parameter. + +```yaml +Type: RarEntry[] +Parameter Sets: (All) +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -Password + +Specifies the password as a `SecureString` to read the encrypted RAR entry. + +> [!NOTE] +> If an entry is encrypted and no password is supplied, the cmdlet will prompt for one. + +```yaml +Type: SecureString +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Raw + +By default, the cmdlet outputs text content as an array of strings (split on newlines). The `-Raw` switch returns the entire content as a single string with newlines preserved. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### PSCompression.FormatHandlers.Rar.RarEntry[] + +You can pipe one or more `RarEntry` objects produced by [`Get-RarEntry`](Get-RarEntry.md) to this cmdlet. + +## OUTPUTS + +### System.String + +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. + +### System.Byte + +Outputs bytes to the pipeline when `-AsByteStream` is specified. + +- __`-AsByteStream`__: Streams the bytes individually one by one (enumerating the entry's byte array). +- __`-AsByteStream` and `-Raw`__: Outputs the raw byte array (`byte[]`) for each entry without enumeration. + +## NOTES + +## RELATED LINKS + +[__`Get-RarEntry`__](Get-RarEntry.md) + +[__`Expand-RarEntry`__](Expand-RarEntry.md) + +[__SharpCompress__](https://github.com/adamhathcock/sharpcompress) diff --git a/docs/en-US/Get-TarEntry.md b/docs/en-US/Get-TarEntry.md index f5c8222..537f96f 100644 --- a/docs/en-US/Get-TarEntry.md +++ b/docs/en-US/Get-TarEntry.md @@ -58,7 +58,7 @@ The `Get-TarEntry` cmdlet lists entries in tar archives, supporting both uncompr ### Example 1: List entries for a specified tar archive ```powershell -PS ..\pwsh> Get-TarEntry .\archive.tar +PS /> Get-TarEntry .\archive.tar Directory: /folder1/ @@ -74,7 +74,7 @@ This example lists all entries in an uncompressed tar archive (`archive.tar`). ### Example 2: List entries from all gzip-compressed tar archives in the current directory ```powershell -PS ..\pwsh> Get-TarEntry *.tar.gz +PS /> Get-TarEntry *.tar.gz Directory: /folder1/ @@ -90,7 +90,7 @@ This example lists entries from all gzip-compressed tar archives in the current ### Example 3: List all file entries from a tar archive ```powershell -PS C:\> Get-TarEntry .\archive.tar -Type Archive +PS /> Get-TarEntry .\archive.tar -Type Archive Directory: /folder1/ @@ -105,7 +105,7 @@ This example lists only file entries (excluding directories) from `archive.tar` ### Example 4: Filter entries with Include and Exclude parameters ```powershell -PS C:\> Get-TarEntry .\archive.tbz2 -Include folder1/* -Exclude *.txt +PS /> Get-TarEntry .\archive.tbz2 -Include folder1/* -Exclude *.txt Directory: /folder1/ @@ -123,8 +123,8 @@ This example lists entries under `folder1/` while excluding any `.txt` files. ### Example 5: List entries from an input stream ```powershell -PS C:\> $stream = Invoke-WebRequest https://example.com/archive.tar.gz -PS C:\> $stream | Get-TarEntry -Algorithm gz | Select-Object -First 3 +PS /> $stream = Invoke-WebRequest https://example.com/archive.tar.gz +PS /> $stream | Get-TarEntry -Algorithm gz | Select-Object -First 3 Directory: /docs/ @@ -292,9 +292,9 @@ You can pipe strings containing paths to tar archives, such as output from `Get- ## OUTPUTS -### PSCompression.TarEntryDirectory +### PSCompression.FormatHandlers.Tar.TarEntryDirectory -### PSCompression.TarEntryFile +### PSCompression.FormatHandlers.Tar.TarEntryFile Outputs objects representing directories or files in the tar archive. diff --git a/docs/en-US/Get-TarEntryContent.md b/docs/en-US/Get-TarEntryContent.md index 48900a9..5b6114e 100644 --- a/docs/en-US/Get-TarEntryContent.md +++ b/docs/en-US/Get-TarEntryContent.md @@ -46,7 +46,7 @@ The `Get-TarEntryContent` cmdlet retrieves the content of `TarEntryFile` objects ### Example 1: Get the content of a tar archive entry ```powershell -PS C:\> Get-TarEntry .\archive.tar -Include folder1/file1.txt | Get-TarEntryContent +PS /> Get-TarEntry .\archive.tar -Include folder1/file1.txt | Get-TarEntryContent ``` This example retrieves the text content of a specific file entry from a tar archive. By default, content is streamed line by line (as an array of strings). @@ -54,7 +54,7 @@ This example retrieves the text content of a specific file entry from a tar arch ### Example 2: Get raw content of a tar archive entry ```powershell -PS C:\> Get-TarEntry .\archive.tar -Include folder1/file1.txt | Get-TarEntryContent -Raw +PS /> Get-TarEntry .\archive.tar -Include folder1/file1.txt | Get-TarEntryContent -Raw ``` This example retrieves the entire text content as a single multi-line string using the `-Raw` switch. @@ -62,8 +62,8 @@ This example retrieves the entire text content as a single multi-line string usi ### Example 3: Get the bytes of a tar archive entry as a stream ```powershell -PS C:\> $bytes = Get-TarEntry .\archive.tar -Include folder1/helloworld.txt | Get-TarEntryContent -AsByteStream -PS C:\> [System.Text.Encoding]::UTF8.GetString($bytes) +PS /> $bytes = Get-TarEntry .\archive.tar -Include folder1/helloworld.txt | Get-TarEntryContent -AsByteStream +PS /> [System.Text.Encoding]::UTF8.GetString($bytes) hello world! ``` @@ -72,14 +72,14 @@ This example retrieves the raw bytes of a file entry as a byte array using `-AsB ### Example 4: Get contents of all `.md` files as byte arrays ```powershell -PS C:\> $bytes = Get-TarEntry .\archive.tar.gz -Algorithm gz -Include *.md | Get-TarEntryContent -AsByteStream -Raw -PS C:\> $bytes[0].GetType() +PS /> $bytes = Get-TarEntry .\archive.tar.gz -Algorithm gz -Include *.md | Get-TarEntryContent -AsByteStream -Raw +PS /> $bytes[0].GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Byte[] System.Array -PS C:\> $bytes[1].Length +PS /> $bytes[1].Length 7767 ``` @@ -88,8 +88,8 @@ This example retrieves the raw bytes of all `.md` files as an array of `byte[]` ### Example 5: Get content from an input stream ```powershell -PS C:\> $stream = Invoke-WebRequest https://example.com/archive.tar.gz -PS C:\> $stream | Get-TarEntry -Include readme.md -Algorithm gz | Get-TarEntryContent -Raw +PS /> $stream = Invoke-WebRequest https://example.com/archive.tar.gz +PS /> $stream | Get-TarEntry -Include readme.md -Algorithm gz | Get-TarEntryContent -Raw ``` This example retrieves the content of `readme.md` from a gzip-compressed tar archive streamed from the web. @@ -103,6 +103,9 @@ This example retrieves the content of `readme.md` from a gzip-compressed tar arc Specifies that the content should be read as a stream of bytes. +> [!TIP] +> By default, `-AsByteStream` streams bytes through the pipeline one by one. To receive the entire un-enumerated byte array (`byte[]`) per entry without pipeline unrolling, combine it with `-Raw`. + ```yaml Type: SwitchParameter Parameter Sets: Bytes @@ -202,8 +205,10 @@ By default, this cmdlet returns the content as an array of strings, one per line ### System.Byte -- When the `-AsByteStream` parameter is used, this cmdlet returns the content as a byte array (`System.Byte[]`). -- When `-AsByteStream` and `-Raw` are combined, it returns an array of byte arrays (one per entry). +Outputs bytes to the pipeline when `-AsByteStream` is specified. + +- __`-AsByteStream`__: Streams the bytes individually one by one (enumerating the entry's byte array). +- __`-AsByteStream` and `-Raw`__: Outputs the raw byte array (`byte[]`) for each entry without enumeration. ## NOTES diff --git a/docs/en-US/Get-ZipEntry.md b/docs/en-US/Get-ZipEntry.md index 0224d20..4d40063 100644 --- a/docs/en-US/Get-ZipEntry.md +++ b/docs/en-US/Get-ZipEntry.md @@ -55,7 +55,7 @@ The `Get-ZipEntry` cmdlet lists entries in zip archives. It supports input from ### Example 1: List entries for a specified file path ```powershell -PS ..\pwsh> Get-ZipEntry path\to\myZip.zip +PS /> Get-ZipEntry path\to\myZip.zip ``` This example lists all entries in the specified zip archive. @@ -63,7 +63,7 @@ This example lists all entries in the specified zip archive. ### Example 2: List entries from all files with `.zip` extension in the current directory ```powershell -PS ..\pwsh> Get-ZipEntry *.zip +PS /> Get-ZipEntry *.zip ``` This example lists entries from all `.zip` files in the current directory. The `-Path` parameter supports wildcards. @@ -71,7 +71,7 @@ This example lists entries from all `.zip` files in the current directory. The ` ### Example 3: List all `Archive` entries from a Zip file ```powershell -PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Type Archive +PS /> Get-ZipEntry path\to\myZip.zip -Type Archive ``` > [!TIP] @@ -80,7 +80,7 @@ PS ..\pwsh> Get-ZipEntry path\to\myZip.zip -Type Archive ### Example 4: Filtering entries with `-Include` and `-Exclude` parameters ```powershell -PS ..\pwsh> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-us* +PS /> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-us* Directory: /PSCompression/docs/en-US/ @@ -101,7 +101,7 @@ Archive 2/22/2024 1:19 PM 1.14 KB 2.95 KB Remove-Zip Archive 2/22/2024 1:19 PM 741.00 B 2.16 KB Rename-ZipEntry.md Archive 2/22/2024 1:19 PM 1.55 KB 5.35 KB Set-ZipEntryContent.md -PS ..\pwsh> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-us* -Exclude *en-US/Compress*, *en-US/Remove* +PS /> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-us* -Exclude *en-US/Compress*, *en-US/Remove* Directory: /PSCompression/docs/en-US/ @@ -128,8 +128,8 @@ Archive 2/22/2024 1:19 PM 1.55 KB 5.35 KB Set-ZipEnt ### Example 5: List entries from an input Stream ```powershell -PS ..\pwsh> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression -PS ..\pwsh> $package | Get-ZipEntry | Select-Object -First 5 +PS /> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression +PS /> $package | Get-ZipEntry | Select-Object -First 5 Directory: / @@ -294,9 +294,9 @@ You can pipe a stream containing a zip archive (e.g., output from `Invoke-WebReq ## OUTPUTS -### PSCompression.ZipEntryDirectory +### PSCompression.FormatHandlers.Zip.ZipEntryDirectory -### PSCompression.ZipEntryFile +### PSCompression.FormatHandlers.Zip.ZipEntryFile ## NOTES diff --git a/docs/en-US/Get-ZipEntryContent.md b/docs/en-US/Get-ZipEntryContent.md index 7cca970..cfd9439 100644 --- a/docs/en-US/Get-ZipEntryContent.md +++ b/docs/en-US/Get-ZipEntryContent.md @@ -41,14 +41,14 @@ Get-ZipEntryContent The `Get-ZipEntryContent` cmdlet retrieves the content of `ZipEntryFile` objects produced by [`Get-ZipEntry`](./Get-ZipEntry.md) or [`New-ZipEntry`](./New-ZipEntry.md). This cmdlet supports text output (line-by-line or raw string) and binary output (byte arrays or streams). It also supports reading password-protected entries. > [!TIP] -> Entries outputted by `Get-ZipEntry` can be piped to this cmdlet. +> Entries output by `Get-ZipEntry` can be piped to this cmdlet. ## EXAMPLES ### Example 1: Get the content of a Zip Archive Entry ```powershell -PS ..pwsh\> Get-ZipEntry .\myZip.zip -Include myrelative/entry.txt | Get-ZipEntryContent +PS /> Get-ZipEntry .\myZip.zip -Include myrelative/entry.txt | Get-ZipEntryContent ``` This example retrieves the text content of a specific file entry from a zip archive. By default, content is streamed line by line (as an array of strings). @@ -56,7 +56,7 @@ This example retrieves the text content of a specific file entry from a zip arch ### Example 2: Get raw content of a Zip Archive Entry ```powershell -PS ..pwsh\> Get-ZipEntry .\myZip.zip -Include myrelative/entry.txt | Get-ZipEntryContent -Raw +PS /> Get-ZipEntry .\myZip.zip -Include myrelative/entry.txt | Get-ZipEntryContent -Raw ``` This example retrieves the entire text content as a single multi-line string using the `-Raw` switch. @@ -64,8 +64,8 @@ This example retrieves the entire text content as a single multi-line string usi ### Example 3: Get the bytes of a Zip Archive Entry as a Stream ```powershell -PS ..pwsh\> $bytes = Get-ZipEntry .\test.zip -Include test/helloworld.txt | Get-ZipEntryContent -AsByteStream -PS ..pwsh\> [System.Text.Encoding]::UTF8.GetString($bytes) +PS /> $bytes = Get-ZipEntry .\test.zip -Include test/helloworld.txt | Get-ZipEntryContent -AsByteStream +PS /> [System.Text.Encoding]::UTF8.GetString($bytes) hello world! ``` @@ -74,14 +74,14 @@ This example retrieves the raw bytes of a file entry as a byte array using `-AsB ### Example 4: Get contents of all `.md` files as byte arrays ```powershell -PS ..pwsh\> $bytes = Get-ZipEntry .\test.zip -Include *.md | Get-ZipEntryContent -AsByteStream -Raw -PS ..pwsh\> $bytes[0].GetType() +PS /> $bytes = Get-ZipEntry .\test.zip -Include *.md | Get-ZipEntryContent -AsByteStream -Raw +PS /> $bytes[0].GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Byte[] System.Array -PS ..pwsh\> $bytes[1].Length +PS /> $bytes[1].Length 7767 ``` @@ -90,8 +90,8 @@ This example retrieves the raw bytes of all `.md` files as an array of `byte[]` ### Example 5: Get content from input Stream ```powershell -PS ..\pwsh> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression -PS ..\pwsh> $package | Get-ZipEntry -Include *.psd1 | Get-ZipEntryContent -Raw | Invoke-Expression +PS /> $package = Invoke-WebRequest https://www.powershellgallery.com/api/v2/package/PSCompression +PS /> $package | Get-ZipEntry -Include *.psd1 | Get-ZipEntryContent -Raw | Invoke-Expression Name Value ---- ----- @@ -117,12 +117,12 @@ This example downloads a NuGet package (a zip archive) from PowerShell Gallery, ### Example 6: Get content from a password protected entry ```powershell -PS ..\pwsh> Get-ZipEntry .\myZip.zip -Include myEncryptedEntry.txt | Get-ZipEntryContent -Password (Read-Host -AsSecureString) +PS /> Get-ZipEntry .\myZip.zip -Include myEncryptedEntry.txt | Get-ZipEntryContent -Password (Read-Host -AsSecureString) ``` This example demonstrates how to read an encrypted entry using `Read-Host -AsSecureString` to provide the password. -> [!TIP] +> [!NOTE] > If an entry is encrypted and no password is supplied, the cmdlet will prompt for one. ## PARAMETERS @@ -200,6 +200,9 @@ Accept wildcard characters: False Specifies that the content should be read as a stream of bytes. +> [!TIP] +> By default, `-AsByteStream` streams bytes through the pipeline one by one. To receive the entire un-enumerated byte array (`byte[]`) per entry without pipeline unrolling, combine it with `-Raw`. + ```yaml Type: SwitchParameter Parameter Sets: Bytes @@ -216,7 +219,7 @@ Accept wildcard characters: False Specifies the password as a `SecureString` to extract the encrypted zip entry. -> [!TIP] +> [!NOTE] > If an entry is encrypted and no password is supplied, the cmdlet will prompt for one. ```yaml @@ -249,8 +252,10 @@ By default, this cmdlet returns the content as an array of strings, one per line ### System.Byte -- When the `-AsByteStream` parameter is used, this cmdlet returns the content as a byte array (`System.Byte[]`). -- When `-AsByteStream` and `-Raw` are combined, it returns an array of byte arrays (one per entry). +Outputs bytes to the pipeline when `-AsByteStream` is specified. + +- __`-AsByteStream`__: Streams the bytes individually one by one (enumerating the entry's byte array). +- __`-AsByteStream` and `-Raw`__: Outputs the raw byte array (`byte[]`) for each entry without enumeration. ## NOTES diff --git a/docs/en-US/New-ZipEntry.md b/docs/en-US/New-ZipEntry.md index 8656540..2310c08 100644 --- a/docs/en-US/New-ZipEntry.md +++ b/docs/en-US/New-ZipEntry.md @@ -63,7 +63,7 @@ When adding from string input (`-Value`), the `-EntryPath` parameter is required ### Example 1: Create empty entries ```powershell -PS ..\pwsh> New-ZipEntry .\test.zip -EntryPath test\entry, newfolder\ +PS /> New-ZipEntry .\test.zip -EntryPath test\entry, newfolder\ Directory: /newfolder/ @@ -83,12 +83,12 @@ This example creates an empty file entry (`test/entry`) and a directory entry (` ### Example 2: Create entries with content from input strings ```powershell -PS ..\pwsh> 'hello', 'world', '!' | New-ZipEntry .\test.zip -EntryPath test\entry, newfolder\ +PS /> 'hello', 'world', '!' | New-ZipEntry .\test.zip -EntryPath test\entry, newfolder\ New-ZipEntry: An entry with path 'test/entry' already exists in 'path\to\test.zip'. New-ZipEntry: An entry with path 'newfolder/' already exists in 'path\to\test.zip'. -PS ..\pwsh> 'hello', 'world', '!' | New-ZipEntry .\test.zip -EntryPath test\entry, newfolder\ -Force -PS ..\pwsh> Get-ZipEntry .\test.zip -Include test/entry | Get-ZipEntryContent +PS /> 'hello', 'world', '!' | New-ZipEntry .\test.zip -EntryPath test\entry, newfolder\ -Force +PS /> Get-ZipEntry .\test.zip -Include test/entry | Get-ZipEntryContent hello world ! @@ -102,8 +102,8 @@ This example pipes three strings into `New-ZipEntry`, creating/overwriting file ### Example 3: Create entries with content from a source file path ```powershell -PS ..\pwsh> $file = 'hello world!' | New-Item mytestfile.txt -PS ..\pwsh> New-ZipEntry .\test.zip -SourcePath $file.FullName -EntryPath newentry.txt +PS /> $file = 'hello world!' | New-Item mytestfile.txt +PS /> New-ZipEntry .\test.zip -SourcePath $file.FullName -EntryPath newentry.txt ``` This example adds the contents of a local file to the zip archive under the specified entry path. @@ -111,8 +111,8 @@ This example adds the contents of a local file to the zip archive under the spec ### Example 4: Archive all files in a specified location ```powershell -PS ..\pwsh> $files = Get-ChildItem -File -Recurse -PS ..\pwsh> $files | ForEach-Object { New-ZipEntry .\test.zip -SourcePath $_.FullName } +PS /> $files = Get-ChildItem -File -Recurse +PS /> $files | ForEach-Object { New-ZipEntry .\test.zip -SourcePath $_.FullName } ``` This example recursively adds all files from the current directory to the zip archive, preserving their relative paths. @@ -123,8 +123,8 @@ This example recursively adds all files from the current directory to the zip ar ### Example 5: Archive all `.txt` files in a specified location using a specified encoding ```powershell -PS ..\pwsh> $files = Get-ChildItem -File -Recurse -Filter *.txt -PS ..\pwsh> $files | ForEach-Object { +PS /> $files = Get-ChildItem -File -Recurse -Filter *.txt +PS /> $files | ForEach-Object { $_ | Get-Content -Encoding ascii | New-ZipEntry .\test.zip -EntryPath $_.FullName -Encoding ascii } @@ -267,9 +267,9 @@ You can pipe one or more strings to this cmdlet to use as content for new file e ## OUTPUTS -### PSCompression.ZipEntryDirectory +### PSCompression.FormatHandlers.Zip.ZipEntryDirectory -### PSCompression.ZipEntryFile +### PSCompression.FormatHandlers.Zip.ZipEntryFile The cmdlet outputs the newly created `ZipEntryDirectory` or `ZipEntryFile` objects. diff --git a/docs/en-US/Remove-ZipEntry.md b/docs/en-US/Remove-ZipEntry.md index 0156f6f..06bba47 100644 --- a/docs/en-US/Remove-ZipEntry.md +++ b/docs/en-US/Remove-ZipEntry.md @@ -30,7 +30,7 @@ The `Remove-ZipEntry` cmdlet removes `ZipEntryFile` or `ZipEntryDirectory` objec ### Example 1: Remove all Zip Archive Entries from a Zip Archive ```powershell -PS ..pwsh\> Get-ZipEntry .\myZip.zip | Remove-ZipEntry +PS /> Get-ZipEntry .\myZip.zip | Remove-ZipEntry ``` This example removes all entries from `myZip.zip`, effectively emptying the archive. @@ -38,7 +38,7 @@ This example removes all entries from `myZip.zip`, effectively emptying the arch ### Example 2: Remove all `.txt` Entries from a Zip Archive ```powershell -PS ..pwsh\> Get-ZipEntry .\myZip.zip -Include *.txt | Remove-ZipEntry +PS /> Get-ZipEntry .\myZip.zip -Include *.txt | Remove-ZipEntry ``` This example removes only the entries matching the `*.txt` pattern from `myZip.zip`. @@ -46,7 +46,7 @@ This example removes only the entries matching the `*.txt` pattern from `myZip.z ### Example 3: Prompt for confirmation before removing entries ```powershell -PS ..pwsh\> Get-ZipEntry .\myZip.zip -Include *.txt | Remove-ZipEntry -Confirm +PS /> Get-ZipEntry .\myZip.zip -Include *.txt | Remove-ZipEntry -Confirm Confirm Are you sure you want to perform this action? diff --git a/docs/en-US/Rename-ZipEntry.md b/docs/en-US/Rename-ZipEntry.md index 0d0f44c..4e8a013 100644 --- a/docs/en-US/Rename-ZipEntry.md +++ b/docs/en-US/Rename-ZipEntry.md @@ -41,7 +41,7 @@ point happens to every entry that is considered a child of the directory you are ### Example 1: Rename a Zip File Entry ```powershell -PS ..pwsh\> Get-ZipEntry .\myZip.zip -Type Archive -Include relativePath/to/myEntryToRename.ext | +PS /> Get-ZipEntry .\myZip.zip -Type Archive -Include relativePath/to/myEntryToRename.ext | Rename-ZipEntry -NewName myNewName.ext ``` @@ -50,7 +50,7 @@ This example renames a specific file entry inside the zip archive to `myNewName. ### Example 2: Rename all entries with `.ext` extension using a delay-bind scriptblock ```powershell -PS ..pwsh\> Get-ZipEntry .\myZip.zip -Type Archive -Include *.ext | +PS /> Get-ZipEntry .\myZip.zip -Type Archive -Include *.ext | Rename-ZipEntry -NewName { $_.BaseName + 'myNewName' + $_.Extension } ``` @@ -62,7 +62,7 @@ PS ..pwsh\> Get-ZipEntry .\myZip.zip -Type Archive -Include *.ext | ### Example 3: Rename a Zip Directory Entry ```powershell -PS ..\pwsh> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-US/* +PS /> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-US/* Directory: /PSCompression/docs/en-US/ @@ -83,8 +83,8 @@ Archive 2/22/2024 1:19 PM 1.14 KB 2.95 KB Remove-Zip Archive 2/22/2024 1:19 PM 741.00 B 2.16 KB Rename-ZipEntry.md Archive 2/22/2024 1:19 PM 1.55 KB 5.35 KB Set-ZipEntryContent.md -PS ..\pwsh> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-US/ | Rename-ZipEntry -NewName 'en-US123' -PS ..\pwsh> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-US123/* +PS /> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-US/ | Rename-ZipEntry -NewName 'en-US123' +PS /> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-US123/* Directory: /PSCompression/docs/en-US123/ @@ -111,7 +111,7 @@ This example renames a directory entry (`en-US/`) to `en-US123/`. All child entr ### Example 4: Prompt for confirmation before renaming entries ```powershell -PS ..pwsh\> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-US123/ | Rename-ZipEntry -NewName 'Test' -Confirm +PS /> Get-ZipEntry .\PSCompression.zip -Include PSCompression/docs/en-US123/ | Rename-ZipEntry -NewName 'Test' -Confirm Confirm Are you sure you want to perform this action? @@ -227,9 +227,9 @@ You can pipe a single `ZipEntryFile` or `ZipEntryDirectory` object produced by [ By default, this cmdlet produces no output. -### PSCompression.ZipEntryFile +### PSCompression.FormatHandlers.Zip.ZipEntryFile -### PSCompression.ZipEntryDirectory +### PSCompression.FormatHandlers.Zip.ZipEntryDirectory When the `-PassThru` switch is used, the cmdlet outputs the renamed entry object. diff --git a/docs/en-US/Set-ZipEntryContent.md b/docs/en-US/Set-ZipEntryContent.md index 75e8f8b..939218e 100644 --- a/docs/en-US/Set-ZipEntryContent.md +++ b/docs/en-US/Set-ZipEntryContent.md @@ -52,9 +52,9 @@ To create a new entry, use [`New-ZipEntry`](./New-ZipEntry.md). ### Example 1: Write new content to a Zip Archive Entry ```powershell -PS ..pwsh\> $entry = New-ZipEntry .\test.zip -EntryPath test\helloworld.txt -PS ..pwsh\> 'hello', 'world', '!' | Set-ZipEntryContent $entry -PS ..pwsh\> $entry | Get-ZipEntryContent +PS /> $entry = New-ZipEntry .\test.zip -EntryPath test\helloworld.txt +PS /> 'hello', 'world', '!' | Set-ZipEntryContent $entry +PS /> $entry | Get-ZipEntryContent hello world ! @@ -65,8 +65,8 @@ This example creates a new file entry and pipes strings to set its content (repl ### Example 2: Append content to a Zip Archive Entry ```powershell -PS ..pwsh\> Set-ZipEntryContent $entry -Value 'hello', 'world', '!' -Append -PS ..pwsh\> $entry | Get-ZipEntryContent +PS /> Set-ZipEntryContent $entry -Value 'hello', 'world', '!' -Append +PS /> $entry | Get-ZipEntryContent hello world ! @@ -80,10 +80,10 @@ This example appends additional strings to the existing entry content using `-Ap ### Example 3: Write raw bytes to a Zip Archive Entry ```powershell -PS ..pwsh\> $entry = Get-ZipEntry .\test.zip -Include test/helloworld.txt -PS ..pwsh\> $bytes = [System.Text.Encoding]::UTF8.GetBytes('hello world!') -PS ..pwsh\> $bytes | Set-ZipEntryContent $entry -AsByteStream -PS ..pwsh\> $entry | Get-ZipEntryContent +PS /> $entry = Get-ZipEntry .\test.zip -Include test/helloworld.txt +PS /> $bytes = [System.Text.Encoding]::UTF8.GetBytes('hello world!') +PS /> $bytes | Set-ZipEntryContent $entry -AsByteStream +PS /> $entry | Get-ZipEntryContent hello world! ``` @@ -92,8 +92,8 @@ This example appends the same byte array to the entry using `-AsByteStream` and ### Example 4: Append raw bytes to a Zip Archive Entry ```powershell -PS ..pwsh\> $bytes | Set-ZipEntryContent $entry -AsByteStream -Append -PS ..pwsh\> $entry | Get-ZipEntryContent +PS /> $bytes | Set-ZipEntryContent $entry -AsByteStream -Append +PS /> $entry | Get-ZipEntryContent hello world!hello world! ``` @@ -239,7 +239,7 @@ You can pipe strings (for text content) or byte arrays (for binary content) to t By default, this cmdlet produces no output. -### PSCompression.ZipEntryFile +### PSCompression.FormatHandlers.Zip.ZipEntryFile When the `-PassThru` switch is used, the cmdlet outputs the updated `ZipEntryFile` object. diff --git a/module/PSCompression.Format.ps1xml b/module/PSCompression.Format.ps1xml index 6afc6f1..9fe2655 100644 --- a/module/PSCompression.Format.ps1xml +++ b/module/PSCompression.Format.ps1xml @@ -2,9 +2,10 @@ - zipentryview + entryview - PSCompression.Abstractions.ZipEntryBase + PSCompression.FormatHandlers.Rar.RarEntry + PSCompression.Abstractions.Entries.ZipEntryBase [PSCompression.Internal._Format]::GetDirectoryPath($_) @@ -48,14 +49,14 @@ - if ($_ -is [PSCompression.ZipEntryFile]) { + if ($_.Type -eq [PSCompression.Enum.EntryType]::Archive) { [PSCompression.Internal._Format]::GetFormattedLength($_.CompressedLength) } - if ($_ -is [PSCompression.ZipEntryFile]) { + if ($_.Type -eq [PSCompression.Enum.EntryType]::Archive) { [PSCompression.Internal._Format]::GetFormattedLength($_.Length) } @@ -71,7 +72,7 @@ tarentryview - PSCompression.Abstractions.TarEntryBase + PSCompression.Abstractions.Entries.TarEntryBase [PSCompression.Internal._Format]::GetDirectoryPath($_) @@ -110,7 +111,7 @@ - if ($_ -is [PSCompression.TarEntryFile]) { + if ($_ -is [PSCompression.FormatHandlers.Tar.TarEntryFile]) { [PSCompression.Internal._Format]::GetFormattedLength($_.Length) } diff --git a/module/PSCompression.psd1 b/module/PSCompression.psd1 index b089f59..d2148de 100644 --- a/module/PSCompression.psd1 +++ b/module/PSCompression.psd1 @@ -11,7 +11,7 @@ RootModule = 'PSCompression.psm1' # Version number of this module. - ModuleVersion = '3.1.0' + ModuleVersion = '4.0.0' # Supported PSEditions # CompatiblePSEditions = @() @@ -29,7 +29,7 @@ Copyright = '(c) Santiago Squarzon. All rights reserved.' # Description of the functionality provided by this module - Description = 'Zip, tar, and string compression utilities for PowerShell!' + Description = 'Zip, tar, rar, and string compression utilities for PowerShell!' # Minimum version of the PowerShell engine required by this module PowerShellVersion = '5.1' @@ -78,8 +78,8 @@ 'Get-ZipEntry' 'Get-ZipEntryContent' 'Set-ZipEntryContent' - 'Remove-ZipEntry' 'New-ZipEntry' + 'Remove-ZipEntry' 'Expand-ZipEntry' 'ConvertTo-GzipString' 'ConvertFrom-GzipString' @@ -96,6 +96,10 @@ 'Get-TarEntryContent' 'Expand-TarEntry' 'Expand-TarArchive' + 'Get-RarEntry' + 'Get-RarEntryContent' + 'Expand-RarEntry' + 'Expand-RarArchive' ) # Variables to export from this module @@ -114,12 +118,16 @@ 'togzipstring' # ConvertTo-GzipString 'tozlibstring' # ConvertTo-ZlibString 'untar' # Expand-TarArchive + 'unrar' # Expand-RarArchive 'untarentry' # Expand-TarEntry 'unzipentry' # Expand-ZipEntry + 'unrarentry' # Expand-RarEntry 'targe' # Get-TarEntry - 'targec' # Get-TarEntryContent 'zipge' # Get-ZipEntry + 'rarge' # Get-RarEntry + 'targec' # Get-TarEntryContent 'zipgec' # Get-ZipEntryContent + 'rargec' # Get-RarEntryContent 'zipne' # New-ZipEntry 'ziprm' # Remove-ZipEntry 'zipren' # Rename-ZipEntry @@ -145,6 +153,8 @@ 'zip-compression' 'tar' 'tar-compression' + 'rar' + 'rar-compression' 'gzip' 'gzip-compression' 'bzip2' diff --git a/module/PSCompression.psm1 b/module/PSCompression.psm1 index b85482f..d04a224 100644 --- a/module/PSCompression.psm1 +++ b/module/PSCompression.psm1 @@ -6,7 +6,7 @@ $moduleName = [Path]::GetFileNameWithoutExtension($PSCommandPath) $frame = 'net8.0' if (-not $IsCoreCLR) { - $frame = 'netstandard2.0' + $frame = 'net472' $asm = [Path]::Combine($PSScriptRoot, 'bin', $frame, "${moduleName}.dll") Import-Module -Name $asm -ErrorAction Stop -PassThru return diff --git a/src/PSCompression/Abstractions/ToCompressedFileCommandBase.cs b/src/PSCompression/Abstractions/Commands/CompressArchiveCommandBase.cs similarity index 84% rename from src/PSCompression/Abstractions/ToCompressedFileCommandBase.cs rename to src/PSCompression/Abstractions/Commands/CompressArchiveCommandBase.cs index 5899e9d..11ecb7e 100644 --- a/src/PSCompression/Abstractions/ToCompressedFileCommandBase.cs +++ b/src/PSCompression/Abstractions/Commands/CompressArchiveCommandBase.cs @@ -5,17 +5,16 @@ using System.Linq; using System.Management.Automation; using PSCompression.Extensions; -using PSCompression.Exceptions; using System.ComponentModel; using IOPath = System.IO.Path; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Commands; [EditorBrowsable(EditorBrowsableState.Never)] -public abstract class ToCompressedFileCommandBase : CommandWithPathBase, IDisposable - where T : IDisposable +public abstract class CompressArchiveCommandBase : PathCommandBase, IDisposable + where TArchive : IDisposable { - private T? _archive; + private TArchive? _archive; private FileStream? _destination; @@ -60,7 +59,7 @@ private FileMode FileMode [ValidateNotNullOrEmpty] public string[]? Exclude { get; set; } - protected abstract T CreateCompressionStream(Stream outputStream); + protected abstract TArchive CreateCompressionStream(Stream outputStream); protected override void BeginProcessing() { @@ -70,7 +69,6 @@ protected override void BeginProcessing() try { Directory.CreateDirectory(IOPath.GetDirectoryName(Destination)!); - _destination = File.Open(Destination, FileMode); _archive = CreateCompressionStream(_destination); } @@ -97,9 +95,7 @@ protected override void ProcessRecord() foreach (string path in EnumerateResolvedPaths()) { if (ShouldExclude(path) || ItemIsDestination(path, Destination)) - { continue; - } if (Directory.Exists(path)) { @@ -112,11 +108,11 @@ protected override void ProcessRecord() } } - private void Traverse(DirectoryInfo dir, T archive) + private void Traverse(DirectoryInfo dir, TArchive archive) { _queue.Enqueue(dir); IEnumerable enumerator; - int length = dir.Parent!.FullName.Length + 1; + int length = dir.Parent?.FullName.Length + 1 ?? 0; while (_queue.Count > 0) { @@ -136,11 +132,9 @@ private void Traverse(DirectoryInfo dir, T archive) foreach (FileSystemInfo item in enumerator) { if (ShouldExclude(item.FullName)) - { continue; - } - if (item is DirectoryInfo directory) + if (item is DirectoryInfo directory && !IsReparsePoint(directory)) { _queue.Enqueue(directory); continue; @@ -148,9 +142,7 @@ private void Traverse(DirectoryInfo dir, T archive) FileInfo file = (FileInfo)item; if (ItemIsDestination(file.FullName, Destination)) - { continue; - } CreateOrUpdateFileEntry(archive, file, file.RelativeTo(length)); } @@ -158,12 +150,12 @@ private void Traverse(DirectoryInfo dir, T archive) } protected abstract void CreateDirectoryEntry( - T archive, + TArchive archive, DirectoryInfo directory, string path); protected abstract void CreateOrUpdateFileEntry( - T archive, + TArchive archive, FileInfo file, string path); @@ -178,22 +170,13 @@ private static bool ItemIsDestination(string source, string destination) => private bool ShouldExclude(string path) { - if (!_processed.Add(path)) - { - return true; - } - - if (_excludePatterns is null) - { - return false; - } + if (!_processed.Add(path)) return true; + if (_excludePatterns is null) return false; foreach (WildcardPattern pattern in _excludePatterns) { if (pattern.IsMatch(path)) - { return true; - } } return false; @@ -204,13 +187,16 @@ protected override void EndProcessing() _archive?.Dispose(); _destination?.Dispose(); - if (PassThru.IsPresent && _destination is not null) + if (PassThru && _destination is not null) { - FileInfo passthru = new(_destination.Name); - WriteObject(passthru.AppendPSProperties(passthru.DirectoryName)); + FileInfo destination = new(_destination.Name); + WriteObject(destination.AppendPSProperties(destination.DirectoryName)); } } + private static bool IsReparsePoint(DirectoryInfo dir) + => dir.Attributes.HasFlag(FileAttributes.ReparsePoint); + protected virtual void Dispose(bool disposing) { if (disposing && !_disposed) diff --git a/src/PSCompression/Abstractions/Commands/ExpandArchiveCommandBase.cs b/src/PSCompression/Abstractions/Commands/ExpandArchiveCommandBase.cs new file mode 100644 index 0000000..df80215 --- /dev/null +++ b/src/PSCompression/Abstractions/Commands/ExpandArchiveCommandBase.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Management.Automation; +using PSCompression.Extensions; + +namespace PSCompression.Abstractions.Commands; + +[EditorBrowsable(EditorBrowsableState.Never)] +public abstract class ExpandArchiveCommandBase : PathCommandBase +{ + [Parameter(Position = 1)] + public string? Destination { get; set; } + + [Parameter] + public SwitchParameter Force { get; set; } + + [Parameter] + public SwitchParameter PassThru { get; set; } + + protected override void BeginProcessing() + { + Destination = Destination is null + // PowerShell is retarded and decided to mix up ProviderPath & Path + ? SessionState.Path.CurrentFileSystemLocation.ProviderPath + : Destination.ResolvePath(this); + + if (File.Exists(Destination)) + { + ThrowTerminatingError(ExceptionExtensions.NotDirectoryPath( + Destination, nameof(Destination))); + } + + Directory.CreateDirectory(Destination); + } + + protected override void ProcessRecord() + { + Dbg.Assert(Destination is not null); + + foreach (string path in EnumerateResolvedPaths()) + { + try + { + List result = ExtractArchive(path, Destination); + + if (PassThru && result.Count > 0) + { + result.Sort((x, y) => + string.Compare( + (string)x.Properties["PSParentPath"].Value, + (string)y.Properties["PSParentPath"].Value, + ignoreCase: true)); + + foreach (PSObject entry in result) + { + WriteObject(entry); + } + } + } + catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) + { + throw; + } + catch (Exception exception) + { + WriteError(exception.ToWriteError(path)); + } + } + } + + protected abstract List ExtractArchive(string source, string destination); +} diff --git a/src/PSCompression/Abstractions/ExpandEntryCommandBase.cs b/src/PSCompression/Abstractions/Commands/ExpandEntryCommandBase.cs similarity index 77% rename from src/PSCompression/Abstractions/ExpandEntryCommandBase.cs rename to src/PSCompression/Abstractions/Commands/ExpandEntryCommandBase.cs index 0bed864..73c7ee5 100644 --- a/src/PSCompression/Abstractions/ExpandEntryCommandBase.cs +++ b/src/PSCompression/Abstractions/Commands/ExpandEntryCommandBase.cs @@ -2,10 +2,10 @@ using System.IO; using System.Management.Automation; using PSCompression.Extensions; -using PSCompression.Exceptions; using System.ComponentModel; +using PSCompression.Abstractions.Entries; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Commands; [EditorBrowsable(EditorBrowsableState.Never)] public abstract class ExpandEntryCommandBase : PSCmdlet @@ -33,8 +33,9 @@ protected override void BeginProcessing() if (File.Exists(Destination)) { - ThrowTerminatingError(ExceptionHelper.NotDirectoryPath( - Destination, nameof(Destination))); + ThrowTerminatingError( + ExceptionExtensions.NotDirectoryPath( + Destination, nameof(Destination))); } Directory.CreateDirectory(Destination); @@ -48,12 +49,8 @@ protected override void ProcessRecord() { try { - FileSystemInfo info = Extract(entry); - - if (PassThru) - { - WriteObject(info.AppendPSProperties()); - } + FileSystemInfo info = Extract(entry, Destination); + if (PassThru) WriteObject(info.AppendPSProperties()); } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { @@ -66,5 +63,5 @@ protected override void ProcessRecord() } } - protected abstract FileSystemInfo Extract(T entry); + protected abstract FileSystemInfo Extract(T entry, string destination); } diff --git a/src/PSCompression/Abstractions/FromCompressedStringCommandBase.cs b/src/PSCompression/Abstractions/Commands/FromCompressedStringCommandBase.cs similarity index 95% rename from src/PSCompression/Abstractions/FromCompressedStringCommandBase.cs rename to src/PSCompression/Abstractions/Commands/FromCompressedStringCommandBase.cs index 6aa42b4..69eeac5 100644 --- a/src/PSCompression/Abstractions/FromCompressedStringCommandBase.cs +++ b/src/PSCompression/Abstractions/Commands/FromCompressedStringCommandBase.cs @@ -3,10 +3,10 @@ using System.IO; using System.Management.Automation; using System.Text; -using PSCompression.Exceptions; +using PSCompression.Attributes; using PSCompression.Extensions; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Commands; [EditorBrowsable(EditorBrowsableState.Never)] public abstract class FromCompressedStringCommandBase : PSCmdlet diff --git a/src/PSCompression/Abstractions/GetEntryCommandBase.cs b/src/PSCompression/Abstractions/Commands/GetEntryCommandBase.cs similarity index 75% rename from src/PSCompression/Abstractions/GetEntryCommandBase.cs rename to src/PSCompression/Abstractions/Commands/GetEntryCommandBase.cs index ba16ff5..423e884 100644 --- a/src/PSCompression/Abstractions/GetEntryCommandBase.cs +++ b/src/PSCompression/Abstractions/Commands/GetEntryCommandBase.cs @@ -4,17 +4,21 @@ using System.ComponentModel; using System.Collections.Generic; using System; -using PSCompression.Exceptions; using ICSharpCode.SharpZipLib.Tar; using ZstdSharp; using ICSharpCode.SharpZipLib.Zip; +using SharpCompress.Common; +using PSCompression.Extensions; +using PSCompression.Enum; +using PSCompression.FormatHandlers.Common; +using PSCompression.Abstractions.Entries; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Commands; [EditorBrowsable(EditorBrowsableState.Never)] -public abstract class GetEntryCommandBase : CommandWithPathBase +public abstract class GetEntryCommandBase : PathCommandBase { - internal abstract ArchiveType ArchiveType { get; } + internal abstract Enum.ArchiveType ArchiveType { get; } [Parameter( ParameterSetName = "Stream", @@ -42,24 +46,17 @@ public abstract class GetEntryCommandBase : CommandWithPathBase protected override void BeginProcessing() { - if (Exclude is null && Include is null) - { - return; - } + if (Exclude is null && Include is null) return; const WildcardOptions Options = WildcardOptions.Compiled | WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase; if (Exclude is not null) - { _excludePatterns = [.. Exclude.Select(e => new WildcardPattern(e, Options))]; - } if (Include is not null) - { _includePatterns = [.. Include.Select(e => new WildcardPattern(e, Options))]; - } } protected override void ProcessRecord() @@ -72,17 +69,13 @@ protected override void ProcessRecord() foreach (string path in EnumerateResolvedPaths()) { - if (path.WriteErrorIfNotArchive( - IsLiteral ? nameof(LiteralPath) : nameof(Path), this)) - { + if (path.WriteErrorIfNotArchive(IsLiteral ? nameof(LiteralPath) : nameof(Path), this)) continue; - } try { - WriteObject( - GetEntriesFromFile(path).ToEntrySort(), - enumerateCollection: true); + foreach (EntryBase entry in GetEntriesFromFile(path).SortEntries()) + WriteObject(entry); } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { @@ -103,14 +96,9 @@ private void HandleFromStream(Stream stream) { try { - if (stream.CanSeek) - { - stream.Seek(0, SeekOrigin.Begin); - } - - WriteObject( - GetEntriesFromStream(stream).ToEntrySort(), - enumerateCollection: true); + stream.Position = 0; + foreach (EntryBase entry in GetEntriesFromStream(stream).SortEntries()) + WriteObject(entry); } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { @@ -118,7 +106,7 @@ private void HandleFromStream(Stream stream) } catch (Exception exception) when (IsInvalidArchive(exception)) { - ThrowTerminatingError(exception.ToInvalidArchive(ArchiveType, isStream: true)); + ThrowTerminatingError(exception.ToInvalidArchive(ArchiveType)); } catch (Exception exception) { @@ -131,36 +119,30 @@ private void HandleFromStream(Stream stream) protected abstract IEnumerable GetEntriesFromStream(Stream stream); private static bool MatchAny( - string name, + string? name, WildcardPattern[] patterns) { foreach (WildcardPattern pattern in patterns) { if (pattern.IsMatch(name)) - { return true; - } } return false; } - protected bool ShouldInclude(string name) + protected bool ShouldInclude(string? name) { if (_includePatterns is null) - { return true; - } return MatchAny(name, _includePatterns); } - protected bool ShouldExclude(string name) + protected bool ShouldExclude(string? name) { if (_excludePatterns is null) - { return false; - } return MatchAny(name, _excludePatterns); } @@ -169,5 +151,9 @@ protected bool ShouldSkipEntry(bool isDirectory) => isDirectory && Type is EntryType.Archive || !isDirectory && Type is EntryType.Directory; private static bool IsInvalidArchive(Exception exception) => - exception is ZipException or TarException or ZstdException or IOException; + exception is ZipException + or TarException + or ZstdException + or IOException + or InvalidFormatException; } diff --git a/src/PSCompression/Abstractions/GetEntryContentCommandBase.cs b/src/PSCompression/Abstractions/Commands/GetEntryContentCommandBase.cs similarity index 89% rename from src/PSCompression/Abstractions/GetEntryContentCommandBase.cs rename to src/PSCompression/Abstractions/Commands/GetEntryContentCommandBase.cs index 6266bee..b50b352 100644 --- a/src/PSCompression/Abstractions/GetEntryContentCommandBase.cs +++ b/src/PSCompression/Abstractions/Commands/GetEntryContentCommandBase.cs @@ -1,8 +1,10 @@ using System.ComponentModel; using System.Management.Automation; using System.Text; +using PSCompression.Abstractions.Entries; +using PSCompression.Attributes; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Commands; [EditorBrowsable(EditorBrowsableState.Never)] public abstract class GetEntryContentCommandBase : PSCmdlet diff --git a/src/PSCompression/Abstractions/CommandWithPathBase.cs b/src/PSCompression/Abstractions/Commands/PathCommandBase.cs similarity index 91% rename from src/PSCompression/Abstractions/CommandWithPathBase.cs rename to src/PSCompression/Abstractions/Commands/PathCommandBase.cs index bb9e292..c91a097 100644 --- a/src/PSCompression/Abstractions/CommandWithPathBase.cs +++ b/src/PSCompression/Abstractions/Commands/PathCommandBase.cs @@ -3,19 +3,18 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Management.Automation; -using PSCompression.Exceptions; using PSCompression.Extensions; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Commands; [EditorBrowsable(EditorBrowsableState.Never)] -public abstract class CommandWithPathBase : PSCmdlet +public abstract class PathCommandBase : PSCmdlet { protected string[] _paths = []; protected bool IsLiteral { - get => MyInvocation.BoundParameters.ContainsKey("LiteralPath"); + get => MyInvocation.HasBound(nameof(LiteralPath)); } [Parameter( diff --git a/src/PSCompression/Abstractions/ToCompressedStringCommandBase.cs b/src/PSCompression/Abstractions/Commands/ToCompressedStringCommandBase.cs similarity index 95% rename from src/PSCompression/Abstractions/ToCompressedStringCommandBase.cs rename to src/PSCompression/Abstractions/Commands/ToCompressedStringCommandBase.cs index 735e2a4..9d70c3c 100644 --- a/src/PSCompression/Abstractions/ToCompressedStringCommandBase.cs +++ b/src/PSCompression/Abstractions/Commands/ToCompressedStringCommandBase.cs @@ -4,10 +4,10 @@ using System.IO.Compression; using System.Management.Automation; using System.Text; -using PSCompression.Exceptions; +using PSCompression.Attributes; using PSCompression.Extensions; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Commands; [EditorBrowsable(EditorBrowsableState.Never)] public abstract class ToCompressedStringCommandBase : PSCmdlet, IDisposable @@ -49,7 +49,7 @@ protected override void ProcessRecord() _compressStream ??= CreateCompressionStream(_outstream, CompressionLevel); _writer ??= new StreamWriter(_compressStream, Encoding); - if (NoNewLine.IsPresent) + if (NoNewLine) { _writer.WriteContent(InputObject); return; diff --git a/src/PSCompression/Abstractions/EntryBase.cs b/src/PSCompression/Abstractions/Entries/EntryBase.cs similarity index 50% rename from src/PSCompression/Abstractions/EntryBase.cs rename to src/PSCompression/Abstractions/Entries/EntryBase.cs index d36b8fc..8b73a41 100644 --- a/src/PSCompression/Abstractions/EntryBase.cs +++ b/src/PSCompression/Abstractions/Entries/EntryBase.cs @@ -1,19 +1,18 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; +using PSCompression.Enum; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Entries; public abstract class EntryBase(string source) { - protected Stream? _stream; + protected Stream? Stream { get; set; } - protected string? _formatDirectoryPath; + internal abstract string? FormatDirectoryPath { get; } - internal string? FormatDirectoryPath { get => _formatDirectoryPath ??= GetFormatDirectoryPath(); } - - [MemberNotNullWhen(true, nameof(_stream))] - internal bool FromStream { get => _stream is not null; } + [MemberNotNullWhen(true, nameof(Stream))] + internal bool FromStream { get => Stream is not null; } public string Source { get; } = source; @@ -21,13 +20,11 @@ public abstract class EntryBase(string source) public abstract string RelativePath { get; } - public abstract DateTime LastWriteTime { get; } + public abstract DateTime? LastWriteTime { get; } public abstract long Length { get; internal set; } public abstract EntryType Type { get; } - protected abstract string GetFormatDirectoryPath(); - public override string ToString() => RelativePath; } diff --git a/src/PSCompression/Abstractions/EntryStreamOpsBase.cs b/src/PSCompression/Abstractions/Entries/EntryStreamOpsBase.cs similarity index 85% rename from src/PSCompression/Abstractions/EntryStreamOpsBase.cs rename to src/PSCompression/Abstractions/Entries/EntryStreamOpsBase.cs index d84d038..fe29782 100644 --- a/src/PSCompression/Abstractions/EntryStreamOpsBase.cs +++ b/src/PSCompression/Abstractions/Entries/EntryStreamOpsBase.cs @@ -1,7 +1,7 @@ using System; using System.IO; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Entries; internal abstract class EntryStreamOpsBase(Stream stream) : IDisposable { @@ -21,6 +21,5 @@ protected virtual void Dispose(bool disposing) public void Dispose() { Dispose(disposing: true); - GC.SuppressFinalize(this); } } diff --git a/src/PSCompression/Abstractions/TarEntryBase.cs b/src/PSCompression/Abstractions/Entries/TarEntryBase.cs similarity index 66% rename from src/PSCompression/Abstractions/TarEntryBase.cs rename to src/PSCompression/Abstractions/Entries/TarEntryBase.cs index 24b88dd..847ff42 100644 --- a/src/PSCompression/Abstractions/TarEntryBase.cs +++ b/src/PSCompression/Abstractions/Entries/TarEntryBase.cs @@ -1,9 +1,9 @@ using System; using System.IO; using ICSharpCode.SharpZipLib.Tar; -using PSCompression.Extensions; +using PSCompression.FormatHandlers.Tar; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Entries; public abstract class TarEntryBase(TarEntry entry, string source) : EntryBase(source) { @@ -11,32 +11,32 @@ public abstract class TarEntryBase(TarEntry entry, string source) : EntryBase(so public override string RelativePath { get; } = entry.Name; - public override DateTime LastWriteTime { get; } = entry.ModTime; + public override DateTime? LastWriteTime { get; } = entry.ModTime; public override long Length { get; internal set; } = entry.Size; - protected TarEntryBase(TarEntry entry, Stream? stream) + protected TarEntryBase(TarEntry entry, Stream stream) : this(entry, $"InputStream.{Guid.NewGuid()}") { - _stream = stream; + Stream = stream; } internal FileSystemInfo ExtractTo( string destination, bool overwrite) { - destination = Path.GetFullPath( - Path.Combine(destination, RelativePath)); + destination = Path.GetFullPath(Path.Combine(destination, RelativePath)); if (this is not TarEntryFile entryFile) { DirectoryInfo dir = new(destination); - dir.Create(overwrite); + dir.Create(); return dir; } FileInfo file = new(destination); - file.Directory!.Create(); + Dbg.Assert(file.Directory is not null, "Files must always have a parent directory."); + file.Directory.Create(); using FileStream destStream = file.Open( overwrite ? FileMode.Create : FileMode.CreateNew, diff --git a/src/PSCompression/Abstractions/ZipEntryBase.IO.Compression.cs b/src/PSCompression/Abstractions/Entries/ZipEntryBase.IO.Compression.cs similarity index 87% rename from src/PSCompression/Abstractions/ZipEntryBase.IO.Compression.cs rename to src/PSCompression/Abstractions/Entries/ZipEntryBase.IO.Compression.cs index 875b8a2..1994532 100644 --- a/src/PSCompression/Abstractions/ZipEntryBase.IO.Compression.cs +++ b/src/PSCompression/Abstractions/Entries/ZipEntryBase.IO.Compression.cs @@ -1,13 +1,15 @@ using System.IO; using System.IO.Compression; -using PSCompression.Exceptions; +using PSCompression.Extensions; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Entries; public abstract partial class ZipEntryBase { public ZipArchive OpenRead() => - FromStream ? new ZipArchive(_stream) : ZipFile.OpenRead(Source); + FromStream + ? new ZipArchive(Stream, ZipArchiveMode.Read, leaveOpen: true) + : ZipFile.OpenRead(Source); public ZipArchive OpenWrite() { @@ -73,6 +75,6 @@ internal static string Move( internal ZipArchive OpenZip(ZipArchiveMode mode) => FromStream - ? new ZipArchive(_stream, mode, true) + ? new ZipArchive(Stream, mode, true) : ZipFile.Open(Source, mode); } diff --git a/src/PSCompression/Abstractions/ZipEntryBase.SharpZipLib.Zip.cs b/src/PSCompression/Abstractions/Entries/ZipEntryBase.SharpZipLib.Zip.cs similarity index 71% rename from src/PSCompression/Abstractions/ZipEntryBase.SharpZipLib.Zip.cs rename to src/PSCompression/Abstractions/Entries/ZipEntryBase.SharpZipLib.Zip.cs index 8374f03..d949ece 100644 --- a/src/PSCompression/Abstractions/ZipEntryBase.SharpZipLib.Zip.cs +++ b/src/PSCompression/Abstractions/Entries/ZipEntryBase.SharpZipLib.Zip.cs @@ -2,19 +2,18 @@ using System.IO; using System.Security; using ICSharpCode.SharpZipLib.Zip; -using PSCompression.Exceptions; +using PSCompression.Enum; using PSCompression.Extensions; -namespace PSCompression.Abstractions; +namespace PSCompression.Abstractions.Entries; -public abstract partial class ZipEntryBase(ZipEntry entry, string source) - : EntryBase(source) +public abstract partial class ZipEntryBase(ZipEntry entry, string source) : EntryBase(source) { public override string? Name { get; protected set; } public override string RelativePath { get; } = entry.Name; - public override DateTime LastWriteTime { get; } = entry.DateTime; + public override DateTime? LastWriteTime { get; } = entry.DateTime; public override long Length { get; internal set; } = entry.Size; @@ -33,19 +32,11 @@ public abstract partial class ZipEntryBase(ZipEntry entry, string source) protected ZipEntryBase(ZipEntry entry, Stream? stream) : this(entry, $"InputStream.{Guid.NewGuid()}") { - _stream = stream; + Stream = stream; } - internal ZipFile OpenRead(SecureString? password) - { - ZipFile zip = FromStream ? new(_stream, leaveOpen: true) : new(Source); - if (password?.Length > 0) - { - zip.Password = password.AsPlainText(); - } - - return zip; - } + internal ZipFile OpenSharpZipLibArchive() + => FromStream ? new(Stream, leaveOpen: true) : new(Source); public FileSystemInfo ExtractTo( string destination, @@ -53,13 +44,11 @@ public FileSystemInfo ExtractTo( SecureString? password = null) { using ZipFile zip = FromStream - ? new(_stream, leaveOpen: true) + ? new(Stream, leaveOpen: true) : new(Source); - if (password?.Length > 0) - { + if (password is { Length: > 0 }) zip.Password = password.AsPlainText(); - } return ExtractTo(destination, overwrite, zip); } @@ -74,18 +63,18 @@ internal FileSystemInfo ExtractTo( Source, out ZipEntry? entry); - destination = Path.GetFullPath( - Path.Combine(destination, RelativePath)); + destination = Path.GetFullPath(Path.Combine(destination, RelativePath)); if (Type == EntryType.Directory) { DirectoryInfo dir = new(destination); - dir.Create(overwrite); + dir.Create(); return dir; } FileInfo file = new(destination); - file.Directory!.Create(); + Dbg.Assert(file.Directory is not null, "Files must always have a parent directory."); + file.Directory.Create(); using Stream source = zip.GetInputStream(entry); using FileStream fs = file.Open( @@ -93,7 +82,6 @@ internal FileSystemInfo ExtractTo( FileAccess.Write); source.CopyTo(fs); - return file; } } diff --git a/src/PSCompression/ArchiveType.cs b/src/PSCompression/ArchiveType.cs deleted file mode 100644 index 1f0d5ba..0000000 --- a/src/PSCompression/ArchiveType.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace PSCompression; - -internal enum ArchiveType -{ - zip, - tar -} diff --git a/src/PSCompression/EncodingCompleter.cs b/src/PSCompression/Attributes/EncodingCompleter.cs similarity index 97% rename from src/PSCompression/EncodingCompleter.cs rename to src/PSCompression/Attributes/EncodingCompleter.cs index aa294e6..1e92049 100644 --- a/src/PSCompression/EncodingCompleter.cs +++ b/src/PSCompression/Attributes/EncodingCompleter.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.InteropServices; -namespace PSCompression; +namespace PSCompression.Attributes; public sealed class EncodingCompleter : IArgumentCompleter { diff --git a/src/PSCompression/EncodingTransformation.cs b/src/PSCompression/Attributes/EncodingTransformation.cs similarity index 97% rename from src/PSCompression/EncodingTransformation.cs rename to src/PSCompression/Attributes/EncodingTransformation.cs index a31ed26..7d7f30c 100644 --- a/src/PSCompression/EncodingTransformation.cs +++ b/src/PSCompression/Attributes/EncodingTransformation.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using System.Text; -namespace PSCompression; +namespace PSCompression.Attributes; public sealed class EncodingTransformation : ArgumentTransformationAttribute { diff --git a/src/PSCompression/Commands/CompressTarArchiveCommand.cs b/src/PSCompression/Commands/CompressTarArchiveCommand.cs index 2bfecb7..1cedf77 100644 --- a/src/PSCompression/Commands/CompressTarArchiveCommand.cs +++ b/src/PSCompression/Commands/CompressTarArchiveCommand.cs @@ -4,8 +4,8 @@ using System.Management.Automation; using System.Text; using ICSharpCode.SharpZipLib.Tar; -using PSCompression.Abstractions; -using PSCompression.Exceptions; +using PSCompression.Abstractions.Commands; +using PSCompression.Enum; using PSCompression.Extensions; namespace PSCompression.Commands; @@ -13,7 +13,7 @@ namespace PSCompression.Commands; [Cmdlet(VerbsData.Compress, "TarArchive")] [OutputType(typeof(FileInfo))] [Alias("tarcompress")] -public sealed class CompressTarArchiveCommand : ToCompressedFileCommandBase +public sealed class CompressTarArchiveCommand : CompressArchiveCommandBase { private Stream? _compressionStream; @@ -32,8 +32,7 @@ protected override string FileExtension protected override TarOutputStream CreateCompressionStream(Stream outputStream) { - if (Algorithm == Algorithm.lz && - MyInvocation.BoundParameters.ContainsKey(nameof(CompressionLevel))) + if (Algorithm == Algorithm.lz && MyInvocation.HasBound(nameof(CompressionLevel))) { WriteWarning( "The lzip algorithm does not support custom CompressionLevel settings. " + diff --git a/src/PSCompression/Commands/CompressZipArchiveCommand.cs b/src/PSCompression/Commands/CompressZipArchiveCommand.cs index 9a335fe..9c7419a 100644 --- a/src/PSCompression/Commands/CompressZipArchiveCommand.cs +++ b/src/PSCompression/Commands/CompressZipArchiveCommand.cs @@ -3,21 +3,18 @@ using System.IO.Compression; using System.Management.Automation; using PSCompression.Extensions; -using PSCompression.Exceptions; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; namespace PSCompression.Commands; [Cmdlet(VerbsData.Compress, "ZipArchive")] [OutputType(typeof(FileInfo))] [Alias("zipcompress")] -public sealed class CompressZipArchiveCommand : ToCompressedFileCommandBase +public sealed class CompressZipArchiveCommand : CompressArchiveCommandBase { private ZipArchiveMode ZipArchiveMode { - get => Force.IsPresent || Update.IsPresent - ? ZipArchiveMode.Update - : ZipArchiveMode.Create; + get => Force || Update ? ZipArchiveMode.Update : ZipArchiveMode.Create; } protected override string FileExtension => ".zip"; diff --git a/src/PSCompression/Commands/ConvertFromBrotliStringCommand.cs b/src/PSCompression/Commands/ConvertFromBrotliStringCommand.cs index 7de4ec9..9bf4e3f 100644 --- a/src/PSCompression/Commands/ConvertFromBrotliStringCommand.cs +++ b/src/PSCompression/Commands/ConvertFromBrotliStringCommand.cs @@ -1,7 +1,7 @@ using System.IO; using System.IO.Compression; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; namespace PSCompression.Commands; diff --git a/src/PSCompression/Commands/ConvertFromDeflateStringCommand.cs b/src/PSCompression/Commands/ConvertFromDeflateStringCommand.cs index 8b4bf8a..626fd70 100644 --- a/src/PSCompression/Commands/ConvertFromDeflateStringCommand.cs +++ b/src/PSCompression/Commands/ConvertFromDeflateStringCommand.cs @@ -1,7 +1,7 @@ using System.IO; using System.IO.Compression; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; namespace PSCompression.Commands; diff --git a/src/PSCompression/Commands/ConvertFromGzipStringCommand.cs b/src/PSCompression/Commands/ConvertFromGzipStringCommand.cs index 6f4442a..94a0754 100644 --- a/src/PSCompression/Commands/ConvertFromGzipStringCommand.cs +++ b/src/PSCompression/Commands/ConvertFromGzipStringCommand.cs @@ -1,7 +1,7 @@ using System.IO; using System.IO.Compression; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; namespace PSCompression.Commands; diff --git a/src/PSCompression/Commands/ConvertFromZLibStringCommand.cs b/src/PSCompression/Commands/ConvertFromZLibStringCommand.cs index 3df4254..a9b37d4 100644 --- a/src/PSCompression/Commands/ConvertFromZLibStringCommand.cs +++ b/src/PSCompression/Commands/ConvertFromZLibStringCommand.cs @@ -1,7 +1,7 @@ using System.IO; using System.IO.Compression; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; namespace PSCompression.Commands; @@ -12,7 +12,7 @@ public sealed class ConvertFromZLibStringCommand : FromCompressedStringCommandBa { protected override Stream CreateDecompressionStream(Stream inputStream) { - inputStream.Seek(2, SeekOrigin.Begin); + inputStream.Position = 2; DeflateStream deflate = new(inputStream, CompressionMode.Decompress); return deflate; } diff --git a/src/PSCompression/Commands/ConvertToBrotliStringCommand.cs b/src/PSCompression/Commands/ConvertToBrotliStringCommand.cs index 82d16f7..e12090d 100644 --- a/src/PSCompression/Commands/ConvertToBrotliStringCommand.cs +++ b/src/PSCompression/Commands/ConvertToBrotliStringCommand.cs @@ -1,7 +1,7 @@ using System.IO; using System.IO.Compression; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; using PSCompression.Extensions; namespace PSCompression.Commands; diff --git a/src/PSCompression/Commands/ConvertToDeflateStringCommand.cs b/src/PSCompression/Commands/ConvertToDeflateStringCommand.cs index f43f8f0..65a8514 100644 --- a/src/PSCompression/Commands/ConvertToDeflateStringCommand.cs +++ b/src/PSCompression/Commands/ConvertToDeflateStringCommand.cs @@ -1,7 +1,7 @@ using System.IO; using System.IO.Compression; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; namespace PSCompression.Commands; diff --git a/src/PSCompression/Commands/ConvertToGzipStringCommand.cs b/src/PSCompression/Commands/ConvertToGzipStringCommand.cs index f1b2366..a8a45fa 100644 --- a/src/PSCompression/Commands/ConvertToGzipStringCommand.cs +++ b/src/PSCompression/Commands/ConvertToGzipStringCommand.cs @@ -1,7 +1,7 @@ using System.IO; using System.IO.Compression; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; namespace PSCompression.Commands; diff --git a/src/PSCompression/Commands/ConvertToZLibStringCommand.cs b/src/PSCompression/Commands/ConvertToZLibStringCommand.cs index f15e547..0f82944 100644 --- a/src/PSCompression/Commands/ConvertToZLibStringCommand.cs +++ b/src/PSCompression/Commands/ConvertToZLibStringCommand.cs @@ -1,7 +1,8 @@ using System.IO; using System.IO.Compression; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; +using PSCompression.FormatHandlers.Streams; namespace PSCompression.Commands; diff --git a/src/PSCompression/Commands/ExpandRarArchiveCommand.cs b/src/PSCompression/Commands/ExpandRarArchiveCommand.cs new file mode 100644 index 0000000..93de83a --- /dev/null +++ b/src/PSCompression/Commands/ExpandRarArchiveCommand.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Security; +using PSCompression.Abstractions.Commands; +using PSCompression.Extensions; +using SharpCompress.Archives; +using SharpCompress.Archives.Rar; +using SharpCompress.Common; +using IOPath = System.IO.Path; + +namespace PSCompression.Commands; + +[Cmdlet(VerbsData.Expand, "RarArchive")] +[OutputType(typeof(FileInfo), typeof(DirectoryInfo))] +[Alias("unrar")] +public sealed class ExpandRarArchiveCommand : ExpandArchiveCommandBase +{ + [Parameter] + public SecureString? Password { get; set; } + + protected override List ExtractArchive(string source, string destination) + { + IRarArchive rar; + using (rar = RarArchive.OpenArchive(source)) + { + if (rar.IsEncrypted) + { + rar.ReaderOptions.Password = Password is null + ? Host.PromptForPassword($"Encrypted RAR Archive '{source}' requires a password.") + : Password.AsPlainText(); + } + } + + using (rar = RarArchive.OpenArchive(source, rar.ReaderOptions)) + rar.WriteToDirectory(destination, new ExtractionOptions(overwrite: Force)); + + return !PassThru ? [] : [.. rar.Entries.Select(e => + { + string dest = IOPath.GetFullPath(IOPath.Combine(destination, e.Key ?? "")); + FileSystemInfo info = e.IsDirectory ? new DirectoryInfo(dest) : new FileInfo(dest); + return info.AppendPSProperties(); + })]; + } +} diff --git a/src/PSCompression/Commands/ExpandRarEntryCommand.cs b/src/PSCompression/Commands/ExpandRarEntryCommand.cs new file mode 100644 index 0000000..5878f20 --- /dev/null +++ b/src/PSCompression/Commands/ExpandRarEntryCommand.cs @@ -0,0 +1,54 @@ +using System.IO; +using System.Management.Automation; +using System.Security; +using PSCompression.Abstractions.Commands; +using PSCompression.Extensions; +using PSCompression.FormatHandlers.Common; +using PSCompression.FormatHandlers.Rar; +using SharpCompress.Archives; +using SharpCompress.Archives.Rar; +using SharpCompress.Common; + +namespace PSCompression.Commands; + +[Cmdlet(VerbsData.Expand, "RarEntry")] +[OutputType(typeof(FileInfo), typeof(DirectoryInfo))] +[Alias("unrarentry")] +public sealed class ExpandRarEntryCommand : ExpandEntryCommandBase +{ + private readonly ArchiveCache _cache = new(e => e.OpenRead()); + + private ExtractionOptions? _options; + + [Parameter] + public SecureString? Password { get; set; } + + protected override FileSystemInfo Extract(RarEntry entry, string destination) + { + destination = Path.GetFullPath(Path.Combine(destination, entry.RelativePath)); + + if (entry.Type == Enum.EntryType.Directory) + { + DirectoryInfo dir = new(destination); + dir.Create(); + return dir; + } + + _options ??= new ExtractionOptions(overwrite: Force); + IRarArchive rar = _cache.GetOrCreate(entry); + + if (entry.IsEncrypted) + { + rar.ReaderOptions.Password = Password is null + ? Host.PromptForPassword(entry) + : Password.AsPlainText(); + } + + IArchiveEntry rarEntry = rar.GetEntry(entry); + FileInfo file = new(destination); + Dbg.Assert(file.Directory is not null, "Files must always have a parent directory."); + file.Directory.Create(); + rarEntry.WriteToFile(destination, _options); + return file; + } +} diff --git a/src/PSCompression/Commands/ExpandTarArchiveCommand.cs b/src/PSCompression/Commands/ExpandTarArchiveCommand.cs index 8a1ee7d..7c9f174 100644 --- a/src/PSCompression/Commands/ExpandTarArchiveCommand.cs +++ b/src/PSCompression/Commands/ExpandTarArchiveCommand.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Management.Automation; using System.Text; using ICSharpCode.SharpZipLib.Tar; -using PSCompression.Abstractions; -using PSCompression.Exceptions; +using PSCompression.Abstractions.Commands; +using PSCompression.Enum; using PSCompression.Extensions; +using PSCompression.FormatHandlers.Tar; using IO = System.IO; namespace PSCompression.Commands; @@ -15,88 +15,34 @@ namespace PSCompression.Commands; [Cmdlet(VerbsData.Expand, "TarArchive")] [OutputType(typeof(FileInfo), typeof(DirectoryInfo))] [Alias("untar")] -public sealed class ExpandTarArchiveCommand : CommandWithPathBase +public sealed class ExpandTarArchiveCommand : ExpandArchiveCommandBase { private bool _shouldInferAlgo; - [Parameter(Position = 1)] - public string? Destination { get; set; } - [Parameter] public Algorithm Algorithm { get; set; } - [Parameter] - public SwitchParameter Force { get; set; } - - [Parameter] - public SwitchParameter PassThru { get; set; } - protected override void BeginProcessing() { - Destination = Destination is null - // PowerShell is retarded and decided to mix up ProviderPath & Path - ? SessionState.Path.CurrentFileSystemLocation.ProviderPath - : Destination.ResolvePath(this); - - if (File.Exists(Destination)) - { - ThrowTerminatingError(ExceptionHelper.NotDirectoryPath( - Destination, nameof(Destination))); - } - - Directory.CreateDirectory(Destination); - - _shouldInferAlgo = !MyInvocation.BoundParameters - .ContainsKey(nameof(Algorithm)); + base.BeginProcessing(); + _shouldInferAlgo = !MyInvocation.HasBound(nameof(Algorithm)); } - protected override void ProcessRecord() + protected override List ExtractArchive(string source, string destination) { - Dbg.Assert(Destination is not null); - - foreach (string path in EnumerateResolvedPaths()) - { - if (_shouldInferAlgo) - { - Algorithm = AlgorithmMappings.Parse(path); - } + if (_shouldInferAlgo) Algorithm = AlgorithmMappings.Parse(source); - try - { - FileSystemInfo[] output = ExtractArchive(path); - - if (PassThru) - { - IOrderedEnumerable result = output - .Select(PathExtensions.AppendPSProperties) - .OrderBy(pso => pso.Properties["PSParentPath"].Value); - - WriteObject(result, enumerateCollection: true); - } - } - catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) - { - throw; - } - catch (Exception exception) - { - WriteError(exception.ToWriteError(path)); - } - } - } - - private FileSystemInfo[] ExtractArchive(string path) - { - using FileStream fs = File.OpenRead(path); + using FileStream fs = File.OpenRead(source); using Stream decompress = Algorithm.FromCompressedStream(fs); using TarInputStream tar = new(decompress, Encoding.UTF8); - List result = []; + List result = []; foreach (TarEntry entry in tar.EnumerateEntries()) { try { - result.Add(ExtractEntry(entry, tar)); + FileSystemInfo info = ExtractEntry(entry, tar, destination); + if (PassThru) result.Add(info.AppendPSProperties()); } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { @@ -111,22 +57,20 @@ private FileSystemInfo[] ExtractArchive(string path) return [.. result]; } - private FileSystemInfo ExtractEntry(TarEntry entry, TarInputStream tar) + private FileSystemInfo ExtractEntry(TarEntry entry, TarInputStream tar, string destination) { - Dbg.Assert(Destination is not null); - - string destination = IO.Path.GetFullPath( - IO.Path.Combine(Destination, entry.Name)); + destination = IO.Path.GetFullPath(IO.Path.Combine(destination, entry.Name)); if (entry.IsDirectory) { DirectoryInfo dir = new(destination); - dir.Create(Force); + dir.Create(); return dir; } FileInfo file = new(destination); - file.Directory?.Create(); + Dbg.Assert(file.Directory is not null, "Files must always have a parent directory."); + file.Directory.Create(); using (FileStream destStream = File.Open( destination, diff --git a/src/PSCompression/Commands/ExpandTarEntryCommand.cs b/src/PSCompression/Commands/ExpandTarEntryCommand.cs index 321ff8c..fbb2068 100644 --- a/src/PSCompression/Commands/ExpandTarEntryCommand.cs +++ b/src/PSCompression/Commands/ExpandTarEntryCommand.cs @@ -1,6 +1,7 @@ using System.IO; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; +using PSCompression.Abstractions.Entries; namespace PSCompression.Commands; @@ -9,6 +10,6 @@ namespace PSCompression.Commands; [Alias("untarentry")] public sealed class ExpandTarEntryCommand : ExpandEntryCommandBase { - protected override FileSystemInfo Extract(TarEntryBase entry) => - entry.ExtractTo(Destination!, Force); + protected override FileSystemInfo Extract(TarEntryBase entry, string destination) => + entry.ExtractTo(destination, Force); } diff --git a/src/PSCompression/Commands/ExpandZipEntryCommand.cs b/src/PSCompression/Commands/ExpandZipEntryCommand.cs index b7f28c7..019143e 100644 --- a/src/PSCompression/Commands/ExpandZipEntryCommand.cs +++ b/src/PSCompression/Commands/ExpandZipEntryCommand.cs @@ -3,8 +3,10 @@ using System.Management.Automation; using System.Security; using ICSharpCode.SharpZipLib.Zip; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; +using PSCompression.Abstractions.Entries; using PSCompression.Extensions; +using PSCompression.FormatHandlers.Common; namespace PSCompression.Commands; @@ -13,27 +15,24 @@ namespace PSCompression.Commands; [Alias("unzipentry")] public sealed class ExpandZipEntryCommand : ExpandEntryCommandBase, IDisposable { + private readonly ArchiveCache _cache = new(entry => entry.OpenSharpZipLibArchive()); + [Parameter] public SecureString? Password { get; set; } - private ZipArchiveCache? _cache; - - protected override FileSystemInfo Extract(ZipEntryBase entry) + protected override FileSystemInfo Extract(ZipEntryBase entry, string destination) { - _cache ??= new ZipArchiveCache(entry => entry.OpenRead(Password)); ZipFile zip = _cache.GetOrCreate(entry); - if (entry.IsEncrypted && Password is null && entry is ZipEntryFile fileEntry) + if (entry.IsEncrypted) { - zip.Password = fileEntry.PromptForPassword(Host); + zip.Password = Password is null + ? Host.PromptForPassword(entry) + : Password.AsPlainText(); } - return entry.ExtractTo(Destination!, Force, zip); + return entry.ExtractTo(destination, Force, zip); } - public void Dispose() - { - _cache?.Dispose(); - GC.SuppressFinalize(this); - } + public void Dispose() => _cache.Dispose(); } diff --git a/src/PSCompression/Commands/GetRarEntryCommand.cs b/src/PSCompression/Commands/GetRarEntryCommand.cs new file mode 100644 index 0000000..41dfebb --- /dev/null +++ b/src/PSCompression/Commands/GetRarEntryCommand.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using PSCompression.Abstractions.Commands; +using PSCompression.Abstractions.Entries; +using PSCompression.FormatHandlers.Rar; +using SharpCompress.Archives; +using SharpCompress.Archives.Rar; +using SharpCompress.Readers; + +namespace PSCompression.Commands; + +[Cmdlet(VerbsCommon.Get, "RarEntry", DefaultParameterSetName = "Path")] +[OutputType(typeof(RarEntry))] +[Alias("rarge")] +public sealed class GetRarEntryCommand : GetEntryCommandBase +{ + private static readonly ReaderOptions s_readerOptions = new() { LeaveStreamOpen = true }; + + internal override Enum.ArchiveType ArchiveType => Enum.ArchiveType.rar; + + protected override IEnumerable GetEntriesFromFile(string path) + { + using IRarArchive rar = RarArchive.OpenArchive(path); + foreach (IArchiveEntry entry in rar.Entries) + { + if (ShouldSkipEntry(entry.IsDirectory)) + continue; + + if (!ShouldInclude(entry.Key) || ShouldExclude(entry.Key)) + continue; + + yield return new RarEntry(entry, path); + } + } + + protected override IEnumerable GetEntriesFromStream(Stream stream) + { + using IRarArchive rar = RarArchive.OpenArchive(stream, s_readerOptions); + foreach (IArchiveEntry entry in rar.Entries) + { + if (ShouldSkipEntry(entry.IsDirectory)) + continue; + + if (!ShouldInclude(entry.Key) || ShouldExclude(entry.Key)) + continue; + + yield return new RarEntry(entry, stream); + } + } +} diff --git a/src/PSCompression/Commands/GetRarEntryContentCommand.cs b/src/PSCompression/Commands/GetRarEntryContentCommand.cs new file mode 100644 index 0000000..df9ea87 --- /dev/null +++ b/src/PSCompression/Commands/GetRarEntryContentCommand.cs @@ -0,0 +1,86 @@ +using System; +using System.IO; +using System.Management.Automation; +using System.Security; +using PSCompression.Abstractions.Commands; +using PSCompression.Extensions; +using PSCompression.FormatHandlers.Common; +using PSCompression.FormatHandlers.Rar; +using SharpCompress.Archives; +using SharpCompress.Archives.Rar; + +namespace PSCompression.Commands; + +[Cmdlet(VerbsCommon.Get, "RarEntryContent", DefaultParameterSetName = "Stream")] +[OutputType(typeof(string), ParameterSetName = ["Stream"])] +[OutputType(typeof(byte), ParameterSetName = ["Bytes"])] +[Alias("rargec")] +public sealed class GetRarEntryContentCommand : GetEntryContentCommandBase, IDisposable +{ + private readonly ArchiveCache _cache = new(e => e.OpenRead()); + + [Parameter] + public SecureString? Password { get; set; } + + protected override void ProcessRecord() + { + foreach (RarEntry entry in Entry) + { + if (entry.Type == Enum.EntryType.Directory || entry.Length == 0) + continue; + + try + { + IRarArchive rar = _cache.GetOrCreate(entry); + + if (entry.IsEncrypted) + { + rar.ReaderOptions.Password = Password is null + ? Host.PromptForPassword(entry) + : Password.AsPlainText(); + } + + ReadEntry(rar.GetEntry(entry)); + } + catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) + { + throw; + } + catch (Exception exception) + { + WriteError(exception.ToOpenError(entry.Source)); + } + } + } + + private void ReadEntry(IArchiveEntry entry) + { + using Stream stream = entry.OpenEntryStream(); + + if (AsByteStream) + { + Dbg.Assert(Buffer is not null); + using EntryByteReader byteReader = new(stream, Buffer); + + if (Raw) + { + byteReader.ReadAllBytes(this); + return; + } + + byteReader.StreamBytes(this); + return; + } + + using StreamReader reader = new(stream, Encoding); + if (Raw) + { + reader.ReadToEnd(this); + return; + } + + reader.ReadLines(this); + } + + public void Dispose() => _cache.Dispose(); +} diff --git a/src/PSCompression/Commands/GetTarEntryCommand.cs b/src/PSCompression/Commands/GetTarEntryCommand.cs index 994e4d7..6c31b6b 100644 --- a/src/PSCompression/Commands/GetTarEntryCommand.cs +++ b/src/PSCompression/Commands/GetTarEntryCommand.cs @@ -3,8 +3,10 @@ using System.Management.Automation; using System.Text; using ICSharpCode.SharpZipLib.Tar; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Commands; +using PSCompression.Abstractions.Entries; using PSCompression.Extensions; +using PSCompression.FormatHandlers.Tar; namespace PSCompression.Commands; @@ -14,13 +16,13 @@ namespace PSCompression.Commands; public sealed class GetTarEntryCommand : GetEntryCommandBase { [Parameter] - public Algorithm Algorithm { get; set; } + public Enum.Algorithm Algorithm { get; set; } - internal override ArchiveType ArchiveType => ArchiveType.tar; + internal override Enum.ArchiveType ArchiveType => Enum.ArchiveType.tar; protected override IEnumerable GetEntriesFromFile(string path) { - if (!MyInvocation.BoundParameters.ContainsKey(nameof(Algorithm))) + if (!MyInvocation.HasBound(nameof(Algorithm))) { Algorithm = AlgorithmMappings.Parse(path); } @@ -32,14 +34,10 @@ protected override IEnumerable GetEntriesFromFile(string path) foreach (TarEntry entry in tar.EnumerateEntries()) { if (ShouldSkipEntry(entry.IsDirectory)) - { continue; - } if (!ShouldInclude(entry.Name) || ShouldExclude(entry.Name)) - { continue; - } yield return entry.IsDirectory ? new TarEntryDirectory(entry, path) @@ -55,14 +53,10 @@ protected override IEnumerable GetEntriesFromStream(Stream stream) foreach (TarEntry entry in tar.EnumerateEntries()) { if (ShouldSkipEntry(entry.IsDirectory)) - { continue; - } if (!ShouldInclude(entry.Name) || ShouldExclude(entry.Name)) - { continue; - } yield return entry.IsDirectory ? new TarEntryDirectory(entry, stream) diff --git a/src/PSCompression/Commands/GetTarEntryContentCommand.cs b/src/PSCompression/Commands/GetTarEntryContentCommand.cs index 462864c..1c36ed0 100644 --- a/src/PSCompression/Commands/GetTarEntryContentCommand.cs +++ b/src/PSCompression/Commands/GetTarEntryContentCommand.cs @@ -1,9 +1,10 @@ using System; using System.IO; using System.Management.Automation; -using PSCompression.Abstractions; -using PSCompression.Exceptions; +using PSCompression.Abstractions.Commands; using PSCompression.Extensions; +using PSCompression.FormatHandlers.Common; +using PSCompression.FormatHandlers.Tar; namespace PSCompression.Commands; @@ -35,10 +36,7 @@ protected override void ProcessRecord() private void ReadEntry(TarEntryFile entry) { using MemoryStream mem = new(); - if (!entry.GetContentStream(mem)) - { - return; - } + if (!entry.GetContentStream(mem)) return; if (AsByteStream) { diff --git a/src/PSCompression/Commands/GetZipEntryCommand.cs b/src/PSCompression/Commands/GetZipEntryCommand.cs index 051e1b8..a95dd71 100644 --- a/src/PSCompression/Commands/GetZipEntryCommand.cs +++ b/src/PSCompression/Commands/GetZipEntryCommand.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; using System.Management.Automation; using System.IO; -using PSCompression.Abstractions; using ICSharpCode.SharpZipLib.Zip; +using PSCompression.FormatHandlers.Zip; +using PSCompression.Abstractions.Entries; +using PSCompression.Abstractions.Commands; namespace PSCompression.Commands; @@ -11,7 +13,7 @@ namespace PSCompression.Commands; [Alias("zipge")] public sealed class GetZipEntryCommand : GetEntryCommandBase { - internal override ArchiveType ArchiveType => ArchiveType.zip; + internal override Enum.ArchiveType ArchiveType => Enum.ArchiveType.zip; protected override IEnumerable GetEntriesFromFile(string path) { @@ -21,14 +23,10 @@ protected override IEnumerable GetEntriesFromFile(string path) foreach (ZipEntry entry in zip) { if (ShouldSkipEntry(entry.IsDirectory)) - { continue; - } if (!ShouldInclude(entry.Name) || ShouldExclude(entry.Name)) - { continue; - } entries.Add(entry.IsDirectory ? new ZipEntryDirectory(entry, path) @@ -47,14 +45,10 @@ protected override IEnumerable GetEntriesFromStream(Stream stream) foreach (ZipEntry entry in zip) { if (ShouldSkipEntry(entry.IsDirectory)) - { continue; - } if (!ShouldInclude(entry.Name) || ShouldExclude(entry.Name)) - { continue; - } entries.Add(entry.IsDirectory ? new ZipEntryDirectory(entry, stream) diff --git a/src/PSCompression/Commands/GetZipEntryContentCommand.cs b/src/PSCompression/Commands/GetZipEntryContentCommand.cs index 3929277..901e153 100644 --- a/src/PSCompression/Commands/GetZipEntryContentCommand.cs +++ b/src/PSCompression/Commands/GetZipEntryContentCommand.cs @@ -3,9 +3,10 @@ using System.Management.Automation; using System.Security; using ICSharpCode.SharpZipLib.Zip; -using PSCompression.Abstractions; -using PSCompression.Exceptions; +using PSCompression.Abstractions.Commands; using PSCompression.Extensions; +using PSCompression.FormatHandlers.Common; +using PSCompression.FormatHandlers.Zip; namespace PSCompression.Commands; @@ -18,20 +19,21 @@ public sealed class GetZipEntryContentCommand : GetEntryContentCommandBase? _cache; + private ArchiveCache _cache = new(entry => entry.OpenSharpZipLibArchive()); protected override void ProcessRecord() { - _cache ??= new ZipArchiveCache(entry => entry.OpenRead(Password)); - foreach (ZipEntryFile entry in Entry) { try { ZipFile zip = _cache.GetOrCreate(entry); - if (entry.IsEncrypted && Password is null) + + if (entry.IsEncrypted) { - zip.Password = entry.PromptForPassword(Host); + zip.Password = Password is null + ? Host.PromptForPassword(entry) + : Password.AsPlainText(); } ReadEntry(entry.Open(zip)); @@ -74,7 +76,6 @@ private void ReadEntry(Stream stream) public void Dispose() { - _cache?.Dispose(); - GC.SuppressFinalize(this); + _cache.Dispose(); } } diff --git a/src/PSCompression/Commands/NewZipEntryCommand.cs b/src/PSCompression/Commands/NewZipEntryCommand.cs index 1ba0a7d..e60a94c 100644 --- a/src/PSCompression/Commands/NewZipEntryCommand.cs +++ b/src/PSCompression/Commands/NewZipEntryCommand.cs @@ -7,8 +7,11 @@ using System.Text; using PSCompression.Extensions; using PSCompression.Exceptions; -using PSCompression.Abstractions; using ICSharpCode.SharpZipLib.Zip; +using PSCompression.Attributes; +using PSCompression.FormatHandlers.Zip; +using PSCompression.FormatHandlers.Common; +using PSCompression.Abstractions.Entries; namespace PSCompression.Commands; @@ -73,7 +76,7 @@ protected override void BeginProcessing() { if (_zip.TryGetEntry(entry, out ZipArchiveEntry? zipentry)) { - if (!Force.IsPresent) + if (!Force) { WriteError(DuplicatedEntryException .Create(entry, Destination) @@ -109,7 +112,7 @@ protected override void BeginProcessing() { if (_zip.TryGetEntry(entry, out ZipArchiveEntry? zipentry)) { - if (!Force.IsPresent) + if (!Force) { WriteError(DuplicatedEntryException .Create(entry, Destination) @@ -170,9 +173,8 @@ protected override void EndProcessing() try { Dispose(); - WriteObject( - GetEntries().ToEntrySort(), - enumerateCollection: true); + foreach (EntryBase entry in GetEntries().SortEntries()) + WriteObject(entry); } catch (Exception _) when (_ is PipelineStoppedException or FlowControlException) { @@ -203,12 +205,9 @@ public void Dispose() if (_writers is not null) { foreach (StreamWriter writer in _writers) - { writer.Dispose(); - } } _zip?.Dispose(); - GC.SuppressFinalize(this); } } diff --git a/src/PSCompression/Commands/RemoveZipEntryCommand.cs b/src/PSCompression/Commands/RemoveZipEntryCommand.cs index 07001b2..4fad886 100644 --- a/src/PSCompression/Commands/RemoveZipEntryCommand.cs +++ b/src/PSCompression/Commands/RemoveZipEntryCommand.cs @@ -1,8 +1,9 @@ using System; using System.IO.Compression; using System.Management.Automation; -using PSCompression.Abstractions; -using PSCompression.Exceptions; +using PSCompression.Abstractions.Entries; +using PSCompression.Extensions; +using PSCompression.FormatHandlers.Common; namespace PSCompression.Commands; @@ -11,7 +12,7 @@ namespace PSCompression.Commands; [Alias("ziprm")] public sealed class RemoveZipEntryCommand : PSCmdlet, IDisposable { - private readonly ZipArchiveCache _cache = new( + private readonly ArchiveCache _cache = new( entry => entry.OpenZip(ZipArchiveMode.Update)); [Parameter(Mandatory = true, ValueFromPipeline = true)] @@ -45,7 +46,6 @@ protected override void ProcessRecord() public void Dispose() { - _cache?.Dispose(); - GC.SuppressFinalize(this); + _cache.Dispose(); } } diff --git a/src/PSCompression/Commands/RenameZipEntryCommand.cs b/src/PSCompression/Commands/RenameZipEntryCommand.cs index 1522bae..7239a93 100644 --- a/src/PSCompression/Commands/RenameZipEntryCommand.cs +++ b/src/PSCompression/Commands/RenameZipEntryCommand.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.IO.Compression; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Entries; using PSCompression.Exceptions; using PSCompression.Extensions; +using PSCompression.FormatHandlers.Common; +using PSCompression.FormatHandlers.Zip; namespace PSCompression.Commands; @@ -13,10 +15,10 @@ namespace PSCompression.Commands; [Alias("zipren")] public sealed class RenameZipEntryCommand : PSCmdlet, IDisposable { - private readonly ZipArchiveCache _zipArchiveCache = new( + private readonly ArchiveCache _archiveCache = new( entry => entry.OpenZip(ZipArchiveMode.Update)); - private ZipEntryCache? _zipEntryCache; + private ZipEntryCache? _entryCache; private readonly ZipEntryMoveCache _moveCache = new(); @@ -37,24 +39,19 @@ public sealed class RenameZipEntryCommand : PSCmdlet, IDisposable protected override void BeginProcessing() { - if (PassThru.IsPresent) - { - _zipEntryCache = new(); - } + if (PassThru) _entryCache = new(); } protected override void ProcessRecord() { if (!ShouldProcess(target: ZipEntry.ToString(), action: "Rename")) - { return; - } try { ZipEntry.ThrowIfFromStream(); NewName.ThrowIfInvalidNameChar(); - _zipArchiveCache.TryAdd(ZipEntry); + _archiveCache.TryAdd(ZipEntry); _moveCache.AddEntry(ZipEntry, NewName); } catch (NotSupportedException exception) @@ -73,23 +70,19 @@ protected override void ProcessRecord() protected override void EndProcessing() { - foreach (var mapping in _moveCache.GetMappings(_zipArchiveCache)) - { + Dictionary> mappings = _moveCache.GetMappings(_archiveCache); + foreach (KeyValuePair> mapping in mappings) Rename(mapping); - } - _zipArchiveCache?.Dispose(); - if (!PassThru.IsPresent || _zipEntryCache is null) - { - return; - } + _archiveCache.Dispose(); + if (!PassThru || _entryCache is null) return; + + IEnumerable entries = _entryCache + .AddRange(_moveCache.GetPassThruMappings()) + .GetEntries() + .SortEntries(); - WriteObject( - _zipEntryCache - .AddRange(_moveCache.GetPassThruMappings()) - .GetEntries() - .ToEntrySort(), - enumerateCollection: true); + foreach (EntryBase entry in entries) WriteObject(entry); } private void Rename(KeyValuePair> mapping) @@ -102,7 +95,7 @@ private void Rename(KeyValuePair> mapping) sourceRelativePath: source, destination: destination, sourceZipPath: mapping.Key, - _zipArchiveCache[mapping.Key]); + _archiveCache[mapping.Key]); } catch (DuplicatedEntryException exception) { @@ -126,7 +119,6 @@ private void Rename(KeyValuePair> mapping) public void Dispose() { - _zipArchiveCache?.Dispose(); - GC.SuppressFinalize(this); + _archiveCache.Dispose(); } } diff --git a/src/PSCompression/Commands/SetZipEntryContentCommand.cs b/src/PSCompression/Commands/SetZipEntryContentCommand.cs index 29ee396..dc1ab60 100644 --- a/src/PSCompression/Commands/SetZipEntryContentCommand.cs +++ b/src/PSCompression/Commands/SetZipEntryContentCommand.cs @@ -3,8 +3,9 @@ using System.IO.Compression; using System.Management.Automation; using System.Text; -using PSCompression.Exceptions; +using PSCompression.Attributes; using PSCompression.Extensions; +using PSCompression.FormatHandlers.Zip; namespace PSCompression.Commands; @@ -111,6 +112,5 @@ public void Dispose() _byteWriter?.Dispose(); _stringWriter?.Dispose(); _zip?.Dispose(); - GC.SuppressFinalize(this); } } diff --git a/src/PSCompression/Dbg.cs b/src/PSCompression/Dbg.cs new file mode 100644 index 0000000..d2adab2 --- /dev/null +++ b/src/PSCompression/Dbg.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; + +namespace PSCompression; + +[ExcludeFromCodeCoverage] +internal static class Dbg +{ + [Conditional("DEBUG")] + public static void Assert( + [DoesNotReturnIf(false)] bool condition, + string? message = null, + [CallerFilePath] string filePath = "", + [CallerLineNumber] int lineNumber = 0) + { + if (!condition) + { + string detail = string.IsNullOrEmpty(message) + ? $"Assertion failed at {Path.GetFileName(filePath)}:{lineNumber}" + : $"{message} ({Path.GetFileName(filePath)}:{lineNumber})"; + + Debug.Assert(false, detail); + } + } +} diff --git a/src/PSCompression/Dbg/Dbg.cs b/src/PSCompression/Dbg/Dbg.cs deleted file mode 100644 index 1a4aba5..0000000 --- a/src/PSCompression/Dbg/Dbg.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; - -namespace PSCompression; - -internal static class Dbg -{ - [Conditional("DEBUG")] - public static void Assert([DoesNotReturnIf(false)] bool condition) => - Debug.Assert(condition); -} diff --git a/src/PSCompression/Algorithm.cs b/src/PSCompression/Enum/Algorithm.cs similarity index 70% rename from src/PSCompression/Algorithm.cs rename to src/PSCompression/Enum/Algorithm.cs index b3967fb..777afe2 100644 --- a/src/PSCompression/Algorithm.cs +++ b/src/PSCompression/Enum/Algorithm.cs @@ -1,4 +1,4 @@ -namespace PSCompression; +namespace PSCompression.Enum; public enum Algorithm { diff --git a/src/PSCompression/Enum/ArchiveType.cs b/src/PSCompression/Enum/ArchiveType.cs new file mode 100644 index 0000000..aea8a3c --- /dev/null +++ b/src/PSCompression/Enum/ArchiveType.cs @@ -0,0 +1,8 @@ +namespace PSCompression.Enum; + +internal enum ArchiveType +{ + zip, + tar, + rar +} diff --git a/src/PSCompression/ZipEntryType.cs b/src/PSCompression/Enum/ZipEntryType.cs similarity index 67% rename from src/PSCompression/ZipEntryType.cs rename to src/PSCompression/Enum/ZipEntryType.cs index 112af46..94b6670 100644 --- a/src/PSCompression/ZipEntryType.cs +++ b/src/PSCompression/Enum/ZipEntryType.cs @@ -1,4 +1,4 @@ -namespace PSCompression; +namespace PSCompression.Enum; public enum EntryType { diff --git a/src/PSCompression/Exceptions/ExceptionHelper.cs b/src/PSCompression/Exceptions/ExceptionHelper.cs deleted file mode 100644 index ce66834..0000000 --- a/src/PSCompression/Exceptions/ExceptionHelper.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.IO.Compression; -using System.Management.Automation; -using ICSharpCode.SharpZipLib.Zip; -using PSCompression.Abstractions; -using PSCompression.Extensions; - -namespace PSCompression.Exceptions; - -internal static class ExceptionHelper -{ - private static readonly char[] s_InvalidFileNameChar = Path.GetInvalidFileNameChars(); - - private static readonly char[] s_InvalidPathChar = Path.GetInvalidPathChars(); - - internal static bool WriteErrorIfNotArchive( - this string path, - string paramname, - PSCmdlet cmdlet, - bool isTerminating = false) - { - if (File.Exists(path)) - { - return false; - } - - ArgumentException exception = new( - $"The specified path '{path}' does not exist or is a Directory.", - paramname); - - ErrorRecord error = new(exception, "NotArchivePath", ErrorCategory.InvalidArgument, path); - - if (isTerminating) - { - cmdlet.ThrowTerminatingError(error); - } - - cmdlet.WriteError(error); - return true; - } - - - internal static ErrorRecord NotDirectoryPath(string path, string paramname) => - new( - new ArgumentException( - $"Destination path is an existing File: '{path}'.", paramname), - "NotDirectoryPath", ErrorCategory.InvalidArgument, path); - - internal static ErrorRecord ToInvalidProviderError(this ProviderInfo provider, string path) => - new( - new NotSupportedException( - $"The resolved path '{path}' is not a FileSystem path but '{provider.Name}'."), - "NotFileSystemPath", ErrorCategory.InvalidArgument, path); - - internal static ErrorRecord ToOpenError(this Exception exception, string path) => - new(exception, "EntryOpen", ErrorCategory.OpenError, path); - - internal static ErrorRecord ToResolvePathError(this Exception exception, string path) => - new(exception, "ResolvePath", ErrorCategory.NotSpecified, path); - - internal static ErrorRecord ToExtractEntryError(this Exception exception, object entry) => - new(exception, "ExtractEntry", ErrorCategory.NotSpecified, entry); - - internal static ErrorRecord ToStreamOpenError(this Exception exception, object item) => - new(exception, "StreamOpen", ErrorCategory.NotSpecified, item); - - internal static ErrorRecord ToWriteError(this Exception exception, object? item) => - new(exception, "WriteError", ErrorCategory.WriteError, item); - - internal static ErrorRecord ToDuplicatedEntryError(this DuplicatedEntryException exception) => - new(exception, "DuplicatedEntry", ErrorCategory.WriteError, exception._path); - - internal static ErrorRecord ToInvalidNameError(this InvalidNameException exception, string name) => - new(exception, "InvalidName", ErrorCategory.InvalidArgument, name); - - internal static ErrorRecord ToEntryNotFoundError(this EntryNotFoundException exception) => - new(exception, "EntryNotFound", ErrorCategory.ObjectNotFound, exception._path); - - internal static ErrorRecord ToEnumerationError(this Exception exception, object item) => - new(exception, "EnumerationError", ErrorCategory.ReadError, item); - - internal static ErrorRecord ToInvalidArchive( - this Exception exception, - ArchiveType type, - bool isStream = false) - { - string basemsg = $"Specified path or stream is not a valid {type} archive, " + - "might be compressed using an unsupported method, " + - "or could be corrupted."; - - if (type == ArchiveType.tar && isStream) - { - basemsg += " When reading a tar archive from a stream, " + - "use the -Algorithm parameter to specify the compression type."; - } - - return new ErrorRecord( - new InvalidDataException(basemsg, exception), - "InvalidArchive", ErrorCategory.InvalidData, null); - } - - - internal static void ThrowIfNotFound( - this ZipArchive zip, - string path, - string source, - [NotNull] out ZipArchiveEntry? entry) - { - if (!zip.TryGetEntry(path, out entry)) - { - throw EntryNotFoundException.Create(path, source); - } - } - - internal static void ThrowIfNotFound( - this ICSharpCode.SharpZipLib.Zip.ZipFile zip, - string path, - string source, - [NotNull] out ZipEntry? entry) - { - if (!zip.TryGetEntry(path, out entry)) - { - throw EntryNotFoundException.Create(path, source); - } - } - - internal static void ThrowIfDuplicate( - this ZipArchive zip, - string path, - string source) - { - if (zip.TryGetEntry(path, out ZipArchiveEntry? _)) - { - throw DuplicatedEntryException.Create(path, source); - } - } - - internal static void ThrowIfInvalidNameChar(this string newname) - { - if (newname.IndexOfAny(s_InvalidFileNameChar) != -1) - { - throw InvalidNameException.Create(newname); - } - } - - internal static void ThrowIfInvalidPathChar(this string path) - { - if (path.IndexOfAny(s_InvalidPathChar) != -1) - { - throw new ArgumentException( - $"Path: '{path}' contains invalid path characters."); - } - } - - internal static void ThrowIfFromStream(this ZipEntryBase entry) - { - if (entry.FromStream) - { - throw new NotSupportedException( - "The operation is not supported for entries created from input Stream."); - } - } -} diff --git a/src/PSCompression/Extensions/CompressionExtensions.cs b/src/PSCompression/Extensions/CompressionExtensions.cs index 830b991..0458e52 100644 --- a/src/PSCompression/Extensions/CompressionExtensions.cs +++ b/src/PSCompression/Extensions/CompressionExtensions.cs @@ -3,10 +3,16 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.IO.Compression; +using System.Linq; using System.Text.RegularExpressions; using ICSharpCode.SharpZipLib.BZip2; using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Zip; +using PSCompression.Enum; +using PSCompression.FormatHandlers.Rar; +using PSCompression.FormatHandlers.Zip; +using SharpCompress.Archives; +using SharpCompress.Archives.Rar; using SharpCompress.Compressors.LZMA; using ZstdSharp; using SharpCompressors = SharpCompress.Compressors; @@ -17,188 +23,191 @@ internal static partial class CompressionExtensions { private const string DirectorySeparator = "/"; -#if NETCOREAPP - [GeneratedRegex(@"[^/]+(?=/$)", +#if NET8_0_OR_GREATER + [GeneratedRegex( + @"[^/]+(?=/$)", RegexOptions.Compiled | RegexOptions.RightToLeft)] private static partial Regex GetDirectoryName(); private static readonly Regex s_reGetDirName = GetDirectoryName(); - - internal static string RelativeTo(this DirectoryInfo directory, int length) => - string.Concat(directory.FullName.AsSpan(length), DirectorySeparator) - .NormalizeEntryPath(); #else private static readonly Regex s_reGetDirName = new( @"[^/]+(?=/$)", RegexOptions.Compiled | RegexOptions.RightToLeft); - - internal static string RelativeTo(this DirectoryInfo directory, int length) => - $"{directory.FullName.Substring(length)}{DirectorySeparator}" - .NormalizeEntryPath(); #endif - internal static string RelativeTo(this FileInfo file, int length) => - file.FullName.Substring(length).NormalizeFileEntryPath(); + extension(DirectoryInfo directory) + { + internal string RelativeTo(int length) => + $"{directory.FullName.Substring(length)}{DirectorySeparator}".NormalizeEntryPath(); + } + + extension(FileInfo file) + { + internal string RelativeTo(int length) => + file.FullName.Substring(length).NormalizeFileEntryPath(); + } - internal static ZipArchiveEntry CreateEntryFromFile( - this ZipArchive zip, - string entry, - FileStream fileStream, - CompressionLevel compressionLevel) + extension(ZipArchive zip) { - if (entry.EndsWith("/") || entry.EndsWith("\\")) + internal ZipArchiveEntry CreateEntryFromFile( + string entry, + FileStream fileStream, + CompressionLevel compressionLevel) { - return zip.CreateEntry(entry); + if (entry.EndsWith("/") || entry.EndsWith("\\")) + { + return zip.CreateEntry(entry); + } + + fileStream.Position = 0; + ZipArchiveEntry newentry = zip.CreateEntry(entry, compressionLevel); + + using (Stream stream = newentry.Open()) + { + fileStream.CopyTo(stream); + } + + return newentry; } - fileStream.Seek(0, SeekOrigin.Begin); - ZipArchiveEntry newentry = zip.CreateEntry(entry, compressionLevel); + internal bool TryGetEntry(string path, [NotNullWhen(true)] out ZipArchiveEntry? entry) + { + entry = zip.GetEntry(path); + return entry is not null; + } + } - using (Stream stream = newentry.Open()) + extension(ICSharpCode.SharpZipLib.Zip.ZipFile zip) + { + internal bool TryGetEntry(string path, [NotNullWhen(true)] out ZipEntry? entry) { - fileStream.CopyTo(stream); + entry = zip.GetEntry(path); + return entry is not null; } + } - return newentry; + extension(ZipEntryFile file) + { + internal string GetNewName(string newname) + { + string normalized = file.RelativePath.NormalizePath(); + return normalized.Contains(DirectorySeparator) + ? string.Join( + DirectorySeparator, + normalized.Substring(0, normalized.Length - file.Name!.Length - 1), + newname) + : newname; + } } - internal static bool TryGetEntry( - this ZipArchive zip, - string path, - [NotNullWhen(true)] out ZipArchiveEntry? entry) + extension(ZipEntryDirectory directory) { - entry = zip.GetEntry(path); - return entry is not null; + internal string ChangeName(string newname) + => s_reGetDirName.Replace(directory.RelativePath.NormalizePath(), newname); } - internal static bool TryGetEntry( - this ICSharpCode.SharpZipLib.Zip.ZipFile zip, - string path, - [NotNullWhen(true)] out ZipEntry? entry) + extension(ZipEntry entry) { - entry = zip.GetEntry(path); - return entry is not null; + internal string GetDirectoryName() => s_reGetDirName.Match(entry.Name).Value; } - internal static string ChangeName( - this ZipEntryFile file, - string newname) + extension(TarEntry entry) { - string normalized = file.RelativePath.NormalizePath(); + internal string GetDirectoryName() => s_reGetDirName.Match(entry.Name).Value; + } - if (!normalized.Contains(DirectorySeparator)) + extension(Stream stream) + { + internal BrotliSharpLib.BrotliStream AsBrotliCompressedStream(CompressionLevel compressionLevel) { - return newname; + BrotliSharpLib.BrotliStream brotli = new(stream, CompressionMode.Compress); + brotli.SetQuality(compressionLevel switch + { + CompressionLevel.NoCompression => 0, + CompressionLevel.Fastest => 1, + _ => 11 + }); + + return brotli; } - return string.Join( - DirectorySeparator, - normalized.Substring(0, normalized.Length - file.Name!.Length - 1), - newname); - } - - internal static string ChangeName( - this ZipEntryDirectory directory, - string newname) - => s_reGetDirName.Replace( - directory.RelativePath.NormalizePath(), - newname); - - internal static string GetDirectoryName(this ZipEntry entry) - => s_reGetDirName.Match(entry.Name).Value; - - internal static string GetDirectoryName(this TarEntry entry) - => s_reGetDirName.Match(entry.Name).Value; + internal BZip2OutputStream AsBZip2CompressedStream(CompressionLevel compressionLevel) + { + int blockSize = compressionLevel switch + { + CompressionLevel.NoCompression => 1, + CompressionLevel.Fastest => 2, + _ => 9 + }; + + return new BZip2OutputStream(stream, blockSize); + } - internal static BrotliSharpLib.BrotliStream AsBrotliCompressedStream( - this Stream stream, - CompressionLevel compressionLevel) - { - BrotliSharpLib.BrotliStream brotli = new(stream, CompressionMode.Compress); - brotli.SetQuality(compressionLevel switch + internal Stream AsZstCompressedStream(CompressionLevel compressionLevel) { - CompressionLevel.NoCompression => 0, - CompressionLevel.Fastest => 1, - _ => 11 - }); + int level = compressionLevel switch + { + CompressionLevel.NoCompression => 1, + CompressionLevel.Fastest => 3, + _ => 19 + }; + + return new CompressionStream(stream, level); + } - return brotli; + internal LZipStream AsLzCompressedStream() => + LZipStream.Create(stream, SharpCompressors.CompressionMode.Compress); } - internal static BZip2OutputStream AsBZip2CompressedStream( - this Stream stream, - CompressionLevel compressionLevel) + extension(Algorithm algorithm) { - int blockSize = compressionLevel switch - { - CompressionLevel.NoCompression => 1, - CompressionLevel.Fastest => 2, - _ => 9 - }; - - return new BZip2OutputStream(stream, blockSize); + internal Stream ToCompressedStream(Stream stream, CompressionLevel compressionLevel) + => algorithm switch + { + Algorithm.gz => new GZipStream(stream, compressionLevel), + Algorithm.zst => stream.AsZstCompressedStream(compressionLevel), + Algorithm.lz => stream.AsLzCompressedStream(), + Algorithm.bz2 => stream.AsBZip2CompressedStream(compressionLevel), + _ => stream + }; + + internal Stream FromCompressedStream(Stream stream) + => algorithm switch + { + Algorithm.gz => new GZipStream(stream, CompressionMode.Decompress), + Algorithm.zst => new DecompressionStream(stream), + Algorithm.lz => LZipStream.Create(stream, SharpCompressors.CompressionMode.Decompress), + Algorithm.bz2 => new BZip2InputStream(stream), + _ => stream + }; } - internal static CompressionStream AsZstCompressedStream( - this Stream stream, - CompressionLevel compressionLevel) + extension(TarOutputStream stream) { - int level = compressionLevel switch + internal void CreateTarEntry(string entryName, DateTime modTime, long size) { - CompressionLevel.NoCompression => 1, - CompressionLevel.Fastest => 3, - _ => 19 - }; - - return new CompressionStream(stream, level); + TarEntry entry = TarEntry.CreateTarEntry(entryName); + entry.TarHeader.Size = size; + entry.TarHeader.ModTime = modTime; + stream.PutNextEntry(entry); + } } - internal static LZipStream AsLzCompressedStream(this Stream outputStream) => - new(outputStream, SharpCompressors.CompressionMode.Compress); - - internal static Stream ToCompressedStream( - this Algorithm algorithm, - Stream stream, - CompressionLevel compressionLevel) - => algorithm switch - { - Algorithm.gz => new GZipStream(stream, compressionLevel), - Algorithm.zst => stream.AsZstCompressedStream(compressionLevel), - Algorithm.lz => stream.AsLzCompressedStream(), - Algorithm.bz2 => stream.AsBZip2CompressedStream(compressionLevel), - _ => stream - }; - - internal static Stream FromCompressedStream( - this Algorithm algorithm, - Stream stream) - => algorithm switch - { - Algorithm.gz => new GZipStream(stream, CompressionMode.Decompress), - Algorithm.zst => new DecompressionStream(stream), - Algorithm.lz => new LZipStream(stream, SharpCompressors.CompressionMode.Decompress), - Algorithm.bz2 => new BZip2InputStream(stream), - _ => stream - }; - - internal static void CreateTarEntry( - this TarOutputStream stream, - string entryName, - DateTime modTime, - long size) + extension(TarInputStream tar) { - TarEntry entry = TarEntry.CreateTarEntry(entryName); - entry.TarHeader.Size = size; - entry.TarHeader.ModTime = modTime; - stream.PutNextEntry(entry); + internal IEnumerable EnumerateEntries() + { + TarEntry? entry; + while ((entry = tar.GetNextEntry()) is not null) + yield return entry; + } } - internal static IEnumerable EnumerateEntries(this TarInputStream tar) + extension(IRarArchive rar) { - TarEntry? entry; - while ((entry = tar.GetNextEntry()) is not null) - { - yield return entry; - } + internal IArchiveEntry GetEntry(RarEntry entry) => + rar.Entries.FirstOrDefault(e => e.Key == entry.RelativePath) ?? throw new IOException( + $"The entry '{entry.RelativePath}' was not found in RAR archive '{entry.Source}'."); } } diff --git a/src/PSCompression/Extensions/ExceptionExtensions.cs b/src/PSCompression/Extensions/ExceptionExtensions.cs new file mode 100644 index 0000000..72ae2e4 --- /dev/null +++ b/src/PSCompression/Extensions/ExceptionExtensions.cs @@ -0,0 +1,184 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.IO.Compression; +using System.Management.Automation; +using ICSharpCode.SharpZipLib.Zip; +using PSCompression.Abstractions.Entries; +using PSCompression.Enum; +using PSCompression.Exceptions; + +namespace PSCompression.Extensions; + +internal static class ExceptionExtensions +{ + private static readonly char[] s_InvalidFileNameChar = Path.GetInvalidFileNameChars(); + + private static readonly char[] s_InvalidPathChar = Path.GetInvalidPathChars(); + + extension(string path) + { + internal bool WriteErrorIfNotArchive( + string paramname, + PSCmdlet cmdlet, + bool isTerminating = false) + { + if (File.Exists(path)) + { + return false; + } + + ArgumentException exception = new( + $"The specified path '{path}' does not exist or is a Directory.", + paramname); + + ErrorRecord error = new(exception, "NotArchivePath", ErrorCategory.InvalidArgument, path); + + if (isTerminating) + { + cmdlet.ThrowTerminatingError(error); + } + + cmdlet.WriteError(error); + return true; + } + + internal void ThrowIfInvalidPathChar() + { + if (path.IndexOfAny(s_InvalidPathChar) != -1) + { + throw new ArgumentException( + $"Path: '{path}' contains invalid path characters."); + } + } + } + + internal static ErrorRecord NotDirectoryPath(string path, string paramname) => + new( + new ArgumentException( + $"Destination path is an existing File: '{path}'.", paramname), + "NotDirectoryPath", ErrorCategory.InvalidArgument, path); + + extension(ProviderInfo provider) + { + internal ErrorRecord ToInvalidProviderError(string path) => + new( + new NotSupportedException( + $"The resolved path '{path}' is not a FileSystem path but '{provider.Name}'."), + "NotFileSystemPath", ErrorCategory.InvalidArgument, path); + } + + extension(Exception exception) + { + internal ErrorRecord ToOpenError(string path) => + new(exception, "EntryOpen", ErrorCategory.OpenError, path); + + internal ErrorRecord ToResolvePathError(string path) => + new(exception, "ResolvePath", ErrorCategory.NotSpecified, path); + + internal ErrorRecord ToExtractEntryError(object entry) => + new(exception, "ExtractEntry", ErrorCategory.NotSpecified, entry); + + internal ErrorRecord ToStreamOpenError(object item) => + new(exception, "StreamOpen", ErrorCategory.NotSpecified, item); + + internal ErrorRecord ToWriteError(object? item) => + new(exception, "WriteError", ErrorCategory.WriteError, item); + + internal ErrorRecord ToEnumerationError(object item) => + new(exception, "EnumerationError", ErrorCategory.ReadError, item); + + internal ErrorRecord ToInvalidArchive(ArchiveType type) + { + string basemsg = $"Specified path or stream is not a valid {type} archive, " + + "might be compressed using an unsupported method, " + + "or could be corrupted. " + + "Try specifying the compression format explicitly using the -Algorithm parameter."; + + return new ErrorRecord( + new InvalidDataException(basemsg, exception), + "InvalidArchive", + ErrorCategory.InvalidData, + null); + } + } + + extension(DuplicatedEntryException exception) + { + internal ErrorRecord ToDuplicatedEntryError() => + new(exception, "DuplicatedEntry", ErrorCategory.WriteError, exception._path); + } + + extension(InvalidNameException exception) + { + internal ErrorRecord ToInvalidNameError(string name) => + new(exception, "InvalidName", ErrorCategory.InvalidArgument, name); + } + + extension(EntryNotFoundException exception) + { + internal ErrorRecord ToEntryNotFoundError() => + new(exception, "EntryNotFound", ErrorCategory.ObjectNotFound, exception._path); + } + + extension(ZipArchive zip) + { + internal void ThrowIfNotFound( + string path, + string source, + [NotNull] out ZipArchiveEntry? entry) + { + if (!zip.TryGetEntry(path, out entry)) + { + throw EntryNotFoundException.Create(path, source); + } + } + + internal void ThrowIfDuplicate( + string path, + string source) + { + if (zip.TryGetEntry(path, out ZipArchiveEntry? _)) + { + throw DuplicatedEntryException.Create(path, source); + } + } + } + + extension(ICSharpCode.SharpZipLib.Zip.ZipFile zip) + { + internal void ThrowIfNotFound( + string path, + string source, + [NotNull] out ZipEntry? entry) + { + if (!zip.TryGetEntry(path, out entry)) + { + throw EntryNotFoundException.Create(path, source); + } + } + } + + extension(string newname) + { + internal void ThrowIfInvalidNameChar() + { + if (newname.IndexOfAny(s_InvalidFileNameChar) != -1) + { + throw InvalidNameException.Create(newname); + } + } + } + + extension(ZipEntryBase entry) + { + internal void ThrowIfFromStream() + { + if (entry.FromStream) + { + throw new NotSupportedException( + "The operation is not supported for entries created from input Stream."); + } + } + } +} diff --git a/src/PSCompression/Extensions/MiscExtensions.cs b/src/PSCompression/Extensions/MiscExtensions.cs index 4c6ba23..018812a 100644 --- a/src/PSCompression/Extensions/MiscExtensions.cs +++ b/src/PSCompression/Extensions/MiscExtensions.cs @@ -5,59 +5,97 @@ using System.Management.Automation.Host; using System.Net; using System.Security; +using PSCompression.Abstractions.Entries; namespace PSCompression.Extensions; internal static class MiscExtensions { - internal static void Deconstruct( - this KeyValuePair keyv, - out TKey key, - out TValue value) + extension(KeyValuePair keyValue) { - key = keyv.Key; - value = keyv.Value; + internal void Deconstruct(out TKey key, out TValue value) + { + key = keyValue.Key; + value = keyValue.Value; + } } - internal static string AsPlainText(this SecureString secureString) => - new NetworkCredential(string.Empty, secureString).Password; + extension(SecureString secureString) + { + internal string AsPlainText() + => new NetworkCredential(string.Empty, secureString).Password; + } - [ExcludeFromCodeCoverage] - internal static string PromptForPassword(this ZipEntryFile entry, PSHost host) + extension(PSHost host) { - host.UI.Write( - $"Encrypted entry '{entry.RelativePath}' in '{entry.Source}' requires a password.\n" + - "Tip: Use -Password to avoid this prompt in the future.\n" + - "Enter password: "); + [ExcludeFromCodeCoverage] + internal string PromptForPassword(EntryBase entry) + { + host.UI.Write( + $"Encrypted entry '{entry.RelativePath}' in '{entry.Source}' requires a password.\n" + + "Use -Password to avoid this prompt in the future.\n\n" + + "Enter password: "); - return host.UI.ReadLineAsSecureString().AsPlainText(); - } + return host.UI.ReadLineAsSecureString().AsPlainText(); + } + + [ExcludeFromCodeCoverage] + internal string PromptForPassword(string message) + { + host.UI.Write(message + "\n" + + "Use -Password to avoid this prompt in the future.\n\n" + + "Enter password: "); - internal static void ReadToEnd(this StreamReader reader, PSCmdlet cmdlet) - => cmdlet.WriteObject(reader.ReadToEnd()); + return host.UI.ReadLineAsSecureString().AsPlainText(); + } + } - internal static void ReadLines(this StreamReader reader, PSCmdlet cmdlet) + extension(StreamReader reader) { - string? line; - while ((line = reader.ReadLine()) is not null) + internal void ReadToEnd(PSCmdlet cmdlet) => cmdlet.WriteObject(reader.ReadToEnd()); + + internal void ReadLines(PSCmdlet cmdlet) { - cmdlet.WriteObject(line); + string? line; + while ((line = reader.ReadLine()) is not null) + { + cmdlet.WriteObject(line); + } } } - internal static void WriteLines(this StreamWriter writer, string[] lines) + extension(StreamWriter writer) { - foreach (string line in lines) + internal void WriteLines(string[] lines) { - writer.WriteLine(line); + foreach (string line in lines) + { + writer.WriteLine(line); + } } + + internal void WriteContent(string[] lines) + { + foreach (string line in lines) + { + writer.Write(line); + } + } + } + + extension(InvocationInfo invocation) + { + internal bool HasBound(string parameter) + => invocation.BoundParameters.ContainsKey(parameter); } - internal static void WriteContent(this StreamWriter writer, string[] lines) + extension(long size) { - foreach (string line in lines) + internal string CalculateRatio(long compressedSize) { - writer.Write(line); + float compressedRatio = (float)compressedSize / size; + if (float.IsNaN(compressedRatio)) compressedRatio = 0; + return string.Format("{0:F2}%", 100 - (compressedRatio * 100)); } } } diff --git a/src/PSCompression/Extensions/PathExtensions.cs b/src/PSCompression/Extensions/PathExtensions.cs index 0b428ca..beaf7a3 100644 --- a/src/PSCompression/Extensions/PathExtensions.cs +++ b/src/PSCompression/Extensions/PathExtensions.cs @@ -4,7 +4,6 @@ using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using Microsoft.PowerShell.Commands; -using PSCompression.Exceptions; namespace PSCompression.Extensions; @@ -25,88 +24,84 @@ public static partial class PathExtensions private const string DirectorySeparator = "/"; - public static string NormalizePath(this string path) => - path.EndsWith("/") || path.EndsWith("\\") - ? NormalizeEntryPath(path) - : NormalizeFileEntryPath(path); - - internal static string ResolvePath(this string path, PSCmdlet cmdlet) + extension(string path) { - string resolved = cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath( - path: path, - provider: out ProviderInfo provider, - drive: out _); - - provider.Validate(path, throwOnInvalidProvider: true, cmdlet); - return resolved; - } + public string NormalizePath() + => path.EndsWith("/") || path.EndsWith("\\") + ? NormalizeEntryPath(path) : NormalizeFileEntryPath(path); - internal static bool Validate( - this ProviderInfo provider, - string path, - bool throwOnInvalidProvider, - PSCmdlet cmdlet) - { - if (provider.ImplementingType == typeof(FileSystemProvider)) + internal string ResolvePath(PSCmdlet cmdlet) { - return true; - } + string resolved = cmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath( + path: path, + provider: out ProviderInfo provider, + drive: out _); - ErrorRecord error = provider.ToInvalidProviderError(path); + provider.Validate(path, throwOnInvalidProvider: true, cmdlet); + return resolved; + } - if (throwOnInvalidProvider) + internal string AddExtensionIfMissing(string extension) { - cmdlet.ThrowTerminatingError(error); + if (!path.EndsWith(extension, StringComparison.InvariantCultureIgnoreCase)) + { + path += extension; + } + + return path; } - cmdlet.WriteError(error); - return false; + internal string NormalizeEntryPath() + => s_reNormalize + .Replace(path, DirectorySeparator) + .TrimStart('/'); + + internal string NormalizeFileEntryPath() + => NormalizeEntryPath(path).TrimEnd('/'); } - internal static string AddExtensionIfMissing(this string path, string extension) + extension(ProviderInfo provider) { - if (!path.EndsWith(extension, StringComparison.InvariantCultureIgnoreCase)) + internal bool Validate( + string path, + bool throwOnInvalidProvider, + PSCmdlet cmdlet) { - path += extension; - } - - return path; - } + if (provider.ImplementingType == typeof(FileSystemProvider)) + { + return true; + } - internal static string NormalizeEntryPath(this string path) - => s_reNormalize - .Replace(path, DirectorySeparator) - .TrimStart('/'); + ErrorRecord error = provider.ToInvalidProviderError(path); - internal static string NormalizeFileEntryPath(this string path) - => NormalizeEntryPath(path).TrimEnd('/'); + if (throwOnInvalidProvider) + { + cmdlet.ThrowTerminatingError(error); + } - internal static void Create(this DirectoryInfo dir, bool force) - { - if (force || !dir.Exists) - { - dir.Create(); - return; + cmdlet.WriteError(error); + return false; } - - throw new IOException($"The directory '{dir.FullName}' already exists."); } - internal static PSObject AppendPSProperties(this FileSystemInfo info) + extension(FileSystemInfo info) { - string? parent = info is DirectoryInfo dir - ? dir.Parent?.FullName - : Unsafe.As(info).DirectoryName; + internal PSObject AppendPSProperties() + { + string? parent = info is DirectoryInfo dir + ? dir.Parent?.FullName + : Unsafe.As(info).DirectoryName; - return info.AppendPSProperties(parent); - } + return info.AppendPSProperties(parent); + } - internal static PSObject AppendPSProperties(this FileSystemInfo info, string? parent) - { - const string Provider = @"Microsoft.PowerShell.Core\FileSystem::"; - PSObject pso = PSObject.AsPSObject(info); - pso.Properties.Add(new PSNoteProperty("PSPath", $"{Provider}{info.FullName}")); - pso.Properties.Add(new PSNoteProperty("PSParentPath", $"{Provider}{parent}")); - return pso; + internal PSObject AppendPSProperties(string? parent) + { + const string Provider = @"Microsoft.PowerShell.Core\FileSystem::"; + PSObject pso = PSObject.AsPSObject(info); + pso.Properties.Add(new PSNoteProperty("PSPath", $"{Provider}{info.FullName}")); + pso.Properties.Add(new PSNoteProperty("PSParentPath", $"{Provider}{parent}")); + return pso; + } } } diff --git a/src/PSCompression/FormatHandlers/Common/ArchiveCache.cs b/src/PSCompression/FormatHandlers/Common/ArchiveCache.cs new file mode 100644 index 0000000..f25cb1a --- /dev/null +++ b/src/PSCompression/FormatHandlers/Common/ArchiveCache.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using PSCompression.Abstractions.Entries; + +namespace PSCompression.FormatHandlers.Common; + +internal sealed class ArchiveCache : IDisposable + where TArchive : IDisposable + where TEntry : EntryBase +{ + private readonly Dictionary _cache; + + private readonly Func _factory; + + internal ArchiveCache(Func factory) + { + _cache = new(StringComparer.OrdinalIgnoreCase); + _factory = factory; + } + + internal TArchive this[string source] => _cache[source]; + + internal void TryAdd(TEntry entry) + { + if (!_cache.ContainsKey(entry.Source)) + _cache[entry.Source] = _factory(entry); + } + + internal TArchive GetOrCreate(TEntry entry) + { + if (!_cache.TryGetValue(entry.Source, out TArchive? archive)) + { + archive = _factory(entry); + _cache[entry.Source] = archive; + } + + return archive; + } + + public void Dispose() + { + foreach (TArchive archive in _cache.Values) + archive.Dispose(); + } +} diff --git a/src/PSCompression/EntryByteReader.cs b/src/PSCompression/FormatHandlers/Common/EntryByteReader.cs similarity index 90% rename from src/PSCompression/EntryByteReader.cs rename to src/PSCompression/FormatHandlers/Common/EntryByteReader.cs index 59b9642..5b75463 100644 --- a/src/PSCompression/EntryByteReader.cs +++ b/src/PSCompression/FormatHandlers/Common/EntryByteReader.cs @@ -1,8 +1,8 @@ using System.IO; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Entries; -namespace PSCompression; +namespace PSCompression.FormatHandlers.Common; internal sealed class EntryByteReader : EntryStreamOpsBase { diff --git a/src/PSCompression/FormatHandlers/Common/EntrySortingExtensions.cs b/src/PSCompression/FormatHandlers/Common/EntrySortingExtensions.cs new file mode 100644 index 0000000..501aa74 --- /dev/null +++ b/src/PSCompression/FormatHandlers/Common/EntrySortingExtensions.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using PSCompression.Abstractions.Entries; +using PSCompression.Enum; + +namespace PSCompression.FormatHandlers.Common; + +internal static class EntrySortingExtensions +{ + private static string? GetParentPathKey(EntryBase entry) => + entry.FormatDirectoryPath; + + private static EntryType GetTypeKey(EntryBase entry) => entry.Type; + + private static int GetDepthKey(EntryBase entry) => + entry.RelativePath.Count(e => e == '/'); + + private static string GetNameKey(EntryBase entry) => entry.Name!; + + extension(IEnumerable entries) + { + internal IEnumerable SortEntries() => + entries + .OrderBy(GetParentPathKey) + .ThenBy(GetTypeKey) + .ThenBy(GetDepthKey) + .ThenBy(GetNameKey); + } +} diff --git a/src/PSCompression/FormatHandlers/Rar/RarEntry.cs b/src/PSCompression/FormatHandlers/Rar/RarEntry.cs new file mode 100644 index 0000000..551dd1c --- /dev/null +++ b/src/PSCompression/FormatHandlers/Rar/RarEntry.cs @@ -0,0 +1,70 @@ +using System; +using System.IO; +using PSCompression.Abstractions.Entries; +using PSCompression.Enum; +using PSCompression.Extensions; +using SharpCompress.Archives; +using SharpCompress.Archives.Rar; +using SharpCompress.Readers; + +namespace PSCompression.FormatHandlers.Rar; + +public sealed class RarEntry : EntryBase +{ + private static readonly ReaderOptions s_readerOptions = new() { LeaveStreamOpen = true }; + + internal override string? FormatDirectoryPath { get; } + + public override string? Name { get; protected set; } + + public override string RelativePath { get; } + + public override DateTime? LastWriteTime { get; } + + public override long Length { get; internal set; } + + public long CompressedLength { get; internal set; } + + public override EntryType Type { get; } + + public bool IsEncrypted { get; } + + public string? CompressionRatio { get; } + + public string? BaseName { get; } + + public string? Extension { get; } + + internal RarEntry(IArchiveEntry entry, string source) : base(source) + { + Name = Path.GetFileName(entry.Key); + RelativePath = $"{entry.Key}"; + Length = entry.Size; + CompressedLength = entry.CompressedSize; + LastWriteTime = entry.LastModifiedTime; + IsEncrypted = entry.IsEncrypted; + (Type, FormatDirectoryPath) = entry.IsDirectory + ? (EntryType.Directory, $"/{RelativePath.NormalizeEntryPath()}") + : (EntryType.Archive, $"/{Path.GetDirectoryName(RelativePath)?.NormalizeEntryPath()}"); + + if (!entry.IsDirectory) + { + CompressionRatio = Length.CalculateRatio(CompressedLength); + BaseName = Path.GetFileNameWithoutExtension(Name); + Extension = Path.GetExtension(RelativePath); + } + } + + internal RarEntry(IArchiveEntry entry, Stream stream) + : this(entry, $"InputStream.{Guid.NewGuid()}") + { + Stream = stream; + } + + internal IRarArchive OpenRead() + { + if (!FromStream) return RarArchive.OpenArchive(Source); + Stream.Position = 0; + return RarArchive.OpenArchive(Stream, s_readerOptions); + } +} diff --git a/src/PSCompression/ZLibStream.cs b/src/PSCompression/FormatHandlers/Streams/ZLibStream.cs similarity index 98% rename from src/PSCompression/ZLibStream.cs rename to src/PSCompression/FormatHandlers/Streams/ZLibStream.cs index 968e8d4..44db9ad 100644 --- a/src/PSCompression/ZLibStream.cs +++ b/src/PSCompression/FormatHandlers/Streams/ZLibStream.cs @@ -3,7 +3,7 @@ using System.IO; using System.IO.Compression; -namespace PSCompression; +namespace PSCompression.FormatHandlers.Streams; // ain't nobody got time for that [ExcludeFromCodeCoverage] diff --git a/src/PSCompression/AlgorithmMappings.cs b/src/PSCompression/FormatHandlers/Tar/AlgorithmMappings.cs similarity index 92% rename from src/PSCompression/AlgorithmMappings.cs rename to src/PSCompression/FormatHandlers/Tar/AlgorithmMappings.cs index 2810e37..aa7c877 100644 --- a/src/PSCompression/AlgorithmMappings.cs +++ b/src/PSCompression/FormatHandlers/Tar/AlgorithmMappings.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.IO; +using PSCompression.Enum; -namespace PSCompression; +namespace PSCompression.FormatHandlers.Tar; internal static class AlgorithmMappings { diff --git a/src/PSCompression/TarEntryDirectory.cs b/src/PSCompression/FormatHandlers/Tar/TarEntryDirectory.cs similarity index 53% rename from src/PSCompression/TarEntryDirectory.cs rename to src/PSCompression/FormatHandlers/Tar/TarEntryDirectory.cs index 986a86a..2bc80d4 100644 --- a/src/PSCompression/TarEntryDirectory.cs +++ b/src/PSCompression/FormatHandlers/Tar/TarEntryDirectory.cs @@ -1,25 +1,28 @@ using System.IO; using ICSharpCode.SharpZipLib.Tar; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Entries; +using PSCompression.Enum; using PSCompression.Extensions; -namespace PSCompression; +namespace PSCompression.FormatHandlers.Tar; public sealed class TarEntryDirectory : TarEntryBase { + public override EntryType Type => EntryType.Directory; + + internal override string? FormatDirectoryPath { get; } + internal TarEntryDirectory(TarEntry entry, string source) : base(entry, source) { Name = entry.GetDirectoryName(); + FormatDirectoryPath = $"/{RelativePath.NormalizeEntryPath()}"; } - internal TarEntryDirectory(TarEntry entry, Stream? stream) + internal TarEntryDirectory(TarEntry entry, Stream stream) : base(entry, stream) { Name = entry.GetDirectoryName(); + FormatDirectoryPath = $"/{RelativePath.NormalizeEntryPath()}"; } - - public override EntryType Type => EntryType.Directory; - - protected override string GetFormatDirectoryPath() => $"/{RelativePath.NormalizeEntryPath()}"; } diff --git a/src/PSCompression/TarEntryFile.cs b/src/PSCompression/FormatHandlers/Tar/TarEntryFile.cs similarity index 75% rename from src/PSCompression/TarEntryFile.cs rename to src/PSCompression/FormatHandlers/Tar/TarEntryFile.cs index 063fc64..7aa7408 100644 --- a/src/PSCompression/TarEntryFile.cs +++ b/src/PSCompression/FormatHandlers/Tar/TarEntryFile.cs @@ -2,10 +2,11 @@ using System.Linq; using System.Text; using ICSharpCode.SharpZipLib.Tar; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Entries; +using PSCompression.Enum; using PSCompression.Extensions; -namespace PSCompression; +namespace PSCompression.FormatHandlers.Tar; public sealed class TarEntryFile : TarEntryBase { @@ -17,27 +18,28 @@ public sealed class TarEntryFile : TarEntryBase public override EntryType Type => EntryType.Archive; + internal override string? FormatDirectoryPath { get; } + internal TarEntryFile(TarEntry entry, string source, Algorithm algorithm) : base(entry, source) { Name = Path.GetFileName(entry.Name); BaseName = Path.GetFileNameWithoutExtension(Name); Extension = Path.GetExtension(RelativePath); + FormatDirectoryPath = $"/{Path.GetDirectoryName(RelativePath)?.NormalizeEntryPath()}"; _algorithm = algorithm; } - internal TarEntryFile(TarEntry entry, Stream? stream, Algorithm algorithm) + internal TarEntryFile(TarEntry entry, Stream stream, Algorithm algorithm) : base(entry, stream) { Name = Path.GetFileName(entry.Name); BaseName = Path.GetFileNameWithoutExtension(Name); Extension = Path.GetExtension(RelativePath); + FormatDirectoryPath = $"/{Path.GetDirectoryName(RelativePath)?.NormalizeEntryPath()}"; _algorithm = algorithm; } - protected override string GetFormatDirectoryPath() => - $"/{Path.GetDirectoryName(RelativePath)?.NormalizeEntryPath()}"; - internal bool GetContentStream(Stream destination) { Stream? sourceStream = null; @@ -46,8 +48,8 @@ internal bool GetContentStream(Stream destination) try { - sourceStream = _stream ?? File.OpenRead(Source); - sourceStream.Seek(0, SeekOrigin.Begin); + sourceStream = Stream ?? File.OpenRead(Source); + sourceStream.Position = 0; decompressedStream = _algorithm.FromCompressedStream(sourceStream); tar = new(decompressedStream, Encoding.UTF8); @@ -61,7 +63,7 @@ internal bool GetContentStream(Stream destination) } tar.CopyTo(destination, (int)entry.Size); - destination.Seek(0, SeekOrigin.Begin); + destination.Position = 0; return true; } finally diff --git a/src/PSCompression/ZipEntryByteWriter.cs b/src/PSCompression/FormatHandlers/Zip/ZipEntryByteWriter.cs similarity index 71% rename from src/PSCompression/ZipEntryByteWriter.cs rename to src/PSCompression/FormatHandlers/Zip/ZipEntryByteWriter.cs index 58725c8..c64ce4f 100644 --- a/src/PSCompression/ZipEntryByteWriter.cs +++ b/src/PSCompression/FormatHandlers/Zip/ZipEntryByteWriter.cs @@ -1,7 +1,8 @@ +using System; using System.IO; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Entries; -namespace PSCompression; +namespace PSCompression.FormatHandlers.Zip; internal sealed class ZipEntryByteWriter : EntryStreamOpsBase { @@ -28,7 +29,10 @@ internal ZipEntryByteWriter(Stream stream, int bufferSize, bool append = false) internal void WriteBytes(byte[] bytes) { - foreach (byte b in bytes) + int offset = 0; + int remaining = bytes.Length; + + while (remaining > 0) { if (_index == _bufferSize) { @@ -36,7 +40,12 @@ internal void WriteBytes(byte[] bytes) _index = 0; } - _buffer[_index++] = b; + int toCopy = Math.Min(remaining, _bufferSize - _index); + Array.Copy(bytes, offset, _buffer, _index, toCopy); + + _index += toCopy; + offset += toCopy; + remaining -= toCopy; } } diff --git a/src/PSCompression/ZipEntryCache.cs b/src/PSCompression/FormatHandlers/Zip/ZipEntryCache.cs similarity index 58% rename from src/PSCompression/ZipEntryCache.cs rename to src/PSCompression/FormatHandlers/Zip/ZipEntryCache.cs index 798e60d..c32cdcf 100644 --- a/src/PSCompression/ZipEntryCache.cs +++ b/src/PSCompression/FormatHandlers/Zip/ZipEntryCache.cs @@ -1,25 +1,26 @@ using System; using System.Collections.Generic; using ICSharpCode.SharpZipLib.Zip; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Entries; +using PSCompression.Enum; using PSCompression.Extensions; -namespace PSCompression; +namespace PSCompression.FormatHandlers.Zip; -public sealed class ZipEntryCache +internal sealed class ZipEntryCache { - private readonly Dictionary> _cache; - - internal ZipEntryCache() => _cache = new(StringComparer.InvariantCultureIgnoreCase); + private readonly Dictionary> _cache = new( + StringComparer.InvariantCultureIgnoreCase); internal List WithSource(string source) { - if (!_cache.ContainsKey(source)) + if (!_cache.TryGetValue(source, out List? value)) { - _cache[source] = []; + value = []; + _cache[source] = value; } - return _cache[source]; + return value; } internal void Add(string source, PathWithType pathWithType) => @@ -28,9 +29,7 @@ internal void Add(string source, PathWithType pathWithType) => internal ZipEntryCache AddRange(IEnumerable<(string, PathWithType)> values) { foreach ((string source, PathWithType pathWithType) in values) - { Add(source, pathWithType); - } return this; } @@ -43,18 +42,16 @@ internal IEnumerable GetEntries() foreach ((string path, EntryType type) in entry.Value) { if (!zip.TryGetEntry(path, out ZipEntry? zipEntry)) - { continue; - } - if (type == EntryType.Archive) - { - yield return new ZipEntryFile(zipEntry, entry.Key); - continue; - } - - yield return new ZipEntryDirectory(zipEntry, entry.Key); + yield return type == EntryType.Archive + ? new ZipEntryFile(zipEntry, entry.Key) + : new ZipEntryDirectory(zipEntry, entry.Key); } } } } + +internal record struct EntryWithPath(ZipEntryBase ZipEntry, string Path); + +internal record struct PathWithType(string Path, EntryType EntryType); diff --git a/src/PSCompression/ZipEntryDirectory.cs b/src/PSCompression/FormatHandlers/Zip/ZipEntryDirectory.cs similarity index 74% rename from src/PSCompression/ZipEntryDirectory.cs rename to src/PSCompression/FormatHandlers/Zip/ZipEntryDirectory.cs index 4cded83..a8d1e38 100644 --- a/src/PSCompression/ZipEntryDirectory.cs +++ b/src/PSCompression/FormatHandlers/Zip/ZipEntryDirectory.cs @@ -4,34 +4,36 @@ using System.IO.Compression; using System.Linq; using ICSharpCode.SharpZipLib.Zip; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Entries; +using PSCompression.Enum; using PSCompression.Extensions; -namespace PSCompression; +namespace PSCompression.FormatHandlers.Zip; public sealed class ZipEntryDirectory : ZipEntryBase { private const StringComparison Comparer = StringComparison.InvariantCultureIgnoreCase; + internal override string? FormatDirectoryPath { get; } + public override EntryType Type => EntryType.Directory; internal ZipEntryDirectory(ZipEntry entry, string source) : base(entry, source) { Name = entry.GetDirectoryName(); + FormatDirectoryPath = $"/{RelativePath.NormalizeEntryPath()}"; } internal ZipEntryDirectory(ZipEntry entry, Stream? stream) : base(entry, stream) { Name = entry.GetDirectoryName(); + FormatDirectoryPath = $"/{RelativePath.NormalizeEntryPath()}"; } internal IEnumerable GetChilds(ZipArchive zip) => zip.Entries.Where(e => !string.Equals(e.FullName, RelativePath, Comparer) && e.FullName.StartsWith(RelativePath, Comparer)); - - protected override string GetFormatDirectoryPath() => - $"/{RelativePath.NormalizeEntryPath()}"; } diff --git a/src/PSCompression/ZipEntryFile.cs b/src/PSCompression/FormatHandlers/Zip/ZipEntryFile.cs similarity index 72% rename from src/PSCompression/ZipEntryFile.cs rename to src/PSCompression/FormatHandlers/Zip/ZipEntryFile.cs index a4f383e..e1be2cc 100644 --- a/src/PSCompression/ZipEntryFile.cs +++ b/src/PSCompression/FormatHandlers/Zip/ZipEntryFile.cs @@ -1,14 +1,16 @@ using System.IO; using System.IO.Compression; using ICSharpCode.SharpZipLib.Zip; -using PSCompression.Abstractions; -using PSCompression.Exceptions; +using PSCompression.Abstractions.Entries; +using PSCompression.Enum; using PSCompression.Extensions; -namespace PSCompression; +namespace PSCompression.FormatHandlers.Zip; public sealed class ZipEntryFile : ZipEntryBase { + internal override string? FormatDirectoryPath { get; } + public string CompressionRatio { get; } public override EntryType Type { get => EntryType.Archive; } @@ -20,31 +22,21 @@ public sealed class ZipEntryFile : ZipEntryBase internal ZipEntryFile(ZipEntry entry, string source) : base(entry, source) { - CompressionRatio = GetRatio(Length, CompressedLength); + CompressionRatio = Length.CalculateRatio(CompressedLength); Name = Path.GetFileName(entry.Name); BaseName = Path.GetFileNameWithoutExtension(Name); Extension = Path.GetExtension(RelativePath); + FormatDirectoryPath = $"/{Path.GetDirectoryName(RelativePath)?.NormalizeEntryPath()}"; } internal ZipEntryFile(ZipEntry entry, Stream? stream) : base(entry, stream) { - CompressionRatio = GetRatio(Length, CompressedLength); + CompressionRatio = Length.CalculateRatio(CompressedLength); Name = Path.GetFileName(entry.Name); BaseName = Path.GetFileNameWithoutExtension(Name); Extension = Path.GetExtension(RelativePath); - } - - private static string GetRatio(long size, long compressedSize) - { - float compressedRatio = (float)compressedSize / size; - - if (float.IsNaN(compressedRatio)) - { - compressedRatio = 0; - } - - return string.Format("{0:F2}%", 100 - (compressedRatio * 100)); + FormatDirectoryPath = $"/{Path.GetDirectoryName(RelativePath)?.NormalizeEntryPath()}"; } internal Stream Open(ZipArchive zip) @@ -82,7 +74,4 @@ internal void Refresh(ZipArchive zip) CompressedLength = entry.CompressedLength; } } - - protected override string GetFormatDirectoryPath() => - $"/{Path.GetDirectoryName(RelativePath)?.NormalizeEntryPath()}"; } diff --git a/src/PSCompression/ZipEntryMoveCache.cs b/src/PSCompression/FormatHandlers/Zip/ZipEntryMoveCache.cs similarity index 72% rename from src/PSCompression/ZipEntryMoveCache.cs rename to src/PSCompression/FormatHandlers/Zip/ZipEntryMoveCache.cs index 07ca5e3..e204dd7 100644 --- a/src/PSCompression/ZipEntryMoveCache.cs +++ b/src/PSCompression/FormatHandlers/Zip/ZipEntryMoveCache.cs @@ -3,31 +3,28 @@ using System.IO.Compression; using System.Linq; using System.Text.RegularExpressions; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Entries; +using PSCompression.Enum; using PSCompression.Extensions; - -namespace PSCompression; +using PSCompression.FormatHandlers.Common; +namespace PSCompression.FormatHandlers.Zip; internal sealed class ZipEntryMoveCache { - private readonly Dictionary> _cache; + private readonly Dictionary> _cache = new( + StringComparer.InvariantCultureIgnoreCase); - private readonly Dictionary> _mappings; - - internal ZipEntryMoveCache() - { - _cache = new(StringComparer.InvariantCultureIgnoreCase); - _mappings = []; - } + private readonly Dictionary> _mappings = []; private Dictionary WithSource(ZipEntryBase entry) { - if (!_cache.ContainsKey(entry.Source)) + if (!_cache.TryGetValue(entry.Source, out Dictionary? source)) { - _cache[entry.Source] = []; + source = []; + _cache[entry.Source] = source; } - return _cache[entry.Source]; + return source; } internal bool IsDirectoryEntry(string source, string path) => @@ -40,22 +37,14 @@ internal void AddEntry(ZipEntryBase entry, string newname) => internal IEnumerable<(string, PathWithType)> GetPassThruMappings() { foreach (var source in _cache) - { foreach ((string path, EntryWithPath entryWithPath) in source.Value) - { - yield return ( - source.Key, - new PathWithType( - _mappings[source.Key][path], - entryWithPath.ZipEntry.Type)); - } - } + yield return (source.Key, new(_mappings[source.Key][path], entryWithPath.ZipEntry.Type)); } internal Dictionary> GetMappings( - ZipArchiveCache cache) + ArchiveCache cache) { - foreach (var source in _cache) + foreach (KeyValuePair> source in _cache) { _mappings[source.Key] = GetChildMappings(cache, source.Value); } @@ -63,8 +52,8 @@ internal Dictionary> GetMappings( return _mappings; } - private Dictionary GetChildMappings( - ZipArchiveCache cache, + private static Dictionary GetChildMappings( + ArchiveCache cache, Dictionary pathChanges) { string newpath; @@ -75,7 +64,7 @@ private Dictionary GetChildMappings( (ZipEntryBase entry, string newname) = pair.Value; if (entry.Type is EntryType.Archive) { - newpath = ((ZipEntryFile)entry).ChangeName(newname); + newpath = ((ZipEntryFile)entry).GetNewName(newname); result[pair.Key] = newpath; continue; } diff --git a/src/PSCompression/PSCompression.csproj b/src/PSCompression/PSCompression.csproj index e100fb4..99624c6 100644 --- a/src/PSCompression/PSCompression.csproj +++ b/src/PSCompression/PSCompression.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net8.0 + net472;net8.0 enable true PSCompression @@ -15,12 +15,19 @@ - + + + + + + + - + + diff --git a/src/PSCompression/PSCompression.sln b/src/PSCompression/PSCompression.sln deleted file mode 100644 index 32ee10c..0000000 --- a/src/PSCompression/PSCompression.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.5.001.0 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSCompression", "PSCompression.csproj", "{04592EEF-913E-46C1-A7C2-20143FA08138}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {04592EEF-913E-46C1-A7C2-20143FA08138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {04592EEF-913E-46C1-A7C2-20143FA08138}.Debug|Any CPU.Build.0 = Debug|Any CPU - {04592EEF-913E-46C1-A7C2-20143FA08138}.Release|Any CPU.ActiveCfg = Release|Any CPU - {04592EEF-913E-46C1-A7C2-20143FA08138}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {761C4EBF-BC3E-404E-974F-5DC394531F3C} - EndGlobalSection -EndGlobal diff --git a/src/PSCompression/PSCompression.slnx b/src/PSCompression/PSCompression.slnx new file mode 100644 index 0000000..08097ae --- /dev/null +++ b/src/PSCompression/PSCompression.slnx @@ -0,0 +1,3 @@ + + + diff --git a/src/PSCompression/Dbg/Nullable.cs b/src/PSCompression/Polyfills/Nullable.cs similarity index 100% rename from src/PSCompression/Dbg/Nullable.cs rename to src/PSCompression/Polyfills/Nullable.cs diff --git a/src/PSCompression/Records.cs b/src/PSCompression/Records.cs deleted file mode 100644 index c2abbf6..0000000 --- a/src/PSCompression/Records.cs +++ /dev/null @@ -1,7 +0,0 @@ -using PSCompression.Abstractions; - -namespace PSCompression; - -internal record struct EntryWithPath(ZipEntryBase ZipEntry, string Path); - -internal record struct PathWithType(string Path, EntryType EntryType); diff --git a/src/PSCompression/SortingOps.cs b/src/PSCompression/SortingOps.cs deleted file mode 100644 index c846a0f..0000000 --- a/src/PSCompression/SortingOps.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using PSCompression.Abstractions; -using PSCompression.Extensions; - -namespace PSCompression; - -internal static class SortingOps -{ - private static string? SortByParent(EntryBase entry) => - Path.GetDirectoryName(entry.RelativePath)?.NormalizeEntryPath(); - - private static int SortByLength(EntryBase entry) => - entry.RelativePath.Count(e => e == '/'); - - private static string SortByName(EntryBase entry) => entry.Name!; - - private static EntryType SortByType(EntryBase entry) => entry.Type; - - internal static IEnumerable ToEntrySort( - this IEnumerable entryCollection) - => entryCollection - .OrderBy(SortByParent) - .ThenBy(SortByType) - .ThenBy(SortByLength) - .ThenBy(SortByName); -} diff --git a/src/PSCompression/ZipArchiveCache.cs b/src/PSCompression/ZipArchiveCache.cs deleted file mode 100644 index 6fd4d2f..0000000 --- a/src/PSCompression/ZipArchiveCache.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; -using PSCompression.Abstractions; - -namespace PSCompression; - -internal sealed class ZipArchiveCache : IDisposable - where TArchive : IDisposable -{ - private readonly Dictionary _cache; - - private readonly Func _factory; - - internal ZipArchiveCache(Func factory) - { - _cache = new(StringComparer.OrdinalIgnoreCase); - _factory = factory; - } - - internal TArchive this[string source] => _cache[source]; - - internal void TryAdd(ZipEntryBase entry) - { - if (!_cache.ContainsKey(entry.Source)) - { - _cache[entry.Source] = _factory(entry); - } - } - - internal TArchive GetOrCreate(ZipEntryBase entry) - { - if (!_cache.ContainsKey(entry.Source)) - { - _cache[entry.Source] = _factory(entry); - } - - return _cache[entry.Source]; - } - - public void Dispose() - { - foreach (TArchive zip in _cache.Values) - { - zip?.Dispose(); - } - } -} diff --git a/src/PSCompression/internal/_Format.cs b/src/PSCompression/internal/_Format.cs index 0c71af5..6068eff 100644 --- a/src/PSCompression/internal/_Format.cs +++ b/src/PSCompression/internal/_Format.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using System.Globalization; using System.Management.Automation; -using PSCompression.Abstractions; +using PSCompression.Abstractions.Entries; namespace PSCompression.Internal; diff --git a/tests/ArchiveCompressionExpansionCommands.tests.ps1 b/tests/ArchiveCompressionExpansionCommands.tests.ps1 index b2b7e90..a6e3ff3 100644 --- a/tests/ArchiveCompressionExpansionCommands.tests.ps1 +++ b/tests/ArchiveCompressionExpansionCommands.tests.ps1 @@ -13,7 +13,7 @@ Import-Module ([Path]::Combine($PSScriptRoot, 'shared.psm1')) Describe 'Archive Compression & Expansion Commands' -Tag 'Archive Compression & Expansion Commands' { BeforeAll { - $algos = [PSCompression.Algorithm].GetEnumValues() + $algos = [PSCompression.Enum.Algorithm].GetEnumValues() $sourceName = 'CompressArchiveTests' $destName = 'CompressArchiveExtract' $testpath = Join-Path $TestDrive $sourceName @@ -226,4 +226,58 @@ Describe 'Archive Compression & Expansion Commands' -Tag 'Archive Compression & Should -Not -Throw } } + + Context 'Expand-RarArchive' -Tag 'Expand-RarArchive' { + BeforeAll { + $rar = Get-Item $PSScriptRoot/../assets/test.rar + $encryptedRar = Get-Item $PSScriptRoot/../assets/testEncrypted.rar + $destination = New-Item (Join-Path $TestDrive 'ExpandRarArchiveTests') -ItemType Directory + $destination | Push-Location + $rar, $encryptedRar | Out-Null + } + + AfterAll { Pop-Location } + + It 'Can extract archive to the current directory' { + { $rar | Expand-RarArchive } | Should -Not -Throw + Get-ChildItem -Recurse | Should -HaveCount 4 + } + + It 'Should throw if files already exist in destination' { + { $rar | Expand-RarArchive } | Should -Throw -ExceptionType ([IOException]) + } + + It 'Can overwrite files' { + { $rar | Expand-RarArchive -Force } | Should -Not -Throw + } + + It 'Can extract archive to a specified destination' { + $tmp = $destination.CreateSubdirectory([guid]::NewGuid()) + { $rar | Expand-RarArchive -Destination $tmp.FullName } | Should -Not -Throw + Get-ChildItem $tmp.FullName -Recurse | Should -HaveCount 4 + } + + It 'Can output the extracted filesystem objects' { + $tmp = $destination.CreateSubdirectory([guid]::NewGuid()) + $extracted = $rar | Expand-RarArchive -Destination $tmp.FullName -PassThru + $extracted | Should -HaveCount 3 + $extracted | Should -BeOfType ([FileSystemInfo]) + $extracted | Where-Object Extension -eq '.txt' | Get-Content | + Should -BeExactly 1, 2, 3 + } + + It 'Can extract encrypted entries' { + $tmp = $destination.CreateSubdirectory([guid]::NewGuid()) + $passw = ConvertTo-SecureString test -AsPlainText -Force + { $encryptedRar | Expand-RarArchive -Destination $tmp.FullName -Password $passw } | + Should -Not -Throw + + $file = Get-ChildItem $tmp.FullName -Filter *.txt -Recurse + Get-Content $file.FullName | Should -BeExactly 'hello world!' + + $passw = ConvertTo-SecureString foo -AsPlainText -Force + { $encryptedRar | Expand-RarArchive -Destination $tmp.FullName -Password $passw -Force } | + Should -Throw + } + } } diff --git a/tests/ArchiveEntryManagementCommands.tests.ps1 b/tests/ArchiveEntryManagementCommands.tests.ps1 index 1f976f4..0ff8ac2 100644 --- a/tests/ArchiveEntryManagementCommands.tests.ps1 +++ b/tests/ArchiveEntryManagementCommands.tests.ps1 @@ -20,7 +20,7 @@ Describe 'Archive Entry Management Commands' { $testTarpath = Join-Path $TestDrive $testTarName $itemCounts = Get-Structure | New-Structure $testTarpath $totalCount = $itemCounts.File + $itemCounts.Directory - $algos = [PSCompression.Algorithm].GetEnumValues() + $algos = [PSCompression.Enum.Algorithm].GetEnumValues() $tarArchives = foreach ($algo in $algos) { $compressTarArchiveSplat = @{ @@ -32,8 +32,10 @@ Describe 'Archive Entry Management Commands' { Compress-TarArchive @compressTarArchiveSplat } - $encryptedZip = Get-Item $PSScriptRoot/../assets/helloworld.zip - $zip, $file, $uri, $tarArchives, $itemCounts, $totalCount, $encryptedZip | Out-Null + $encryptedZip = Get-Item $PSScriptRoot/../assets/test.zip + $rar = Get-Item $PSScriptRoot/../assets/test.rar + $encryptedRar = Get-Item $PSScriptRoot/../assets/testEncrypted.rar + $zip, $file, $uri, $tarArchives, $itemCounts, $totalCount, $encryptedZip, $rar, $encryptedRar | Out-Null } Context 'New-ZipEntry' -Tag 'New-ZipEntry' { @@ -64,12 +66,12 @@ Describe 'Archive Entry Management Commands' { It 'Can create new zip file entries' { New-ZipEntry $zip.FullName -EntryPath test\newentry.txt | - Should -BeOfType ([PSCompression.ZipEntryFile]) + Should -BeOfType ([PSCompression.FormatHandlers.Zip.ZipEntryFile]) } It 'Can create new zip directory entries' { New-ZipEntry $zip.FullName -EntryPath test\ | - Should -BeOfType ([PSCompression.ZipEntryDirectory]) + Should -BeOfType ([PSCompression.FormatHandlers.Zip.ZipEntryDirectory]) } It 'Can create multiple entries' { @@ -79,7 +81,7 @@ Describe 'Archive Entry Management Commands' { It 'Should not create an entry with the same path' { { New-ZipEntry $zip.FullName -EntryPath foo.txt, bar.txt, baz.txt } | - Should -Throw + Should -Throw -ExceptionType ([PSCompression.Exceptions.DuplicatedEntryException]) } It 'Can replace an existing entry with -Force' { @@ -111,6 +113,12 @@ Describe 'Archive Entry Management Commands' { New-ZipEntry @newZipEntrySplat | Get-ZipEntryContent | Should -Be 'hello world!' + + { New-ZipEntry @newZipEntrySplat } | + Should -Throw -ExceptionType ([PSCompression.Exceptions.DuplicatedEntryException]) + + { New-ZipEntry @newZipEntrySplat -Force } | + Should -Not -Throw } It 'Can create entries with content from file without specifying an EntryPath' { @@ -148,16 +156,16 @@ Describe 'Archive Entry Management Commands' { Context 'Get-ZipEntry' -Tag 'Get-ZipEntry' { It 'Can list entries in a zip archive' { $zip | Get-ZipEntry | - Should -BeOfType ([PSCompression.Abstractions.ZipEntryBase]) + Should -BeOfType ([PSCompression.Abstractions.Entries.ZipEntryBase]) } It 'Can list entries from a Stream' { Invoke-WebRequest $uri -UseBasicParsing | Get-ZipEntry | - Should -BeOfType ([PSCompression.Abstractions.ZipEntryBase]) + Should -BeOfType ([PSCompression.Abstractions.Entries.ZipEntryBase]) Use-Object ($stream = $zip.OpenRead()) { $stream | Get-ZipEntry | - Should -BeOfType ([PSCompression.Abstractions.ZipEntryBase]) + Should -BeOfType ([PSCompression.Abstractions.Entries.ZipEntryBase]) } } @@ -195,12 +203,12 @@ Describe 'Archive Entry Management Commands' { It 'Can list zip file entries' { $zip | Get-ZipEntry -Type Archive | - Should -BeOfType ([PSCompression.ZipEntryFile]) + Should -BeOfType ([PSCompression.FormatHandlers.Zip.ZipEntryFile]) } It 'Can list zip directory entries' { $zip | Get-ZipEntry -Type Directory | - Should -BeOfType ([PSCompression.ZipEntryDirectory]) + Should -BeOfType ([PSCompression.FormatHandlers.Zip.ZipEntryDirectory]) } It 'Can list a specific entry with the -Include parameter' { @@ -218,7 +226,7 @@ Describe 'Archive Entry Management Commands' { Context 'Get-TarEntry' -Tag 'Get-TarEntry' { It 'Can list entries in a tar archive' { $tarArchives | Get-TarEntry | - Should -BeOfType ([PSCompression.Abstractions.TarEntryBase]) + Should -BeOfType ([PSCompression.Abstractions.Entries.TarEntryBase]) } It 'Can list entries from a Stream' { @@ -232,7 +240,7 @@ Describe 'Archive Entry Management Commands' { Use-Object ($stream = $archive.OpenRead()) { $stream | Get-TarEntry -Algorithm $algo | - Should -BeOfType ([PSCompression.Abstractions.TarEntryBase]) + Should -BeOfType ([PSCompression.Abstractions.Entries.TarEntryBase]) } } } @@ -284,17 +292,17 @@ Describe 'Archive Entry Management Commands' { It 'Can list tar file entries' { $tarArchives | Get-TarEntry -Type Archive | - Should -BeOfType ([PSCompression.TarEntryFile]) + Should -BeOfType ([PSCompression.FormatHandlers.Tar.TarEntryFile]) } It 'Can list tar directory entries' { $tarArchives | Get-TarEntry -Type Directory | - Should -BeOfType ([PSCompression.TarEntryDirectory]) + Should -BeOfType ([PSCompression.FormatHandlers.Tar.TarEntryDirectory]) } It 'Can list a specific entry with the -Include parameter' { $tarArchives | Get-TarEntry -Include "${testTarName}/testfolder05/testfile00.txt" | - Should -BeOfType ([PSCompression.TarEntryFile]) + Should -BeOfType ([PSCompression.FormatHandlers.Tar.TarEntryFile]) } It 'Can exclude entries using the -Exclude parameter' { @@ -304,6 +312,41 @@ Describe 'Archive Entry Management Commands' { } } + Context 'Get-RarEntry' -Tag 'Get-RarEntry' { + It 'Lists entries from path' { + Get-RarEntry $rar | Should -BeOfType ([PSCompression.FormatHandlers.Rar.RarEntry]) + } + + It 'Can list by entry type' { + [PSCompression.Enum.EntryType].GetEnumNames() | ForEach-Object { + (Get-RarEntry $rar -Type $_).Type | Sort-Object -Unique | Should -Be $_ + } + } + + It 'Can include entries by pattern' { + Get-RarEntry $rar -Include *.txt | Should -HaveCount 1 + } + + It 'Can exclude entries by pattern' { + Get-RarEntry $rar -Exclude *.txt | Should -HaveCount 2 + } + + It 'List entries from a stream' { + Use-Object ($stream = (Get-Item $rar).OpenRead()) { + $stream | Get-RarEntry | Should -BeOfType ([PSCompression.FormatHandlers.Rar.RarEntry]) + [PSCompression.Enum.EntryType].GetEnumNames() | ForEach-Object { + ($stream | Get-RarEntry -Type $_).Type | Sort-Object -Unique | Should -Be $_ + } + } + + { $stream | Get-RarEntry } | Should -Throw -ExceptionType ([ObjectDisposedException]) + } + + It 'Should throw on invalid RAR archive' { + { Get-RarEntry $zip.FullName } | Should -Throw -ExceptionType ([InvalidDataException]) + } + } + Context 'Get-ZipEntryContent' -Tag 'Get-ZipEntryContent' { It 'Can read content from zip file entries' { $zip | Get-ZipEntry -Type Archive | @@ -359,7 +402,7 @@ Describe 'Archive Entry Management Commands' { } It 'Can read encrypted entries' { - $passw = ConvertTo-SecureString 'test' -AsPlainText -Force + $passw = ConvertTo-SecureString test -AsPlainText -Force Use-Object ($stream = $encryptedZip.OpenRead()) { $stream | Get-ZipEntry -Type Archive | @@ -448,6 +491,51 @@ Describe 'Archive Entry Management Commands' { } } + Context 'Get-RarEntryContent' -Tag 'Get-RarEntryContent' { + BeforeAll { + $entry = Get-RarEntry $rar + $encryptedEntry = Get-RarEntry $encryptedRar -Include *.txt + $entry, $encryptedEntry | Out-Null + } + + It 'Can read content from an entry' { + $entry | Get-RarEntryContent | Should -BeExactly 1, 2, 3 + } + + It 'Can read all content as a single string' { + $entry | Get-RarEntryContent -Raw | Should -HaveCount 1 + } + + It 'Can read as a byte stream' { + $entry | Get-RarEntryContent -AsByteStream | Should -BeExactly 49, 10, 50, 10, 51, 10 + } + + It 'Can read bytes and output as a single byte array' { + $entry | Get-RarEntryContent -AsByteStream -Raw | Should -BeOfType ([byte[]]) + } + + It 'Can read encrypted entries' { + $encryptedEntry.IsEncrypted | Should -BeTrue + $passw = ConvertTo-SecureString test -AsPlainText -Force + $encryptedEntry | Get-RarEntryContent -Password $passw | Should -BeExactly 'hello world!' + $passw = ConvertTo-SecureString foo -AsPlainText -Force + { $encryptedEntry | Get-RarEntryContent -Password $passw } | Should -Throw + } + + It 'Can read from a stream' { + Use-Object ($stream = (Get-Item $rar).OpenRead()) { + $stream, $stream | Get-RarEntry -Include *.txt | + Get-RarEntryContent | Should -BeExactly 1, 2, 3, 1, 2, 3 + } + + $disposed = Use-Object ($stream = (Get-Item $rar).OpenRead()) { + $stream | Get-RarEntry -Include *.txt + } + + { $disposed | Get-RarEntryContent } | Should -Throw -ExceptionType ([ObjectDisposedException]) + } + } + Context 'Remove-ZipEntry' -Tag 'Remove-ZipEntry' { It 'No entry is removed with -WhatIf' { $zip | Get-ZipEntry | Remove-ZipEntry -WhatIf @@ -458,7 +546,7 @@ Describe 'Archive Entry Management Commands' { { $zip | Get-ZipEntry -Type Archive | Remove-ZipEntry } | Should -Not -Throw - $zip | Get-ZipEntry | Should -Not -BeOfType ([PSCompression.ZipEntryFile]) + $zip | Get-ZipEntry | Should -Not -BeOfType ([PSCompression.FormatHandlers.Zip.ZipEntryFile]) } It 'Should throw if trying to remove entries created from input Stream' { @@ -469,7 +557,7 @@ Describe 'Archive Entry Management Commands' { It 'Can remove directory entries' { $entries = $zip | Get-ZipEntry -Type Directory { Remove-ZipEntry -InputObject $entries } | Should -Not -Throw - $zip | Get-ZipEntry | Should -Not -BeOfType ([PSCompression.ZipEntryDirectory]) + $zip | Get-ZipEntry | Should -Not -BeOfType ([PSCompression.FormatHandlers.Zip.ZipEntryDirectory]) } It 'Should not throw if there are no entries to remove' { @@ -531,7 +619,7 @@ Describe 'Archive Entry Management Commands' { It 'Outputs the source entry with -PassThru' { 'hello world!' | Set-ZipEntryContent $entry -PassThru | - Should -BeOfType ([PSCompression.ZipEntryFile]) + Should -BeOfType ([PSCompression.FormatHandlers.Zip.ZipEntryFile]) } } @@ -545,48 +633,50 @@ Describe 'Archive Entry Management Commands' { } It 'No entry is renamed with -WhatIf' { - $zip | Get-ZipEntry | - Rename-ZipEntry -NewName { 'test' + $_.Name } -WhatIf - - $zip | Get-ZipEntry | - Should -Not -Match '^testtest' + $zip | Get-ZipEntry | Rename-ZipEntry -NewName { 'test' + $_.Name } -WhatIf + $zip | Get-ZipEntry | Should -Not -Match '^testtest' } It 'Can rename file entries using a delay-bind ScriptBlock' { - { $zip | Get-ZipEntry -Type Archive | Rename-ZipEntry -NewName { 'test' + $_.Name } } | - Should -Not -Throw + { + $zip | Get-ZipEntry -Type Archive | Rename-ZipEntry -NewName { 'test' + $_.Name } + } | Should -Not -Throw - $zip | Get-ZipEntry -Type Archive | - ForEach-Object Name | - Should -Match '^testtest' + $zip | Get-ZipEntry -Type Archive | ForEach-Object Name | Should -Match '^testtest' } It 'Should throw if trying to rename entries created from input Stream' { - { Invoke-WebRequest $uri -UseBasicParsing | Get-ZipEntry | Rename-ZipEntry -NewName { 'test' + $_.Name } } | - Should -Throw -ExceptionType ([NotSupportedException]) + { + Invoke-WebRequest $uri -UseBasicParsing | + Get-ZipEntry | + Rename-ZipEntry -NewName { 'test' + $_.Name } + } | Should -Throw -ExceptionType ([NotSupportedException]) } It 'Produces output with -PassThru' { $zip | Get-ZipEntry -Type Archive | Rename-ZipEntry -NewName { $_.Name -replace 'test' } -PassThru | - Should -BeOfType ([PSCompression.ZipEntryFile]) + Should -BeOfType ([PSCompression.FormatHandlers.Zip.ZipEntryFile]) } It 'Can rename directory entries and all its child entries' { $dir = $zip | Get-ZipEntry -Type Directory -Include testfolder00/ $childs = $zip | Get-ZipEntry -Include testfolder00/* - Rename-ZipEntry $dir -NewName myNewName + $passthru = Rename-ZipEntry $dir -NewName myNewName -PassThru + $passthru | Should -BeOfType ([PSCompression.FormatHandlers.Zip.ZipEntryDirectory]) $zip | Get-ZipEntry -Include myNewName/* | Should -HaveCount $childs.Count } It 'Should throw if an entry with the same Name already exists' { - { $zip | Get-ZipEntry -Type Directory -Include testfolder01/ | - Rename-ZipEntry -NewName testfolder02 } | - Should -Throw + { + $zip | Get-ZipEntry -Type Directory -Include testfolder01/ | + Rename-ZipEntry -NewName testfolder02 + } | Should -Throw - { $zip | Get-ZipEntry -Type Archive -Include testfolder01/file00.txt | - Rename-ZipEntry -NewName file01.txt } | - Should -Throw + { + $zip | Get-ZipEntry -Type Archive -Include testfolder01/file00.txt | + Rename-ZipEntry -NewName file01.txt + } | Should -Throw } It 'Should throw if renaming an entry that no longer exists' { @@ -697,7 +787,7 @@ Describe 'Archive Entry Management Commands' { } It 'Can extract an encrypted entry' { - $passw = ConvertTo-SecureString 'test' -AsPlainText -Force + $passw = ConvertTo-SecureString test -AsPlainText -Force $dest = Join-Path $TestDrive encryptedTestFolder Use-Object ($stream = $encryptedZip.OpenRead()) { $info = $stream | Get-ZipEntry -Type Archive | @@ -720,7 +810,7 @@ Describe 'Archive Entry Management Commands' { Context 'Expand-TarEntry' -Tag 'Expand-TarEntry' { It 'Can extract entries to a destination directory' { $tarArchives | ForEach-Object { - $destination = "extract_$($_.Extension)" + $destination = "extract_$($_.Extension.TrimStart('.'))" { $_ | Get-TarEntry | Expand-TarEntry -Destination $destination } | Should -Not -Throw @@ -732,12 +822,8 @@ Describe 'Archive Entry Management Commands' { It 'Can extract tar entries created from input Stream' { foreach ($archive in $tarArchives) { - if ($archive.Extension -eq '.tar') { - $algo = 'none' - } - else { - $algo = $archive.Extension.TrimStart('.') - } + $algo = $archive.Extension.TrimStart('.') + if ($algo -eq 'tar') { $algo = 'none' } $destination = "extract_${algo}_fromStream" @@ -753,14 +839,10 @@ Describe 'Archive Entry Management Commands' { It 'Should throw when a Stream is Diposed' { foreach ($archive in $tarArchives) { - if ($archive.Extension -eq '.tar') { - $algo = 'none' - } - else { - $algo = $archive.Extension.TrimStart('.') - } + $algo = $archive.Extension.TrimStart('.') + if ($algo -eq 'tar') { $algo = 'none' } - $destination = "extract_$($_.Extension)_fromStream" + $destination = "extract_${algo}_fromStream" $entries = Use-Object ($stream = $archive.OpenRead()) { $stream | Get-TarEntry -Algorithm $algo @@ -784,7 +866,7 @@ Describe 'Archive Entry Management Commands' { It 'Should not overwrite files without -Force' { $tarArchives | ForEach-Object { - $destination = "extract_$($_.Extension)" + $destination = "extract_$($_.Extension.TrimStart('.'))" { $_ | Get-TarEntry | Expand-TarEntry -Destination $destination } | Should -Throw -ExceptionType ([IOException]) @@ -793,7 +875,7 @@ Describe 'Archive Entry Management Commands' { It 'Can overwrite files if using -Force' { $tarArchives | ForEach-Object { - $destination = "extract_$($_.Extension)" + $destination = "extract_$($_.Extension.TrimStart('.'))" { $_ | Get-TarEntry | Expand-TarEntry -Destination $destination -Force } | Should -Not -Throw @@ -803,7 +885,7 @@ Describe 'Archive Entry Management Commands' { It 'Should extract entries to the current directory when -Destination is not specified' { foreach ($archive in $tarArchives) { try { - New-Item "expandToCurrent_$($archive.Extension)" -ItemType Directory | Push-Location + New-Item "expandToCurrent_$($archive.Extension.TrimStart('.'))" -ItemType Directory | Push-Location { $archive | Get-TarEntry | Expand-TarEntry } | Should -Not -Throw Get-ChildItem | Should -Not -HaveCount 0 } @@ -814,4 +896,74 @@ Describe 'Archive Entry Management Commands' { } } } + + Context 'Expand-RarEntry' -Tag 'Expand-RarEntry' { + BeforeAll { + $entries = $rar | Get-RarEntry + $destination = New-Item (Join-Path $TestDrive 'ExpandRarEntryTests') -ItemType Directory + $destination | Push-Location + $entries | Out-Null + } + + AfterAll { Pop-Location } + + It 'Can extract entries to the current directory' { + { $entries | Expand-RarEntry } | Should -Not -Throw + Get-ChildItem -Recurse | Should -HaveCount 4 + } + + It 'Should throw if files already exist in destination' { + { $entries | Expand-RarEntry } | Should -Throw -ExceptionType ([IOException]) + } + + It 'Can overwrite files' { + { $entries | Expand-RarEntry -Force } | Should -Not -Throw + } + + It 'Can extract entries to a specified destination' { + $tmp = $destination.CreateSubdirectory([guid]::NewGuid()) + { $entries | Expand-RarEntry -Destination $tmp.FullName } | Should -Not -Throw + Get-ChildItem $tmp.FullName -Recurse | Should -HaveCount 4 + } + + It 'Can extract entries from a stream' { + $tmp = $destination.CreateSubdirectory([guid]::NewGuid()) + Use-Object ($stream = $rar.OpenRead()) { + { $stream | Get-RarEntry | Expand-RarEntry -Destination $tmp.FullName } | + Should -Not -Throw + } + + Get-ChildItem $tmp.FullName -Recurse | Should -HaveCount 4 + + $disposedStreamEntries = Use-Object ($stream = $rar.OpenRead()) { + $stream | Get-RarEntry + } + + { $disposedStreamEntries | Expand-RarEntry -Destination $tmp.FullName -Force } | + Should -Throw -ExceptionType ([ObjectDisposedException]) + } + + It 'Can output the extracted entries' { + $tmp = $destination.CreateSubdirectory([guid]::NewGuid()) + $extracted = $entries | Expand-RarEntry -Destination $tmp.FullName -PassThru + $extracted | Should -HaveCount 3 + $extracted | Should -BeOfType ([FileSystemInfo]) + $extracted | Where-Object Extension -EQ '.txt' | Get-Content | + Should -BeExactly 1, 2, 3 + } + + It 'Can extract encrypted entries' { + $tmp = $destination.CreateSubdirectory([guid]::NewGuid()) + $passw = ConvertTo-SecureString test -AsPlainText -Force + { Get-RarEntry $encryptedRar | Expand-RarEntry -Destination $tmp.FullName -Password $passw } | + Should -Not -Throw + + $file = Get-ChildItem $tmp.FullName -Filter *.txt -Recurse + Get-Content $file.FullName | Should -BeExactly 'hello world!' + + $passw = ConvertTo-SecureString foo -AsPlainText -Force + { Get-RarEntry $encryptedRar | Expand-RarEntry -Destination $tmp.FullName -Password $passw -Force } | + Should -Throw + } + } } diff --git a/tests/DirectoryEntryTypes.tests.ps1 b/tests/DirectoryEntryTypes.tests.ps1 index 115e0da..2085879 100644 --- a/tests/DirectoryEntryTypes.tests.ps1 +++ b/tests/DirectoryEntryTypes.tests.ps1 @@ -9,15 +9,16 @@ Import-Module ([Path]::Combine($PSScriptRoot, 'shared.psm1')) Describe 'Directory Entry Types' { BeforeAll { $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force - New-ZipEntry $zip.FullName -EntryPath afolder/ - $tarArchive = New-Item (Join-Path $TestDrive afolder) -ItemType Directory -Force | - Compress-TarArchive -Destination 'testTarFile' -PassThru + $zipEntry = New-ZipEntry $zip.FullName -EntryPath afolder/ + $tarEntry = New-Item (Join-Path $TestDrive afolder) -ItemType Directory -Force | + Compress-TarArchive -Destination 'testTarFile' -PassThru | + Get-TarEntry - $tarArchive | Out-Null + $zipEntry, $tarEntry | Out-Null } It 'Should be of type Directory' { - ($zip | Get-ZipEntry).Type | Should -BeExactly ([PSCompression.EntryType]::Directory) - ($tarArchive | Get-TarEntry).Type | Should -BeExactly ([PSCompression.EntryType]::Directory) + $zipEntry.Type | Should -BeExactly ([PSCompression.Enum.EntryType]::Directory) + $tarEntry.Type | Should -BeExactly ([PSCompression.Enum.EntryType]::Directory) } } diff --git a/tests/EncodingTransformation.tests.ps1 b/tests/EncodingTransformation.tests.ps1 index b144c89..1586523 100644 --- a/tests/EncodingTransformation.tests.ps1 +++ b/tests/EncodingTransformation.tests.ps1 @@ -34,7 +34,7 @@ Describe 'EncodingTransformation Class' { $encodings['ansi'] = [Encoding]::GetEncoding([Acp]::GetACP()) } - $transform = [PSCompression.EncodingTransformation]::new() + $transform = [PSCompression.Attributes.EncodingTransformation]::new() $transform | Out-Null } diff --git a/tests/FileEntryTypes.tests.ps1 b/tests/FileEntryTypes.tests.ps1 index 720aecd..6fb9a3e 100644 --- a/tests/FileEntryTypes.tests.ps1 +++ b/tests/FileEntryTypes.tests.ps1 @@ -12,57 +12,64 @@ Import-Module ([Path]::Combine($PSScriptRoot, 'shared.psm1')) Describe 'File Entry Types' { BeforeAll { $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force - 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt - - $tarArchive = New-Item (Join-Path $TestDrive helloworld.txt) -ItemType File -Force | - Compress-TarArchive -Destination 'testTarDirectory' -PassThru - - $tarArchive | Out-Null + $zipEntry = 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt + $rarEntry = Get-RarEntry $PSScriptRoot/../assets/test.rar -Include *.txt + $tarEntry = New-Item (Join-Path $TestDrive helloworld.txt) -ItemType File -Force | + Compress-TarArchive -Destination 'testTarDirectory' -PassThru | + Get-TarEntry + $tarArchive, $zipEntry, $tarEntry, $rarEntry | Out-Null } It 'Should be of type Archive' { - ($zip | Get-ZipEntry).Type | Should -BeExactly ([PSCompression.EntryType]::Archive) - - ($tarArchive | Get-TarEntry).Type | Should -BeExactly ([PSCompression.EntryType]::Archive) + $zipEntry.Type | Should -BeExactly ([PSCompression.Enum.EntryType]::Archive) + $tarEntry.Type | Should -BeExactly ([PSCompression.Enum.EntryType]::Archive) + $rarEntry.Type | Should -BeExactly ([PSCompression.Enum.EntryType]::Archive) } - It 'Should Have a BaseName Property' { - ($zip | Get-ZipEntry).BaseName | Should -BeOfType ([string]) - ($zip | Get-ZipEntry).BaseName | Should -BeExactly helloworld + It 'Should have a BaseName property' { + $zipEntry.BaseName | Should -BeExactly helloworld + $rarEntry.BaseName | Should -BeExactly test + $tarEntry.BaseName | Should -BeExactly helloworld + } - ($tarArchive | Get-TarEntry).BaseName | Should -BeOfType ([string]) - ($tarArchive | Get-TarEntry).BaseName | Should -BeExactly helloworld + It 'Should have an Extension property' { + $zipEntry.Extension | Should -BeExactly .txt + $tarEntry.Extension | Should -BeExactly .txt + $rarEntry.Extension | Should -BeExactly .txt } - It 'Should Have an Extension Property' { - ($zip | Get-ZipEntry).Extension | Should -BeOfType ([string]) - ($zip | Get-ZipEntry).Extension | Should -BeExactly .txt + It 'Should have an IsEncrypted property' { + $zipEntry.IsEncrypted | Should -BeFalse + $rarEntry.IsEncrypted | Should -BeFalse + } - ($tarArchive | Get-TarEntry).Extension | Should -BeOfType ([string]) - ($tarArchive | Get-TarEntry).Extension | Should -BeExactly .txt + It 'Should have an AESKeySize property' { + $zipEntry.AESKeySize | Should -BeOfType ([int]) + $zipEntry.AESKeySize | Should -BeExactly 0 } - It 'Should Have an IsEncrypted Property' { - ($zip | Get-ZipEntry).IsEncrypted | Should -BeOfType ([bool]) - ($zip | Get-ZipEntry).IsEncrypted | Should -BeFalse + It 'Should have a CompressionMethod property' { + $zipEntry.CompressionMethod | Should -Be Deflated } - It 'Should Have an AESKeySize Property' { - ($zip | Get-ZipEntry).AESKeySize | Should -BeOfType ([int]) - ($zip | Get-ZipEntry).AESKeySize | Should -BeExactly 0 + It 'Should have a CompressionRatio property' { + $zipEntry.CompressionRatio | Should -BeOfType ([string]) + $rarEntry.CompressionRatio | Should -BeOfType ([string]) } - It 'Should Have a CompressionMethod Property' { - ($zip | Get-ZipEntry).CompressionMethod | Should -Be Deflated + It 'Should have a LastWriteTime property' { + $zipEntry.LastWriteTime | Should -BeOfType ([datetime]) + $rarEntry.LastWriteTime | Should -BeOfType ([datetime]) + $tarEntry.LastWriteTime | Should -BeOfType ([datetime]) } - It 'Should Have a Comment Property' { - ($zip | Get-ZipEntry).Comment | Should -BeOfType ([string]) - ($zip | Get-ZipEntry).Comment | Should -BeExactly '' + It 'Should have a Comment property' { + $zipEntry.Comment | Should -BeOfType ([string]) + $zipEntry.Comment | Should -BeExactly '' } It 'Should Open the source zip' { - Use-Object ($stream = ($zip | Get-ZipEntry).OpenRead()) { + Use-Object ($stream = $zipEntry.OpenRead()) { $stream | Should -BeOfType ([ZipArchive]) } } diff --git a/tests/ZipEntryBase.tests.ps1 b/tests/ZipEntryBase.tests.ps1 index 2da3da8..da0c07e 100644 --- a/tests/ZipEntryBase.tests.ps1 +++ b/tests/ZipEntryBase.tests.ps1 @@ -14,7 +14,7 @@ Describe 'ZipEntryBase Class' { $zip = New-Item (Join-Path $TestDrive test.zip) -ItemType File -Force 'hello world!' | New-ZipEntry $zip.FullName -EntryPath helloworld.txt New-ZipEntry $zip.FullName -EntryPath somefolder/ - $encryptedZip = Get-Item $PSScriptRoot/../assets/helloworld.zip + $encryptedZip = Get-Item $PSScriptRoot/../assets/test.zip $encryptedZip | Out-Null } @@ -42,7 +42,7 @@ Describe 'ZipEntryBase Class' { } It 'Can extract an encrypted entry' { - $passw = ConvertTo-SecureString 'test' -AsPlainText -Force + $passw = ConvertTo-SecureString test -AsPlainText -Force $dest = Join-Path $TestDrive encryptedTestFolder Use-Object ($stream = $encryptedZip.OpenRead()) { $info = ($stream | Get-ZipEntry -Type Archive).ExtractTo($dest, $false, $passw) diff --git a/tests/shared.psm1 b/tests/shared.psm1 index b1bcdb3..6736bf3 100644 --- a/tests/shared.psm1 +++ b/tests/shared.psm1 @@ -1,6 +1,5 @@ using namespace System.Management.Automation using namespace System.Runtime.InteropServices -using namespace PSCompression function Complete-Input { param([string] $Expression) @@ -10,7 +9,7 @@ function Complete-Input { function Test-Completer { param( - [ArgumentCompleter([EncodingCompleter])] + [ArgumentCompleter([PSCompression.Attributes.EncodingCompleter])] [string] $Test ) } diff --git a/tools/InvokeBuild.ps1 b/tools/InvokeBuild.ps1 index 95272b0..2daaac1 100644 --- a/tools/InvokeBuild.ps1 +++ b/tools/InvokeBuild.ps1 @@ -55,7 +55,7 @@ task Package { Path = $ProjectInfo.Project.Release Repository = $repoParams.Name } - Publish-Module @publishModuleSplat + Publish-Module @publishModuleSplat -EA 0 } finally { Unregister-PSRepository -Name $repoParams.Name diff --git a/tools/ProjectBuilder/Project.cs b/tools/ProjectBuilder/Project.cs index a84530f..53d7365 100644 --- a/tools/ProjectBuilder/Project.cs +++ b/tools/ProjectBuilder/Project.cs @@ -65,6 +65,7 @@ public void CopyToRelease() framework, "publish")); + if (!buildFolder.Exists) continue; string binFolder = Path.Combine(Release, "bin", framework); buildFolder.CopyRecursive(binFolder); } diff --git a/tools/requiredModules.psd1 b/tools/requiredModules.psd1 index b49d079..64e7bee 100644 --- a/tools/requiredModules.psd1 +++ b/tools/requiredModules.psd1 @@ -1,7 +1,6 @@ @{ - InvokeBuild = '5.12.1' + InvokeBuild = '5.14.23' platyPS = '0.14.2' - PSScriptAnalyzer = '1.23.0' - Pester = '5.7.1' + Pester = '6.2.0' PSUsing = '1.0.0' }