Payload/scripts/kaden/utils/strip-bom.ps1
|
<# .SYNOPSIS Rewrite files to UTF-8 without a BOM. .DESCRIPTION Repo PowerShell is UTF-8 without a BOM (PSScriptAnalyzerSettings.psd1 excludes PSUseBOMForUnicodeEncodedFile for that reason; lint-powershell.ps1 fails when EF BB BF is present). This script strips a UTF-8 BOM, and converts UTF-16 LE or BE (BOM) to UTF-8 without a BOM. Files that already have no BOM are left unchanged. Pass one or more file paths. Relative paths resolve from the current directory, then from the repository root. Rewrites stay inside the repository unless -AllowOutsideRepo is set. pwsh -File passes each path as its own argument. Use bare paths, or -Path for the first path and the rest bare. A direct call inside pwsh can use -Path a.ps1, b.ps1. .PARAMETER Path One or more file paths. Alias: Paths. Further paths may follow. .PARAMETER AllowOutsideRepo Rewrite a file that resolves outside this repository. .EXAMPLE pwsh -NoProfile -File .\scripts\kaden\utils\strip-bom.ps1 scripts\kaden\harness\lib\HarnessContract.psm1 tests\harness\Some.Tests.ps1 .EXAMPLE pwsh -NoProfile -File .\scripts\kaden\utils\strip-bom.ps1 -Path scripts\a.ps1 tests\harness\Some.Tests.ps1 .EXAMPLE Get-Help .\scripts\kaden\utils\strip-bom.ps1 -Parameter Path #> #Requires -Version 5.1 [CmdletBinding(DefaultParameterSetName = 'Files')] param( [Parameter(ParameterSetName = 'Files', Position = 0)] [Alias('Paths')] [string[]]$Path = @(), [Parameter(ParameterSetName = 'Files', ValueFromRemainingArguments = $true, DontShow = $true)] [string[]]$ExtraPath, [Parameter(ParameterSetName = 'Files')] [switch]$AllowOutsideRepo, [Parameter(ParameterSetName = 'Help')] [Alias('h', 'help')] [switch]$ShowHelp ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" $cliKitManifest = Join-Path $PSScriptRoot "PsCliUiKit\PsCliUiKit.psd1" if (-not (Test-Path -LiteralPath $cliKitManifest)) { throw "PsCliUiKit manifest not found: $cliKitManifest" } Import-Module -Name $cliKitManifest -Force function Resolve-StripBomRepoRoot { $current = $PSScriptRoot while ($true) { $marker = Join-Path $current "scripts\kaden\harness-invoke.ps1" if (Test-Path -LiteralPath $marker) { return $current } $parent = Split-Path $current -Parent if ([string]::IsNullOrWhiteSpace($parent) -or $parent -eq $current) { return $null } $current = $parent } } function Resolve-StripBomInputPath { param( [Parameter(Mandatory = $true)][string]$InputPath, [string]$RepoRoot = '' ) if ([string]::IsNullOrWhiteSpace($InputPath)) { throw "Path is empty." } if (Test-Path -LiteralPath $InputPath) { $resolved = (Resolve-Path -LiteralPath $InputPath).Path } elseif (-not [string]::IsNullOrWhiteSpace($RepoRoot) -and -not [System.IO.Path]::IsPathRooted($InputPath)) { $candidate = Join-Path $RepoRoot $InputPath if (-not (Test-Path -LiteralPath $candidate)) { throw "File not found: $InputPath" } $resolved = (Resolve-Path -LiteralPath $candidate).Path } else { throw "File not found: $InputPath" } $item = Get-Item -LiteralPath $resolved if ($item.PSIsContainer) { throw "Not a file: $resolved" } return $resolved } function Test-StripBomPathInsideRepo { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$RepoRoot ) $rootFull = [System.IO.Path]::GetFullPath($RepoRoot) $separator = [System.IO.Path]::DirectorySeparatorChar if (-not $rootFull.EndsWith($separator)) { $rootFull += $separator } $full = [System.IO.Path]::GetFullPath($Path) return $full.StartsWith($rootFull, [System.StringComparison]::OrdinalIgnoreCase) } function ConvertTo-StripBomUtf8 { param([Parameter(Mandatory = $true)][string]$LiteralPath) $bytes = [System.IO.File]::ReadAllBytes($LiteralPath) $kind = "none" $text = $null if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $kind = "utf8-bom" $utf8Strict = New-Object System.Text.UTF8Encoding $false, $true $text = $utf8Strict.GetString($bytes, 3, $bytes.Length - 3) } elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { if (($bytes.Length % 2) -ne 0) { throw "UTF-16 LE byte length is odd: $LiteralPath" } $kind = "utf16-le-bom" $utf16 = New-Object System.Text.UnicodeEncoding $false, $true, $true $text = $utf16.GetString($bytes, 2, $bytes.Length - 2) } elseif ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { if (($bytes.Length % 2) -ne 0) { throw "UTF-16 BE byte length is odd: $LiteralPath" } $kind = "utf16-be-bom" $utf16be = New-Object System.Text.UnicodeEncoding $true, $true, $true $text = $utf16be.GetString($bytes, 2, $bytes.Length - 2) } if ($kind -eq "none") { return [pscustomobject]@{ Path = $LiteralPath Result = "unchanged" Detail = "" } } $utf8 = New-Object System.Text.UTF8Encoding $false $tmp = "$LiteralPath.nobom" try { [System.IO.File]::WriteAllText($tmp, $text, $utf8) Move-Item -LiteralPath $tmp -Destination $LiteralPath -Force } finally { if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Force } } $after = [System.IO.File]::ReadAllBytes($LiteralPath) $stillBom = ($after.Length -ge 3 -and $after[0] -eq 0xEF -and $after[1] -eq 0xBB -and $after[2] -eq 0xBF) if ($stillBom) { throw "Rewrite left a UTF-8 BOM on $LiteralPath" } $detail = "" $result = "removed" if ($kind -eq "utf16-le-bom") { $result = "converted" $detail = "utf-16 le" } elseif ($kind -eq "utf16-be-bom") { $result = "converted" $detail = "utf-16 be" } return [pscustomobject]@{ Path = $LiteralPath Result = $result Detail = $detail } } try { if ($ShowHelp) { (Get-Help -Name $PSCommandPath -Full | Out-String -Width 100) | Out-Host return } $requested = New-Object System.Collections.Generic.List[string] foreach ($entry in @($Path)) { if (-not [string]::IsNullOrWhiteSpace($entry)) { [void]$requested.Add($entry) } } if ($null -ne $ExtraPath) { foreach ($entry in @($ExtraPath)) { if (-not [string]::IsNullOrWhiteSpace($entry)) { [void]$requested.Add($entry) } } } if ($requested.Count -eq 0) { throw "No files given. Get-Help `"$PSCommandPath`" -Parameter Path" } $repoRoot = Resolve-StripBomRepoRoot $resolvedPaths = New-Object System.Collections.Generic.List[string] foreach ($inputPath in $requested) { $full = Resolve-StripBomInputPath -InputPath $inputPath -RepoRoot $repoRoot [void]$resolvedPaths.Add($full) } if (-not $AllowOutsideRepo) { if ([string]::IsNullOrWhiteSpace($repoRoot)) { throw "Could not locate the repository root. Get-Help `"$PSCommandPath`" -Parameter AllowOutsideRepo" } foreach ($full in $resolvedPaths) { if (-not (Test-StripBomPathInsideRepo -Path $full -RepoRoot $repoRoot)) { throw "Refusing to rewrite a file outside the repository: $full. Get-Help `"$PSCommandPath`" -Parameter AllowOutsideRepo" } } } Write-CliSection -Title "Strip BOM" $rewritten = 0 $unchanged = 0 foreach ($full in $resolvedPaths) { $result = ConvertTo-StripBomUtf8 -LiteralPath $full if ($result.Result -eq "unchanged") { $unchanged++ Write-CliKeyValue -Label "unchanged" -LabelWidth 10 -Value "[muted]$($result.Path)[-]" } elseif ($result.Result -eq "converted") { $rewritten++ Write-CliKeyValue -Label "converted" -LabelWidth 10 -Value "[success]$($result.Path)[-] [muted]($($result.Detail))[-]" } else { $rewritten++ Write-CliKeyValue -Label "removed" -LabelWidth 10 -Value "[success]$($result.Path)[-]" } } $summaryState = if ($rewritten -gt 0) { "Success" } else { "Info" } Write-CliStatus -State $summaryState -Message "$rewritten rewritten, $unchanged unchanged" } catch { Write-CliSection -Title "Strip BOM failed" -Style "error" Write-CliStatus -State "Error" -Message $_.Exception.Message exit 2 } finally { Write-CliEnd } |