16 Commits

Author SHA1 Message Date
339ad0a4bc git_sync.ps1: -OrphanExempt, texte fara binar prin design
Un text .??2 al carui binar e gitignored si se genereaza local din text (txt2vcx) nu e
orfan, dar era raportat ca atare la fiecare sync. Pattern-urile din -OrphanExempt sunt
sarite inainte de verificarea binarului, deci nici raportate nici sterse la
-OrphanCleanup. Implicit acopera stub-ul de formular verificare.sc2 din testele
headless de achizitie import.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EiizMcQA6NEYCftpqCtPtC
2026-08-03 22:49:45 +03:00
57c4a63d06 vfp_symbols.ps1: reconstruire la schimbarea radacinii; default-uri pe fluxul in-tree
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A5okKjUKMM5Xk1egq2w81P
2026-08-03 08:56:00 +03:00
9b6671b0ed actualizare 2026-08-03 08:30:21 +03:00
7f06f3ca4c vfp_symbols.ps1: index de simboluri peste cache-ul text si .prg-urile proiectului
Retine intervalul de linii al fiecarei clase/metode/proceduri, ca o potrivire de grep sa
poata fi atribuita clasei si metodei care o contine - ce grep nu poate spune intr-un .vc2,
unde antetul clasei poate fi cu mii de linii deasupra potrivirii si ~2/3 din fisier sunt
proprietati si metadata.

Moduri: -Grep (eticheteaza rezultatele, -CodeOnly taie metadata), -Where fisier:linie,
-Find (unde e definit), -Class (lant de mostenire + metode cu intervale). Indexul se
reconstruieste singur cand cache-ul e mai nou.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ceaeZUhPwyDG4LXPzigUJ
2026-08-03 07:55:57 +03:00
234f6a5e7f Muta roa_sync.ps1 (+ testul) in COMUN\scripts - aici ramane doar stub
Scriptul de sesiune se distribuie de-acum prin svn update (COMUN e prezent in
toate proiectele ROA), nu prin pull manual pe clona foxbin2prg. Stub-ul
deleaga la <ProjectRoot>\COMUN\scripts\roa_sync.ps1 (cu svn update prealabil
daca fisierul lipseste inca), pentru lansatoarele roa_sync.bat neactualizate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvccPZXr4A9ihcTtExNoJT
2026-07-27 11:58:55 +03:00
51cab198c4 roa_sync: reconciliere cu origin + push gardat doar commit-uri de sync
Dupa commit-ul local de sync: fetch origin, apoi ff-only (doar in urma) sau
rebase --empty=drop (divergenta - sync-urile cu continut deja identic pe
origin devin goale si sunt aruncate automat, fara interventie manuala);
conflict real -> rebase --abort + exit nenul. Push origin main DOAR cand
toate commit-urile ahead sunt de forma sync SVN[ r<rev>], cu maxim 3
reincercari la push respins (push simultan de pe alta masina). Switch nou
-GitOnly (sare svn update + regenerarea textelor). Harness nou
test_roa_sync.ps1: 8 scenarii pe fixture-uri git in TEMP, inclusiv
reproducerea incidentului din 2026-07-27 (local byte-identic cu origin).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvccPZXr4A9ihcTtExNoJT
2026-07-27 11:52:23 +03:00
558973f0c6 Fb2P_Diff: actualizare proiect + texte regenerate; PROMPT_cautare_vfp: flux editare/write-back
Modificari pre-existente (09.07) comise separat de fluxul git-text;
.pj2/.sc2 reimprospatate din binare cu git_sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:22:36 +03:00
76ca98f0bf Adauga roa_sync.ps1 (sync de inceput de sesiune: svn update + git_sync + commit sync + inchidere branch-uri claude)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:14:00 +03:00
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
16f7d75363 vcx2txt.ps1: conversie incrementala reala - fara reconversii false la fiecare rulare
- Start-Process -Wait in loc de "&" (exe GUI - controlul se preda inainte de final,
  iar stergerea binarului copiat omora conversia in curs).
- succes decis pe rescrierea efectiva a textului sau raportul E0 al exe-ului, nu pe
  simpla existenta a unui text vechi (esecurile erau mascate ca "OK").
- FoxBin2Prg nu actualizeaza mtime cand textul regenerat e identic (ex. binare
  reatinse de Project > Build fara schimbari reale) - sincronizam mtime-ul textului,
  altfel fisierul parea stale si se reconvertea la nesfarsit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 13:13:39 +03:00
f36a26a9c6 Adauga write-back text->binar VFP: txt2vcx.ps1 + vfp_filemap.ps1 + test_roundtrip.ps1
Fluxul complet bin->text->edit->bin pentru .vcx/.scx (faza 1; .mnx/.frx raman
read-only): regenerare+compilare in staging unic per rulare, guard-uri
(COMUN cu -AllowComun, staleness, binar lipsa, folder foxbin2prg), fidelity
check pe octeti inainte de orice copiere, copiere .vct->.vcx cu restaurare
la esec partial, refresh cache. Harta de extensii extrasa in vfp_filemap.ps1,
dot-sourced si de vcx2txt.ps1 (comportament identic).

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 18:30:21 +03:00
b449e0aa2f Muta cache-urile text VFP in afara clonei foxbin2prg
ReCreate_FoxBin2Prg.prg face Prg2Bin recursiv pe tot folderul tool-ului (get_FilesFromDirectory intra in orice subfolder), deci _textcache_* aflate sub foxbin2prg erau scanate si .??2-urile din alte proiecte reconvertite in binare parazitare.

Cache-urile stau acum in D:\ROA\_vfp_textcache\{roacnpro,roagest,roaimob}, in afara clonei, unde ReCreate nu le mai poate atinge. Actualizat default CacheRoot in vcx2txt.ps1, exemplele din CLAUDE.md / PROMPT_cautare_vfp.md / git_svn_parallel_init_prompt.md si comentariul din .git/info/exclude.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 12:46:06 +03:00
ffdaa88d74 sync_romfast.bat: repara "batch file cannot be found" cauzat de checkout master
Scriptul exista doar pe branch-ul romfast, deci "git checkout master" il
stergea de pe disc in timp ce cmd.exe il citea linie cu linie. Acum se
auto-copiaza in %TEMP% si ruleaza de acolo, imun la schimbarea de branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:33:06 +03:00
98e1295eec Adauga sync_romfast.bat/.ps1 pentru sincronizare romfast<->upstream cu optiune de stash
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 12:26:16 +03:00
7e4c0154f2 CLAUDE.md: documenteaza remote-urile (origin/github + romfast/gitea) si fluxul de sincronizare 2026-07-09 11:43:27 +03:00
d873d52aea Adaugari locale ROA: vcx2txt.ps1 (cautare cod in binare VFP), prompturi si CLAUDE.md 2026-07-09 11:42:07 +03:00
17 changed files with 2442 additions and 4 deletions

2
.gitignore vendored
View File

@@ -20,6 +20,8 @@
#programms #programms
!*.prg !*.prg
!*.fpw !*.fpw
!*.ps1
!*.bat
#header #header
!*.h !*.h

144
CLAUDE.md Normal file
View File

@@ -0,0 +1,144 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What this is
A local shared copy of **FoxBin2Prg** (the VFPX tool by Fernando D. Bozzo, maintained by Lutz Scheffler): a Visual FoxPro 9 program that converts VFP binary files to PRG-style text and back. Supported conversions: `PJX↔PJ2`, `SCX↔SC2`, `VCX↔VC2`, `FRX↔FR2`, `LBX↔LB2`, `DBC↔DC2`, `DBF↔DB2`, `MNX↔MN2`.
This directory is a git clone with two remotes:
- **`origin`** = `https://github.com/fdbozzo/foxbin2prg.git` (upstream, read-only). `master` tracks `origin/master` and stays a pristine mirror.
- **`romfast`** = `git@gitea.romfast.ro:romfast/foxbin2prg.git` (our fork on Gitea, SSH). Working branch **`romfast`** (default on Gitea) = upstream `master` + the ROA additions committed (`vcx2txt.ps1`, `PROMPT_cautare_vfp.md`, `CLAUDE.md`). The pre-git local state is kept on branch `local-snapshot-v1.21.04`.
Update flow (sync fork from upstream): `git fetch origin``git checkout master && git merge --ff-only origin/master``git checkout romfast && git merge master``git push romfast romfast master`. After any update, regenerate binaries/EXE in VFP9 with `DO ReCreate_FoxBin2Prg.prg` (an error about `TESTS\FXUResults.db2` is expected). Text caches live outside this folder (`D:\ROA\_vfp_textcache\`, see below) so `ReCreate` never touches them; compiled binaries are kept out of git via the upstream `.gitignore` plus `.git/info/exclude` (which also still ignores any stray local `_textcache*`).
It serves two purposes:
1. Upstream tool source (for occasional patches to FoxBin2Prg itself).
2. A **shared search tool for the ROA VFP projects** (`D:\ROA\ROAGEST`, `D:\ROA\ROAIMOB`, `D:\ROA\ROAACNPRO`, …) via the local additions below.
## Local additions (ROA-specific, not part of upstream)
- **`git_sync.ps1`** — the **in-tree** flow (primary for migrated ROA projects): folder-mode recursive over the whole project tree (incl. `COMUN\`), generates the `.??2` text **next to each binary** (not in an external cache) so git tracks readable text instead of binaries. Incremental, converts in a temp staging area and copies back only the text (never touches the SVN working-copy binaries), cleans up orphan `.??2`, continues past per-file failures and exits nonzero on any. Params: `-ProjectRoot`, `-DbfList` (tables converted to `.db2` with data), `-RoundtripExempt`, `-SubDir`. Run at session start and before every `git commit`; do not commit while it reports failures.
- **`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`). Search-only / non-migrated projects; migrated projects use `git_sync.ps1` instead.
- **`vfp_symbols.ps1`** — symbol index over the text cache and the project's `.prg`: for every class/method/procedure it records the **line range**, so a hit can be attributed to its owner. Answers what grep cannot in a `.vc2` (where the class header may be 13k lines above the hit and ~2/3 of the file is properties/metadata): `-Grep <regex>` labels each match with `class.method` (`-CodeOnly` drops metadata hits), `-Where 'file:line'` names the enclosing member, `-Find <name>` locates the **definition**, `-Class <name>` prints the inheritance chain plus members with their ranges. Rebuilds automatically when the cache is newer (~4s); index is a regenerable `_symbols.tsv` in the cache root.
- **`txt2vcx.ps1`** — the write-back counterpart: regenerates `.vcx/.scx` binaries from edited `.vc2/.sc2` text and copies them back into the project (see "Golden rule" below for scope/limits). `-CacheRoot` defaults to `-ProjectRoot` (in-tree flow: cache→project mapping is identity); pass a separate root only for the old external-cache flow.
- **`vfp_filemap.ps1`** — shared bin↔memo↔text extension map (`.vcx`/`.vct`/`.vc2`, `.scx`/`.sct`/`.sc2`, `.pjx`/`.pjt`/`.pj2`, `.dbf`/`.fpt`/`.db2`, …), dot-sourced by `git_sync.ps1`, `vcx2txt.ps1` and `txt2vcx.ps1` so the mapping stays in one place. Also hosts `Invoke-FoxBin2PrgSafe` (timeout + `taskkill`).
- **`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); parametrized per project. See examples below.
- **`test_git_sync.ps1`** — standalone tests for `git_sync.ps1` on temp fixtures: incremental (up-to-date files skipped), orphan cleanup, nonzero exit on partial failure, and binaries byte-identical after any run.
- **Text caches** (external-cache flow, non-migrated / search-only projects) live **outside this folder**, under `D:\ROA\_vfp_textcache\` (one subfolder per project: `roacnpro`, `roagest`, `roaimob`) — per-project regenerable text caches. Migrated projects (ROACONT, COMUN, …) keep their `.??2` in-tree via `git_sync.ps1` instead of a cache here. 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 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.
- Only `.vcx`/`.scx` support write-back. `.frx/.mnx/.lbx/.pjx/.dbc/.dbf` are **read-only text** (edit in the VFP IDE): menus still need GENMENU to produce `.mpr`, and the other formats are too fragile for round-trip write-back. `git_sync.ps1` still generates their `.??2` for readable diffs, but never writes them back.
- Six FFC/third-party classes under `COMUN\` are in `git_sync.ps1`'s `-RoundtripExempt` list (`#INCLUDE` makes the isolated roundtrip a false-negative): `accessibility.vcx`, `oinventar.vcx`, `_gdiplus.vcx`, `_reportlistener.vcx` (in `COMUN\clase\`), `foxcharts.vcx` (`COMUN\utile\foxcharts\`), `_webview.vcx` (`COMUN\utile\web\`). Their text is valid (bin→text runs) but roundtrip is not guaranteed — edit them only in the IDE.
- 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)
& 'D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1' -Project 'D:\ROA\ROAGEST\roagest.pjx' -ProjectRoot 'D:\ROA\ROAGEST' -CacheRoot 'D:\ROA\_vfp_textcache\roagest'
# Include reports/menus too: -Types vcx,scx,frx,mnx
# One-off conversion of a file/folder not in the project: -Source 'path\to\lib.vcx'
# Other flags: -Force (reconvert all), -Clean (wipe cache first)
```
Then search the cache with Grep (e.g. `PROCEDURE do_salvare`), citing `file:line` from the cache.
Attribute hits to their owning class/method (build the index first, or let it build itself):
```powershell
& 'D:\ROA\UTIL\foxbin2prg\vfp_symbols.ps1' -CacheRoot 'D:\ROA\_vfp_textcache\roagest' -ProjectRoot 'D:\ROA\ROAGEST' -Grep 'do_salvare' -CodeOnly
# -Where 'oavize.vc2:8412' what class/method owns that line (needed before a txt2vcx edit)
# -Find 'do_salvare' where it is DEFINED (grep also returns the call sites)
# -Class frm_avize inheritance chain + members with line ranges
```
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`).
```foxpro
* After clone/pull: regenerate all binaries from text and compile the EXEs.
* NEVER run foxbin2prg.prg directly for this — it uses a special config.
DO ReCreate_FoxBin2Prg.prg
* Convert a single file (direction inferred from extension)
DO FOXBIN2PRG.PRG WITH "<path>\file.scx" && binary -> text (.sc2)
DO FOXBIN2PRG.PRG WITH "<path>\file.sc2" && text -> binary (.scx)
* Convert a whole directory
DO FOXBIN2PRG.PRG WITH "<path>","Bin2Prg"
DO FOXBIN2PRG.PRG WITH "<path>","Prg2Bin"
* Config templates
DO FOXBIN2PRG.PRG WITH "-c","template.cfg" && default settings
DO FOXBIN2PRG.PRG WITH "-C","config.cfg" && active settings
DO FOXBIN2PRG.PRG WITH "-t","table.dbf.cfg" && per-table template
* Interactive help / option list
DO FOXBIN2PRG.PRG WITH "?"
```
### Tests
Unit tests live in `TESTS/` and use **FoxUnit** (path configured in `TESTS/foxunit_path.txt`). Files: `ut__foxbin2prg__<class>__<method>.prg` (unit) and `ft__foxbin2prg__<class>.prg` (functional). Run them from VFP via FoxUnit; test fixtures live in `TESTS/DATOS_READONLY/`.
## Architecture
Nearly everything is in one ~34,000-line file: **`foxbin2prg.prg`**. Key class hierarchy inside it:
- **`c_foxbin2prg` (As Session)** — entry point / public API. Parameter parsing, config resolution (`evaluateConfiguration`), file dispatch, progress UI, error logging. Also usable as COM-ish object (`CREATEOBJECT('c_foxbin2prg')`, `.execute(...)`), which is how the `.vbs` helpers and SCM integrations drive it.
- **`c_conversor_base`** — shared conversion logic (backup handling, timestamps, comment/property parsing, exclusion blocks).
- **`c_conversor_bin_a_prg`** and per-type subclasses (`c_conversor_vcx_a_prg`, `c_conversor_scx_a_prg`, `c_conversor_pjx_a_prg`, `c_conversor_dbf_a_prg`, …) — binary → text.
- **`c_conversor_prg_a_bin`** and per-type subclasses — text → binary.
- **`CL_*` model classes** (`CL_CLASE`, `CL_PROJECT`, `CL_DBC*`, `CL_DBF_*`, `CL_MENU*`, …) — object model the text files are parsed into / generated from.
- **`CL_CFG` / `CL_DBF_CFG`** — config file handling (`FOXBIN2PRG.CFG` with directory inheritance, per-table `.dbf.cfg`).
- **`CL_LANG`** — localization (EN/ES/FR/DE), auto-selected.
Supporting files: `foxbin2prg_keywords.dbf/.cdx` (VFP keyword table used during generation — has its own `.db2` text form), `props_*.txt` (per-control default property lists used to suppress default values in output), `Create_FoxBin2Prg.cfg` (the special self-conversion config), `Fb2P_Diff/` and `FileName_Caps/` (companion sub-projects, built by `ReCreate_FoxBin2Prg.prg`), `BuildProcess/` (VFPXDeployment build hooks), `ThorUpdater/` (Thor/VFPX release plumbing).
**Text format is position-sensitive:** the generated `.??2` files look like PRG/XML but are parsed line by line; structure and line layout must be preserved when editing them. Properties/methods are emitted alphabetically sorted.
## Upstream contribution conventions (from `.github/CONTRIBUTING.md`)
If patching FoxBin2Prg itself:
- Two version constants at the top of `foxbin2prg.prg`: bump only the minor part of `DC_FB2PRG_VERSION_REAL` (e.g. `'1.21.04'`); do **not** change `DN_FB2PRG_VERSION` (`1.21`) — it's written into every text file and forces mass recommits.
- Add the change to the history section at the top of `foxbin2prg.prg` and to `docs/ChangeLog.md`; update the version in `README.md`.
- Style: mixed-case keywords, tab indentation, spaces around operators and after list items; do not run BeautifyX with mDots insertion.
- After changes, regenerate text with `DO Create_FoxBin2Prg.prg` and compile in **VFP9 SP2**.
## Documentation
- `docs/FoxBin2Prg_Internals.md` — settings reference and internals (ZOrder, timestamps, class-per-file, DBF export hooks, API).
- `docs/FoxBin2Prg_Run.md` — full command-line/parameter reference.
- `docs/FoxBin2Prg_git.md`, `docs/FoxBin2Prg_SCM.md` — SCM integration.
- Spanish docs in `Documentacion/` are unmaintained.

View File

@@ -32,7 +32,7 @@ _AutoIncrement = "0"
*<BuildProj> *<BuildProj>
*<.HomeDir = 'e:\se\vfpx\foxbin2prg\fb2p_diff' /> *<.HomeDir = 'd:\roa\util\foxbin2prg\fb2p_diff' />
FOR EACH loProject IN _VFP.Projects FOXOBJECT FOR EACH loProject IN _VFP.Projects FOXOBJECT
loProject.Close() loProject.Close()
@@ -70,9 +70,9 @@ ENDWITH
WITH loProject WITH loProject
*<ProjectProperties> *<ProjectProperties>
.SetMain(lcCurdir + 'main_fb2p_diff.prg') .SetMain("main_fb2p_diff.prg")
.Icon = lcCurdir + 'fb2p_diff.ico' .Icon = "fb2p_diff.ico"
.Debug = .T. .Debug = .T.
.Encrypted = .F. .Encrypted = .F.
*<.CmntStyle = 1 /> *<.CmntStyle = 1 />

Binary file not shown.

Binary file not shown.

View File

@@ -3,5 +3,4 @@
*-------------------------------------------------------------------------------------------------------------------------------------------------------- *--------------------------------------------------------------------------------------------------------------------------------------------------------
*< FOXBIN2PRG: Version="1.21" SourceFile="fb2p_diff.scx" CPID="1252" /> (Solo para binarios VFP 9 / Only for VFP 9 binaries) *< FOXBIN2PRG: Version="1.21" SourceFile="fb2p_diff.scx" CPID="1252" /> (Solo para binarios VFP 9 / Only for VFP 9 binaries)
* *
* *

127
PROMPT_cautare_vfp.md Normal file
View File

@@ -0,0 +1,127 @@
# Prompt: căutare și editare în cod VFP (foxbin2prg) — reutilizabil în orice proiect Visual FoxPro
Acest fișier conține un **prompt gata de copiat** pentru Claude Code (sau alt agent), care
configurează și folosește sistemul `foxbin2prg` ca să poată **căuta** cod-sursă aflat **în interiorul
fișierelor binare VFP** (`.vcx/.vct`, `.scx/.sct`, `.frx/.frt`, `.mnx/.mnt`, `.dbc/.dct`) într-un
proiect nou — și, pentru `.vcx`/`.scx`, să poată și **modifica** acel cod direct pe text, cu
write-back în binar (`txt2vcx.ps1`), fără trecere prin IDE-ul VFP.
Unealta e partajată și stă la `D:\ROA\UTIL\foxbin2prg\` — nu trebuie copiată per proiect. Doar
spui agentului care e proiectul curent (folderul + fișierul `.pjx`).
---
## Cum se folosește
1. Deschide proiectul VFP în Claude Code.
2. Copiază blocul de mai jos în prompt.
3. Înlocuiește cele **3 valori** marcate `<<...>>` cu datele proiectului tău:
- `<<RADACINA_PROIECT>>` = folderul rădăcină al proiectului (ex. `D:\ROA\ROAGEST`)
- `<<FISIER_PJX>>` = calea către `.pjx` (ex. `D:\ROA\ROAGEST\roagest.pjx`)
- `<<NUME_PROIECT>>` = etichetă scurtă pentru folderul de cache (ex. `roagest`)
---
## ▼▼▼ PROMPT DE COPIAT ▼▼▼
```
Acest proiect este o aplicație Visual FoxPro 9. O mare parte din cod (proceduri, metode,
parametri) NU stă în fișiere .prg, ci în interiorul fișierelor binare VFP: librării de clase
(.vcx+.vct), forme (.scx+.sct), rapoarte (.frx+.frt), meniuri (.mnx+.mnt). Un grep direct pe
acestea nu e lizibil (sunt tabele DBF cu memo binar).
Ca să poți căuta în acel cod, folosește unealta partajată foxbin2prg, care convertește binarele
în reprezentarea lor TEXT (.vc2/.sc2/.fr2/.mn2/.dc2) într-un cache, și apoi caută în cache.
PARAMETRII PROIECTULUI CURENT:
- Rădăcină proiect: <<RADACINA_PROIECT>>
- Fișier proiect (.pjx): <<FISIER_PJX>>
- Cache text: D:\ROA\_vfp_textcache\<<NUME_PROIECT>>
REGULA DE AUR: cache-ul text e sursa pentru CĂUTARE (toate tipurile de binar) și, DOAR pentru
.vcx/.scx, și pentru EDITARE cu write-back (txt2vcx.ps1, vezi PAS 3). Pentru .frx/.mnx/.dbc
textul rămâne doar pentru citire — modificările la ele se fac în IDE-ul VFP, pe binar.
PAS 1 — populează cache-ul text (la începutul sesiunii, sau după ce s-a modificat ceva în IDE).
Rulează în PowerShell (e incremental — sare peste ce e deja la zi):
& 'D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1' -Project '<<FISIER_PJX>>' -ProjectRoot '<<RADACINA_PROIECT>>' -CacheRoot 'D:\ROA\_vfp_textcache\<<NUME_PROIECT>>'
(Folosește un -CacheRoot distinct per proiect ca să nu amesteci cache-urile. Implicit scriptul
convertește doar .vcx și .scx; pentru a include și rapoarte/meniuri adaugă: -Types vcx,scx,frx,mnx)
Dacă scriptul se plânge că lipsește FoxBin2Prg.EXE, trebuie compilat o singură dată: deschide
'D:\ROA\UTIL\foxbin2prg\foxbin2prg.pj2' în VFP 9 și Build > Build Executable. Anunță-mă să fac asta.
PAS 2 — caută în cache cu unealta Grep (NU în binarele originale):
Grep pe folderul 'D:\ROA\_vfp_textcache\<<NUME_PROIECT>>' pentru numele de metodă/procedură/variabilă căutat.
Ex.: caută "PROCEDURE do_salvare" sau numele tabelei/cursorului implicat.
CONVERSIE PUNCTUALĂ (un fișier sau folder care NU e în proiect, ex. o librărie din COMUN):
& 'D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1' -Source 'CALE\catre\librarie.vcx' -CacheRoot 'D:\ROA\_vfp_textcache\<<NUME_PROIECT>>'
ALTE OPȚIUNI: -Force (reconvertește tot), -Clean (șterge cache-ul întâi).
PAS 3 — modificare de cod în .vcx/.scx (flux write-back pe text). Ordinea e obligatorie:
a. Investighează pe cache, propune modificarea și AȘTEAPTĂ ACORDUL pe abordare înainte să
editezi ceva.
b. Refresh cache (PAS 1) dacă a rulat vreo sesiune de IDE de la ultimul refresh (staleness-ul
se verifică pe mtime și txt2vcx refuză corect scrierea peste modificări nevăzute). Înainte
de write-back închide COMPLET VFP IDE și exe-ul aplicației — procesele reziduale țin lock
pe binare.
c. Editează .vc2/.sc2 BYTE-SAFE: formatul e position-sensitive (nu reformata/reflow linii),
proprietățile sunt alfabetizate (una nouă se inserează la locul ei alfabetic). Headerul
CPID="1252" poate minți despre codepage-ul real al diacriticelor — citește/scrie fișierul
ca bytes (PowerShell), conținutul NOU doar ASCII; nu folosi unelte care re-encodează.
d. Write-back:
& 'D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1' -TextFile 'D:\ROA\_vfp_textcache\<<NUME_PROIECT>>\Clase\lib.vc2' -ProjectRoot '<<RADACINA_PROIECT>>' -CacheRoot 'D:\ROA\_vfp_textcache\<<NUME_PROIECT>>'
Regenerează + compilează binarul în staging, verifică fidelitatea (roundtrip byte-identic
față de textul editat) și abia apoi copiază binarul în proiect; la orice eșec proiectul
rămâne neatins. Ținte sub COMUN\ cer flag-ul -AllowComun ȘI aprobare explicită — o
modificare acolo afectează toate aplicațiile ROA.
e. Diff lizibil pentru review: extrage binarul vechi din git (git show HEAD:cale > temp, atât
.vcx cât și .vct), regenerează-i textul cu vcx2txt.ps1 -Source într-un folder temporar și
compară cu cache-ul editat: git diff --no-index. COMMIT DOAR după ce omul confirmă pe diff
și testează în IDE.
EXCEPȚII: pentru .mnx (GENMENU) și .frx (fragile la round-trip) nu există write-back — scrie
propunerea de modificare într-un fișier docs/propuneri_<subiect>.md (cod + unde se inserează),
iar aplicarea se face manual în IDE-ul VFP.
FLUX TIPIC: la prima sesiune rulează PAS 1 o dată, apoi caută cu Grep în cache (PAS 2). La
sesiunile următoare rescrie doar ce s-a schimbat. Când citești codul unei metode, dă referințe
fișier:linie din cache.
```
## ▲▲▲ SFÂRȘIT PROMPT ▲▲▲
---
## Note pentru adaptare
- `<<NUME_PROIECT>>` îl poți pune orice etichetă scurtă (ex. `roagest`), doar ca numele
folderului de cache să fie unic per proiect.
- Ține cache-ul în `D:\ROA\_vfp_textcache\`, adică **în afara** folderului `foxbin2prg`. Motiv:
`ReCreate_FoxBin2Prg.prg` face `Prg2Bin` recursiv pe tot folderul tool-ului și ar reconverti
`.??2`-urile din orice cache aflat sub el înapoi în binare parazitare.
- Scriptul citește lista de fișiere direct din `.pjx`, deci convertește **doar** ce folosește
efectiv proiectul (rapid), nu tot `COMUN\`.
- Cache-ul (`D:\ROA\_vfp_textcache\<proiect>`) e regenerabil și e în afara arborelui de versionare
al proiectului (și în afara clonei foxbin2prg) — poate fi șters oricând.
- Conversia NU necesită IDE-ul VFP deschis (folosește `FoxBin2Prg.EXE` compilat).
- Fără backup-uri `.bak` lângă binarele din proiect — git e mecanismul de restore; `txt2vcx.ps1`
ține o copie de siguranță doar în folderul temp al rulării.
- Editările de volum se pot delega la subagenți (Sonnet, în background) — sesiunea principală
orchestrează, verifică diff/fidelity și intervine doar la deblocări și decizii.
- Detalii complete despre write-back (guard-uri, encoding, dialoguri VFP modale la text corupt,
testul de regresie `test_roundtrip.ps1`): `D:\ROA\UTIL\foxbin2prg\CLAUDE.md`.
- Exemplu funcțional, rodat pe ROAGEST: `D:\ROA\ROAGEST\docs\flux-editare-vfp-text.md` și
`D:\ROA\ROAGEST\docs\CLAUDE.md` (modul de lucru complet, cu review pe diff înainte de commit).
- Ghid complet doar-căutare (varianta ROAACNPRO, cu exemple): `D:\ROA\ROAACNPRO\docs\cautare_vcx_vct.md`.
- Tabel corespondență binar → text: `.vcx``.vc2`, `.scx``.sc2`, `.frx``.fr2`, `.mnx``.mn2`,
`.dbc``.dc2`, `.lbx``.lb2`.

331
git_sync.ps1 Normal file
View File

@@ -0,0 +1,331 @@
<#
.SYNOPSIS
Sincronizeaza in ARBORE (langa binare) versiunile text FoxBin2Prg ale binarelor VFP
ale unui proiect ROA, pentru ca git sa urmareasca text lizibil in loc de binare.
.DESCRIPTION
Folder-mode recursiv pe TOT arborele proiectului (inclusiv COMUN\), NU project-mode din .pjx:
altfel binarele nereferentiate in .pjx ar disparea din git. Pentru fiecare binar suportat
genereaza fisierul text (.vc2/.sc2/.fr2/.mn2/.lb2/.pj2/.dc2/.db2) IN ACELASI FOLDER cu binarul.
Tipuri convertite: vcx, scx, frx, mnx, lbx, pjx, dbc (recursiv) + dbf-urile dintr-o lista
explicita per proiect (-DbfList). Incremental: sare peste text-ul deja la zi (mtime binar vs text).
CONVERSIE IN STAGING TEMP (nu in arbore): FoxBin2Prg.EXE lasa artefacte (.err, _*.BAK) si
vcx2txt sterge copia binara la final - cu destinatia = arborele ar sterge binarul din working
copy-ul SVN. Aici copiem binar+memo (+.dbf.cfg) intr-un temp, convertim acolo, si copiem inapoi
DOAR textul (raw bytes, byte-preserving - encoding real cp1250 sub header CPID=1252, nu decodam).
Binarele din arbore NU sunt atinse niciodata.
Fidelitate per tip:
- vcx/scx: roundtrip complet (bin->text->bin->text, comparare pe octeti) - au write-back.
- frx/mnx/lbx/pjx/dbc/dbf: doar bin->text cu exit-code + timeout + text nevid (write-back nesuportat).
Esec partial: la esecul unui fisier continua cu restul, colecteaza esecurile, le raporteaza la
final si iese cu exit code nenul.
Curatare orfani: textele .??2 fara binar corespondent (binar sters din SVN) si .db2-urile ale
caror tabele au IESIT din lista -DbfList sunt DOAR RAPORTATE implicit; se sterg numai cu
-OrphanCleanup explicit. Textele din -OrphanExempt sunt sarite de la verificare (text fara
binar prin design).
Toate invocarile FoxBin2Prg.EXE trec prin Invoke-FoxBin2PrgSafe (timeout + taskkill).
.PARAMETER ProjectRoot
Radacina proiectului (implicit D:\ROA\ROACONT).
.PARAMETER Types
Tipurile binare (fara dbf) convertite recursiv (implicit vcx,scx,frx,mnx,lbx,pjx,dbc).
.PARAMETER DbfList
Lista de pattern-uri relative la ProjectRoot pentru dbf-urile convertite in .db2 cu DATE
(implicit lista ROACONT: Locale\locale*.dbf, Help\*.dbf, DATE\Optiuni_*.dbf).
.PARAMETER SubDir
Optional. Limiteaza rularea la un subfolder (relativ la ProjectRoot) - util pentru rulari in pasi.
Curatarea de orfani se face doar cand se ruleaza pe tot arborele (SubDir gol).
.PARAMETER TimeoutSeconds
Timeout per invocare FoxBin2Prg (implicit 60s).
.PARAMETER MaxDb2Mb
Prag: un .db2 peste aceasta dimensiune e raportat (tabela ar trebui scoasa din lista). Implicit 5.
.PARAMETER Force
Reconverteste chiar daca textul e la zi.
.PARAMETER NoOrphanCleanup
Pastrat pentru compatibilitate: forteaza doar-raportare chiar si cu -OrphanCleanup.
.PARAMETER OrphanCleanup
Sterge efectiv textele orfane. Implicit orfanii sunt DOAR raportati, nu stersi.
.PARAMETER OrphanExempt
Pattern-uri (-like, relative la ProjectRoot) de texte .??2 care NU sunt orfani chiar fara binar
langa ele: binarul e gitignored si se genereaza local din text (txt2vcx), textul e sursa.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\git_sync.ps1 -ProjectRoot D:\ROA\ROACONT
#>
[CmdletBinding()]
param(
[string]$ProjectRoot = 'D:\ROA\ROACONT',
[string[]]$Types = @('vcx','scx','frx','mnx','lbx','pjx','dbc'),
[string[]]$DbfList = @('Locale\locale*.dbf','Help\*.dbf','DATE\Optiuni_*.dbf',
'COMUN\clase\Locale\*.dbf','COMUN\datemenu\*.dbf','COMUN\datemenu\xold\*.dbf',
'COMUN\Drepturi utilizatori\*\*.dbf','COMUN\utile\GridExtras\gridextras.dbf',
'COMUN\utile\ListProperty\lproperty.dbf','COMUN\utile\hpdf\*.dbf','COMUN\utile\hpdf\ReportOutput\*.dbf',
# acelasi set si fara prefixul COMUN\, pentru rulari cu -ProjectRoot chiar pe radacina COMUN
'clase\Locale\*.dbf','datemenu\*.dbf','datemenu\xold\*.dbf',
'Drepturi utilizatori\*\*.dbf','utile\GridExtras\gridextras.dbf',
'utile\ListProperty\lproperty.dbf','utile\hpdf\*.dbf','utile\hpdf\ReportOutput\*.dbf'),
# clase FFC/third-party cu #INCLUDE: roundtrip izolat = fals-negativ; editare doar in IDE, fara write-back
[string[]]$RoundtripExempt = @('COMUN\clase\accessibility.vcx','COMUN\clase\oinventar.vcx','COMUN\clase\_gdiplus.vcx','COMUN\clase\_reportlistener.vcx','COMUN\utile\foxcharts\foxcharts.vcx','COMUN\utile\web\_webview.vcx'),
# texte a caror sursa de adevar e textul: binarul e gitignored si se genereaza local, nu sunt orfani
[string[]]$OrphanExempt = @('*utile\Teste\achizitie_import\stub_verificare\verificare.sc2'),
[string]$SubDir = '',
[int]$TimeoutSeconds = 60,
[int]$MaxDb2Mb = 5,
[switch]$Force,
[switch]$NoOrphanCleanup,
[switch]$OrphanCleanup
)
$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)."
}
if (-not (Test-Path -LiteralPath $ProjectRoot -PathType Container)) {
throw "ProjectRoot nu exista: $ProjectRoot"
}
. (Join-Path $PSScriptRoot 'vfp_filemap.ps1')
$map = $script:VfpFileMap
$ProjectRoot = (Get-Item -LiteralPath $ProjectRoot).FullName
# extensiile text valide (pt curatare orfani), lowercase
$script:TextExts = @{}
foreach ($k in $map.Keys) { $script:TextExts[$map[$k][1]] = $k } # .vc2 -> .vcx
# set cai absolute (lowercase) ale binarelor scutite de garda roundtrip - doar bin->text valid
$script:ExemptSet = New-Object System.Collections.Generic.HashSet[string]
foreach ($rel in $RoundtripExempt) { [void]$script:ExemptSet.Add((Join-Path $ProjectRoot $rel).ToLower()) }
$runStamp = "{0}-{1}" -f $PID, (Get-Date -Format 'yyyyMMddHHmmssfff')
$stagingRoot = Join-Path $env:TEMP ("git_sync_" + (Split-Path $ProjectRoot -Leaf) + "\$runStamp")
function Invoke-Fb2p {
# toate apelurile trec prin watchdog (timeout + taskkill) - vezi vfp_filemap.ps1
param([string]$InputFile, [string]$Recompile = '')
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
}
return $r
}
function Find-InDir([string]$dir, [string]$name) {
# gaseste un fisier dupa nume (case-insensitive) intr-un folder; $null daca lipseste
if (-not (Test-Path -LiteralPath $dir -PathType Container)) { return $null }
return (Get-ChildItem -LiteralPath $dir -Filter $name -File -ErrorAction SilentlyContinue | Select-Object -First 1)
}
function Test-BytesEqual([byte[]]$a, [byte[]]$b) {
# normalizeaza CRLF/CR->LF si compara ca string Latin1 (byte<->char lossless) - rapid pe fisiere mari;
# bucla per-octet in PowerShell era patologic de lenta (minute/fisier) pe textele de sute de KB.
$enc = [System.Text.Encoding]::GetEncoding(28591)
$sa = $enc.GetString($a) -replace "`r`n?", "`n"
$sb = $enc.GetString($b) -replace "`r`n?", "`n"
return $sa -eq $sb
}
# --- Converteste un binar in staging si copiaza inapoi DOAR textul. Intoarce status string. ---
function Convert-One {
param([Parameter(Mandatory)][System.IO.FileInfo]$Bin)
$ext = $Bin.Extension.ToLower()
$memoX = $map[$ext][0]
$txtX = $map[$ext][1]
$dir = $Bin.DirectoryName
$base = [IO.Path]::GetFileNameWithoutExtension($Bin.Name)
$memo = Find-InDir $dir ($base + $memoX)
# text tinta existent (case-insensitive) pentru incremental + destinatie
$txtExisting = Find-InDir $dir ($base + $txtX)
if (-not $Force -and $txtExisting) {
$newestBin = $Bin.LastWriteTime
if ($memo -and $memo.LastWriteTime -gt $newestBin) { $newestBin = $memo.LastWriteTime }
if ($txtExisting.LastWriteTime -ge $newestBin) { return 'uptodate' }
}
$stageDir = Join-Path $stagingRoot ([Guid]::NewGuid().ToString('N').Substring(0,12))
New-Item -ItemType Directory -Force -Path $stageDir | Out-Null
try {
# cfg in staging (mostenit si de subfolderul rc): DontShowErrors suprima MessageBox-ul propriu
# "End of Process (with errors)"; ShowProgressbar 0 = fara fereastra de progres (rulare headless).
$cfgLines = [System.Collections.Generic.List[string]]@('DontShowErrors: 1','ShowProgressbar: 0')
if ($ext -eq '.dbf') { $cfgLines.Add('DBF_Conversion_Support: 4') } # forteaza exportul DATELOR
Set-Content -LiteralPath (Join-Path $stageDir 'foxbin2prg.cfg') -Value $cfgLines -Encoding Ascii
$stageBin = Join-Path $stageDir $Bin.Name
Copy-Item -LiteralPath $Bin.FullName -Destination $stageBin -Force
if ($memo) { Copy-Item -LiteralPath $memo.FullName -Destination (Join-Path $stageDir $memo.Name) -Force }
if ($ext -eq '.dbc') {
# dbc = tabela cu index structural .dcx; fara el FoxBin2Prg cere indexul intr-un dialog modal (hang)
$dcx = Find-InDir $dir ($base + '.dcx')
if ($dcx) { Copy-Item -LiteralPath $dcx.FullName -Destination (Join-Path $stageDir $dcx.Name) -Force }
}
if ($ext -eq '.dbf') {
# index structural + (daca e tabela legata de DBC) containerul: FoxBin2Prg le cere ca sa deschida tabela
$cdx = Find-InDir $dir ($base + '.cdx')
if ($cdx) { Copy-Item -LiteralPath $cdx.FullName -Destination (Join-Path $stageDir $cdx.Name) -Force }
Get-ChildItem -LiteralPath $dir -File -ErrorAction SilentlyContinue |
Where-Object { '.dbc','.dct','.dcx' -contains $_.Extension.ToLower() } |
ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $stageDir $_.Name) -Force }
# onoreaza order/condition custom din .dbf.cfg comis, daca exista
$cfg = Find-InDir $dir ($base + '.dbf.cfg')
if ($cfg) { Copy-Item -LiteralPath $cfg.FullName -Destination (Join-Path $stageDir $cfg.Name) -Force }
}
$r = Invoke-Fb2p -InputFile $stageBin
if ($r.TimedOut) { return 'fail:timeout bin->text' }
if ($r.ExitCode -ne 0) { return "fail:bin->text ErrorLevel=$($r.ExitCode)" }
$stageTxt = Find-InDir $stageDir ($base + $txtX)
if (-not $stageTxt) { return 'fail:text negenerat' }
if ($stageTxt.Length -eq 0) { return 'fail:text gol' }
# scutite (clase FFC cu #INCLUDE): roundtrip izolat = fals-negativ; doar bin->text valid, fara write-back
$exempt = $script:ExemptSet.Contains($Bin.FullName.ToLower())
# fidelitate per tip: roundtrip complet doar vcx/scx (mai putin cele scutite)
if (($ext -eq '.vcx' -or $ext -eq '.scx') -and -not $exempt) {
$rc2 = Join-Path $stageDir 'rc'
New-Item -ItemType Directory -Force -Path $rc2 | Out-Null
$txt1 = Join-Path $rc2 $stageTxt.Name
Copy-Item -LiteralPath $stageTxt.FullName -Destination $txt1 -Force
$r2 = Invoke-Fb2p -InputFile $txt1 -Recompile $ProjectRoot
if ($r2.TimedOut -or $r2.ExitCode -ne 0) { return "fail:roundtrip text->bin ErrorLevel=$($r2.ExitCode)" }
$bin2 = Find-InDir $rc2 ($base + $ext)
if (-not $bin2) { return 'fail:roundtrip bin nerecreat' }
$r3 = Invoke-Fb2p -InputFile $bin2.FullName
if ($r3.TimedOut -or $r3.ExitCode -ne 0) { return "fail:roundtrip bin->text2 ErrorLevel=$($r3.ExitCode)" }
$txt2 = Find-InDir $rc2 ($base + $txtX)
if (-not $txt2) { return 'fail:roundtrip text2 negenerat' }
if (-not (Test-BytesEqual ([IO.File]::ReadAllBytes($stageTxt.FullName)) ([IO.File]::ReadAllBytes($txt2.FullName)))) {
return 'fail:roundtrip text1 != text2'
}
}
if ($ext -eq '.dbf' -and $stageTxt.Length -gt ($MaxDb2Mb * 1MB)) {
$script:BigDb2 += ("{0} ({1:N1} MB)" -f (Get-RelPath $Bin.FullName), ($stageTxt.Length / 1MB))
}
# copiaza inapoi DOAR textul (raw bytes), pastrand numele produs de FoxBin2Prg
$destTxt = if ($txtExisting) { $txtExisting.FullName } else { Join-Path $dir $stageTxt.Name }
Copy-Item -LiteralPath $stageTxt.FullName -Destination $destTxt -Force
# mtime text >= mtime binar, altfel pare stale si se reconverteste mereu
$destItem = Get-Item -LiteralPath $destTxt
$newestBin = $Bin.LastWriteTime
if ($memo -and $memo.LastWriteTime -gt $newestBin) { $newestBin = $memo.LastWriteTime }
if ($destItem.LastWriteTime -lt $newestBin) { $destItem.LastWriteTime = $newestBin.AddSeconds(1) }
if ($exempt) { return 'ok-exempt' }
return 'ok'
} finally {
Remove-Item -Recurse -Force -LiteralPath $stageDir -ErrorAction SilentlyContinue
}
}
function Get-RelPath([string]$full) {
if ($full.ToLower().StartsWith($ProjectRoot.ToLower())) { return $full.Substring($ProjectRoot.Length).TrimStart('\') }
return $full
}
# =========================== MAIN ===========================
$scanRoot = if ($SubDir) { Join-Path $ProjectRoot $SubDir } else { $ProjectRoot }
if (-not (Test-Path -LiteralPath $scanRoot)) { throw "SubDir nu exista: $scanRoot" }
Write-Host "=== git_sync: ProjectRoot=$ProjectRoot SubDir=$(if($SubDir){$SubDir}else{'(tot)'}) Types=$($Types -join ',') ==="
# --- lista dbf din patternurile per proiect (cai absolute, set pt orfani) ---
$script:DbfInList = New-Object System.Collections.Generic.HashSet[string]
foreach ($pat in $DbfList) {
$full = Join-Path $ProjectRoot $pat
Get-ChildItem -Path $full -File -ErrorAction SilentlyContinue |
Where-Object { $_.Extension.ToLower() -eq '.dbf' } |
ForEach-Object { [void]$script:DbfInList.Add($_.FullName.ToLower()) }
}
# --- construieste lista de binare de convertit ---
$script:BigDb2 = @()
$binList = @()
$typeExts = $Types | ForEach-Object { '.' + $_.ToLower() }
$binList += Get-ChildItem -LiteralPath $scanRoot -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $typeExts -contains $_.Extension.ToLower() }
# dbf-urile din lista aflate sub scanRoot
$binList += $script:DbfInList | ForEach-Object { Get-Item -LiteralPath $_ -ErrorAction SilentlyContinue } |
Where-Object { $_ -and $_.FullName.ToLower().StartsWith(((Get-Item -LiteralPath $scanRoot).FullName).ToLower()) }
$ok=0; $up=0; $failures=@()
foreach ($b in $binList) {
$status = Convert-One -Bin $b
$rel = Get-RelPath $b.FullName
switch -Wildcard ($status) {
'ok' { $ok++; Write-Host "OK $rel" }
'ok-exempt' { $ok++; Write-Host "OK $rel (roundtrip scutit)" }
'uptodate' { $up++ }
'fail:*' { $failures += [PSCustomObject]@{ File=$rel; Reason=$status.Substring(5) }; Write-Warning "ESEC $rel -- $($status.Substring(5))" }
}
}
# --- curatare orfani (doar pe tot arborele) ---
$orphansDeleted = @()
if (-not $SubDir) {
$allText = Get-ChildItem -LiteralPath $ProjectRoot -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $script:TextExts.ContainsKey($_.Extension.ToLower()) }
foreach ($t in $allText) {
$tRel = Get-RelPath $t.FullName
if (@($OrphanExempt | Where-Object { $tRel -like $_ }).Count -gt 0) { continue }
$tExt = $t.Extension.ToLower()
$binExt = $script:TextExts[$tExt]
$base = [IO.Path]::GetFileNameWithoutExtension($t.Name)
$bin = Find-InDir $t.DirectoryName ($base + $binExt)
$isOrphan = $false
if (-not $bin) {
$isOrphan = $true # binar sters
} elseif ($tExt -eq '.db2' -and -not $script:DbfInList.Contains($bin.FullName.ToLower())) {
$isOrphan = $true # tabela a iesit din lista
}
if ($isOrphan) {
$orphansDeleted += $tRel
if ($OrphanCleanup -and -not $NoOrphanCleanup) { Remove-Item -LiteralPath $t.FullName -Force -ErrorAction SilentlyContinue }
}
}
}
Remove-Item -Recurse -Force -LiteralPath $stagingRoot -ErrorAction SilentlyContinue
# --- raport ---
Write-Host ''
Write-Host "--- Sumar git_sync ---"
Write-Host "Convertite: $ok Sarite(la zi): $up Esuate: $($failures.Count)"
if ($orphansDeleted.Count -gt 0) {
$verb = if ($OrphanCleanup -and -not $NoOrphanCleanup) { 'sterse' } else { 'raportate (nesterse)' }
Write-Host "Orfani ${verb}: $($orphansDeleted.Count)"
$orphansDeleted | ForEach-Object { Write-Host " orfan: $_" }
}
if ($script:BigDb2.Count -gt 0) {
Write-Warning "db2 peste ${MaxDb2Mb} MB (scoate tabela din lista, ramane binara):"
$script:BigDb2 | ForEach-Object { Write-Warning " $_" }
}
if ($failures.Count -gt 0) {
Write-Host ''
Write-Host "ESECURI (nu comite pana nu sunt explicate):"
$failures | ForEach-Object { Write-Host " $($_.File) -- $($_.Reason)" }
exit 1
}
Write-Host "OK: fara esecuri."
exit 0

45
roa_sync.ps1 Normal file
View File

@@ -0,0 +1,45 @@
<#
.SYNOPSIS
STUB de compatibilitate: roa_sync.ps1 s-a MUTAT in COMUN\scripts\roa_sync.ps1
(se distribuie automat prin svn update, ca restul COMUN).
.DESCRIPTION
Deleaga la copia proiectului: <ProjectRoot>\COMUN\scripts\roa_sync.ps1. Daca lipseste
(working copy neactualizat inca), face intai svn update pe proiect ca sa o aduca.
Actualizeaza lansatoarele (roa_sync.bat) sa cheme direct COMUN\scripts\roa_sync.ps1.
#>
[CmdletBinding()]
param(
[string]$ProjectRoot = '',
[switch]$GitOnly
)
$ErrorActionPreference = 'Stop'
if (-not $ProjectRoot) {
$cwd = (Get-Location).Path
$hasGit = Test-Path -LiteralPath (Join-Path $cwd '.git')
$hasPjx = @(Get-ChildItem -LiteralPath $cwd -Filter '*.pjx' -File -ErrorAction SilentlyContinue).Count -gt 0
if ($hasGit -and $hasPjx) {
$ProjectRoot = $cwd
} else {
Write-Host "EROARE: -ProjectRoot nedat si directorul curent nu pare radacina de proiect ROA (lipseste .git sau .pjx): $cwd"
exit 1
}
}
$target = Join-Path $ProjectRoot 'COMUN\scripts\roa_sync.ps1'
if (-not (Test-Path -LiteralPath $target)) {
Write-Host "roa_sync.ps1 s-a mutat in COMUN\scripts si lipseste inca din working copy - rulez svn update ca sa il aduc."
& svn update $ProjectRoot
if (-not (Test-Path -LiteralPath $target)) {
Write-Host "EROARE: $target lipseste si dupa svn update. Verifica externalul COMUN."
exit 1
}
}
Write-Host "NOTA: roa_sync.ps1 s-a mutat in COMUN\scripts\. Actualizeaza lansatorul sa cheme direct: $target"
$extra = @()
if ($GitOnly) { $extra += '-GitOnly' }
& powershell -ExecutionPolicy Bypass -NoProfile -File $target -ProjectRoot $ProjectRoot @extra
exit $LASTEXITCODE

72
sync_romfast.bat Normal file
View File

@@ -0,0 +1,72 @@
@echo off
rem Sincronizeaza romfast cu upstream. Fara /push: doar local. Cu /push: trimite pe Gitea.
rem Se auto-copiaza in TEMP si ruleaza de acolo, ca sa nu dispara de pe disc
rem cand scriptul face "git checkout master" (fisierul exista doar pe romfast).
setlocal enabledelayedexpansion
if not "%SYNC_ROMFAST_RELAUNCHED%"=="1" (
set "SYNC_ROMFAST_ORIGDIR=%~dp0"
set "SYNC_ROMFAST_RELAUNCHED=1"
set "SYNC_ROMFAST_TMPBAT=!TEMP!\sync_romfast_!RANDOM!.bat"
copy /y "%~f0" "!SYNC_ROMFAST_TMPBAT!" >nul
call "!SYNC_ROMFAST_TMPBAT!" %*
set "SYNC_ROMFAST_EC=!ERRORLEVEL!"
del "!SYNC_ROMFAST_TMPBAT!" >nul 2>&1
exit /b !SYNC_ROMFAST_EC!
)
cd /d "%SYNC_ROMFAST_ORIGDIR%"
for /f "delims=" %%u in ('git remote get-url origin') do set GITHUB=%%u
for /f "delims=" %%u in ('git remote get-url romfast') do set GITEA=%%u
set STASHED=0
set DIRTY=
for /f "delims=" %%s in ('git status --porcelain') do set DIRTY=1
if defined DIRTY (
echo Ai modificari necomise:
git status -s
set /p ANSWER="Ce fac cu ele? (c = commit, s = stash, n = opresc) [c/s/n] "
if /i "!ANSWER!"=="s" (
git stash push -u -m "sync_romfast auto-stash" -- . ":(exclude)sync_romfast.bat" ":(exclude)sync_romfast.ps1" || exit /b 1
set STASHED=1
) else if /i "!ANSWER!"=="c" (
set /p MSG="Mesaj de commit (Enter = mesaj implicit): "
if "!MSG!"=="" set MSG=Actualizari locale inainte de sincronizare
git add -A
git commit -m "!MSG!"
) else (
echo Oprit. Comite sau fa stash manual, apoi ruleaza din nou scriptul.
exit /b 1
)
)
echo GitHub (origin) %GITHUB% --^> master
echo Gitea (romfast) %GITEA% --^> romfast
git fetch origin || exit /b 1
git checkout master || exit /b 1
git merge --ff-only origin/master || (echo master a divergat de origin/master, verifica manual. & exit /b 1)
git checkout romfast || exit /b 1
git merge master || (echo Conflicte la merge master -^> romfast, rezolva manual ^(git status^). & exit /b 1)
git log --oneline -5
if "%STASHED%"=="1" (
echo Restaurez modificarile din stash...
git stash pop || (echo Conflicte la restaurarea stash-ului, rezolva manual ^(git stash list / git stash pop^). & exit /b 1)
)
set DOPUSH=0
if /i "%~1"=="/push" set DOPUSH=1
if "%DOPUSH%"=="0" (
set /p ANSWER="Local la zi. Fac push acum catre Gitea (romfast)? (y/n) "
if /i "!ANSWER!"=="y" set DOPUSH=1
)
if "%DOPUSH%"=="1" (
git push romfast master || exit /b 1
git push romfast romfast || exit /b 1
echo Push facut catre %GITEA%
) else (
echo Push sarit. Inainte de push: DO ReCreate_FoxBin2Prg.prg in VFP9, apoi ruleaza cu /push.
)

65
sync_romfast.ps1 Normal file
View File

@@ -0,0 +1,65 @@
# Sincronizeaza romfast cu upstream. Fara -Push: doar local. Cu -Push: trimite pe Gitea.
param([switch]$Push)
$ErrorActionPreference = 'Stop'
Set-Location $PSScriptRoot
$GITHUB = git remote get-url origin
$GITEA = git remote get-url romfast
$stashed = $false
$dirty = git status --porcelain
if ($dirty) {
Write-Host "Ai modificari necomise:" -ForegroundColor Yellow
git status -s
$answer = Read-Host "Ce fac cu ele? (c = commit, s = stash, n = opresc) [c/s/n]"
switch ($answer) {
's' {
git stash push -u -m "sync_romfast auto-stash" -- . ":(exclude)sync_romfast.bat" ":(exclude)sync_romfast.ps1"
$stashed = $true
}
'c' {
$msg = Read-Host "Mesaj de commit (Enter = mesaj implicit)"
if (-not $msg) { $msg = "Actualizari locale inainte de sincronizare" }
git add -A
git commit -m $msg
}
default {
Write-Host "Oprit. Comite sau fa stash manual, apoi ruleaza din nou scriptul." -ForegroundColor Red
exit 1
}
}
}
Write-Host "GitHub (origin) $GITHUB --> master" -ForegroundColor Cyan
Write-Host "Gitea (romfast) $GITEA --> romfast" -ForegroundColor Cyan
git fetch origin
git checkout master
git merge --ff-only origin/master
if ($LASTEXITCODE -ne 0) { throw "master a divergat de origin/master, verifica manual." }
git checkout romfast
git merge master
if ($LASTEXITCODE -ne 0) { throw "Conflicte la merge master -> romfast, rezolva manual (git status)." }
git log --oneline -5
if ($stashed) {
Write-Host "Restaurez modificarile din stash..." -ForegroundColor Cyan
git stash pop
if ($LASTEXITCODE -ne 0) { throw "Conflicte la restaurarea stash-ului, rezolva manual (git stash list / git stash pop)." }
}
if (-not $Push) {
$answer = Read-Host "Local la zi. Fac push acum catre Gitea (romfast)? (y/n)"
if ($answer -eq 'y') { $Push = $true }
}
if ($Push) {
git push romfast master
git push romfast romfast
Write-Host "Push facut catre $GITEA" -ForegroundColor Green
} else {
Write-Host "Push sarit. Inainte de push: DO ReCreate_FoxBin2Prg.prg in VFP9, apoi ruleaza cu -Push." -ForegroundColor Yellow
}

256
test_git_sync.ps1 Normal file
View File

@@ -0,0 +1,256 @@
<#
.SYNOPSIS
Teste standalone (fara Pester) pentru git_sync.ps1, pe fixture-uri construite in TEMP.
Acopera cele 4 comportamente noi cerute de plan (Sectiunea 8, decizie eng-review D2):
(A) incremental - a doua rulare pe fixture neschimbat nu reconverteste nimic (Convertite=0, Sarite>0);
(B) curatare orfani - un .vc2 fara binar se sterge si se raporteaza; un .db2 a carui
tabela a iesit din -DbfList se sterge si se raporteaza;
(C) esec partial - un .vcx corupt produce exit nenul si apare in esecuri, DAR fisierele
valide din aceeasi rulare se convertesc totusi;
(D) protectia binarului - dupa orice rulare, toate binarele fixture-ului sunt byte-identice
(SHA256 inainte/dupa). Verificat si dupa fiecare rulare din A/B/C.
(E) scutire roundtrip - fisierul din -RoundtripExempt produce text fara garda de roundtrip;
(F) scutire orfani - textul din -OrphanExempt nu e raportat ca orfan, restul da.
Fixture: copie a celui mai mic .vcx+.vct real din Clase si a setului Locale (dbf+memo+indecsi+
container dbc) din proiectul sursa. NU se scrie nimic in proiectul sursa - doar se citeste/copiaza.
Se copiaza DOAR binarele si memo-urile, niciodata textul .??2 deja prezent in sursa.
.PARAMETER SourceProject
Proiectul din care se copiaza esantioanele de binare (implicit ROACONT).
#>
[CmdletBinding()]
param(
[string]$SourceProject = 'D:\ROA\ROACONT'
)
$ErrorActionPreference = 'Stop'
$script:GitSync = 'D:\ROA\UTIL\foxbin2prg\git_sync.ps1'
$script:Failures = @()
# extensiile considerate "binar" in fixture (tot ce NU e text .??2 generat)
$script:BinExts = @('.vcx','.vct','.dbf','.fpt','.cdx','.dbc','.dct','.dcx')
function Assert([bool]$cond, [string]$msg) {
if ($cond) { Write-Host " PASS: $msg" -ForegroundColor Green }
else { Write-Host " FAIL: $msg" -ForegroundColor Red; $script:Failures += $msg }
}
# --- Ruleaza git_sync.ps1 ca proces copil si parseaza sumarul + orfanii + exit code ---
function Invoke-GitSync {
param([string]$Fixture, [string[]]$DbfList, [string[]]$Exempt, [string[]]$OrphanExempt, [int]$TimeoutSeconds = 30)
# -Command (nu -File): powershell.exe -File colapseaza 'a','b' intr-un singur token, DbfList ar fi gol
$dbfLit = if ($DbfList -and $DbfList.Count) { '@(' + (($DbfList | ForEach-Object { "'" + $_ + "'" }) -join ',') + ')' } else { '@()' }
$exLit = if ($Exempt -and $Exempt.Count) { '@(' + (($Exempt | ForEach-Object { "'" + $_ + "'" }) -join ',') + ')' } else { '@()' }
$cmd = "& '$script:GitSync' -ProjectRoot '$Fixture' -DbfList $dbfLit -RoundtripExempt $exLit -TimeoutSeconds $TimeoutSeconds"
# fara -OrphanExempt explicit ramane valoarea implicita din git_sync.ps1 (testul F o verifica)
if ($OrphanExempt) {
$cmd += ' -OrphanExempt @(' + (($OrphanExempt | ForEach-Object { "'" + $_ + "'" }) -join ',') + ')'
}
# EAP=Continue local: git_sync ruleaza FoxBin2Prg care scrie pe stderr (ex. 'not a table');
# sub EAP=Stop, capturarea 2>&1 a acelui stderr ar arunca NativeCommandError si ar opri testul.
$prevEAP = $ErrorActionPreference
$ErrorActionPreference = 'Continue'
$sw = [Diagnostics.Stopwatch]::StartNew()
$raw = & powershell -ExecutionPolicy Bypass -Command $cmd 2>&1 | Out-String
$exit = $LASTEXITCODE
$sw.Stop()
$ErrorActionPreference = $prevEAP
$conv = $null; $skip = $null; $fail = $null
$m = [regex]::Match($raw, 'Convertite:\s*(\d+)\s+Sarite\(la zi\):\s*(\d+)\s+Esuate:\s*(\d+)')
if ($m.Success) { $conv = [int]$m.Groups[1].Value; $skip = [int]$m.Groups[2].Value; $fail = [int]$m.Groups[3].Value }
$orphans = @()
foreach ($line in ($raw -split "`r?`n")) {
$om = [regex]::Match($line, '^\s*orfan:\s*(.+?)\s*$')
if ($om.Success) { $orphans += $om.Groups[1].Value }
}
return [PSCustomObject]@{ Exit=$exit; Convertite=$conv; Sarite=$skip; Esuate=$fail; Orphans=$orphans; Raw=$raw; ElapsedSec=[math]::Round($sw.Elapsed.TotalSeconds,1); TimeoutSec=$TimeoutSeconds }
}
# --- Snapshot SHA256 al tuturor binarelor din fixture (pt protectia binarului) ---
function Get-BinSnapshot([string]$root) {
$h = @{}
Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $script:BinExts -contains $_.Extension.ToLower() } |
ForEach-Object { $h[$_.FullName.ToLower()] = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash }
return $h
}
function Assert-BinariesUnchanged([hashtable]$before, [string]$root, [string]$ctx) {
$after = Get-BinSnapshot $root
$why = ''
if ($before.Count -ne $after.Count) { $why = "numar binare $($before.Count)->$($after.Count)" }
else {
foreach ($k in $before.Keys) {
if (-not $after.ContainsKey($k)) { $why = "lipsa dupa rulare: $k"; break }
if ($after[$k] -ne $before[$k]) { $why = "modificat: $k"; break }
}
}
Assert ($why -eq '') "${ctx}: toate binarele byte-identice dupa rulare$(if($why){" ($why)"})"
}
# --- Construieste un fixture in TEMP: atentie.vcx+.vct + setul Locale (fara text .??2) ---
function New-Fixture([string]$tag) {
$fx = Join-Path $env:TEMP ("gs_test_{0}_{1}_{2}" -f $tag, $PID, ([Guid]::NewGuid().ToString('N').Substring(0,8)))
New-Item -ItemType Directory -Force -Path (Join-Path $fx 'Clase'), (Join-Path $fx 'Locale') | Out-Null
# doar .vcx + .vct (memo); NU .vc2 care exista deja in sursa dintr-o rulare anterioara
Get-ChildItem (Join-Path $SourceProject 'Clase') -Filter 'atentie.*' -File |
Where-Object { @('.vcx','.vct') -contains $_.Extension.ToLower() } |
ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $fx 'Clase') -Force }
# set Locale: doua tabele (pt scenariul de scoatere din lista) + indecsi + container dbc
foreach ($f in 'locale.dbf','locale.FPT','locale.CDX','locale.dbc','locale.DCT','locale.DCX','locale_lang.dbf','locale_lang.FPT','locale_lang.cdx') {
$s = Join-Path $SourceProject "Locale\$f"
if (Test-Path -LiteralPath $s) { Copy-Item -LiteralPath $s -Destination (Join-Path $fx 'Locale') -Force }
}
return $fx
}
# Sterge fixture-ul + folderul-parinte de staging lasat de git_sync (TEMP\git_sync_<leaf>, ramane gol)
function Remove-Fixture([string]$fx) {
Remove-Item -Recurse -Force -LiteralPath $fx -ErrorAction SilentlyContinue
$stagingParent = Join-Path $env:TEMP ("git_sync_" + (Split-Path $fx -Leaf))
Remove-Item -Recurse -Force -LiteralPath $stagingParent -ErrorAction SilentlyContinue
}
$script:BothDbf = @('Locale\locale.dbf','Locale\locale_lang.dbf')
# ============================== SCENARIILE ==============================
function Test-Incremental {
Write-Host "`n--- (A) incremental: a doua rulare nu reconverteste nimic ---"
$fx = New-Fixture 'inc'
try {
$snap = Get-BinSnapshot $fx
$r1 = Invoke-GitSync -Fixture $fx -DbfList $script:BothDbf
Assert ($r1.Exit -eq 0) "A: prima rulare exit 0 (exit=$($r1.Exit))"
Assert ($r1.Convertite -gt 0) "A: prima rulare converteste (Convertite=$($r1.Convertite) > 0)"
Assert-BinariesUnchanged $snap $fx "A(run1)"
$r2 = Invoke-GitSync -Fixture $fx -DbfList $script:BothDbf
Assert ($r2.Exit -eq 0) "A: a doua rulare exit 0 (exit=$($r2.Exit))"
Assert ($r2.Convertite -eq 0) "A: a doua rulare NU reconverteste (Convertite=$($r2.Convertite) = 0)"
Assert ($r2.Sarite -gt 0) "A: a doua rulare sare fisierele la zi (Sarite=$($r2.Sarite) > 0)"
Assert-BinariesUnchanged $snap $fx "A(run2)"
} finally { Remove-Fixture $fx }
}
function Test-Orphans {
Write-Host "`n--- (B) curatare orfani: .vc2 fara binar + .db2 iesit din lista ---"
$fx = New-Fixture 'orf'
try {
# orfan de tip 1: text .vc2 fara binar .vcx corespondent
$orfVc2 = Join-Path $fx 'Clase\_orfan.vc2'
Set-Content -LiteralPath $orfVc2 -Value '* text orfan fara binar corespondent' -Encoding Ascii
$snap = Get-BinSnapshot $fx
# run1: ambele tabele in lista -> produce db2-uri; orfanul fara binar se sterge
$r1 = Invoke-GitSync -Fixture $fx -DbfList $script:BothDbf
Assert (-not (Test-Path -LiteralPath $orfVc2)) "B: .vc2 orfan (fara binar) sters"
Assert (($r1.Orphans -join ';') -match '(?i)_orfan\.vc2') "B: .vc2 orfan raportat in sumar"
Assert (Test-Path -LiteralPath (Join-Path $fx 'Locale\locale_lang.db2')) "B: locale_lang.db2 creat in run1"
Assert-BinariesUnchanged $snap $fx "B(run1)"
# run2: locale_lang scos din lista -> db2-ul lui iese din lista -> orfan
$r2 = Invoke-GitSync -Fixture $fx -DbfList @('Locale\locale.dbf')
Assert (-not (Test-Path -LiteralPath (Join-Path $fx 'Locale\locale_lang.db2'))) "B: db2 al tabelei iesite din lista sters"
Assert (($r2.Orphans -join ';') -match '(?i)locale_lang\.db2') "B: db2 iesit din lista raportat in sumar"
Assert (Test-Path -LiteralPath (Join-Path $fx 'Locale\locale.db2')) "B: db2 al tabelei ramase in lista pastrat"
Assert-BinariesUnchanged $snap $fx "B(run2)"
} finally { Remove-Fixture $fx }
}
function Test-PartialFailure {
Write-Host "`n--- (C) esec partial: binar corupt esueaza, restul se convertesc ---"
$fx = New-Fixture 'fail'
try {
# binar corupt: octeti garbage in .vcx si .vct (FoxBin2Prg: 'not a table', ErrorLevel != 0)
$g1 = New-Object byte[] 800; (New-Object Random).NextBytes($g1)
[IO.File]::WriteAllBytes((Join-Path $fx 'Clase\corupt.vcx'), $g1)
$g2 = New-Object byte[] 600; (New-Object Random).NextBytes($g2)
[IO.File]::WriteAllBytes((Join-Path $fx 'Clase\corupt.vct'), $g2)
$snap = Get-BinSnapshot $fx
$r = Invoke-GitSync -Fixture $fx -DbfList @('Locale\locale.dbf')
Assert ($r.Exit -ne 0) "C: exit nenul la esec partial (exit=$($r.Exit))"
Assert ($r.Esuate -ge 1) "C: cel putin un esec raportat (Esuate=$($r.Esuate))"
Assert ($r.Raw -match '(?i)corupt\.vcx') "C: fisierul corupt apare in lista de esecuri"
Assert ($r.Convertite -ge 1) "C: fisierele valide se convertesc totusi (Convertite=$($r.Convertite))"
Assert (Test-Path -LiteralPath (Join-Path $fx 'Clase\atentie.vc2')) "C: text valid (atentie.vc2) produs in ciuda esecului"
# esecul trebuie sa fie rapid (ErrorLevel, nu dialog modal): sub watchdog inseamna ca nu a aparut MessageBox VFP
Assert ($r.ElapsedSec -lt $r.TimeoutSec) "C: esec rapid fara dialog modal ($($r.ElapsedSec)s < watchdog $($r.TimeoutSec)s)"
Assert-BinariesUnchanged $snap $fx "C"
} finally { Remove-Fixture $fx }
}
function Test-BinaryProtection {
Write-Host "`n--- (D) protectia binarului: byte-identic dupa conversie completa ---"
$fx = New-Fixture 'bin'
try {
$snap = Get-BinSnapshot $fx
Assert ($snap.Count -gt 0) "D: fixture-ul contine binare de protejat ($($snap.Count))"
$r = Invoke-GitSync -Fixture $fx -DbfList $script:BothDbf
Assert ($r.Exit -eq 0) "D: rulare curata (exit=$($r.Exit))"
Assert-BinariesUnchanged $snap $fx "D"
} finally { Remove-Fixture $fx }
}
function Test-Exempt {
Write-Host "`n--- (E) scutire roundtrip: fisierul din -RoundtripExempt produce text fara garda roundtrip ---"
$fx = New-Fixture 'exempt'
try {
$snap = Get-BinSnapshot $fx
$r = Invoke-GitSync -Fixture $fx -DbfList @('Locale\locale.dbf') -Exempt @('Clase\atentie.vcx')
Assert ($r.Exit -eq 0) "E: rulare curata cu scutire (exit=$($r.Exit))"
Assert ($r.Raw -match '(?i)atentie\.vcx \(roundtrip scutit\)') "E: fisierul scutit raportat 'roundtrip scutit'"
Assert (Test-Path -LiteralPath (Join-Path $fx 'Clase\atentie.vc2')) "E: text produs pentru fisierul scutit"
Assert-BinariesUnchanged $snap $fx "E"
} finally { Remove-Fixture $fx }
}
function Test-OrphanExempt {
Write-Host "`n--- (F) scutire orfani: textul din -OrphanExempt nu e raportat, restul da ---"
$fx = New-Fixture 'orphanexempt'
try {
# textul scutit implicit: stub-ul de test din COMUN, al carui .scx e gitignored si generat local
$stubDir = Join-Path $fx 'COMUN\utile\Teste\achizitie_import\stub_verificare'
New-Item -ItemType Directory -Path $stubDir -Force | Out-Null
$stub = Join-Path $stubDir 'verificare.sc2'
Set-Content -LiteralPath $stub -Value '* stub de test, binarul se genereaza local' -Encoding Ascii
# martor: text fara binar in afara scutirii -> trebuie raportat in continuare
$ctrl = Join-Path $fx 'Clase\_orfan.sc2'
Set-Content -LiteralPath $ctrl -Value '* text orfan fara binar corespondent' -Encoding Ascii
$r = Invoke-GitSync -Fixture $fx -DbfList @('Locale\locale.dbf')
$orf = $r.Orphans -join ';'
Assert ($r.Exit -eq 0) "F: rulare curata cu scutire de orfani (exit=$($r.Exit))"
Assert ($orf -notmatch '(?i)verificare\.sc2') "F: textul scutit NU e raportat ca orfan"
Assert (Test-Path -LiteralPath $stub) "F: textul scutit ramane pe disc"
Assert ($orf -match '(?i)_orfan\.sc2') "F: orfanul din afara scutirii e raportat in continuare"
# cu pattern explicit care nu-l acopera, stub-ul redevine orfan raportat
$r2 = Invoke-GitSync -Fixture $fx -DbfList @('Locale\locale.dbf') -OrphanExempt @('*\nimic_care_nu_exista.sc2')
Assert ((($r2.Orphans -join ';')) -match '(?i)verificare\.sc2') "F: fara pattern-ul potrivit, stub-ul e raportat ca orfan"
} finally { Remove-Fixture $fx }
}
# ============================== MAIN ==============================
if (-not (Test-Path -LiteralPath $script:GitSync)) { throw "Nu gasesc git_sync.ps1: $script:GitSync" }
if (-not (Test-Path -LiteralPath $SourceProject)) { throw "Proiect sursa inexistent: $SourceProject" }
Write-Host "=== test_git_sync.ps1 -- SourceProject=$SourceProject ==="
Test-Incremental
Test-Orphans
Test-PartialFailure
Test-BinaryProtection
Test-Exempt
Test-OrphanExempt
Write-Host "`n=== REZULTAT ==="
if ($script:Failures.Count -eq 0) {
Write-Host "TOATE testele au trecut." -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
}

507
test_roundtrip.ps1 Normal file
View File

@@ -0,0 +1,507 @@
<#
.SYNOPSIS
Teste standalone (fara Pester) pentru fluxul bin<->text VFP (FoxBin2Prg) si scriptul
de write-back txt2vcx.ps1. Parametrizat per proiect (implicit ROAGEST).
.DESCRIPTION
Faze:
- fidelity : esantion din FIECARE tip convertit, auto-descoperit din arborele proiectului.
vcx/scx -> roundtrip complet bin->text1->bin->text2 (comparare pe octeti);
frx/mnx/lbx/dbc/pjx/dbf -> doar validare bin->text (exit code + timeout + text nevid).
Totul in temp, nimic scris in proiect.
- edit : editare reala via txt2vcx.ps1 pe fixture-uri cunoscute (implicit ROAGEST);
daca fixture-ul lipseste in proiect, se sare (warn). Restaurarea binarelor
se face din backup temp (NU git checkout - binarele sunt git-ignored dupa migrare).
- negative : text stricat, fisier gol, guard-uri (staleness, binar lipsa, cale in afara
cache-ului, extensie nesuportata, COMUN, folder-foxbin2prg, -DryRun),
test multi-fisier. Textul stricat ruleaza DOAR in cache temp, niciodata in arbore.
- smoke : fidelity pe cate un .vcx din proiectele frate (-SmokeProjects).
- all : toate cele de mai sus, in ordine.
Restaurare: orice binar atins de write-back e copiat in backup temp INAINTE si restaurat
la final (in finally), cu asertie byte-identic fata de original. Cache-ul text implicit e
un director temporar creat pentru rulare (decuplat de arborele proiectului).
Exit 1 daca orice Assert a esuat, CU EXCEPTIA celor marcate warn-only.
.PARAMETER ProjectRoot
Radacina proiectului principal testat (implicit ROAGEST).
.PARAMETER CacheRoot
Cache-ul text folosit de teste. Gol (implicit) => director temporar creat/sters de test,
ca sa nu se scrie niciodata text in arborele proiectului.
.PARAMETER Phase
all | fidelity | edit | negative | smoke
.PARAMETER SmokeProjects
Lista proiectelor frate pentru faza smoke.
#>
[CmdletBinding()]
param(
[string]$ProjectRoot = 'D:\ROA\ROAGEST',
[string]$CacheRoot = '',
[ValidateSet('all','fidelity','edit','negative','smoke')]
[string]$Phase = 'all',
[string[]]$SmokeProjects = @('D:\ROA\ROAIMOB','D:\ROA\ROAACNPRO')
)
$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:BackupRoot = Join-Path $env:TEMP ("fb2p_test_backup_{0}_{1}" -f $PID, ([Guid]::NewGuid().ToString('N').Substring(0,8)))
$script:Backups = @{} # cheie: cale binar (lower) -> info backup, pt restaurare la final
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 - FoxBin2Prg poate afisa un MessageBox modal nativ VFP la text stricat;
# fara asta, faza negativa ar ramane agatata la infinit intr-o rulare automata.
param([string]$InputFile, [string]$Recompile = '', [int]$TimeoutSeconds = 60)
# cfg in dir-ul input-ului: DontShowErrors suprima MessageBox-ul propriu FoxBin2Prg (ca in git_sync)
$cfgLines = [System.Collections.Generic.List[string]]@('DontShowErrors: 1','ShowProgressbar: 0')
if ([IO.Path]::GetExtension($InputFile).ToLower() -eq '.dbf') { $cfgLines.Add('DBF_Conversion_Support: 4') }
Set-Content -LiteralPath (Join-Path (Split-Path -Parent $InputFile) 'foxbin2prg.cfg') -Value $cfgLines -Encoding Ascii
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
}
function Get-RelPath([string]$Root, [string]$Full) {
return $Full.Substring($Root.TrimEnd('\').Length).TrimStart('\')
}
# --- Auto-descoperire: un esantion mic non-COMUN de tipul cerut, cu memo companion ---
function Find-SampleBinary {
param(
[Parameter(Mandatory)][string]$ProjRoot,
[Parameter(Mandatory)][string]$Ext,
[bool]$RequireMemo = $true,
[int]$MaxBytes = 500000
)
if (-not (Test-Path -LiteralPath $ProjRoot)) { return $null }
$memoExt = $script:VfpFileMap[$Ext][0]
Get-ChildItem -LiteralPath $ProjRoot -Recurse -File -Filter ("*" + $Ext) -ErrorAction SilentlyContinue |
Where-Object {
$_.FullName -notmatch '(?i)\\COMUN\\' -and
$_.Length -le $MaxBytes -and $_.Length -gt 0 -and
(-not $RequireMemo -or (Test-Path -LiteralPath ([IO.Path]::ChangeExtension($_.FullName, $memoExt))))
} |
Sort-Object Length |
Select-Object -First 1
}
# --- Backup/restore binare din temp (inlocuieste git checkout - binarele sunt git-ignored) ---
function Backup-ProjectBinary([string]$fullBin) {
$key = $fullBin.ToLower()
if ($script:Backups.ContainsKey($key)) { return }
$ext = [IO.Path]::GetExtension($fullBin).ToLower()
$memoExt = $script:VfpFileMap[$ext][0]
$dir = Split-Path $fullBin -Parent
$base = [IO.Path]::GetFileNameWithoutExtension($fullBin)
$memoEntry = Get-ChildItem -LiteralPath $dir -Filter ($base + $memoExt) -ErrorAction SilentlyContinue | Select-Object -First 1
$memoFull = if ($memoEntry) { $memoEntry.FullName } else { [IO.Path]::ChangeExtension($fullBin, $memoExt) }
$bdir = Join-Path $script:BackupRoot ([Guid]::NewGuid().ToString('N').Substring(0,8))
New-Item -ItemType Directory -Force -Path $bdir | Out-Null
$binBak = Join-Path $bdir (Split-Path $fullBin -Leaf)
Copy-Item -LiteralPath $fullBin -Destination $binBak -Force
$memoBak = $null
if (Test-Path -LiteralPath $memoFull) {
$memoBak = Join-Path $bdir (Split-Path $memoFull -Leaf)
Copy-Item -LiteralPath $memoFull -Destination $memoBak -Force
}
$script:Backups[$key] = @{ BinFull=$fullBin; BinBak=$binBak; MemoFull=$memoFull; MemoBak=$memoBak; OrigHash=(Get-FileHashBytes $fullBin) }
}
function Restore-ProjectBinaries {
foreach ($k in @($script:Backups.Keys)) {
$b = $script:Backups[$k]
Copy-Item -LiteralPath $b.BinBak -Destination $b.BinFull -Force
if ($b.MemoBak) { Copy-Item -LiteralPath $b.MemoBak -Destination $b.MemoFull -Force }
$now = Get-FileHashBytes $b.BinFull
Assert ($now -eq $b.OrigHash) "Restore: $(Split-Path $b.BinFull -Leaf) byte-identic cu originalul dupa restore"
}
}
# --- Fidelitate: roundtrip complet bin->text1->bin->text2 (doar in temp) - pt vcx/scx ---
function Test-FidelityRoundtrip {
param(
[Parameter(Mandatory)][string]$RelPath,
[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 = $script:VfpFileMap[$ext]
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)) {
if ($WarnOnly) { AssertWarn $false "Faza A ($RelPath): Bin2Prg #1 a esuat (ErrorLevel=$rc1)"; 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
}
}
# --- Validare bin->text (fara roundtrip) - pt frx/mnx/lbx/dbc/pjx/dbf (write-back nesuportat) ---
function Test-BinToTextValid {
param(
[Parameter(Mandatory)][string]$RelPath,
[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 = $script:VfpFileMap[$ext]
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_valid_{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)
Copy-Item -LiteralPath $full -Destination $bin1 -Force
if (Test-Path -LiteralPath $memoFull) { Copy-Item -LiteralPath $memoFull -Destination (Join-Path $work ($baseName + $memoExt)) -Force }
$rc = Invoke-Fb2p -InputFile $bin1
$text1 = Join-Path $work ($baseName + $txtExt)
$ok = ($rc -eq 0) -and (Test-Path -LiteralPath $text1) -and ((Get-Item -LiteralPath $text1).Length -gt 0)
$msg = "Faza A ($RelPath): bin->text valid (exit=$rc, text nevid)"
if ($WarnOnly) { AssertWarn $ok $msg } else { Assert $ok $msg }
} finally {
Remove-Item -Recurse -Force -LiteralPath $work -ErrorAction SilentlyContinue
}
}
# specificatie per tip convertit: roundtrip doar vcx/scx; restul doar validare bin->text
$script:FidelitySpec = @(
@{ Ext='.vcx'; Roundtrip=$true; WarnOnly=$false; RequireMemo=$true }
@{ Ext='.scx'; Roundtrip=$true; WarnOnly=$false; RequireMemo=$true }
@{ Ext='.frx'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$true }
@{ Ext='.mnx'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$true }
@{ Ext='.lbx'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$true }
@{ Ext='.dbc'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$true }
@{ Ext='.pjx'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$true }
@{ Ext='.dbf'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$false }
)
function Invoke-FidelityForProject([string]$ProjRoot) {
foreach ($s in $script:FidelitySpec) {
$sample = Find-SampleBinary -ProjRoot $ProjRoot -Ext $s.Ext -RequireMemo $s.RequireMemo
if (-not $sample) { AssertWarn $false "Faza A: niciun esantion $($s.Ext) in $ProjRoot, sarit"; continue }
$rel = Get-RelPath $ProjRoot $sample.FullName
Write-Host " esantion $($s.Ext): $rel ($([int]($sample.Length/1024)) KB)"
if ($s.Roundtrip) { Test-FidelityRoundtrip -RelPath $rel -ProjRoot $ProjRoot -WarnOnly $s.WarnOnly }
else { Test-BinToTextValid -RelPath $rel -ProjRoot $ProjRoot -WarnOnly $s.WarnOnly }
}
}
# --- Editare reala prin txt2vcx.ps1, cu backup/restore (nu git) ---
function Test-EditRoundtrip {
param(
[Parameter(Mandatory)][string]$RelPath,
[Parameter(Mandatory)][string]$CaptionOld,
[Parameter(Mandatory)][string]$CaptionNew,
[Parameter(Mandatory)][string]$ProcedureMarker
)
$full = Join-Path $ProjectRoot $RelPath
if (-not (Test-Path -LiteralPath $full)) { AssertWarn $false "Faza B: fixture lipsa in proiect, sarit: $RelPath"; return }
$ext = [IO.Path]::GetExtension($RelPath).ToLower()
$pair = $script:VfpFileMap[$ext]
$txtExt = $pair[1]
$relDir = Split-Path $RelPath -Parent
$base = [IO.Path]::GetFileNameWithoutExtension($RelPath)
$relText = if ($relDir) { Join-Path $relDir ($base + $txtExt) } else { $base + $txtExt }
Write-Host "-- Faza B: $RelPath --"
Backup-ProjectBinary $full
$origHash = $script:Backups[$full.ToLower()].OrigHash
& $vcx2txt -Source $full -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null
$cacheText = Join-Path $CacheRoot $relText
Assert (Test-Path -LiteralPath $cacheText) "Faza B ($RelPath): cache text regenerat"
$text = $script:Enc.GetString([IO.File]::ReadAllBytes($cacheText))
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"
if ($idx -ge 0) {
$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))
& $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
$rc = $LASTEXITCODE
Assert ($rc -eq 0) "Faza B ($RelPath): txt2vcx.ps1 exit 0"
Assert ((Get-FileHashBytes $full) -ne $origHash) "Faza B ($RelPath): binarul din proiect a fost rescris (hash diferit de original)"
$bakStray = Join-Path (Join-Path $ProjectRoot $relDir) ($base + '.bak')
Assert (-not (Test-Path -LiteralPath $bakStray)) "Faza B ($RelPath): fara .bak lasat in proiect"
$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"
# binarul e restaurat din backup in finally-ul global
}
# ============================== MAIN ==============================
$ownCache = $false
if ([string]::IsNullOrWhiteSpace($CacheRoot)) {
$CacheRoot = Join-Path $env:TEMP ("fb2p_test_cache_{0}_{1}" -f $PID, ([Guid]::NewGuid().ToString('N').Substring(0,8)))
$ownCache = $true
}
if (-not (Test-Path -LiteralPath $CacheRoot)) { New-Item -ItemType Directory -Force -Path $CacheRoot | Out-Null }
New-Item -ItemType Directory -Force -Path $script:BackupRoot | Out-Null
Write-Host "=== test_roundtrip.ps1 -- ProjectRoot=$ProjectRoot CacheRoot=$CacheRoot Phase=$Phase ==="
# fixture-uri de editare cunoscute (implicit ROAGEST); se sar automat daca lipsesc in proiect
$editFixtures = @(
@{ Rel='Clase\oavize.vcx'; Old='Lb_titlu_alb_b121.Caption = "Date aviz"'; New='Lb_titlu_alb_b121.Caption = "Atentie-diacritice-ok"'; Marker='PROCEDURE Init' }
@{ Rel='Ferestre\fundal.scx'; Old='Caption = ""'; New='Caption = "test"'; Marker='PROCEDURE Show' }
)
try {
if ($Phase -eq 'all' -or $Phase -eq 'fidelity') {
Write-Host "`n--- FAZA A: fidelitate (esantion din fiecare tip convertit) ---"
Invoke-FidelityForProject $ProjectRoot
}
if ($Phase -eq 'all' -or $Phase -eq 'edit') {
Write-Host "`n--- FAZA B: editare reala (write-back) ---"
foreach ($fx in $editFixtures) {
Test-EditRoundtrip -RelPath $fx.Rel -CaptionOld $fx.Old -CaptionNew $fx.New -ProcedureMarker $fx.Marker
}
}
if ($Phase -eq 'all' -or $Phase -eq 'smoke') {
Write-Host "`n--- FAZA C: smoke pe proiecte frate ---"
foreach ($sp in $SmokeProjects) {
if (-not (Test-Path -LiteralPath $sp)) { AssertWarn $false "Faza C: proiect lipsa $sp, sarit"; continue }
$s = Find-SampleBinary -ProjRoot $sp -Ext '.vcx'
if ($s) { Test-FidelityRoundtrip -RelPath (Get-RelPath $sp $s.FullName) -ProjRoot $sp }
else { AssertWarn $false "Faza C: niciun .vcx in $sp, sarit" }
}
}
if ($Phase -eq 'all' -or $Phase -eq 'negative') {
Write-Host "`n--- FAZA D: negativ + guard-uri + multi-fisier ---"
$vcxSample = Find-SampleBinary -ProjRoot $ProjectRoot -Ext '.vcx'
if (-not $vcxSample) {
AssertWarn $false "Faza D: niciun .vcx in $ProjectRoot, faza sarita"
} else {
$vcxRel = Get-RelPath $ProjectRoot $vcxSample.FullName
$vcxFull = $vcxSample.FullName
$vcxDir = Split-Path $vcxRel -Parent
$vcxBase = [IO.Path]::GetFileNameWithoutExtension($vcxRel)
$vcxRelText = if ($vcxDir) { Join-Path $vcxDir ($vcxBase + '.vc2') } else { $vcxBase + '.vc2' }
Write-Host " esantion negativ .vcx: $vcxRel"
Backup-ProjectBinary $vcxFull
$origVcxHash = Get-FileHashBytes $vcxFull # baseline curent (independent de backup) pt asertii "neatins"
& $vcx2txt -Source $vcxFull -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null
$cacheText = Join-Path $CacheRoot $vcxRelText
Assert (Test-Path -LiteralPath $cacheText) "Faza D: text esantion generat pentru $vcxRel"
$origBytes = [IO.File]::ReadAllBytes($cacheText)
# (a) text stricat: adauga o clasa neinchisa -> recompilare esueaza (doar in cache temp)
$text = $script:Enc.GetString($origBytes)
$brokenSuffix = "`r`nDEFINE CLASS _brokenzzz AS Custom`r`n"
[IO.File]::WriteAllBytes($cacheText, $script:Enc.GetBytes($text + $brokenSuffix))
& $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
Assert ($LASTEXITCODE -ne 0) "Faza D (a): txt2vcx exit != 0 pe text stricat (DEFINE CLASS neinchis)"
Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (a): binarul esantion neatins in proiect"
[IO.File]::WriteAllBytes($cacheText, $origBytes)
# (b) fisier text gol
[IO.File]::WriteAllBytes($cacheText, [byte[]]@())
& $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
Assert ($LASTEXITCODE -ne 0) "Faza D (b): txt2vcx exit != 0 pe fisier text gol"
Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (b): binarul esantion neatins in proiect"
[IO.File]::WriteAllBytes($cacheText, $origBytes)
# (guard) staleness: binar mai nou decat textul -> refuz fara -Force
$origMtime = (Get-Item -LiteralPath $vcxFull).LastWriteTime
(Get-Item -LiteralPath $vcxFull).LastWriteTime = (Get-Date).AddDays(1)
try {
[IO.File]::WriteAllBytes($cacheText, $origBytes)
& $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
Assert ($LASTEXITCODE -ne 0) "Faza D (guard staleness): refuz fara -Force cand binarul e mai nou"
Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (guard staleness): binar neatins"
} finally {
(Get-Item -LiteralPath $vcxFull).LastWriteTime = $origMtime
}
# (guard) binar tinta lipsa -> refuz PERMANENT (si cu -Force)
$missingCacheText = Join-Path $CacheRoot $(if ($vcxDir) { Join-Path $vcxDir '_nu_exista_deloc.vc2' } else { '_nu_exista_deloc.vc2' })
New-Item -ItemType Directory -Force -Path (Split-Path $missingCacheText -Parent) | Out-Null
[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
Assert ($LASTEXITCODE -ne 0) "Faza D (guard binar lipsa): refuz chiar si cu -Force"
} finally { Remove-Item -LiteralPath $missingCacheText -Force -ErrorAction SilentlyContinue }
# (guard) cale text in afara CacheRoot -> refuz
$outsideText = Join-Path $env:TEMP ('_outside_' + [Guid]::NewGuid().ToString('N').Substring(0,8) + '.vc2')
[IO.File]::WriteAllBytes($outsideText, $origBytes)
try {
& $txt2vcx -TextFile $outsideText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
Assert ($LASTEXITCODE -ne 0) "Faza D (guard cale in afara CacheRoot): refuz"
} finally { Remove-Item -LiteralPath $outsideText -Force -ErrorAction SilentlyContinue }
# (guard) extensie nesuportata (.mn2)
$unsupported = Join-Path $CacheRoot $(if ($vcxDir) { Join-Path $vcxDir '_fake.mn2' } else { '_fake.mn2' })
[IO.File]::WriteAllBytes($unsupported, $script:Enc.GetBytes("* fake mn2`r`n"))
try {
& $txt2vcx -TextFile $unsupported -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
Assert ($LASTEXITCODE -ne 0) "Faza D (guard extensie nesuportata .mn2): refuz"
} finally { Remove-Item -LiteralPath $unsupported -Force -ErrorAction SilentlyContinue }
# (guard) COMUN fara -AllowComun -> refuz (-DryRun, zero scrieri)
$comunText = Join-Path $CacheRoot 'COMUN\clase\_fake.vc2'
New-Item -ItemType Directory -Force -Path (Split-Path $comunText -Parent) | Out-Null
[IO.File]::WriteAllBytes($comunText, $origBytes)
try {
& $txt2vcx -TextFile $comunText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun
Assert ($LASTEXITCODE -ne 0) "Faza D (guard COMUN, fara -AllowComun): refuz"
} finally { Remove-Item -Recurse -Force -LiteralPath (Split-Path $comunText -Parent) -ErrorAction SilentlyContinue }
# (guard) folder-foxbin2prg -> refuz necondiționat (-DryRun)
$fb2pCache = Join-Path $env:TEMP ('_fb2pguard_' + [Guid]::NewGuid().ToString('N').Substring(0,8))
New-Item -ItemType Directory -Force -Path $fb2pCache | Out-Null
$fb2pText = Join-Path $fb2pCache 'fake.vc2'
[IO.File]::WriteAllBytes($fb2pText, $origBytes)
try {
& $txt2vcx -TextFile $fb2pText -ProjectRoot 'D:\ROA\UTIL\foxbin2prg' -CacheRoot $fb2pCache -DryRun
Assert ($LASTEXITCODE -ne 0) "Faza D (guard folder-foxbin2prg): refuz necondiționat"
} finally { Remove-Item -Recurse -Force -LiteralPath $fb2pCache -ErrorAction SilentlyContinue }
# (-DryRun caz valid): nimic modificat in proiect, exit 0
[IO.File]::WriteAllBytes($cacheText, $origBytes)
& $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun
Assert ($LASTEXITCODE -eq 0) "Faza D (-DryRun, caz valid): exit 0"
Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (-DryRun, caz valid): binar neatins in proiect"
# --- multi-fisier: un scx valid (scris) + vcx stricat (refuzat) in aceeasi rulare ---
$scxSample = Find-SampleBinary -ProjRoot $ProjectRoot -Ext '.scx'
if ($scxSample) {
$scxRel = Get-RelPath $ProjectRoot $scxSample.FullName
$scxFull = $scxSample.FullName
$scxDir = Split-Path $scxRel -Parent
$scxBase = [IO.Path]::GetFileNameWithoutExtension($scxRel)
$scxRelText = if ($scxDir) { Join-Path $scxDir ($scxBase + '.sc2') } else { $scxBase + '.sc2' }
Backup-ProjectBinary $scxFull
$scxOrigMtime = (Get-Item -LiteralPath $scxFull).LastWriteTime
& $vcx2txt -Source $scxFull -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null
$scxCacheText = Join-Path $CacheRoot $scxRelText
$scxText = $script:Enc.GetString([IO.File]::ReadAllBytes($scxCacheText))
$m = [regex]::Match($scxText, "(?m)^\tPROCEDURE \w+\r?\n")
if ($m.Success) {
$pos = $m.Index + $m.Length
$scxText = $scxText.Substring(0,$pos) + "`t`t* test multi`r`n" + $scxText.Substring($pos)
[IO.File]::WriteAllBytes($scxCacheText, $script:Enc.GetBytes($scxText))
}
[IO.File]::WriteAllBytes($cacheText, $script:Enc.GetBytes($text + $brokenSuffix))
& $txt2vcx -TextFile @($scxCacheText, $cacheText) -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
Assert ($LASTEXITCODE -eq 1) "Faza D (multi-fisier): exit agregat = 1 (unul din doua a esuat)"
Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (multi-fisier): vcx stricat neatins in proiect"
Assert (((Get-Item -LiteralPath $scxFull).LastWriteTime) -ne $scxOrigMtime) "Faza D (multi-fisier): scx valid a fost scris in proiect (mtime schimbat)"
[IO.File]::WriteAllBytes($cacheText, $origBytes)
} else {
AssertWarn $false "Faza D (multi-fisier): niciun .scx in $ProjectRoot, sarit"
}
}
}
} finally {
Write-Host "`n--- Cleanup final: restore binare din backup + curatare temp ---"
Restore-ProjectBinaries
if ($ownCache -and (Test-Path -LiteralPath $CacheRoot)) { Remove-Item -Recurse -Force -LiteralPath $CacheRoot -ErrorAction SilentlyContinue }
if (Test-Path -LiteralPath $script:BackupRoot) { Remove-Item -Recurse -Force -LiteralPath $script:BackupRoot -ErrorAction SilentlyContinue }
}
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
}

320
txt2vcx.ps1 Normal file
View File

@@ -0,0 +1,320 @@
<#
.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))

168
vcx2txt.ps1 Normal file
View File

@@ -0,0 +1,168 @@
<#
.SYNOPSIS
Converteste in TEXT (.vc2/.sc2/.fr2/.mn2/.dc2/.lb2) librariile/formele VFP binare,
ca sa poata fi cautat codul din proceduri/metode cu grep.
.DESCRIPTION
Implicit ("project mode"), converteste DOAR fisierele incluse in proiect (citite din .pjx),
nu tot folderul COMUN (care contine multe librarii nefolosite in ROAACNPRO).
Pentru fiecare fisier copiaza binarul + memo-ul (.vct/.sct/...) intr-un folder de cache care
oglindeste structura proiectului (ca sa NU murdareasca arborele SVN), ruleaza FoxBin2Prg.exe
acolo, si pastreaza doar fisierul TEXT. Incremental: sare peste ce e deja la zi.
NU e nevoie de IDE-ul VFP deschis (se foloseste FoxBin2Prg.exe compilat).
.PARAMETER Source
Optional. Daca e dat, ignora proiectul si converteste exact: un fisier .vcx/.scx/... SAU un
folder (recursiv). Folosit pentru conversii punctuale.
.PARAMETER Project
Fisierul .pjx din care se citeste lista de fisiere (implicit roaacnpro.PJX).
.PARAMETER Types
Ce extensii sa includa in project mode (implicit vcx,scx = cod). Ex: -Types vcx,scx,frx,mnx
.PARAMETER ProjectRoot
Radacina proiectului (implicit D:\ROA\ROAACNPRO). Caile din .pjx sunt relative la ea.
.PARAMETER CacheRoot
Unde se scrie textul (implicit D:\ROA\_vfp_textcache\roacnpro).
IMPORTANT: tine cache-ul in AFARA folderului foxbin2prg, altfel ReCreate_FoxBin2Prg.prg
(care face Prg2Bin recursiv pe tot folderul tool-ului) reconverteste .??2-urile din cache in binare.
.PARAMETER Clean
Sterge tot cache-ul inainte de conversie (util dupa ce s-au convertit accidental fisiere extra).
.PARAMETER Force
Reconverteste chiar daca textul exista si e mai nou decat sursa.
.EXAMPLE
# doar fisierele proiectului (recomandat, rapid)
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1
.EXAMPLE
# include si rapoartele si meniurile
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1 -Types vcx,scx,frx,mnx
.EXAMPLE
# conversie punctuala a unei librarii (chiar daca nu e in proiect)
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1 -Source D:\ROA\ROAACNPRO\COMUN\clase\caut.vcx
#>
[CmdletBinding()]
param(
[string]$Source,
[string]$Project = 'D:\ROA\ROAACNPRO\roaacnpro.PJX',
[string[]]$Types = @('vcx','scx'),
[string]$ProjectRoot = 'D:\ROA\ROAACNPRO',
[string]$CacheRoot = 'D:\ROA\_vfp_textcache\roacnpro',
[switch]$Clean,
[switch]$Force
)
$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)."
}
# 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
$ext = $f.Extension.ToLower()
if (-not $map.ContainsKey($ext)) { return 'skip' }
$memoX = $map[$ext][0]; $txtX = $map[$ext][1]
$memo = [IO.Path]::ChangeExtension($f.FullName, $memoX)
$rel = $f.FullName
if ($rel.ToLower().StartsWith($ProjectRoot.ToLower())) {
$rel = $rel.Substring($ProjectRoot.Length).TrimStart('\')
} else { $rel = $f.Name }
$relDir = Split-Path $rel -Parent
$workDir = if ($relDir) { Join-Path $CacheRoot $relDir } else { $CacheRoot }
$txtOut = Join-Path $workDir ([IO.Path]::GetFileNameWithoutExtension($f.Name) + $txtX)
if (-not $Force -and (Test-Path $txtOut) -and ((Get-Item $txtOut).LastWriteTime -ge $f.LastWriteTime)) {
return 'uptodate'
}
if (-not (Test-Path $workDir)) { New-Item -ItemType Directory -Force -Path $workDir | Out-Null }
$binCopy = Join-Path $workDir $f.Name
Copy-Item -LiteralPath $f.FullName -Destination $binCopy -Force
if (Test-Path $memo) { Copy-Item -LiteralPath $memo -Destination (Join-Path $workDir (Split-Path $memo -Leaf)) -Force }
# FoxBin2Prg.EXE e aplicatie GUI: "&" poate preda controlul inainte de final, iar stergerea
# binarului copiat ar omori conversia in curs -> Start-Process -Wait asteapta terminarea reala.
$started = Get-Date
$outLog = Join-Path $workDir ([IO.Path]::GetFileNameWithoutExtension($f.Name) + '.fb2p_out.tmp')
Start-Process -FilePath $exe -ArgumentList "`"$binCopy`" `"`" `"`" `"`" 1 0 1" -Wait -WindowStyle Hidden -RedirectStandardOutput $outLog
$exeOut = ''
if (Test-Path $outLog) { $exeOut = Get-Content -Raw $outLog; Remove-Item -LiteralPath $outLog -Force -ErrorAction SilentlyContinue }
Remove-Item -LiteralPath $binCopy -Force -ErrorAction SilentlyContinue
$memoCopy = Join-Path $workDir (Split-Path $memo -Leaf)
if (Test-Path $memoCopy) { Remove-Item -LiteralPath $memoCopy -Force -ErrorAction SilentlyContinue }
# succes = textul rescris acum SAU exe-ul raporteaza 0 erori (linia "O,P1,E0,S1,X0,<txt>").
# FoxBin2Prg NU actualizeaza mtime cand textul regenerat e identic cu cel existent (ex. binar
# reatins de Project > Build fara schimbari reale) -> sincronizam mtime-ul textului, altfel
# fisierul pare stale si se reconverteste la fiecare rulare.
$fresh = (Test-Path $txtOut) -and ((Get-Item $txtOut).LastWriteTime -ge $started)
if ($fresh -or ((Test-Path $txtOut) -and ($exeOut -match ',E0,'))) {
if ((Get-Item $txtOut).LastWriteTime -lt $f.LastWriteTime) { (Get-Item $txtOut).LastWriteTime = Get-Date }
return 'ok'
}
return 'fail'
}
# --- Construieste lista de fisiere ---
$list = @()
if (-not [string]::IsNullOrWhiteSpace($Source)) {
if (Test-Path $Source -PathType Leaf) { $list = @((Get-Item -LiteralPath $Source).FullName) }
elseif (Test-Path $Source -PathType Container) {
$list = Get-ChildItem -LiteralPath $Source -Recurse -File |
Where-Object { $map.ContainsKey($_.Extension.ToLower()) } |
ForEach-Object { $_.FullName }
} else { throw "Sursa nu exista: $Source" }
} else {
# PROJECT MODE: citeste .pjx -> pj2 si extrage fisierele
if (-not (Test-Path $Project)) { throw "Nu gasesc proiectul: $Project" }
$tmp = Join-Path $env:TEMP ('pjx_' + [IO.Path]::GetFileNameWithoutExtension($Project))
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
$pjxName = [IO.Path]::GetFileName($Project)
Copy-Item -LiteralPath $Project -Destination (Join-Path $tmp $pjxName) -Force
$pjt = [IO.Path]::ChangeExtension($Project, '.pjt')
if (Test-Path $pjt) { Copy-Item -LiteralPath $pjt -Destination (Join-Path $tmp ([IO.Path]::GetFileName($pjt))) -Force }
& $exe (Join-Path $tmp $pjxName) '' '' '' '1' '0' '1' | Out-Null
$pj2 = Join-Path $tmp ([IO.Path]::GetFileNameWithoutExtension($Project) + '.pj2')
if (-not (Test-Path $pj2)) { throw "Conversia .pjx a esuat: $pj2" }
$extPat = ($Types | ForEach-Object { [regex]::Escape($_) }) -join '|'
$rx = [regex]("(?i)\.ADD\('([^']*\.(?:$extPat))'\)")
foreach ($m in $rx.Matches((Get-Content -Raw $pj2))) {
$relp = $m.Groups[1].Value
$abs = Join-Path $ProjectRoot $relp
if (Test-Path $abs) { $list += (Get-Item -LiteralPath $abs).FullName }
else { Write-Warning "lipsa: $relp" }
}
$list = $list | Sort-Object -Unique
}
if ($Clean -and (Test-Path $CacheRoot)) { Remove-Item -Recurse -Force $CacheRoot }
if (-not $list) { Write-Host 'Nimic de convertit.'; return }
$ok=0;$up=0;$fail=0
foreach ($p in $list) {
switch (Convert-One $p) {
'ok' { $ok++; $r = $p; if ($r.ToLower().StartsWith($ProjectRoot.ToLower())) { $r = $r.Substring($ProjectRoot.Length).TrimStart('\') }; Write-Host "OK $r" }
'uptodate' { $up++ }
'fail' { $fail++; Write-Warning "ESEC $p" }
}
}
Write-Host ''
Write-Host "Gata. Convertite: $ok Sarite(la zi): $up Esuate: $fail (tipuri: $($Types -join ','))"
Write-Host "Text in: $CacheRoot"
Write-Host "Cauta cu: grep -rn 'expresie' `"$CacheRoot`""

81
vfp_filemap.ps1 Normal file
View File

@@ -0,0 +1,81 @@
<#
.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')
'.pjx' = @('.pjt', '.pj2'); '.dbf' = @('.fpt', '.db2')
}
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 }
}

321
vfp_symbols.ps1 Normal file
View File

@@ -0,0 +1,321 @@
<#
.SYNOPSIS
Index de simboluri (clase, metode, proceduri) peste cache-ul text VFP si peste .prg-urile
proiectului. Raspunde la "in ce clasa/metoda e linia N" si "unde e definit X".
.DESCRIPTION
Grep-ul gaseste textul, dar intr-un .vc2 de 18.000 de linii nu spune in ce metoda a carei clase
a nimerit (header-ul clasei poate fi cu 13.000 de linii mai sus, iar ~2/3 din fisier sunt
proprietati si metadata, nu cod). Scriptul construieste un index cu intervalul de linii al
fiecarei clase/metode si il foloseste ca sa etichete rezultatele.
Indexul e derivat 100% din cache-ul text (vezi vcx2txt.ps1) - se poate sterge oricand.
.PARAMETER Grep
Cauta expresia (regex) in fisierele indexate si eticheteaza fiecare rezultat cu clasa.metoda.
.PARAMETER CodeOnly
Cu -Grep: arata doar potrivirile din corpul metodelor/procedurilor (taie metadata si listele
de proprietati din .vc2/.sc2).
.PARAMETER Where
"fisier:linie" -> ce clasa/metoda contine linia respectiva.
.PARAMETER Find
Numele unui simbol (accepta * si ?) -> unde e DEFINIT (nu si unde e apelat).
.PARAMETER Class
Numele unei clase -> lantul de mostenire + metodele ei, cu intervalele de linii.
.PARAMETER Build
Reconstruieste indexul (implicit se reconstruieste automat cand e invechit).
.EXAMPLE
# construieste/actualizeaza indexul
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\vfp_symbols.ps1
.EXAMPLE
# cautare etichetata, doar in cod
... vfp_symbols.ps1 -Grep 'calcul_cheiaj' -CodeOnly
.EXAMPLE
... vfp_symbols.ps1 -Where 'oacnpro.vc2:14937'
... vfp_symbols.ps1 -Find 'do_calcul_*'
... vfp_symbols.ps1 -Class frm_calcul_cheiaj
#>
[CmdletBinding()]
param(
[string]$Grep,
[switch]$CodeOnly,
[string]$Where,
[string]$Find,
[string]$Class,
# implicit: fluxul in-tree din ROAACNPRO (textul .vc2/.sc2 sta langa binare, nu intr-un cache)
[string]$CacheRoot = 'D:\ROA\ROAACNPRO',
[string]$ProjectRoot = 'D:\ROA\ROAACNPRO',
[string[]]$PrgDirs = @('Programe', 'COMUN\programe'),
[string]$ExcludePrg = '\\programe\\roa\\',
[string]$IndexFile,
[switch]$Build,
[switch]$Stats
)
$ErrorActionPreference = 'Stop'
if (-not $IndexFile) {
# la fluxul in-tree indexul nu se scrie in repo, ci in zona de cache comuna
$IndexFile = if ($CacheRoot -like 'D:\ROA\_vfp_textcache\*') { Join-Path $CacheRoot '_symbols.tsv' }
else { Join-Path ('D:\ROA\_vfp_textcache\' + (Split-Path $ProjectRoot -Leaf).ToLower()) '_symbols.tsv' }
}
# Codepage-ul din antetul .vc2 nu e de incredere (vezi CLAUDE.md), dar aici se citesc doar
# cuvinte-cheie si identificatori ASCII, iar fisierele nu sunt niciodata rescrise.
$cp1252 = [System.Text.Encoding]::GetEncoding(1252)
# .vc2/.sc2 = clase/forme convertite din binar; .prg = sursa directa (are si Define Class, ex. roa.prg)
$rxClass = [regex]'(?i)^\s*DEFINE\s+CLASS\s+([\w]+)\s+AS\s+([\w\.]+)\s*(?:OF\s+"?([^"]*?)"?\s*)?$'
$rxEndDef = [regex]'(?i)^\s*ENDDEFINE\b'
$rxProc = [regex]'(?i)^\s*(?:(?:HIDDEN|PROTECTED)\s+)*(?:PROCEDURE|FUNCTION)\s+([\w\.]+)'
$rxEndPrc = [regex]'(?i)^\s*(?:ENDPROC|ENDFUNC)\b'
function Get-SourceFiles {
$files = @()
# -Include e ignorat cu -LiteralPath in PS 5.1 -> filtrarea se face cu Where-Object
if (Test-Path $CacheRoot) {
$files += Get-ChildItem -LiteralPath $CacheRoot -Recurse -File |
Where-Object { $_.Extension -match '(?i)^\.(vc2|sc2)$' }
}
foreach ($d in $PrgDirs) {
$full = Join-Path $ProjectRoot $d
if (Test-Path $full) {
$files += Get-ChildItem -LiteralPath $full -Recurse -File -Filter *.prg |
Where-Object { $_.FullName -notmatch '\\\.svn\\' -and
($ExcludePrg -eq '' -or $_.FullName -notmatch "(?i)$ExcludePrg") }
}
}
$files
}
function Parse-File($file) {
$lines = [System.IO.File]::ReadAllLines($file.FullName, $cp1252)
$recs = New-Object System.Collections.ArrayList
$cls = $null; $clsLine = 0; $clsParent = ''; $clsOf = ''
$prc = $null; $prcLine = 0
# inchide procedura curenta la linia $end si o adauga in lista
$closeProc = {
param($end)
if ($prc) {
$q = if ($cls) { "$cls.$prc" } else { $prc }
$k = if ($cls) { 'method' } else { 'proc' }
[void]$recs.Add([pscustomobject]@{
kind = $k; qname = $q; class = [string]$cls; member = $prc
file = $file.FullName; line = $prcLine; endline = $end
parent = ''; parentfile = ''
})
$script:prcClosed = $true
}
}
for ($i = 0; $i -lt $lines.Length; $i++) {
$ln = $lines[$i]
$num = $i + 1
$t = $ln.TrimStart()
if ($t.Length -eq 0 -or $t[0] -eq '*') { continue } # comentariile nu deschid blocuri
$m = $rxClass.Match($ln)
if ($m.Success) {
& $closeProc ($num - 1); $prc = $null
if ($cls) {
[void]$recs.Add([pscustomobject]@{
kind='class'; qname=$cls; class=$cls; member=''
file=$file.FullName; line=$clsLine; endline=($num-1)
parent=$clsParent; parentfile=$clsOf })
}
$cls = $m.Groups[1].Value; $clsLine = $num
$clsParent = $m.Groups[2].Value; $clsOf = $m.Groups[3].Value
continue
}
if ($rxEndDef.IsMatch($ln)) {
& $closeProc ($num - 1); $prc = $null
if ($cls) {
[void]$recs.Add([pscustomobject]@{
kind='class'; qname=$cls; class=$cls; member=''
file=$file.FullName; line=$clsLine; endline=$num
parent=$clsParent; parentfile=$clsOf })
$cls = $null
}
continue
}
$m = $rxProc.Match($ln)
if ($m.Success) {
& $closeProc ($num - 1) # VFP permite proceduri fara ENDPROC
$prc = $m.Groups[1].Value; $prcLine = $num
continue
}
if ($rxEndPrc.IsMatch($ln) -and $prc) {
& $closeProc $num; $prc = $null
continue
}
}
& $closeProc $lines.Length
if ($cls) {
[void]$recs.Add([pscustomobject]@{
kind='class'; qname=$cls; class=$cls; member=''
file=$file.FullName; line=$clsLine; endline=$lines.Length
parent=$clsParent; parentfile=$clsOf })
}
$recs
}
function Build-Index {
$src = Get-SourceFiles
if (-not $src) { throw "Nu am gasit surse. Ruleaza intai vcx2txt.ps1 (cache: $CacheRoot)." }
$all = New-Object System.Collections.ArrayList
foreach ($f in $src) { foreach ($r in (Parse-File $f)) { [void]$all.Add($r) } }
$dir = Split-Path $IndexFile -Parent
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
$all | Select-Object kind, qname, class, member, file, line, endline, parent, parentfile |
Export-Csv -LiteralPath $IndexFile -Delimiter "`t" -NoTypeInformation -Encoding UTF8
$nc = ($all | Where-Object kind -eq 'class').Count
$nm = ($all | Where-Object kind -eq 'method').Count
$np = ($all | Where-Object kind -eq 'proc').Count
Write-Host "Index: $($src.Count) fisiere -> $nc clase, $nm metode, $np proceduri"
Write-Host "Scris in: $IndexFile"
$all
}
function Test-IndexStale {
if (-not (Test-Path $IndexFile)) { return $true }
# acelasi -IndexFile poate veni dintr-o alta radacina (alt -CacheRoot): datele vechi nu par invechite
$head = Get-Content -LiteralPath $IndexFile -TotalCount 2
if ($head.Count -ge 2) {
$cols = $head[1].Split("`t")
if ($cols.Count -gt 4) {
$src = $cols[4].Trim('"')
if (-not ($src.StartsWith($CacheRoot, 'OrdinalIgnoreCase') -or
$src.StartsWith($ProjectRoot, 'OrdinalIgnoreCase'))) { return $true }
}
}
$idx = (Get-Item $IndexFile).LastWriteTime
foreach ($f in (Get-SourceFiles)) { if ($f.LastWriteTime -gt $idx) { return $true } }
$false
}
function Get-Index {
if ($Build -or (Test-IndexStale)) { return Build-Index }
Import-Csv -LiteralPath $IndexFile -Delimiter "`t" | ForEach-Object {
$_.line = [int]$_.line; $_.endline = [int]$_.endline; $_
}
}
# Indexul se grupeaza o singura data pe fisier: cautarea liniara prin toate inregistrarile
# la fiecare potrivire face -Grep de ~100x mai lent pe simboluri des folosite (ex. goExecutor).
$script:ownerIdx = $null
function Init-OwnerIndex($index) {
$h = @{}
foreach ($r in $index) {
$k = $r.file.ToLower()
if (-not $h.ContainsKey($k)) { $h[$k] = New-Object System.Collections.ArrayList }
[void]$h[$k].Add($r)
}
$script:ownerIdx = $h
}
# proprietarul unei linii: metoda bate clasa (intervalul cel mai strans)
function Get-Owner($file, $lineNo) {
$k = $file.ToLower()
$recs = $script:ownerIdx[$k]
if (-not $recs) {
foreach ($kk in $script:ownerIdx.Keys) { # accepta si doar numele fisierului
if ($kk.EndsWith('\' + $k)) { $recs = $script:ownerIdx[$kk]; break }
}
}
if (-not $recs) { return $null }
$best = $null; $bestSpan = [int]::MaxValue
foreach ($r in $recs) {
if ($r.line -le $lineNo -and $r.endline -ge $lineNo) {
$span = $r.endline - $r.line
if ($span -lt $bestSpan) { $bestSpan = $span; $best = $r }
}
}
$best
}
# --- moduri ---
if ($Where) {
$idx = Get-Index
if ($Where -notmatch '^(?<f>.+?):(?<l>\d+)\s*$') { throw "Format asteptat: 'fisier:linie'" }
Init-OwnerIndex $idx
$o = Get-Owner $Matches['f'] ([int]$Matches['l'])
if ($o) { "{0} [{1}] {2}:{3}-{4}" -f $o.qname, $o.kind, (Split-Path $o.file -Leaf), $o.line, $o.endline }
else { "(in afara oricarei clase/metode - metadata sau cod la nivel de fisier)" }
return
}
if ($Find) {
$idx = Get-Index
# numele unei clase intoarce definitia ei, nu toti membrii (pentru membri: -Class)
$hits = $idx | Where-Object { $_.member -like $Find -or $_.qname -like $Find -or
($_.kind -eq 'class' -and $_.class -like $Find) }
if (-not $hits) { Write-Host "Nicio definitie pentru '$Find'."; return }
$hits | Sort-Object kind, qname | ForEach-Object {
"{0,-7} {1,-45} {2}:{3}" -f $_.kind, $_.qname, $_.file.Replace($CacheRoot + '\', '').Replace($ProjectRoot + '\', ''), $_.line
}
return
}
if ($Class) {
$idx = Get-Index
$c = $idx | Where-Object { $_.kind -eq 'class' -and $_.class -eq $Class } | Select-Object -First 1
if (-not $c) { Write-Host "Clasa '$Class' nu e in index."; return }
Write-Host "Lant de mostenire:"
$cur = $c; $depth = 0; $seen = @{}
while ($cur -and $depth -lt 12) {
"{0}{1} ({2}:{3})" -f (' ' * $depth), $cur.class, (Split-Path $cur.file -Leaf), $cur.line
if (-not $cur.parent -or $seen[$cur.parent]) { break }
$seen[$cur.class] = $true
$next = $idx | Where-Object { $_.kind -eq 'class' -and $_.class -eq $cur.parent } | Select-Object -First 1
if (-not $next) { "{0}{1} (clasa de baza VFP sau librarie neindexata: {2})" -f (' ' * ($depth+1)), $cur.parent, $cur.parentfile; break }
$cur = $next; $depth++
}
Write-Host ''
Write-Host "Metode proprii ($($c.class)):"
$idx | Where-Object { $_.kind -eq 'method' -and $_.class -eq $Class } | Sort-Object line |
ForEach-Object { " {0,-50} {1}-{2}" -f $_.member, $_.line, $_.endline }
return
}
if ($Grep) {
$idx = Get-Index
$files = Get-SourceFiles
$res = $files | Select-String -Pattern $Grep -Encoding default
if (-not $res) { Write-Host "Nicio potrivire pentru '$Grep'."; return }
Init-OwnerIndex $idx
$shown = 0; $hidden = 0
foreach ($r in $res) {
$o = Get-Owner $r.Path $r.LineNumber
if (-not $o -or $o.kind -eq 'class') {
if ($CodeOnly) { $hidden++; continue }
}
$owner = if ($o -and $o.kind -ne 'class') { $o.qname } elseif ($o) { "$($o.qname) (metadata)" } else { '(nivel fisier)' }
$rel = $r.Path.Replace($CacheRoot + '\', '').Replace($ProjectRoot + '\', '')
"{0}:{1}: [{2}] {3}" -f $rel, $r.LineNumber, $owner, $r.Line.Trim()
$shown++
}
Write-Host ''
Write-Host "$shown potriviri$(if ($hidden) { " ($hidden ascunse: metadata/proprietati)" })"
return
}
if ($Stats) {
$idx = Get-Index
$idx | Group-Object kind | Sort-Object Count -Descending | ForEach-Object { "{0,-8} {1}" -f $_.Name, $_.Count }
return
}
Build-Index | Out-Null