Files
foxbin2prg/txt2vcx.ps1
Marius Mutu a05687a38f Flux git-text in-arbore: git_sync.ps1 + teste, txt2vcx in-place, filemap pjx/dbf
- git_sync.ps1 nou: conversie recursiva bin->text in arbore (staging temp,
  incremental, orfani, RoundtripExempt, DbfList cu date, anti-dialog cfg)
- vfp_filemap.ps1: mapari .pjx->.pj2 si .dbf->.db2
- txt2vcx.ps1: CacheRoot implicit = ProjectRoot (text in-arbore) + cfg anti-dialog in staging
- test_roundtrip.ps1: parametrizat per proiect, restaurare din backup temp (nu git checkout)
- test_git_sync.ps1 nou: incremental, orfani, esec partial, protectie binar, scutire roundtrip
- CLAUDE.md: fluxul nou documentat

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:04:23 +03:00

321 lines
16 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 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))