?param( [string[]]$Files, [ValidateSet("Rules","BootPack","KB","Scripts","All")] [string]$Scope="Rules", [switch]$Preview, [switch]$Write, [switch]$RecalcManifest ) # locate registry and resolver $here = $PSScriptRoot if(-not $here){ $here = Split-Path -Parent $MyInvocation.MyCommand.Path } $registry = Split-Path -Parent $here $scriptsDir = $here . (Join-Path $scriptsDir "encoding_policy_resolver_v1.0.ps1") # encodings $Utf8NoBom = [Text.UTF8Encoding]::new($false) $Utf8Bom = [Text.UTF8Encoding]::new($true) $Ascii = [Text.ASCIIEncoding]::new() $Cp1252 = [Text.Encoding]::GetEncoding(1252) function HasBomUtf8([byte[]]$b){ $b.Length -ge 3 -and $b[0]-eq 0xEF -and $b[1]-eq 0xBB -and $b[2]-eq 0xBF } function ReadNoBom([byte[]]$b){ if(HasBomUtf8 $b){ $b=$b[3..($b.Length-1)] }; $Utf8NoBom.GetString($b) } function Normalize([string]$s,[string]$nl){ $t=$s -replace "`r`n","`n"; $t=$t -replace "`r","`n" if($nl -eq "CRLF"){ return [string]::Join("`r`n",$t -split "`n") } else { return $t } } function MojibakeScore([string]$s){ $bad1=[char]0x00C3; $bad2=[char]0x00E2; $bad3=[char]0x00C2 ($s.ToCharArray() | Where-Object { $_ -eq $bad1 -or $_ -eq $bad2 -or $_ -eq $bad3 }).Count } function Cp1252ToUtf8([string]$s){ $bytes = $Cp1252.GetBytes($s) return $Utf8NoBom.GetString($bytes) } function Get-Targets{ $t=@() if($Scope -eq "Rules"){ $t += Get-ChildItem -LiteralPath (Join-Path $registry "rules") -Filter "*.txt" -File -EA SilentlyContinue } elseif($Scope -eq "BootPack"){ $t += Get-ChildItem -LiteralPath (Join-Path $registry "bootpack") -Filter "*.txt" -File -EA SilentlyContinue } elseif($Scope -eq "KB"){ $t += Get-ChildItem -LiteralPath (Join-Path $registry "bootpack") -Filter "bootpack_paste.txt" -File -EA SilentlyContinue } elseif($Scope -eq "Scripts"){ $t += Get-ChildItem -LiteralPath (Join-Path $registry "scripts") -Filter "*.ps1" -File -EA SilentlyContinue $t += Get-ChildItem -LiteralPath (Join-Path $registry "scripts") -Filter "*.cmd" -File -EA SilentlyContinue $t += Get-ChildItem -LiteralPath (Join-Path $registry "scripts") -Filter "*.sh" -File -EA SilentlyContinue } elseif($Scope -eq "All"){ $t += Get-ChildItem -LiteralPath (Join-Path $registry "rules") -Filter "*.txt" -File -EA SilentlyContinue $t += Get-ChildItem -LiteralPath (Join-Path $registry "bootpack") -Filter "*.txt" -File -EA SilentlyContinue $t += Get-ChildItem -LiteralPath (Join-Path $registry "scripts") -Filter "*.ps1" -File -EA SilentlyContinue $t += Get-ChildItem -LiteralPath (Join-Path $registry "scripts") -Filter "*.cmd" -File -EA SilentlyContinue $t += Get-ChildItem -LiteralPath (Join-Path $registry "scripts") -Filter "*.sh" -File -EA SilentlyContinue } if($Files){ foreach($f in $Files){ if(Test-Path -LiteralPath $f){ $t += Get-Item -LiteralPath $f } } } $t | Sort-Object FullName -Unique } function TargetPolicy([string]$path){ $profile = Get-ProjectProfile $ext = [IO.Path]::GetExtension($path) if(-not $ext){ $ext="" } $ext = $ext.ToLowerInvariant() if($ext -eq ".ps1"){ $enc = "UTF8NOBOM" if($profile -eq "SEEDBOX"){ $enc="UTF8BOM" } return @{ enc=$enc; nl="CRLF"; ascii=$false; recalc=$false } } if($ext -eq ".cmd"){ return @{ enc="ASCII"; nl="CRLF"; ascii=$true; recalc=$false } } if($ext -eq ".sh" ){ return @{ enc="ASCII"; nl="LF"; ascii=$true; recalc=$false } } $recalc = ([IO.Path]::GetFileName($path) -ieq "bootpack.txt") return @{ enc="UTF8NOBOM"; nl="CRLF"; ascii=$false; recalc=$recalc } } function SafeWrite([string]$p,[string]$text,[string]$enc,[string]$nl){ $text = Normalize $text $nl $e = $Utf8NoBom switch($enc){ "UTF8BOM"{$e=$Utf8Bom} "UTF8NOBOM"{$e=$Utf8NoBom} "ASCII"{$e=$Ascii} default{$e=$Utf8NoBom} } $bak = $p + ".bak_" + (Get-Date -Format "yyyyMMdd_HHmmss") if(Test-Path $p){ Copy-Item -LiteralPath $p -Destination $bak -Force } $tmp = $p + ".tmp" [IO.File]::WriteAllText($tmp,$text,$e) $shaTmp=(Get-FileHash -Algorithm SHA256 -LiteralPath $tmp).Hash Move-Item -LiteralPath $tmp -Destination $p -Force $shaFin=(Get-FileHash -Algorithm SHA256 -LiteralPath $p).Hash @{bak=$bak; shaTmp=$shaTmp; shaFinal=$shaFin} } function RecalcManifestSha([string]$raw){ $m=[regex]::Match($raw,'(?ms)^\[SYNC_MANIFEST\]\s*(.*?)(?=^\[|\Z)') if(-not $m.Success){ return $null } $blk=$m.Groups[1].Value $core=@() foreach($ln in ($blk -split "\r?\n")){ if($ln -match '^\s*MANIFEST_SHA256\s*:'){ continue } $core += $ln.TrimEnd() } $str=[string]::Join("`r`n",$core) [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes($str))).Replace("-","").ToLower() } # logging $ts=Get-Date -Format "yyyyMMdd_HHmmss" $logsDir = Join-Path $registry "logs"; if(!(Test-Path $logsDir)){ New-Item -ItemType Directory -Force -Path $logsDir|Out-Null } $log = Join-Path $logsDir ("run_encoding_guard_"+$ts+".txt") function LogLine([string]$msg){ Add-Content -LiteralPath $log -Value $msg -Encoding UTF8; Write-Host $msg } $targets = Get-Targets LogLine ("ENCODING GUARD (policy-aware, PS5.1-safe) - "+(Get-Date -Format s)) foreach($fi in $targets){ $p=$fi.FullName try{ $bytes=[IO.File]::ReadAllBytes($p) $hadBom=HasBomUtf8 $bytes $txt=ReadNoBom $bytes $score=MojibakeScore $txt $pol=TargetPolicy $p $action="OK" $fixed=$txt if($score -gt 3){ $action="FIX: cp1252->utf8"; $fixed = Cp1252ToUtf8 $txt } $norm = Normalize $fixed $pol.nl if($norm -ne $fixed){ if($action -eq "OK"){ $action="FIX: NL" } else { $action += " + NL" }; $fixed=$norm } if($pol.enc -eq "UTF8BOM" -and -not $hadBom){ if($action -eq "OK"){ $action="FIX: add BOM" } else { $action += " + add BOM" } } if($pol.enc -eq "UTF8NOBOM" -and $hadBom){ if($action -eq "OK"){ $action="FIX: strip BOM" } else { $action += " + strip BOM" } } if($pol.ascii){ $nonAscii = ($fixed.ToCharArray() | Where-Object { [int]$_ -gt 127 }).Count if($nonAscii -gt 0){ if($action -eq "OK"){ $action="WARN: non-ASCII" } else { $action += " + WARN non-ASCII" } } } LogLine ("{0} :: target={1}/{2} :: score={3} :: {4}" -f $p,$pol.enc,$pol.nl,$score,$action) $needRecalc = ($RecalcManifest -and $pol.recalc -and ($fixed -ne $txt)) if(-not $Write -and -not $needRecalc){ if($Preview){ LogLine " (preview) no write" } continue } if($needRecalc){ $sha=RecalcManifestSha $fixed if($sha){ $fixed=[regex]::Replace($fixed,'(?mi)^(MANIFEST_SHA256\s*:\s*)([0-9a-f]+)\s*$',('$1'+$sha),1) LogLine (" manifest recalculated => {0}" -f $sha) } else { LogLine " [WARN] SYNC_MANIFEST block not found; no recalculation." } } if($Write){ $res=SafeWrite -p $p -text $fixed -enc $pol.enc -nl $pol.nl LogLine (" Backup: {0}" -f $res.bak) LogLine (" SHA tmp/final: {0} / {1}" -f $res.shaTmp,$res.shaFinal) LogLine " STATUS=WRITE-OK" } else { LogLine " (preview) no write" } } catch{ LogLine ("[ERR] {0} :: {1}" -f $p,$_.Exception.Message) } } Write-Host ("Log -> "+$log)