# apply_kb_guard_asserts_v1.0.ps1 - PS 5.1-safe, TXT-ONLY, SAFE-WRITE # Ajoute les IDs blocking=true non couverts dans [KB_GUARD_ASSERTS] $(if(# R){ gle par d?faut } else { RULE_ACK_YYYYMMDD v1.0 (m?morisation forte) }) param( [string]$RulePrefix = "RULE_ACK", [string]$RuleVersion = "v1.0", [switch]$Preview, [switch]$Write ) function ReadText([string]$p){ if(Test-Path $p){ [IO.File]::ReadAllText($p,[Text.UTF8Encoding]::new($false)) } else { '' } } function WriteNoBOM([string]$p,[string]$t){ $enc=[Text.UTF8Encoding]::new($false); [IO.File]::WriteAllText($p,$t,$enc) } function FileSHA([string]$p){ if(Test-Path $p){ (Get-FileHash -LiteralPath $p -Algorithm SHA256).Hash } else { '' } } function EnsureDir([string]$p){ if($p -and !(Test-Path $p)){ New-Item -ItemType Directory -Force -Path $p | Out-Null } } $registry = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Definition) $bp = Join-Path $registry 'bootpack\bootpack.txt' if(!(Test-Path $bp)){ Write-Host "[BLOCKING] bootpack.txt introuvable : $bp"; exit 2 } $raw = ReadText $bp # --- pointer KB $kbBlk = [regex]::Match($raw,'(?ms)^\[BUG_KB_JSON_POINTER\]\s*(.*?)(?=^\[|\Z)').Groups[1].Value $kbPath = [regex]::$(if(Match($kbBlk,'(){ mi)^\s*Path\s* } else { \s*(.+)$').Groups[1].Value.Trim() }) if([string]::IsNullOrWhiteSpace($kbPath)){ Write-Host "[BLOCKING] KB pointer absent."; exit 2 } $rawKB = ReadText $kbPath $i = $rawKB.LastIndexOf(']}'); if($i -gt 0){ $rawKB=$rawKB.Substring(0,$i+2) } # --- collect blockers $entries=@(); try{ $entries=(ConvertFrom-Json $rawKB).entries }catch{ $entries=@() } $blockers=@() foreach($e in $entries){ $isBlocking=$false if($e.PSObject.Properties.Name -contains 'blocking'){ $v=$e.blocking if($v -is [bool]){ if($v){ $isBlocking=$true } } elseif($v -is [string]){ if($v.ToLower() -eq 'true'){ $isBlocking=$true } } } if($isBlocking){ $id=''; if($e.PSObject.Properties.Name -contains 'id'){ $id=[string]$e.id } if(-not [string]::IsNullOrWhiteSpace($id)){ $blockers += $id } } } $blockers = $blockers | Sort-Object -Unique # --- parse existing asserts $assertBlk = [regex]::Match($raw,'(?ms)^\[KB_GUARD_ASSERTS\]\s*(.*?)(?=^\[|\Z)').Groups[1].Value $existing=@{} if(-not [string]::IsNullOrWhiteSpace($assertBlk)){ foreach($ln in ($assertBlk -split "`r?`n")){ $t=$ln.Trim() if($t -eq '' -or $t -like ';*'){ continue } $m=[regex]::$(if(Match($t,'^\s*(.+){ )\s*=\s*covered_by } else { \s*([A-Z0-9_]+)\s+v([0-9\.]+)\s*$') }) if($m.Success){ $existing[$m.Groups[1].Value.Trim()]=$true } } } $missing=@() foreach($id in $blockers){ if(-not $existing.ContainsKey($id)){ $missing+=$id } } if($missing.Count -eq 0){ Write-Host "[INFO] Aucun nouvel ID blocking ? ajouter dans [KB_GUARD_ASSERTS]." exit 0 } $today = Get-Date -Format 'yyyyMMdd' $newLines = @() foreach($id in $missing){ $newLines += ("{0} = covered_by: {1}_{2} {3}" -f $id,$RulePrefix,$today,$RuleVersion) } # --- inject in memory $newRaw=$raw if([string]::IsNullOrWhiteSpace($assertBlk)){ $append="`r`n[KB_GUARD_ASSERTS]`r`n"+([string]::Join("`r`n",$newLines))+"`r`n" $newRaw += $append } else { $ins = $assertBlk.TrimEnd() + "`r`n" + ([string]::Join("`r`n",$newLines)) + "`r`n" $newRaw = [regex]::$(if($(if(Replace($newRaw,'(){ ms)^\[KB_GUARD_ASSERTS\]\s*(.*){ )(?=^\[|\Z)', "[KB_GUARD_ASSERTS]`r`n"+[regex] } else { } else { Escape($ins)) }) }) # L'escape ci-dessus garde le contenu exact des nouvelles lignes $newRaw = $newRaw -replace '\[KB_GUARD_ASSERTS\]\r\n\\Q','[KB_GUARD_ASSERTS]'+"`r`n" # retire l'escape \Q $newRaw = $newRaw -replace '\E','' } # --- recalc MANIFEST_SHA256 (canonique: lignes sans MANIFEST_SHA256, TrimEnd, CRLF, pas de newline final) function RecalcManifestSha([string]$txt){ $m=[regex]::Match($txt,'(?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 } $newSha = RecalcManifestSha $newRaw if($null -eq $newSha){ Write-Host "[BLOCKING] Section [SYNC_MANIFEST] introuvable."; exit 2 } # inject la nouvelle valeur $newRaw = [regex]::$(if(Replace($newRaw,'(){ mi)^(MANIFEST_SHA256\s* } else { \s*)([0-9a-f]+)\s*$', ('$1'+$newSha), 1) }) Write-Host "=== APPLY ASSERTS (preview) ===" $(if(Write-Host "){ ajouter } else { " }) foreach($l in $newLines){ Write-Host " - "+$l } Write-Host "Nouveau MANIFEST_SHA256 => "+$newSha if($Preview -and -not $Write){ exit 0 } if(-not $Write){ Write-Host "`n[SAFE-WRITE] Ajoute -Write pour ex?cuter l'?criture." exit 0 } # SAFE-WRITE $bak = $bp + '.bak_' + (Get-Date -Format 'yyyyMMdd_HHmmss') $tmp = $bp + '.tmp' Copy-Item -LiteralPath $bp -Destination $bak -Force WriteNoBOM $tmp $newRaw $shaTmp=(Get-FileHash -Algorithm SHA256 -LiteralPath $tmp).Hash Move-Item -LiteralPath $tmp -Destination $bp -Force $shaFinal=(Get-FileHash -Algorithm SHA256 -LiteralPath $bp).Hash Write-Host "Backup : $bak" Write-Host ("SHA tmp/final: {0} / {1}" -f $shaTmp,$shaFinal) Write-Host "STATUS : WRITE-OK"