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