param( [string[]]$Files, [ValidateSet("Rules","BootPack","KB","All")] [string]$Scope="Rules", [switch]$Preview, [switch]$Write, [switch]$RecalcManifest ) function Ensure-Dir([string]$p){ if($p -and !(Test-Path $p)){ New-Item -ItemType Directory -Force -Path $p|Out-Null } } function ReadBytes([string]$p){ [IO.File]::ReadAllBytes($p) } function HasBomUtf8([byte[]]$b){ return ($b.Length -ge 3 -and $b[0]-eq 0xEF -and $b[1]-eq 0xBB -and $b[2]-eq 0xBF) } $Utf8NoBom = [Text.UTF8Encoding]::new($false) $Cp1252 = [Text.Encoding]::GetEncoding(1252) # Résolution de la racine du registre (.\_registry) depuis l'emplacement du script $scriptPath = $MyInvocation.MyCommand.Definition $scriptDir = Split-Path -Parent $scriptPath $registry = Split-Path -Parent $scriptDir # ..\ (remonte à _registry) function BytesToUtf8NoBom([byte[]]$b){ if(HasBomUtf8 $b){ $b = $b[3..($b.Length-1)] } return $Utf8NoBom.GetString($b) } function NormalizeCRLF([string]$s){ $t=$s -replace "`r`n","`n"; $t=$t -replace "`r","`n" return [string]::Join("`r`n", $t -split "`n") } function MojibakeScore([string]$s){ $m1 = ([regex]::Matches($s,'Ã.')).Count $m2 = ([regex]::Matches($s,'â.{0,2}')).Count $m3 = ([regex]::Matches($s,'Â')).Count return $m1+$m2+$m3 } function Cp1252ToUtf8([string]$s){ $bytes = $Cp1252.GetBytes($s) return $Utf8NoBom.GetString($bytes) } function RecalcManifestSha([string]$raw){ $m=[regex]::Match($raw,'(?ms)^\[SYNC_MANIFEST\]\s*(.*?)(?=^\[|\Z)') if(-not $m.Success){ return $null } $blk=$m.Groups[1].Value $coreLines=@() foreach($ln in ($blk -split "`r?`n")){ if($ln -match '^\s*MANIFEST_SHA256\s*:'){ continue } $coreLines += $ln.TrimEnd() } $core=[string]::Join("`r`n",$coreLines) $sha=[BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes($core))).Replace('-','').ToLower() return $sha } function SafeWrite([string]$path,[string]$content){ $bak = $path + '.bak_' + (Get-Date -Format 'yyyyMMdd_HHmmss') if(Test-Path $path){ Copy-Item -LiteralPath $path -Destination $bak -Force } $tmp = $path + '.tmp' [IO.File]::WriteAllText($tmp,$content,$Utf8NoBom) $shaTmp = (Get-FileHash -Algorithm SHA256 -LiteralPath $tmp).Hash Move-Item -LiteralPath $tmp -Destination $path -Force $shaFinal = (Get-FileHash -Algorithm SHA256 -LiteralPath $path).Hash return @{bak=$bak; shaTmp=$shaTmp; shaFinal=$shaFinal} } # Sélection des cibles $targets=@() switch($Scope){ 'Rules' { $targets += Get-ChildItem -LiteralPath (Join-Path $registry 'rules') -Filter '*.txt' -File -ErrorAction SilentlyContinue } 'BootPack'{ $targets += Get-ChildItem -LiteralPath (Join-Path $registry 'bootpack') -Filter '*.txt' -File -ErrorAction SilentlyContinue } 'KB' { $targets += Get-ChildItem -LiteralPath (Join-Path $registry 'bootpack') -Filter 'bootpack_paste.txt' -File -ErrorAction SilentlyContinue } 'All' { $targets += Get-ChildItem -LiteralPath (Join-Path $registry 'rules') -Filter '*.txt' -File -ErrorAction SilentlyContinue $targets += Get-ChildItem -LiteralPath (Join-Path $registry 'bootpack') -Filter '*.txt' -File -ErrorAction SilentlyContinue } } if($Files){ foreach($f in $Files){ if(Test-Path $f){ $targets += Get-Item -LiteralPath $f } } } $targets = $targets | Sort-Object FullName -Unique # Log $ts=Get-Date -Format 'yyyyMMdd_HHmmss' $logsDir = Join-Path $registry 'logs' Ensure-Dir $logsDir $log = Join-Path $logsDir ('run_encoding_guard_'+$ts+'.txt') "ENCODING GUARD - "+(Get-Date -Format s) | Out-File -LiteralPath $log -Encoding ascii foreach($fi in $targets){ $p=$fi.FullName try{ $bytes = ReadBytes $p $hadBom = HasBomUtf8 $bytes $txt = BytesToUtf8NoBom $bytes $score = MojibakeScore $txt $action='OK' $fixed=$txt if($score -gt 3){ $action='FIX: cp1252->utf8' $fixed = Cp1252ToUtf8 $txt } $norm = NormalizeCRLF $fixed if($norm -ne $fixed){ if($action -eq 'OK'){ $action='FIX: CRLF' } else { $action += ' + CRLF' } $fixed = $norm } if($hadBom){ if($action -eq 'OK'){ $action='FIX: strip BOM' } else { $action += ' + strip BOM' } } $needRecalc = $false if($RecalcManifest -and ([IO.Path]::GetFileName($p) -ieq 'bootpack.txt')){ if($fixed -ne $txt){ $needRecalc=$true } } "{0} :: score={1} :: {2}" -f $p,$score,$action | Tee-Object -FilePath $log -Append | Out-Null if($action -eq 'OK' -and -not $needRecalc){ continue } if(-not $Write){ " (preview) aucune écriture" | Tee-Object -FilePath $log -Append | Out-Null continue } if($needRecalc){ $sha = RecalcManifestSha $fixed if($null -ne $sha){ $fixed = [regex]::Replace($fixed,'(?mi)^(MANIFEST_SHA256\s*:\s*)([0-9a-f]+)\s*$', ('$1'+$sha), 1) " manifest recalculé => $sha" | Tee-Object -FilePath $log -Append | Out-Null } else { " [WARN] pas de section [SYNC_MANIFEST] détectée; pas de recalcul." | Tee-Object -FilePath $log -Append | Out-Null } } $res = SafeWrite -path $p -content $fixed " Backup: {0}" -f $res.bak | Tee-Object -FilePath $log -Append | Out-Null " SHA tmp/final: {0} / {1}" -f $res.shaTmp,$res.shaFinal | Tee-Object -FilePath $log -Append | Out-Null " STATUS=WRITE-OK" | Tee-Object -FilePath $log -Append | Out-Null } catch{ "[ERR] "+$p+" :: "+$_.Exception.Message | Tee-Object -FilePath $log -Append | Out-Null } } "Log -> "+$log | Write-Host