Files
foxbin2prg/txt2vcx.ps1
Marius Mutu f36a26a9c6 Adauga write-back text->binar VFP: txt2vcx.ps1 + vfp_filemap.ps1 + test_roundtrip.ps1
Fluxul complet bin->text->edit->bin pentru .vcx/.scx (faza 1; .mnx/.frx raman
read-only): regenerare+compilare in staging unic per rulare, guard-uri
(COMUN cu -AllowComun, staleness, binar lipsa, folder foxbin2prg), fidelity
check pe octeti inainte de orice copiere, copiere .vct->.vcx cu restaurare
la esec partial, refresh cache. Harta de extensii extrasa in vfp_filemap.ps1,
dot-sourced si de vcx2txt.ps1 (comportament identic).

Invoke-FoxBin2PrgSafe: wrapper cu timeout+taskkill pentru toate invocarile
exe-ului - la text stricat Prg2Bin intoarce ErrorLevel 0 dar binarul corupt
poate ridica MessageBox modal VFP nesuprimabil la reconversie.

test_roundtrip.ps1: faze fidelitate/editare/smoke siblings/negativ+guard-uri,
41 PASS pe ROAGEST + smoke ROAIMOB/ROAACNPRO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 18:30:21 +03:00

317 lines
15 KiB
PowerShell

<#
.SYNOPSIS
Scrie inapoi in proiect binarele VFP (.vcx+.vct / .scx+.sct) regenerate din textul editat
in cache (.vc2/.sc2). Fratele "invers" al vcx2txt.ps1.
.DESCRIPTION
Flux complet text->bin, cu verificari de siguranta inainte de a atinge proiectul:
1. Mapare cache -> proiect (calea text trebuie sa fie sub -CacheRoot).
2. Guard-uri: extensie suportata (.vc2/.sc2 - faza 1), tinta nu e sub folderul
foxbin2prg, tinta sub COMUN\ necesita -AllowComun, staleness (binarul din
proiect mai nou decat textul din cache => refuz fara -Force), binar tinta
lipsa => refuz PERMANENT (nici -Force nu trece peste asta).
3. Regenerare (Prg2Bin + compilare) intr-un folder de staging unic per rulare.
4. Fidelity check IN STAGING (bin -> text, comparat pe octeti cu textul editat,
normalizat CRLF) inainte de orice copiere in proiect.
5. Copiere in proiect (.vct intai, apoi .vcx), cu backup temporar (nu in proiect)
si acces exclusiv verificat inainte.
6. Refresh cache: textul canonic de la fidelity check se scrie peste .vc2-ul din
cache, cu LastWriteTime >= mtime-ul noilor binare (evita staleness fals la
urmatoarea rulare).
7. Sumar OK/ESEC per fisier; proceseaza toate fisierele; exit 1 daca vreunul a esuat.
Encoding: fisierele .vc2/.sc2 sunt Windows-1252 (VFP CODEPAGE=1252). Orice citire/
scriere foloseste explicit acest encoding; comparatiile de fidelitate se fac PE
OCTETI (ReadAllBytes), normalizate CRLF -> nu pe stringuri cu encoding implicit.
.PARAMETER TextFile
Unul sau mai multe fisiere .vc2/.sc2 (absolute sau relative la locatia curenta),
care trebuie sa fie sub -CacheRoot.
.PARAMETER ProjectRoot
Radacina proiectului tinta (implicit D:\ROA\ROAACNPRO, ca vcx2txt.ps1).
.PARAMETER CacheRoot
Radacina cache-ului text (implicit D:\ROA\_vfp_textcache\roacnpro).
.PARAMETER AllowComun
Obligatoriu pentru a scrie tinte aflate sub COMUN\ (repo partajat intre toate
aplicatiile ROA - blast radius mare).
.PARAMETER Force
Sare peste verificarea de staleness (binar mai nou decat textul din cache).
NU acopera cazul "binar tinta lipsa" - acela e refuz permanent.
.PARAMETER NoVerify
Sare peste fidelity check (bin->text->compara). Cache-ul tot se reimprospateaza
(ruleaza bin->text pe binarul nou doar pentru refresh).
.PARAMETER DryRun
Ruleaza maparea, guard-urile si regenerarea/compilarea/fidelity check-ul IN STAGING,
dar nu copiaza nimic in proiect si nu atinge cache-ul.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1 `
-TextFile 'D:\ROA\_vfp_textcache\roagest\Clase\atentie.vc2' `
-ProjectRoot 'D:\ROA\ROAGEST' -CacheRoot 'D:\ROA\_vfp_textcache\roagest'
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][string[]]$TextFile,
[string]$ProjectRoot = 'D:\ROA\ROAACNPRO',
[string]$CacheRoot = 'D:\ROA\_vfp_textcache\roacnpro',
[switch]$AllowComun,
[switch]$Force,
[switch]$NoVerify,
[switch]$DryRun
)
$ErrorActionPreference = 'Stop'
$exe = 'D:\ROA\UTIL\foxbin2prg\FoxBin2Prg.EXE'
if (-not (Test-Path $exe)) {
throw "Nu gasesc $exe. Compileaza intai FoxBin2Prg (deschide foxbin2prg.pj2 in VFP si Build)."
}
. (Join-Path $PSScriptRoot 'vfp_filemap.ps1')
$script:Enc = [Text.Encoding]::GetEncoding(1252)
$script:FoxBin2PrgFolder = 'D:\ROA\UTIL\foxbin2prg'
function Normalize-Crlf([byte[]]$bytes) {
# normalizeaza CRLF -> LF, pe octeti, ca sa compare fidelity-ul indiferent de terminatorul de linie
$out = New-Object System.Collections.Generic.List[byte]
for ($i = 0; $i -lt $bytes.Length; $i++) {
if ($bytes[$i] -eq 0x0D) {
if ($i + 1 -lt $bytes.Length -and $bytes[$i+1] -eq 0x0A) { continue } # skip CR of CRLF
$out.Add(0x0A) | Out-Null # CR izolat -> LF
} else {
$out.Add($bytes[$i]) | Out-Null
}
}
return $out.ToArray()
}
function Test-BytesEqual([byte[]]$a, [byte[]]$b) {
$na = Normalize-Crlf $a
$nb = Normalize-Crlf $b
if ($na.Length -ne $nb.Length) { return $false }
for ($i = 0; $i -lt $na.Length; $i++) { if ($na[$i] -ne $nb[$i]) { return $false } }
return $true
}
function Test-ExclusiveAccess([string]$path) {
try {
$fs = [IO.File]::Open($path, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None)
$fs.Close()
return $true
} catch {
return $false
}
}
function Invoke-FoxBin2Prg {
# Watchdog obligatoriu: FoxBin2Prg.EXE poate afisa un MessageBox MODAL nativ al
# motorului VFP (nu al propriului error-handler) cand recompilarea unui text stricat
# esueaza - cDontShowErrors='1' NU suprima acest dialog. Fara watchdog, o rulare
# automata ar ramane agatata la infinit. Orice timeout => tratat ca ESEC (ExitCode -1).
param([string]$InputFile, [string]$Recompile = '', [int]$TimeoutSeconds = 60)
if ($Recompile) {
$r = Invoke-FoxBin2PrgSafe -ExePath $exe -ExeArgs @($InputFile,'','','','1','0','1','',$Recompile) -TimeoutSeconds $TimeoutSeconds
} else {
$r = Invoke-FoxBin2PrgSafe -ExePath $exe -ExeArgs @($InputFile,'','','','1','0','1') -TimeoutSeconds $TimeoutSeconds
}
if ($r.TimedOut) { Write-Warning "Invoke-FoxBin2Prg: timeout pe '$InputFile' - tratat ca ESEC." }
return $r.ExitCode
}
function Write-Summary($results) {
Write-Host ''
Write-Host '--- Sumar txt2vcx ---'
$fail = 0
foreach ($r in $results) {
if ($r.Ok) { Write-Host "OK $($r.Text)" }
else { Write-Host "ESEC $($r.Text) -- $($r.Reason)"; $fail++ }
}
Write-Host ''
if ($fail -gt 0) {
Write-Host "$fail/$($results.Count) fisier(e) au esuat."
Write-Host 'Ruleaza git diff --stat in proiect pentru a revizui ce s-a schimbat.'
} else {
Write-Host "Toate cele $($results.Count) fisier(e) au fost scrise cu succes."
Write-Host 'Ruleaza git diff --stat in proiect pentru a revizui ce s-a schimbat.'
}
return $fail
}
$results = @()
$runStamp = "{0}-{1}" -f $PID, (Get-Date -Format 'yyyyMMddHHmmssfff')
$stagingRoot = Join-Path $env:TEMP ("txt2bin_" + (Split-Path $ProjectRoot -Leaf) + "\$runStamp")
foreach ($tf in $TextFile) {
$reason = $null
$ok = $false
$stagingDir = $null
try {
# --- resolve full path fara sa ceara existenta anterioara garantata pe alte disk-uri ---
$fullTextPath = if ([IO.Path]::IsPathRooted($tf)) { $tf } else { Join-Path (Get-Location) $tf }
if (-not (Test-Path -LiteralPath $fullTextPath -PathType Leaf)) {
throw "Fisierul text nu exista: $fullTextPath"
}
$fullTextPath = (Get-Item -LiteralPath $fullTextPath).FullName
# --- 1. Mapare cache -> proiect ---
$cacheRootFull = (Get-Item -LiteralPath $CacheRoot).FullName
if (-not $fullTextPath.ToLower().StartsWith($cacheRootFull.ToLower() + '\')) {
throw "Calea text trebuie sa fie sub CacheRoot ($cacheRootFull): $fullTextPath"
}
$relText = $fullTextPath.Substring($cacheRootFull.Length).TrimStart('\')
$textExt = [IO.Path]::GetExtension($fullTextPath).ToLower()
# --- 2a. Guard extensie (doar .vc2/.sc2 in faza 1) ---
$pair = Get-VfpBinExtensionForTextExtension -TextExtension $textExt
if (-not $pair -or ($pair[0] -ne '.vcx' -and $pair[0] -ne '.scx')) {
throw "Extensie nesuportata in faza 2 (doar .vc2/.sc2): $textExt"
}
$binExt = $pair[0]
$memoExt = $pair[1]
$relBinNoExt = [IO.Path]::ChangeExtension($relText, $null).TrimEnd('.')
$relBin = [IO.Path]::ChangeExtension($relText, $binExt)
$binDir = Split-Path (Join-Path $ProjectRoot $relBin) -Parent
$binBaseName = [IO.Path]::GetFileNameWithoutExtension($relBin)
# --- 2b. Guard: refuz necondiționat sub folderul foxbin2prg ---
$projectRootFull = if (Test-Path -LiteralPath $ProjectRoot) { (Get-Item -LiteralPath $ProjectRoot).FullName } else { $ProjectRoot }
$prospectiveBinFull = Join-Path $projectRootFull $relBin
if ($prospectiveBinFull.ToLower().StartsWith($script:FoxBin2PrgFolder.ToLower() + '\')) {
throw "Refuz: tinta sub folderul foxbin2prg ($script:FoxBin2PrgFolder) - nu e permis sa scrii aici."
}
# --- 2c. Guard COMUN ---
if ($relBin -match '(?i)^COMUN\\' -and -not $AllowComun) {
throw "Tinta e sub COMUN\ (repo partajat) - ruleaza cu -AllowComun daca esti sigur."
}
# --- Rezolva numele real (casing) al binarului si memo-ului din director ---
if (-not (Test-Path -LiteralPath $binDir -PathType Container)) {
throw "Director tinta lipsa: $binDir"
}
$binEntry = Get-ChildItem -LiteralPath $binDir -Filter ($binBaseName + $binExt) -ErrorAction SilentlyContinue | Select-Object -First 1
$memoEntry = Get-ChildItem -LiteralPath $binDir -Filter ($binBaseName + $memoExt) -ErrorAction SilentlyContinue | Select-Object -First 1
# --- 2d. Guard: binar tinta lipsa -> refuz PERMANENT, -Force nu trece peste asta ---
if (-not $binEntry -or -not $memoEntry) {
throw "Binarul/memo-ul tinta nu exista in proiect ($binBaseName$binExt / $binBaseName$memoExt) - fisier nou, trebuie creat in IDE (SET CLASSLIB/.pjx) inainte de write-back."
}
$binFull = $binEntry.FullName
$memoFull = $memoEntry.FullName
# --- 2e. Guard staleness ---
$binMtime = $binEntry.LastWriteTime
$memoMtime = $memoEntry.LastWriteTime
$newestBinMtime = if ($binMtime -gt $memoMtime) { $binMtime } else { $memoMtime }
$textMtime = (Get-Item -LiteralPath $fullTextPath).LastWriteTime
if ($newestBinMtime -gt $textMtime -and -not $Force) {
throw "Staleness: binarul/memo-ul din proiect ($newestBinMtime) e mai nou decat textul din cache ($textMtime) - probabil modificat in IDE. Ruleaza vcx2txt.ps1 -Force intai, sau -Force aici daca esti sigur ca vrei sa suprascrii."
}
# --- 3. Regenerare in staging ---
$stagingDir = Join-Path $stagingRoot $relBinNoExt
New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
$stagingText = Join-Path $stagingDir ($binBaseName + $textExt)
Copy-Item -LiteralPath $fullTextPath -Destination $stagingText -Force
$rc = Invoke-FoxBin2Prg -InputFile $stagingText -Recompile $ProjectRoot
$stagingBin = Join-Path $stagingDir ($binBaseName + $binExt)
$stagingMemo = Join-Path $stagingDir ($binBaseName + $memoExt)
if ($rc -ne 0 -or -not (Test-Path -LiteralPath $stagingBin) -or -not (Test-Path -LiteralPath $stagingMemo)) {
throw "Prg2Bin/compilare esuata (ErrorLevel=$rc) sau lipsesc $binExt/$memoExt in staging."
}
# --- 4. Fidelity check IN STAGING ---
$canonicalText = $null
if (-not $NoVerify) {
$verifyDir = Join-Path $stagingDir 'verify'
New-Item -ItemType Directory -Force -Path $verifyDir | Out-Null
$verifyBin = Join-Path $verifyDir ($binBaseName + $binExt)
$verifyMemo = Join-Path $verifyDir ($binBaseName + $memoExt)
Copy-Item -LiteralPath $stagingBin -Destination $verifyBin -Force
Copy-Item -LiteralPath $stagingMemo -Destination $verifyMemo -Force
$rc2 = Invoke-FoxBin2Prg -InputFile $verifyBin
$verifyText = Join-Path $verifyDir ($binBaseName + $textExt)
if ($rc2 -ne 0 -or -not (Test-Path -LiteralPath $verifyText)) {
throw "Fidelity check: Bin2Prg pe binarul regenerat a esuat (ErrorLevel=$rc2) - probabil text stricat (ex. lipsa ENDPROC)."
}
$editedBytes = [IO.File]::ReadAllBytes($stagingText)
$verifyBytes = [IO.File]::ReadAllBytes($verifyText)
if (-not (Test-BytesEqual $editedBytes $verifyBytes)) {
throw "Fidelity check: textul regenerat difera de textul editat (posibil proprietate nealfabetizata sau linie reformatata)."
}
$canonicalText = $verifyText
}
if ($DryRun) {
$ok = $true
$reason = '(dry-run: regenerare+fidelity OK in staging, nimic copiat in proiect)'
} else {
# --- 5. Copiere in proiect ---
if (-not (Test-ExclusiveAccess $binFull) -or -not (Test-ExclusiveAccess $memoFull)) {
throw "Nu pot obtine acces exclusiv la $binBaseName$binExt/$memoExt - inchide IDE-ul VFP si reincearca."
}
$backupDir = Join-Path $stagingDir 'backup_proiect'
New-Item -ItemType Directory -Force -Path $backupDir | Out-Null
$backupBin = Join-Path $backupDir ($binBaseName + $binExt)
$backupMemo = Join-Path $backupDir ($binBaseName + $memoExt)
Copy-Item -LiteralPath $binFull -Destination $backupBin -Force
Copy-Item -LiteralPath $memoFull -Destination $backupMemo -Force
Copy-Item -LiteralPath $stagingMemo -Destination $memoFull -Force
try {
Copy-Item -LiteralPath $stagingBin -Destination $binFull -Force
} catch {
# restaureaza memo-ul vechi din backup, ca sa nu ramana o pereche rupta
Copy-Item -LiteralPath $backupMemo -Destination $memoFull -Force
throw "Copierea $binExt a esuat, memo-ul a fost restaurat din backup: $_"
}
# --- 6. Refresh cache ---
if (-not $NoVerify -and $canonicalText) {
Copy-Item -LiteralPath $canonicalText -Destination $fullTextPath -Force
} else {
# fara fidelity text disponibil (NoVerify) - ruleaza bin->text pe binarul nou doar pentru refresh
$refreshDir = Join-Path $stagingDir 'refresh'
New-Item -ItemType Directory -Force -Path $refreshDir | Out-Null
$refreshBin = Join-Path $refreshDir ($binBaseName + $binExt)
$refreshMemo = Join-Path $refreshDir ($binBaseName + $memoExt)
Copy-Item -LiteralPath $binFull -Destination $refreshBin -Force
Copy-Item -LiteralPath $memoFull -Destination $refreshMemo -Force
Invoke-FoxBin2Prg -InputFile $refreshBin | Out-Null
$refreshText = Join-Path $refreshDir ($binBaseName + $textExt)
if (Test-Path -LiteralPath $refreshText) {
Copy-Item -LiteralPath $refreshText -Destination $fullTextPath -Force
}
}
$newBinMtime = (Get-Item -LiteralPath $binFull).LastWriteTime
$newMemoMtime = (Get-Item -LiteralPath $memoFull).LastWriteTime
$newestMtime = if ($newBinMtime -gt $newMemoMtime) { $newBinMtime } else { $newMemoMtime }
(Get-Item -LiteralPath $fullTextPath).LastWriteTime = $newestMtime.AddSeconds(1)
$ok = $true
}
# curatare staging la succes
if ($ok -and -not $DryRun) {
Remove-Item -Recurse -Force -LiteralPath $stagingDir -ErrorAction SilentlyContinue
} elseif ($ok -and $DryRun) {
Remove-Item -Recurse -Force -LiteralPath $stagingDir -ErrorAction SilentlyContinue
}
} catch {
$ok = $false
$reason = $_.Exception.Message
if ($stagingDir) { Write-Warning "Staging pastrat pentru diagnostic: $stagingDir" }
}
$results += [PSCustomObject]@{ Text = $tf; Ok = $ok; Reason = $reason }
}
$failCount = Write-Summary $results
exit ([int]($failCount -gt 0))