<# .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 versiunilor text. Implicit egala cu ProjectRoot (flux text in-arbore, langa binare): maparea cache->proiect devine identitate, iar refresh-ul textului e in-place. Da o radacina separata pentru fluxul vechi cu cache extern. .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 = $ProjectRoot, [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 # cfg in staging: DontShowErrors suprima MessageBox-ul propriu la write-back esuat (mostenit de verify/refresh) Set-Content -LiteralPath (Join-Path $stagingDir 'foxbin2prg.cfg') -Value @('DontShowErrors: 1','ShowProgressbar: 0') -Encoding Ascii $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))