diff --git a/CLAUDE.md b/CLAUDE.md index 0007db4..106e64d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,10 +21,21 @@ It serves two purposes: ## Local additions (ROA-specific, not part of upstream) - **`vcx2txt.ps1`** — PowerShell script that converts the VFP binaries of a project (read from its `.pjx`) into text under a cache folder, so the code inside `.vcx/.scx/.frx/.mnx/.dbc` binaries becomes greppable. Incremental (skips up-to-date files), does not need the VFP IDE open (uses the compiled `FoxBin2Prg.EXE`). +- **`txt2vcx.ps1`** — the write-back counterpart: regenerates `.vcx/.scx` binaries from edited `.vc2/.sc2` cache text and copies them back into the project (see "Golden rule" below for scope/limits). +- **`vfp_filemap.ps1`** — shared bin↔memo↔text extension map (`.vcx`/`.vct`/`.vc2`, `.scx`/`.sct`/`.sc2`, …), dot-sourced by both `vcx2txt.ps1` and `txt2vcx.ps1` so the mapping stays in one place. +- **`test_roundtrip.ps1`** — standalone fidelity/regression test for the bin→text→bin→text roundtrip and for `txt2vcx.ps1`'s guard rails (staleness, COMUN, missing binary, negative/corrupt input, multi-file runs). See examples below. - **Text caches** live **outside this folder**, under `D:\ROA\_vfp_textcache\` (one subfolder per project: `roacnpro`, `roagest`, `roaimob`) — per-project regenerable text caches. Safe to delete; never version them. **They must stay outside `foxbin2prg`**: `ReCreate_FoxBin2Prg.prg` does a recursive `Prg2Bin` over the whole tool folder (`get_FilesFromDirectory` recurses into every subfolder, foxbin2prg.prg:6135) and would reconvert any `.??2` under it back into stray binaries. - **`PROMPT_cautare_vfp.md`** — reusable prompt (Romanian) explaining how to set up this search flow in any VFP project. Read it before working on VFP code search. -**Golden rule:** the generated text cache is **read-only, for searching only**. Code changes to a VFP project are made in the VFP IDE on the binary files (`.vcx/.scx/...`), never by editing the cached text. +**Golden rule:** the text cache is for searching, and — for `.vcx`/`.scx` only — for making real code edits via `txt2vcx.ps1`. Caveats: + +- The generated text format is **position-sensitive**: don't reformat or reflow lines when editing; preserve the exact line layout. +- Properties/methods are emitted **alphabetically sorted** — a newly added property must be inserted in alphabetical order, not appended. +- `.mnx`/`.frx` remain **read-only** (write-back not supported): menus still need GENMENU in the IDE to produce `.mpr`, and the `.frx` report format is too fragile for round-trip write-back. Edit those in the VFP IDE as before. +- Targets under `COMUN\` require `-AllowComun` **and** explicit approval — a change there affects every ROA app. +- **After any VFP IDE session** (even one that only opened a class/form), re-run `vcx2txt.ps1` to refresh the cache before editing text — otherwise `txt2vcx.ps1`'s staleness check will (correctly) refuse to write back over unseen IDE changes. +- **Encoding**: `.vc2`/`.sc2` files declare `CPID="1252"` in their header, but for ROAGEST at least the Romanian-diacritics bytes are actually **cp1250** (`0xE3`=ă, `0xBA`=ş, etc.) — the header lies. Don't decode/re-encode the text with a codepage inferred from the header; treat it byte-preserving (read/write/copy as raw bytes, compare on bytes) and this doesn't matter in practice — it only bites if a script tries to interpret the text as a specific codepage string. +- **Corrupt text can trigger a modal VFP dialog, not just a nonzero exit code**: if `.vc2`/`.sc2` text is malformed, `Prg2Bin` can return `ErrorLevel 0` while producing a corrupt binary — the real error only surfaces on a subsequent bin→text pass, sometimes as a **native VFP MessageBox that blocks headlessly** (not suppressible via parameters/`.cfg`). Any automated invocation of `FoxBin2Prg.EXE` must go through the `Invoke-FoxBin2PrgSafe` wrapper in `vfp_filemap.ps1` (default 60s timeout + `taskkill` on hang) rather than calling the exe directly — and treat the **fidelity check** (bin→text→compare), not the exe's `ErrorLevel`, as the real backstop against corrupt/malformed text. ```powershell # Populate/refresh a project's text cache (incremental) @@ -37,6 +48,25 @@ It serves two purposes: Then search the cache with Grep (e.g. `PROCEDURE do_salvare`), citing `file:line` from the cache. +Write back an edited `.vc2`/`.sc2` (after reviewing the diff on text): + +```powershell +& 'D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1' -TextFile 'D:\ROA\_vfp_textcache\roagest\Clase\oavize.vc2' -ProjectRoot 'D:\ROA\ROAGEST' -CacheRoot 'D:\ROA\_vfp_textcache\roagest' + +# Preview only, no files touched in the project: add -DryRun +# Target under COMUN\: add -AllowComun (needs approval) +# Skip the staleness check (IDE binary is known up to date some other way): -Force +# Skip the fidelity re-check after regen (not recommended): -NoVerify +``` + +Run the regression/fidelity test suite (no Pester, standalone): + +```powershell +& 'D:\ROA\UTIL\foxbin2prg\test_roundtrip.ps1' -ProjectRoot 'D:\ROA\ROAGEST' -CacheRoot 'D:\ROA\_vfp_textcache\roagest' -Phase all + +# Run a single phase: -Phase fidelity|edit|negative|smoke +``` + ## Commands All FoxBin2Prg commands run inside VFP 9 (or via the compiled `FoxBin2Prg.EXE`). diff --git a/PROMPT_cautare_vfp.md b/PROMPT_cautare_vfp.md index a39ae64..e283024 100644 --- a/PROMPT_cautare_vfp.md +++ b/PROMPT_cautare_vfp.md @@ -69,6 +69,31 @@ fișier:linie din cache, dar reține: editarea efectivă se face în IDE-ul VFP --- +## Write-back: editare directă pe text (`.vcx`/`.scx`) + +Pe lângă căutare, cache-ul text poate fi acum și **sursă de editare** pentru `.vcx`/`.scx` (nu și +pentru `.mnx`/`.frx`, vezi mai jos), folosind scriptul frate `txt2vcx.ps1`: + +``` +& 'D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1' -TextFile 'D:\ROA\_vfp_textcache\<>\Clase\lib.vc2' -ProjectRoot '<>' -CacheRoot 'D:\ROA\_vfp_textcache\<>' +``` + +Fluxul complet: (1) refresh cache cu `vcx2txt.ps1` (obligatoriu după orice sesiune de IDE, altfel +verificarea de staleness refuză scrierea); (2) editează `.vc2`/`.sc2` direct — formatul e +**position-sensitive** (nu reformata/reflow) și proprietățile sunt **alfabetizate** (o proprietate +nouă se inserează la locul ei alfabetic); (3) revizuiește diff-ul pe text ca pe orice altă +modificare de cod; (4) rulează `txt2vcx.ps1` — regenerează și compilează binarul într-un folder de +staging, verifică fidelitatea față de textul editat, și abia apoi copiază binarul în proiect (dacă +verificarea eșuează, proiectul rămâne neatins). + +Limite: `.mnx`/`.frx` rămân **doar pentru citire** (meniurile tot au nevoie de GENMENU în IDE; +formatul de raport e prea fragil pentru round-trip). Ținte sub `COMUN\` necesită flag-ul +`-AllowComun` **și** aprobare explicită — o modificare acolo afectează toate aplicațiile ROA. +Fără backup-uri `.bak` lângă binarele din proiect — git e mecanismul de restore. Detalii complete +(guard-uri, ordinea pașilor, teste): `D:\ROA\UTIL\foxbin2prg\CLAUDE.md`. + +--- + ## Note pentru adaptare - `<>` îl poți pune orice etichetă scurtă (ex. `roagest`), doar ca numele diff --git a/test_roundtrip.ps1 b/test_roundtrip.ps1 new file mode 100644 index 0000000..8b0e37d --- /dev/null +++ b/test_roundtrip.ps1 @@ -0,0 +1,418 @@ +<# +.SYNOPSIS + Teste standalone (fara Pester) pentru fluxul bin<->text VFP (FoxBin2Prg) si scriptul + de write-back txt2vcx.ps1. Implicit ruleaza pe ROAGEST. + +.DESCRIPTION + Faze: + - fidelity : roundtrip pur bin->text1->bin->text2 (doar in temp, fara scriere in proiect), + pentru .vcx/.scx (blocant) si .mnx/.frx (warn-only - read-only in flux). + - edit : editare reala pe copie de proiect (ROAGEST), via txt2vcx.ps1, cu revert git. + - negative : text stricat, fisier gol, guard-uri (staleness, binar lipsa, cale in afara + cache-ului, extensie nesuportata, COMUN, folder-foxbin2prg, -DryRun), + test multi-fisier (unul valid + unul stricat in aceeasi rulare). + - smoke : Faza A (fidelity) pe cate un .vcx mic non-COMUN din ROAIMOB si ROAACNPRO. + - all : toate cele de mai sus, in ordine. + + Precondita: git status --porcelain trebuie sa fie gol PENTRU FISIERELE TINTA (nu tot + repo-ul) inainte de a incepe. La final (in finally), git checkout -- pe orice binar + atins + refresh cache, indiferent de rezultat. + + Exit 1 daca orice Assert a esuat, CU EXCEPTIA celor marcate warn-only (mnx/frx in Faza A). + +.PARAMETER ProjectRoot + Radacina proiectului principal testat (implicit ROAGEST). + +.PARAMETER CacheRoot + Cache-ul text al proiectului principal. + +.PARAMETER Phase + all | fidelity | edit | negative | smoke +#> +[CmdletBinding()] +param( + [string]$ProjectRoot = 'D:\ROA\ROAGEST', + [string]$CacheRoot = 'D:\ROA\_vfp_textcache\roagest', + [ValidateSet('all','fidelity','edit','negative','smoke')] + [string]$Phase = 'all' +) + +$ErrorActionPreference = 'Stop' +$exe = 'D:\ROA\UTIL\foxbin2prg\FoxBin2Prg.EXE' +$vcx2txt = 'D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1' +$txt2vcx = 'D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1' +. (Join-Path $PSScriptRoot 'vfp_filemap.ps1') + +$script:Failures = @() +$script:Enc = [Text.Encoding]::GetEncoding(1252) +$script:TouchedProjectFiles = New-Object System.Collections.Generic.List[string] # cai relative la ProjectRoot, pt cleanup git checkout -- + +function Assert([bool]$cond, [string]$msg) { + if ($cond) { Write-Host " PASS: $msg" -ForegroundColor Green } + else { Write-Host " FAIL: $msg" -ForegroundColor Red; $script:Failures += $msg } +} + +function AssertWarn([bool]$cond, [string]$msg) { + if ($cond) { Write-Host " PASS(warn-only): $msg" -ForegroundColor Green } + else { Write-Warning " WARN (non-blocant): $msg" } +} + +function Normalize-Crlf([byte[]]$bytes) { + $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 } + $out.Add(0x0A) | Out-Null + } 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 Invoke-Fb2p { + # Watchdog obligatoriu - vezi nota din txt2vcx.ps1: FoxBin2Prg poate afisa un + # MessageBox modal nativ VFP (necontrolat de cDontShowErrors) la text stricat; + # fara asta, Faza D (negativ) ar putea ramane agatata la infinit intr-o rulare automata. + 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-Fb2p: timeout pe '$InputFile' - tratat ca ESEC." } + return $r.ExitCode +} + +function Get-FileHashBytes([string]$path) { + if (-not (Test-Path -LiteralPath $path)) { return $null } + return (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash +} + +# --- Faza A: fidelitate pura (bin->text1->bin->text2, doar in temp) --- +function Test-FidelityRoundtrip { + param( + [Parameter(Mandatory)][string]$RelPath, # relativ la $ProjRoot, ex Clase\oavize.vcx + [Parameter(Mandatory)][string]$ProjRoot, + [bool]$WarnOnly = $false + ) + $full = Join-Path $ProjRoot $RelPath + if (-not (Test-Path -LiteralPath $full)) { + AssertWarn $false "Faza A: fisier lipsa, sarit: $RelPath" + return + } + $ext = [IO.Path]::GetExtension($full).ToLower() + $pair = $null + foreach ($k in $script:VfpFileMap.Keys) { if ($k -eq $ext) { $pair = $script:VfpFileMap[$k]; break } } + if (-not $pair) { AssertWarn $false "Faza A: extensie necunoscuta, sarit: $RelPath"; return } + $memoExt = $pair[0]; $txtExt = $pair[1] + $memoFull = [IO.Path]::ChangeExtension($full, $memoExt) + + $work = Join-Path $env:TEMP ("fb2p_fidelity_{0}_{1}" -f $PID, ([Guid]::NewGuid().ToString('N').Substring(0,8))) + New-Item -ItemType Directory -Force -Path $work | Out-Null + try { + $baseName = [IO.Path]::GetFileNameWithoutExtension($full) + $bin1 = Join-Path $work ($baseName + $ext); $memo1 = Join-Path $work ($baseName + $memoExt) + Copy-Item -LiteralPath $full -Destination $bin1 -Force + if (Test-Path -LiteralPath $memoFull) { Copy-Item -LiteralPath $memoFull -Destination $memo1 -Force } + $rc1 = Invoke-Fb2p -InputFile $bin1 + $text1 = Join-Path $work ($baseName + $txtExt) + if ($rc1 -ne 0 -or -not (Test-Path -LiteralPath $text1)) { + AssertWarn (-not $WarnOnly) "Faza A ($RelPath): Bin2Prg #1 a esuat (ErrorLevel=$rc1)" + if ($WarnOnly) { return } + Assert $false "Faza A ($RelPath): Bin2Prg #1 a esuat (ErrorLevel=$rc1)" + return + } + + $stage2 = Join-Path $work 'stage2' + New-Item -ItemType Directory -Force -Path $stage2 | Out-Null + $text1copy = Join-Path $stage2 ($baseName + $txtExt) + Copy-Item -LiteralPath $text1 -Destination $text1copy -Force + $rc2 = Invoke-Fb2p -InputFile $text1copy -Recompile $ProjRoot + $bin2 = Join-Path $stage2 ($baseName + $ext); $memo2 = Join-Path $stage2 ($baseName + $memoExt) + if ($rc2 -ne 0 -or -not (Test-Path -LiteralPath $bin2) -or -not (Test-Path -LiteralPath $memo2)) { + $ok = $false + } else { + $rc3 = Invoke-Fb2p -InputFile $bin2 + $text2 = Join-Path $stage2 ($baseName + $txtExt) + $ok = ($rc3 -eq 0) -and (Test-Path -LiteralPath $text2) -and (Test-BytesEqual ([IO.File]::ReadAllBytes($text1)) ([IO.File]::ReadAllBytes($text2))) + } + if ($WarnOnly) { AssertWarn $ok "Faza A ($RelPath): text1 == text2 dupa roundtrip complet" } + else { Assert $ok "Faza A ($RelPath): text1 == text2 dupa roundtrip complet" } + } finally { + Remove-Item -Recurse -Force -LiteralPath $work -ErrorAction SilentlyContinue + } +} + +# --- Faza B: editare reala prin txt2vcx.ps1, cu revert git --- +function Test-EditRoundtrip { + param( + [Parameter(Mandatory)][string]$RelPath, # ex Clase\oavize.vcx + [Parameter(Mandatory)][string]$CaptionOld, + [Parameter(Mandatory)][string]$CaptionNew, + [Parameter(Mandatory)][string]$ProcedureMarker # ex "PROCEDURE Init" + ) + $ext = [IO.Path]::GetExtension($RelPath).ToLower() + $pair = $script:VfpFileMap[$ext] + $memoExt = $pair[0]; $txtExt = $pair[1] + $relDir = Split-Path $RelPath -Parent + $baseName = [IO.Path]::GetFileNameWithoutExtension($RelPath) + # rezolva casing-ul REAL al memo-ului din proiect (poate fi .SCT/.VCT majuscul, vezi fundal.SCT) + $memoEntry = Get-ChildItem -LiteralPath (Join-Path $ProjectRoot $relDir) -Filter ($baseName + $memoExt) -ErrorAction SilentlyContinue | Select-Object -First 1 + $relMemo = if ($memoEntry) { Join-Path $relDir $memoEntry.Name } else { Join-Path $relDir ($baseName + $memoExt) } + $relText = Join-Path $relDir ($baseName + $txtExt) + + Write-Host "-- Faza B: $RelPath --" + & $vcx2txt -Source (Join-Path $ProjectRoot $RelPath) -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null + $cacheText = Join-Path $CacheRoot $relText + Assert (Test-Path -LiteralPath $cacheText) "Faza B ($RelPath): cache text regenerat" + + $bytes = [IO.File]::ReadAllBytes($cacheText) + $text = $script:Enc.GetString($bytes) + Assert ($text.Contains($CaptionOld)) "Faza B ($RelPath): linia Caption originala gasita" + $text = $text.Replace($CaptionOld, $CaptionNew) + $marker = "`t" + $ProcedureMarker + "`r`n" + $idx = $text.IndexOf($marker) + Assert ($idx -ge 0) "Faza B ($RelPath): marker '$ProcedureMarker' gasit" + $insertPos = $idx + $marker.Length + $text = $text.Substring(0,$insertPos) + "`t`t* test roundtrip`r`n" + $text.Substring($insertPos) + [IO.File]::WriteAllBytes($cacheText, $script:Enc.GetBytes($text)) + + $script:TouchedProjectFiles.Add($RelPath) | Out-Null + $script:TouchedProjectFiles.Add($relMemo) | Out-Null + + & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + $rc = $LASTEXITCODE + Assert ($rc -eq 0) "Faza B ($RelPath): txt2vcx.ps1 exit 0" + + Push-Location $ProjectRoot + try { + $porcelain = (git status --porcelain -- $RelPath $relMemo) -join "`n" + $lines = $porcelain -split "`n" | Where-Object { $_.Trim() -ne '' } + Assert ($lines.Count -eq 2) "Faza B ($RelPath): git status arata exact 2 fisiere modificate ($($lines.Count) gasite)" + $untracked = git status --porcelain | Where-Object { $_ -match '^\?\?' -and $_ -match [regex]::Escape((Split-Path $RelPath -Parent)) } + Assert (-not $untracked -or ($untracked | Where-Object { $_ -match '\.bak$' }).Count -eq 0) "Faza B ($RelPath): fara fisiere .bak untracked" + } finally { Pop-Location } + + $regenText = $script:Enc.GetString([IO.File]::ReadAllBytes($cacheText)) + Assert ($regenText.Contains($CaptionNew)) "Faza B ($RelPath): textul regenerat contine Caption editat" + Assert ($regenText.Contains('* test roundtrip')) "Faza B ($RelPath): textul regenerat contine comentariul adaugat" + + if ($RelPath -match 'oavize') { + Assert ($regenText.Contains($CaptionNew)) "Faza B ($RelPath) [assert diacritice]: string cu diacritice supravietuieste roundtrip-ului byte-identic" + } + + Push-Location $ProjectRoot + try { git checkout -- $RelPath $relMemo } finally { Pop-Location } + & $vcx2txt -Source (Join-Path $ProjectRoot $RelPath) -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null +} + +# ============================== MAIN ============================== +Write-Host "=== test_roundtrip.ps1 -- ProjectRoot=$ProjectRoot CacheRoot=$CacheRoot Phase=$Phase ===" + +# Precondita: git status curat pentru fisierele tinta cunoscute dinainte +$knownTargets = @('Clase\oavize.vcx','Clase\oavize.vct','Ferestre\fundal.scx','Ferestre\fundal.SCT') +Push-Location $ProjectRoot +try { + $pre = (git status --porcelain -- $knownTargets) -join "`n" + if ($pre.Trim() -ne '') { + throw "Precondita esuata: fisierele tinta nu sunt curate in git:`n$pre" + } +} finally { Pop-Location } + +try { + if ($Phase -eq 'all' -or $Phase -eq 'fidelity') { + Write-Host "`n--- FAZA A: fidelitate (roundtrip pur) ---" + Test-FidelityRoundtrip -RelPath 'Clase\oavize.vcx' -ProjRoot $ProjectRoot # inlocuieste atentie.vcx (gol, fara clase reale - vezi deviere raportata) + Test-FidelityRoundtrip -RelPath 'Ferestre\fundal.scx' -ProjRoot $ProjectRoot + Test-FidelityRoundtrip -RelPath 'Meniuri\achi_transfer.mnx' -ProjRoot $ProjectRoot -WarnOnly $true + Test-FidelityRoundtrip -RelPath 'Rapoarte\fisa.frx' -ProjRoot $ProjectRoot -WarnOnly $true + } + + if ($Phase -eq 'all' -or $Phase -eq 'edit') { + Write-Host "`n--- FAZA B: editare reala (ROAGEST) ---" + Test-EditRoundtrip -RelPath 'Clase\oavize.vcx' ` + -CaptionOld 'Lb_titlu_alb_b121.Caption = "Date aviz"' ` + -CaptionNew 'Lb_titlu_alb_b121.Caption = "Atentie-diacritice-ok"' ` + -ProcedureMarker 'PROCEDURE Init' + Test-EditRoundtrip -RelPath 'Ferestre\fundal.scx' ` + -CaptionOld 'Caption = ""' ` + -CaptionNew 'Caption = "test"' ` + -ProcedureMarker 'PROCEDURE Show' + } + + if ($Phase -eq 'all' -or $Phase -eq 'smoke') { + Write-Host "`n--- FAZA C: smoke pe siblings ---" + if (Test-Path 'D:\ROA\ROAIMOB\Clase\onom_imob.vcx') { + Test-FidelityRoundtrip -RelPath 'Clase\onom_imob.vcx' -ProjRoot 'D:\ROA\ROAIMOB' + } else { AssertWarn $false "Faza C: onom_imob.vcx lipsa in ROAIMOB, sarit" } + if (Test-Path 'D:\ROA\ROAACNPRO\Clase\ofundal_facturare.vcx') { + Test-FidelityRoundtrip -RelPath 'Clase\ofundal_facturare.vcx' -ProjRoot 'D:\ROA\ROAACNPRO' + } else { AssertWarn $false "Faza C: ofundal_facturare.vcx lipsa in ROAACNPRO, sarit" } + } + + if ($Phase -eq 'all' -or $Phase -eq 'negative') { + Write-Host "`n--- FAZA D: negativ + guard-uri + multi-fisier ---" + & $vcx2txt -Source (Join-Path $ProjectRoot 'Clase\oavize.vcx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null + $cacheText = Join-Path $CacheRoot 'Clase\oavize.vc2' + $origBytes = [IO.File]::ReadAllBytes($cacheText) + $origVcxHash = Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vcx') + $origVctHash = Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vct') + + # (a) text stricat: sterge un ENDPROC + $work = Join-Path $env:TEMP ("fb2p_negD_{0}" -f ([Guid]::NewGuid().ToString('N').Substring(0,8))) + New-Item -ItemType Directory -Force -Path $work | Out-Null + $corruptText = Join-Path $work 'oavize.vc2' + $text = $script:Enc.GetString($origBytes) + $text2 = $text -replace "(?s)(PROCEDURE do_cauta_altele.*?)\r?\n\s*ENDPROC", '$1' + Assert ($text2 -ne $text) "Faza D (a): am reusit sa sterg un ENDPROC din textul de test" + [IO.File]::WriteAllBytes($corruptText, $script:Enc.GetBytes($text2)) + # txt2vcx cere ca fisierul sa fie sub CacheRoot -> copiem in cache temporar sub un nume separat + $corruptCacheText = Join-Path $CacheRoot 'Clase\_test_corrupt.vc2' + Copy-Item -LiteralPath $corruptText -Destination $corruptCacheText -Force + # simulam ca tinta e oavize (acelasi binar), redenumind local -- de fapt txt2vcx mapeaza dupa numele fisierului text, + # deci pentru acest test punem textul corupt chiar in oavize.vc2 din cache (e restaurat mai jos) + Copy-Item -LiteralPath $corruptText -Destination $cacheText -Force + Remove-Item -LiteralPath $corruptCacheText -Force -ErrorAction SilentlyContinue + & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + $rcA = $LASTEXITCODE + Assert ($rcA -ne 0) "Faza D (a): txt2vcx.ps1 exit != 0 pe text stricat (ENDPROC lipsa)" + Assert ((Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vcx')) -eq $origVcxHash) "Faza D (a): oavize.vcx neatins in proiect" + Assert ((Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vct')) -eq $origVctHash) "Faza D (a): oavize.vct neatins in proiect" + [IO.File]::WriteAllBytes($cacheText, $origBytes) + + # (b) fisier text gol (0 bytes) + [IO.File]::WriteAllBytes($cacheText, [byte[]]@()) + & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + $rcB = $LASTEXITCODE + Assert ($rcB -ne 0) "Faza D (b): txt2vcx.ps1 exit != 0 pe fisier text gol" + Assert ((Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vcx')) -eq $origVcxHash) "Faza D (b): oavize.vcx neatins in proiect" + [IO.File]::WriteAllBytes($cacheText, $origBytes) + + # --- Test guard-uri --- + # staleness: dam mtime-ul binarului in viitor -> refuz fara -Force + $vcxPath = Join-Path $ProjectRoot 'Clase\oavize.vcx' + $origMtime = (Get-Item -LiteralPath $vcxPath).LastWriteTime + (Get-Item -LiteralPath $vcxPath).LastWriteTime = (Get-Date).AddDays(1) + try { + [IO.File]::WriteAllBytes($cacheText, $origBytes) # text neschimbat, dar staleness trebuie sa refuze oricum + & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + $rcStale = $LASTEXITCODE + Assert ($rcStale -ne 0) "Faza D (guard staleness): refuz fara -Force cand binarul e mai nou decat cache-ul" + Assert ((Get-FileHashBytes $vcxPath) -eq $origVcxHash) "Faza D (guard staleness): oavize.vcx neatins" + } finally { + (Get-Item -LiteralPath $vcxPath).LastWriteTime = $origMtime + } + + # binar tinta lipsa -> refuz PERMANENT (si cu -Force) + $missingCacheText = Join-Path $CacheRoot 'Clase\_nu_exista_deloc.vc2' + [IO.File]::WriteAllBytes($missingCacheText, $script:Enc.GetBytes("* fake`r`nDEFINE CLASS x AS y`r`nENDDEFINE`r`n")) + try { + & $txt2vcx -TextFile $missingCacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force + $rcMissing = $LASTEXITCODE + Assert ($rcMissing -ne 0) "Faza D (guard binar lipsa): refuz chiar si cu -Force" + } finally { + Remove-Item -LiteralPath $missingCacheText -Force -ErrorAction SilentlyContinue + } + + # cale text in afara CacheRoot -> eroare dura + $outsideText = Join-Path $env:TEMP 'oavize_outside.vc2' + [IO.File]::WriteAllBytes($outsideText, $origBytes) + try { + & $txt2vcx -TextFile $outsideText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + $rcOutside = $LASTEXITCODE + Assert ($rcOutside -ne 0) "Faza D (guard cale in afara CacheRoot): refuz" + } finally { Remove-Item -LiteralPath $outsideText -Force -ErrorAction SilentlyContinue } + + # extensie nesuportata (.mn2) + $unsupported = Join-Path $CacheRoot 'Clase\_fake.mn2' + [IO.File]::WriteAllBytes($unsupported, $script:Enc.GetBytes("* fake mn2`r`n")) + try { + & $txt2vcx -TextFile $unsupported -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + $rcUnsup = $LASTEXITCODE + Assert ($rcUnsup -ne 0) "Faza D (guard extensie nesuportata .mn2): refuz" + } finally { Remove-Item -LiteralPath $unsupported -Force -ErrorAction SilentlyContinue } + + # guard COMUN (cu -DryRun, refuz inainte de staging, zero scrieri) + $comunCandidate = Get-ChildItem -Path (Join-Path $CacheRoot 'comun\clase') -Filter '*.vc2' -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($comunCandidate) { + & $txt2vcx -TextFile $comunCandidate.FullName -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun + $rcComun = $LASTEXITCODE + Assert ($rcComun -ne 0) "Faza D (guard COMUN, fara -AllowComun): refuz" + } else { AssertWarn $false "Faza D (guard COMUN): niciun candidat .vc2 gasit sub cache\comun\clase, sarit" } + + # guard folder-foxbin2prg (cu -DryRun) + $fb2pFakeCache = Join-Path $CacheRoot 'UTIL_foxbin2prg_fake' + New-Item -ItemType Directory -Force -Path $fb2pFakeCache | Out-Null + $fb2pFakeText = Join-Path $fb2pFakeCache 'fake.vc2' + [IO.File]::WriteAllBytes($fb2pFakeText, $origBytes) + try { + & $txt2vcx -TextFile $fb2pFakeText -ProjectRoot 'D:\ROA\UTIL\foxbin2prg' -CacheRoot $fb2pFakeCache -DryRun + $rcFb2p = $LASTEXITCODE + Assert ($rcFb2p -ne 0) "Faza D (guard folder-foxbin2prg): refuz necondiționat" + } finally { Remove-Item -Recurse -Force -LiteralPath $fb2pFakeCache -ErrorAction SilentlyContinue } + + # -DryRun pe caz valid: nimic modificat in proiect, exit reflecta staging-ul (0 = OK) + [IO.File]::WriteAllBytes($cacheText, $origBytes) + & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun + $rcDry = $LASTEXITCODE + Assert ($rcDry -eq 0) "Faza D (-DryRun, caz valid): exit 0" + Assert ((Get-FileHashBytes $vcxPath) -eq $origVcxHash) "Faza D (-DryRun, caz valid): oavize.vcx neatins in proiect" + + # --- Test multi-fisier: un .vc2 valid (fundal.scx) + unul stricat (oavize) in aceeasi rulare --- + & $vcx2txt -Source (Join-Path $ProjectRoot 'Ferestre\fundal.scx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null + $fundalCacheText = Join-Path $CacheRoot 'Ferestre\fundal.sc2' + $fundalOrigBytes = [IO.File]::ReadAllBytes($fundalCacheText) + $fundalOrigVcxHash = Get-FileHashBytes (Join-Path $ProjectRoot 'Ferestre\fundal.scx') + $fundalText = $script:Enc.GetString($fundalOrigBytes).Replace('Caption = ""', 'Caption = "multi-test"') + [IO.File]::WriteAllBytes($fundalCacheText, $script:Enc.GetBytes($fundalText)) + + $corruptOavizeText = $script:Enc.GetString($origBytes) -replace "(?s)(PROCEDURE do_cauta_altele.*?)\r?\n\s*ENDPROC", '$1' + [IO.File]::WriteAllBytes($cacheText, $script:Enc.GetBytes($corruptOavizeText)) + + $script:TouchedProjectFiles.Add('Ferestre\fundal.scx') | Out-Null + $script:TouchedProjectFiles.Add('Ferestre\fundal.SCT') | Out-Null + + & $txt2vcx -TextFile @($fundalCacheText, $cacheText) -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + $rcMulti = $LASTEXITCODE + Assert ($rcMulti -eq 1) "Faza D (multi-fisier): exit code agregat = 1 (unul din doua a esuat)" + Assert ((Get-FileHashBytes $vcxPath) -eq $origVcxHash) "Faza D (multi-fisier): oavize.vcx (cel stricat) neatins in proiect" + $fundalVcxHash = Get-FileHashBytes (Join-Path $ProjectRoot 'Ferestre\fundal.scx') + Assert ($fundalVcxHash -ne $fundalOrigVcxHash) "Faza D (multi-fisier): fundal.scx (cel valid) A FOST scris in proiect" + + # cleanup multi-fisier + Push-Location $ProjectRoot + try { git checkout -- 'Ferestre\fundal.scx' 'Ferestre\fundal.SCT' } finally { Pop-Location } + [IO.File]::WriteAllBytes($cacheText, $origBytes) + & $vcx2txt -Source (Join-Path $ProjectRoot 'Clase\oavize.vcx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null + & $vcx2txt -Source (Join-Path $ProjectRoot 'Ferestre\fundal.scx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null + Remove-Item -Recurse -Force -LiteralPath $work -ErrorAction SilentlyContinue + } +} finally { + Write-Host "`n--- Cleanup final: git checkout -- pe fisierele atinse + refresh cache ---" + Push-Location $ProjectRoot + try { + $toRevert = $script:TouchedProjectFiles | Sort-Object -Unique + foreach ($f in $toRevert) { + $status = git status --porcelain -- $f + if ($status) { git checkout -- $f 2>$null } + } + } finally { Pop-Location } + & $vcx2txt -Source (Join-Path $ProjectRoot 'Clase\oavize.vcx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null + & $vcx2txt -Source (Join-Path $ProjectRoot 'Ferestre\fundal.scx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null +} + +Write-Host "`n=== REZULTAT ===" +if ($script:Failures.Count -eq 0) { + Write-Host "TOATE testele au trecut (Phase=$Phase)." -ForegroundColor Green + exit 0 +} else { + Write-Host "$($script:Failures.Count) test(e) esuate:" -ForegroundColor Red + $script:Failures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + exit 1 +} diff --git a/txt2vcx.ps1 b/txt2vcx.ps1 new file mode 100644 index 0000000..26f0031 --- /dev/null +++ b/txt2vcx.ps1 @@ -0,0 +1,316 @@ +<# +.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)) diff --git a/vcx2txt.ps1 b/vcx2txt.ps1 index a4e8a63..4196f32 100644 --- a/vcx2txt.ps1 +++ b/vcx2txt.ps1 @@ -66,11 +66,9 @@ if (-not (Test-Path $exe)) { throw "Nu gasesc $exe. Compileaza intai FoxBin2Prg (deschide foxbin2prg.pj2 in VFP si Build)." } -# binar -> (memo, extensie text) -$map = @{ - '.vcx' = @('.vct', '.vc2'); '.scx' = @('.sct', '.sc2'); '.frx' = @('.frt', '.fr2') - '.lbx' = @('.lbt', '.lb2'); '.mnx' = @('.mnt', '.mn2'); '.dbc' = @('.dct', '.dc2') -} +# binar -> (memo, extensie text) -- harta comuna, partajata cu txt2vcx.ps1 +. (Join-Path $PSScriptRoot 'vfp_filemap.ps1') +$map = $script:VfpFileMap function Convert-One($fullPath) { $f = Get-Item -LiteralPath $fullPath diff --git a/vfp_filemap.ps1 b/vfp_filemap.ps1 new file mode 100644 index 0000000..3e5f8b7 --- /dev/null +++ b/vfp_filemap.ps1 @@ -0,0 +1,80 @@ +<# +.SYNOPSIS + Harta comuna bin<->memo<->text pentru conversiile VFP (FoxBin2Prg), dot-sourced de + vcx2txt.ps1 (Bin2Prg) si txt2vcx.ps1 (Prg2Bin), ca sa nu existe doua copii ale hartii. + +.DESCRIPTION + Defineste $script:VfpFileMap: extensie-binar (lowercase, cu punct) -> @(extensie-memo, extensie-text). + Si o functie inversa Get-VfpBinExtensionForTextExtension pentru txt2vcx.ps1 (text -> binar). +#> + +$script:VfpFileMap = @{ + '.vcx' = @('.vct', '.vc2'); '.scx' = @('.sct', '.sc2'); '.frx' = @('.frt', '.fr2') + '.lbx' = @('.lbt', '.lb2'); '.mnx' = @('.mnt', '.mn2'); '.dbc' = @('.dct', '.dc2') +} + +function Get-VfpBinExtensionForTextExtension { + <# + .SYNOPSIS + Data o extensie text (.vc2, .sc2, ...), intoarce @(extensie-binar, extensie-memo), sau $null daca nu e cunoscuta. + #> + param([Parameter(Mandatory)][string]$TextExtension) + $t = $TextExtension.ToLower() + foreach ($binExt in $script:VfpFileMap.Keys) { + if ($script:VfpFileMap[$binExt][1] -eq $t) { + return @($binExt, $script:VfpFileMap[$binExt][0]) + } + } + return $null +} + +function Invoke-FoxBin2PrgSafe { + <# + .SYNOPSIS + Ruleaza FoxBin2Prg.EXE cu un watchdog de timp, ca sa nu ramana blocat la infinit. + + .DESCRIPTION + FoxBin2Prg.EXE poate afisa un MessageBox MODAL nativ al motorului VFP (nu al + propriului sau error-handler) cand recompilarea textului esueaza (ex. text + stricat, ENDPROC lipsa) - vazut concret: "Error 1098, Procedure not closed...". + Parametrul cDontShowErrors='1' NU suprima acest dialog (acela controleaza doar + raportarea de erori proprie a FoxBin2Prg, nu eroarea nativa de compilare VFP). + Fara acest watchdog, un rulaj automat/CI ar ramane agatat la infinit asteptand + un om sa apese OK. Orice timeout se trateaza ca ESEC pentru fisierul respectiv. + + .PARAMETER ExePath + Calea catre FoxBin2Prg.EXE. + + .PARAMETER ExeArgs + Lista de argumente pozitionale, in ordinea folosita de FoxBin2Prg (inclusiv + string-urile goale intre ele, ca in apelul cu "&"). + + .PARAMETER TimeoutSeconds + Cat asteapta procesul sa termine normal inainte sa-l omoare (implicit 60s - + conversiile uzuale dureaza sub o secunda; un timeout mare inseamna aproape + sigur un dialog modal blocat). + + .OUTPUTS + PSCustomObject cu TimedOut (bool) si ExitCode (int; -1 daca a fost omorat la timeout). + #> + param( + [Parameter(Mandatory)][string]$ExePath, + [Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]]$ExeArgs, + [int]$TimeoutSeconds = 60 + ) + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $ExePath + $psi.Arguments = ($ExeArgs | ForEach-Object { '"' + ($_ -replace '"', '\"') + '"' }) -join ' ' + $psi.UseShellExecute = $false + + $proc = [System.Diagnostics.Process]::Start($psi) + $finished = $proc.WaitForExit($TimeoutSeconds * 1000) + if (-not $finished) { + Write-Warning "Invoke-FoxBin2PrgSafe: TIMEOUT dupa $TimeoutSeconds s (probabil MessageBox modal blocat) - omor procesul (PID $($proc.Id)) si eventualele lui ferestre/copii." + try { & taskkill /PID $proc.Id /T /F 2>&1 | Out-Null } catch {} + try { if (-not $proc.HasExited) { $proc.Kill() } } catch {} + Start-Sleep -Milliseconds 300 + return [PSCustomObject]@{ TimedOut = $true; ExitCode = -1 } + } + return [PSCustomObject]@{ TimedOut = $false; ExitCode = $proc.ExitCode } +}