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){ ([regex]::Matches($s,'?.')).Count + ([regex]::Matches($s,'?.{0,2}')).Count + ([regex]::Matches($s,'?')).Count } function Cp1252ToUtf8([string]$s){ $bytes = $Cp1252.GetBytes($s) return $Utf8NoBom.GetString($bytes) } function Get-Targets{ $t=@() switch($Scope){ "Rules" { $t += Get-ChildItem -LiteralPath (Join-Path $registry 'rules') -Filter '*.txt' -File -EA SilentlyContinue } "BootPack"{ $t += Get-ChildItem -LiteralPath (Join-Path $registry 'bootpack') -Filter '*.txt' -File -EA SilentlyContinue } "KB" { $t += Get-ChildItem -LiteralPath (Join-Path $registry 'bootpack') -Filter 'bootpack_paste.txt' -File -EA SilentlyContinue } "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 } "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 $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) aucune ecriture" } 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 recalcule => {0}" -f $sha) } else { LogLine " [WARN] pas de section [SYNC_MANIFEST] detectee; pas de recalcul." } } 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) aucune ecriture" } } catch{ LogLine ("[ERR] {0} :: {1}" -f $p,$_.Exception.Message) } } Write-Host ("Log -> "+$log)