-
-
Notifications
You must be signed in to change notification settings - Fork 0
fix(pwsh): replace utf8NoBOM Set-Content in 3 unexecuted .tmpl scripts #253
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
kurone-kito
merged 2 commits into
master
from
issue/214-fix-pwsh-utf8nobom-ps5-1-incompatibility
Aug 14, 2026
Merged
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| # Tests for the PowerShell SSH key deployment script. | ||
| # Exercises: BOM-less UTF-8 encoding (PS5.1-compatible), exact byte | ||
| # preservation (no added trailing newline), skip-if-exists behavior, | ||
| # and the Windows ACL restriction applied to private keys only. | ||
|
|
||
| BeforeAll { | ||
| $script:Fixture = Join-Path $PSScriptRoot 'fixtures/deploy-ssh-keys.ps1' | ||
| } | ||
|
|
||
| Describe 'deploy-ssh-keys' { | ||
|
|
||
| BeforeEach { | ||
| $script:OriginalHome = $HOME | ||
| $script:OriginalUserName = $env:USERNAME | ||
| $script:HomeDir = 'TestDrive:\home' | ||
| if (Test-Path $script:HomeDir) { | ||
| Remove-Item $script:HomeDir -Recurse -Force | ||
| } | ||
| New-Item -ItemType Directory -Path $script:HomeDir -Force | Out-Null | ||
| Set-Variable -Name HOME -Value $script:HomeDir -Scope Global -Force | ||
| $env:USERNAME = 'sample-user' | ||
| $global:DotfilesTestIcaclsCalls = @() | ||
|
|
||
| function global:icacls { | ||
| $global:DotfilesTestIcaclsCalls += , @($args) | ||
| $global:LASTEXITCODE = 0 | ||
| } | ||
|
|
||
| $script:SshDirReal = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath( | ||
| (Join-Path $script:HomeDir '.ssh')) | ||
| } | ||
|
|
||
| AfterEach { | ||
| Set-Variable -Name HOME -Value $script:OriginalHome -Scope Global -Force | ||
| $env:USERNAME = $script:OriginalUserName | ||
| Remove-Item Function:\icacls -ErrorAction SilentlyContinue | ||
| } | ||
|
|
||
| It 'writes both key files with no BOM' { | ||
| & $script:Fixture | Out-Null | ||
|
|
||
| $privateBytes = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_secondary')) | ||
| $publicBytes = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_secondary.pub')) | ||
|
|
||
| # A UTF-8 BOM is the 3-byte sequence EF BB BF; neither file should | ||
| # start with it. | ||
| ($privateBytes.Length -ge 3 -and $privateBytes[0] -eq 0xEF -and | ||
| $privateBytes[1] -eq 0xBB -and $privateBytes[2] -eq 0xBF) | Should -BeFalse | ||
| ($publicBytes.Length -ge 3 -and $publicBytes[0] -eq 0xEF -and | ||
| $publicBytes[1] -eq 0xBB -and $publicBytes[2] -eq 0xBF) | Should -BeFalse | ||
| } | ||
|
|
||
| It 'writes exact byte-for-byte content with no added trailing newline' { | ||
| & $script:Fixture | Out-Null | ||
|
|
||
| $expectedPrivate = 'TEST-ONLY-PRIVATE-KEY-CONTENT-secondary' | ||
| $expectedPublic = 'ssh-ed25519 AAAASECONDARY secondary@test' | ||
| $expectedPrivateBytes = [System.Text.UTF8Encoding]::new($false).GetBytes($expectedPrivate) | ||
| $expectedPublicBytes = [System.Text.UTF8Encoding]::new($false).GetBytes($expectedPublic) | ||
|
|
||
| $actualPrivateBytes = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_secondary')) | ||
| $actualPublicBytes = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_secondary.pub')) | ||
|
|
||
| [System.Linq.Enumerable]::SequenceEqual($actualPrivateBytes, $expectedPrivateBytes) | Should -BeTrue | ||
| [System.Linq.Enumerable]::SequenceEqual($actualPublicBytes, $expectedPublicBytes) | Should -BeTrue | ||
| } | ||
|
|
||
| It 'preserves embedded newlines in multi-line key content exactly' { | ||
| & $script:Fixture | Out-Null | ||
|
|
||
| $expected = "TEST-ONLY-PRIVATE-KEY-CONTENT-primary`nline two" | ||
| $expectedBytes = [System.Text.UTF8Encoding]::new($false).GetBytes($expected) | ||
| $actualBytes = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_primary')) | ||
|
|
||
| [System.Linq.Enumerable]::SequenceEqual($actualBytes, $expectedBytes) | Should -BeTrue | ||
| } | ||
|
|
||
| It 'writes both key pairs for multiple configured keys' { | ||
| & $script:Fixture | Out-Null | ||
|
|
||
| Join-Path $script:SshDirReal 'id_primary' | Should -Exist | ||
| Join-Path $script:SshDirReal 'id_primary.pub' | Should -Exist | ||
| Join-Path $script:SshDirReal 'id_secondary' | Should -Exist | ||
| Join-Path $script:SshDirReal 'id_secondary.pub' | Should -Exist | ||
| } | ||
|
|
||
| It 'restricts private key permissions to the current user via icacls' { | ||
| & $script:Fixture | Out-Null | ||
|
|
||
| # @(...) forces array semantics: Where-Object unwraps a single | ||
| # matching element when that element is itself an array, which | ||
| # would otherwise make .Count report the inner args count instead | ||
| # of the number of matching icacls calls. | ||
| $privateCalls = @($global:DotfilesTestIcaclsCalls | | ||
| Where-Object { $_ -contains (Join-Path $script:SshDirReal 'id_primary') }) | ||
| $privateCalls.Count | Should -Be 1 | ||
| $privateCalls[0] | Should -Contain 'sample-user:(F)' | ||
| } | ||
|
|
||
| It 'does not run icacls against the public key' { | ||
| & $script:Fixture | Out-Null | ||
|
|
||
| $publicCalls = @($global:DotfilesTestIcaclsCalls | | ||
| Where-Object { $_ -contains (Join-Path $script:SshDirReal 'id_primary.pub') }) | ||
| $publicCalls.Count | Should -Be 0 | ||
| } | ||
|
|
||
| It 'skips writing a key that already exists' { | ||
| New-Item -ItemType Directory -Path $script:SshDirReal -Force | Out-Null | ||
| $existingPath = Join-Path $script:SshDirReal 'id_primary' | ||
| [System.IO.File]::WriteAllText($existingPath, 'PRE-EXISTING-CONTENT', | ||
| [System.Text.UTF8Encoding]::new($false)) | ||
|
|
||
| & $script:Fixture | Out-Null | ||
|
|
||
| $content = [System.IO.File]::ReadAllText($existingPath) | ||
| $content | Should -Be 'PRE-EXISTING-CONTENT' | ||
| } | ||
|
|
||
| It 'produces no diff when re-run with keys already deployed' { | ||
| & $script:Fixture | Out-Null | ||
| $before = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_primary')) | ||
|
|
||
| & $script:Fixture | Out-Null | ||
| $after = [System.IO.File]::ReadAllBytes((Join-Path $script:SshDirReal 'id_primary')) | ||
|
|
||
| [System.Linq.Enumerable]::SequenceEqual($before, $after) | Should -BeTrue | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| #!/usr/bin/env pwsh | ||
| # Pre-rendered test fixture for run_once_before_20-deploy-ssh-keys.ps1.tmpl. | ||
| # Contains two hardcoded SSH key entries: | ||
| # primary - filename 'id_primary' | ||
| # secondary - filename 'id_secondary' | ||
| # | ||
| # This script is intentionally NOT a chezmoi template, and the key | ||
| # content below is synthetic placeholder text (not a real key pair) -- | ||
| # it exists only to exercise the encoding/newline/permission handling | ||
| # this script performs, not real SSH functionality. | ||
| $ErrorActionPreference = 'Stop' | ||
|
|
||
| $sshDir = Join-Path $HOME '.ssh' | ||
| New-Item -ItemType Directory -Path $sshDir -Force | Out-Null | ||
|
|
||
| # [System.IO.File] does not understand PS provider paths (e.g. TestDrive:\...), | ||
| # so resolve to a real filesystem path before using it with .NET I/O below. | ||
| $sshDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($sshDir) | ||
|
|
||
| $deployState = Join-Path $HOME '.local/bin/secret-deploy-state.ps1' | ||
| function Record-State { | ||
| param([string]$Category, [string]$Name, [string]$Path) | ||
| if (-not (Test-Path $deployState)) { return } | ||
| try { & $deployState record $Category $Name $Path | Out-Null } catch { } | ||
| } | ||
|
|
||
| $sshKeys = [ordered]@{ | ||
| primary = @{ | ||
| filename = 'id_primary' | ||
| private = "TEST-ONLY-PRIVATE-KEY-CONTENT-primary`nline two" | ||
| public = 'ssh-ed25519 AAAAPRIMARY primary@test' | ||
| } | ||
| secondary = @{ | ||
| filename = 'id_secondary' | ||
| private = 'TEST-ONLY-PRIVATE-KEY-CONTENT-secondary' | ||
| public = 'ssh-ed25519 AAAASECONDARY secondary@test' | ||
| } | ||
| } | ||
|
|
||
| foreach ($key in $sshKeys.Keys) { | ||
| $sshKey = $sshKeys[$key] | ||
| Write-Host "Deploying SSH key: $key ($($sshKey.filename))..." | ||
| $privateKey = Join-Path $sshDir $sshKey.filename | ||
| $publicKey = Join-Path $sshDir "$($sshKey.filename).pub" | ||
|
|
||
| if (-not (Test-Path $privateKey)) { | ||
| $privateKeyContent = $sshKey.private | ||
| # PS5.1 rejects the utf8NoBOM encoding literal (PS6+ only); write the | ||
| # BOM-less UTF-8 bytes directly instead. No trailing newline is added, | ||
| # matching the prior -NoNewline call. | ||
| [System.IO.File]::WriteAllText($privateKey, $privateKeyContent, | ||
| [System.Text.UTF8Encoding]::new($false)) | ||
| # Restrict to current user only (icacls works without elevation) | ||
| icacls $privateKey /inheritance:r /grant:r "${env:USERNAME}:(F)" 2>&1 | Out-Null | ||
| if ($LASTEXITCODE -ne 0) { | ||
| Write-Warning "Failed to restrict permissions on ${privateKey} (icacls exit code: $LASTEXITCODE)" | ||
| } | ||
| Write-Host ' Private key written.' | ||
| Record-State -Category 'sshKey' -Name $key -Path $privateKey | ||
| } else { | ||
| Write-Host ' Private key already exists; skipping.' | ||
| } | ||
|
|
||
| if (-not (Test-Path $publicKey)) { | ||
| $publicKeyContent = $sshKey.public | ||
| # PS5.1 rejects the utf8NoBOM encoding literal (PS6+ only); write the | ||
| # BOM-less UTF-8 bytes directly instead. No trailing newline is added, | ||
| # matching the prior -NoNewline call. | ||
| [System.IO.File]::WriteAllText($publicKey, $publicKeyContent, | ||
| [System.Text.UTF8Encoding]::new($false)) | ||
| Write-Host ' Public key written.' | ||
| Record-State -Category 'sshKey' -Name "$key.pub" -Path $publicKey | ||
| } else { | ||
| Write-Host ' Public key already exists; skipping.' | ||
| } | ||
| } | ||
|
|
||
| Write-Host 'SSH key deployment complete.' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.