Reverse engineering di 9253000117a.prt.35: contenitore (offset dall'ultimo campo dell'intestazione, indice a catena), grammatica completa delle sezioni (id progressivi dei campi, tabella "dopo X viene Y", marcatori di chiusura), tabella dei double verificata sulla norma delle matrici, sezioni compresse e catalogo del contenuto. Parser Python in scripts/psb/. docs/history.md raccoglie stato, metodo e punti aperti per riprendere il lavoro. Co-Authored-By: Claude <noreply@anthropic.com>
161 lines
10 KiB
Markdown
161 lines
10 KiB
Markdown
# Reverse engineering dei file Creo: stato e ripresa
|
||
|
||
Diario di passaggio di consegne per il lavoro sul formato dei `.prt`/`.asm`. Serve a chi riprende
|
||
(anche in un'altra sessione) per sapere **dove siamo, cosa è provato, da dove ripartire**, senza
|
||
rifare la strada. Il *risultato* del lavoro sta in [`formato-psb.md`](formato-psb.md): qui c'è
|
||
solo lo stato, le decisioni e i vicoli ciechi. Quando un punto si chiude, il contenuto va nel doc
|
||
di formato e qui resta una riga.
|
||
|
||
Ultimo aggiornamento: 2026-11-10, ramo `reverse-engineering-creo`.
|
||
|
||
## Stato in una riga
|
||
|
||
Il formato di serializzazione (PSB) è decodificato e tutte le sezioni in chiaro di
|
||
`9253000117a.prt.35` si leggono fino all'ultimo byte con nome e valore di ogni campo; **nessun
|
||
valore è stato ancora confrontato con Creo**, e tre cose bloccano "tutti i dati senza Creo"
|
||
(vedi "Cosa manca").
|
||
|
||
## Da non perdere
|
||
|
||
- **Niente è committato.** In sospeso: `docs/formato-psb.md`, `docs/history.md`, `scripts/psb/`,
|
||
e le modifiche a `CHANGELOG.md`, `docs/README.md`, `docs/architettura.md`,
|
||
`docs/formato-prt-geometria.md`.
|
||
- `9253000117a.prt.35` è nella root, non tracciato: è il campione di tutto il lavoro. Decidere
|
||
se tenerlo nel repo (2 MB) o in `raw/`.
|
||
- `scripts/psb/__pycache__/` va ignorato, non committato.
|
||
- `docs/formato-prt-feature.md` contiene byte NUL: gli strumenti di modifica lo rifiutano. Le
|
||
correzioni che lo riguardano sono in `formato-psb.md` §9, non applicate al file. Va
|
||
ripulito dai NUL (conservando la codifica) prima di poterlo correggere.
|
||
|
||
## Come ripartire in cinque minuti
|
||
|
||
Dalla root del repo, con Python 3.12 (solo libreria standard):
|
||
|
||
```powershell
|
||
python scripts/psb/prt.py 9253000117a.prt.35 # indice delle sezioni
|
||
python scripts/psb/psb.py 9253000117a.prt.35 '*' out # dump di ogni sezione in out/
|
||
python scripts/psb/psb.py 9253000117a.prt.35 LargeText - t # traccia campo per campo (debug)
|
||
python scripts/psb/unz.py 9253000117a.prt.35 out # decomprime le sezioni .Z
|
||
python scripts/psb/pbin.py out/SolidPrimdata.bin # legge una sezione decompressa
|
||
python scripts/psb/batch.py "raw/*" # regressione sugli 89 file
|
||
python scripts/psb/norms.py 9253000117a.prt.35 # controllo norma 1 delle matrici
|
||
python scripts/psb/tagmap.py 9253000117a.prt.35 "raw/*" # tag double -> prefisso, dalle norme
|
||
python scripts/psb/ctx.py raw/x.prt.1,VisibGeom,cf8 # hexdump attorno a un offset
|
||
```
|
||
|
||
Valori attesi oggi, da usare come regressione prima e dopo ogni modifica al parser:
|
||
|
||
| Controllo | Atteso |
|
||
|---|---|
|
||
| `psb.py 9253000117a.prt.35` | 29 sezioni `OK`, 5 no: `AllFeatur` (`pop underflow` sull'ultimo byte), `MdlStatus` e `FeatDefs` (pila non a zero), più l'intestazione delle due ultime |
|
||
| `norms.py` | `good rows 5397 bad 0` |
|
||
| `batch.py "raw/*"` | 2152 sezioni lette, 74 no (misurato **prima** delle ultime correzioni ai tag: va rilanciato) |
|
||
|
||
Se `norms.py` dà righe "bad" dopo una modifica, si è rotta la tabella dei `double`: è il
|
||
controllo più sensibile che abbiamo.
|
||
|
||
## Cosa è provato, e come
|
||
|
||
| Cosa | Prova |
|
||
|---|---|
|
||
| Contenitore (offset = ultimo campo della prima riga, riga `#Nome` inclusa, indice a catena) | tutte le sezioni cominciano con `e0 00 <radice>` |
|
||
| Grammatica e tabella "dopo X viene Y" | lettura fino all'ultimo byte di tutte le sezioni; l'id nell'intestazione degli array coincide con quello calcolato |
|
||
| Tag dei `double` | norma 1 su 5397 righe; prefissi `3fd0`-`3fef` e `bfd0`-`bfef` verificati uno a uno sugli 89 file |
|
||
| Costanti π, π/2, 2π, 3π/2 e negative | stesso rumore di mantissa nelle forme modificate (`02 xx ...`) |
|
||
| Sezioni `.Z` | la dimensione decompressa coincide con quella dichiarata nell'indice |
|
||
| Significato dei campi | **nessuna**: dedotto dai nomi |
|
||
|
||
## Cosa manca, in ordine di valore
|
||
|
||
1. **Confronto con Creo.** Aprire un file piccolo, annotare quote, parametri, massa, posizione
|
||
di una faccia, e confrontare con il dump. Finché non c'è, tutto il doc è "livello 1 e 2".
|
||
2. **Tabella dei `float` (tag `50`-`df`).** È una tabella di prefissi scelti per frequenza, non
|
||
una formula. Il parser usa `FBASE` lineare, **sbagliato** per quei tag (lunghezza giusta,
|
||
valore no). Senza, la mesh di `SolidPrimdata` (`mv_p_NxNyNzxyz`) non si esporta. Metodo che
|
||
ha funzionato per le 29 voci note: normali unitarie, risolvere il modulo dalla norma. Va
|
||
ripetuto su tutti i file di `raw/` (ognuno ha `SolidPrimdata`), poi sostituire `FBASE` con
|
||
un dizionario e far fallire il parser sui tag ignoti invece di inventare.
|
||
3. **La pila dei contenitori.** I valori sono giusti, l'annidamento no in alcuni punti:
|
||
- `LargeText` di 24 file di `raw/`: alberi bilanciati (`lesser_child`, `greater_child`,
|
||
`balance_factor`, `backup_size`). La regola `post` (puntatore "al successivo" scritto al
|
||
livello del padre) copre un solo campo per struttura; qui i figli sono due.
|
||
- `srf_flip_dat` (geometria), `end2` (`FeatDefs`): "contenitore non in pila".
|
||
- fine di `AllFeatur`/`MdlStatus`/`FeatDefs`: pila non a zero.
|
||
Punto di partenza: `default_close` e `locate` in `psb.py`, con la traccia (`- t`).
|
||
4. **Sezioni tipo `b`** (`IndentTag`, `FeatRefData`) e **tipo `11` Wildfire**: illeggibili.
|
||
Stessa lunghezza del chiaro, primi 4 byte in chiaro. Già escluso (vedi
|
||
`formato-prt-feature.md` §1.3): XOR a chiave fissa, zlib, LZW. Non riprovare senza una
|
||
coppia chiaro/cifrato dello stesso modello.
|
||
5. **`DispDataTable`** (tipo `c`): comincia con `1f 9d 10` ma l'LZW standard fallisce al
|
||
secondo codice. Non indagato.
|
||
6. **`PSB_Tokens`** (`ND:0:*`): letti i quattro array, non il significato delle lettere di
|
||
`type_har`. Ipotesi da cui partire: `I` consuma un intero, `F` un double, una lettera per le
|
||
stringhe; `M`/`O` aprono oggetto e campo. Contare le occorrenze contro le lunghezze degli
|
||
array la conferma o la smentisce in pochi minuti.
|
||
7. **Costanti `double` ignote**: `04` (vista come componente di tangente e come estremo di
|
||
inviluppo), `0d` (−0,5 da un solo caso), buco `bffe` contro `bffd` non deciso.
|
||
8. **Portare il parser in C#** accanto ad `AsmRecordReader`: solo dopo i punti 1 e 3.
|
||
|
||
## Il ragionamento da adottare
|
||
|
||
È il metodo che ha portato fin qui. Va seguito anche per i punti aperti: ogni volta che è stato
|
||
saltato, si è perso tempo.
|
||
|
||
1. **Partire dal modello di chi scrive, non dai byte.** Il file è l'uscita di un serializzatore
|
||
generico che percorre strutture C. La domanda utile non è "cosa significano questi byte" ma
|
||
"che regola semplice userebbe un programma per scriverli". Ogni marcatore ridondante (`f7`,
|
||
`f0`-`f4`) esiste perché il lettore, da solo, avrebbe previsto un'altra cosa: capire la
|
||
previsione spiega il marcatore.
|
||
2. **Un'ipotesi alla volta, e un controllo che la possa smentire.** Prima di scrivere codice
|
||
dire cosa ci si aspetta di vedere se l'ipotesi è vera. Esempio: "se gli id sono progressivi,
|
||
l'id nell'intestazione di `unit_arr` deve essere 0x14" (contato a mano: lo era).
|
||
3. **Il parser è lo strumento di misura.** Una regola è buona se fa arrivare la lettura
|
||
all'ultimo byte di *tutte* le sezioni; l'errore dice dove guardare. Si corregge la regola
|
||
generale, mai il caso singolo: un'eccezione scritta per una sezione ha sempre rotto un'altra.
|
||
4. **Cercare i controesempi, non le conferme.** Quando una regola spiega cinque casi, elencare
|
||
per iscritto i casi noti (con i byte) e verificare che li spieghi tutti, compreso quello
|
||
scomodo. La regola sulle chiusure implicite è nata mettendo in tabella dieci casi e cercando
|
||
l'unica formulazione che non ne contraddiceva nessuno.
|
||
5. **Ancorare i numeri a vincoli fisici, non all'aspetto.** Un valore "plausibile" non è una
|
||
prova. Lo sono: la norma 1 delle righe di una matrice di rotazione, valori noti (π, √2/2,
|
||
0,1, 25,4), la coerenza tra sezioni (lo stesso punto scritto due volte). Per i tag dei
|
||
numeri: far risolvere il prefisso al vincolo (`tagmap.py`), non estrapolare una formula.
|
||
6. **Separare ciò che è provato da ciò che è dedotto.** Tokenizzazione (sequenza campo-valore),
|
||
annidamento e significato dei campi sono tre cose con tre livelli di prova diversi. Nel doc
|
||
ognuna va dichiarata col suo livello; "regge sui campioni" non è "è così".
|
||
7. **Allargare il campione appena una regola regge.** Un file solo nasconde le tabelle: i buchi
|
||
nei prefissi negativi e la tabella dei `float` sono emersi solo confrontando più file.
|
||
Dopo ogni modifica: `norms.py` sul campione, poi `batch.py` su `raw/`.
|
||
8. **Quando due spiegazioni reggono entrambe, cercare il dato che le distingue** invece di
|
||
scegliere la più elegante (esempio: i tag `91`-`a0` sembravano negativi; li ha decisi una
|
||
curva i cui parametri dovevano crescere da 3π/2).
|
||
9. **Scrivere subito nel doc, con il sintomo e i byte.** Una scoperta non scritta si rifà.
|
||
Scrivere anche ciò che è stato escluso e perché.
|
||
10. **Dire quello che non si sa.** Una costante ignota resta "ignota" (`<c04>` nel dump), non
|
||
riceve un valore inventato; un parser che non capisce si ferma, non restituisce un risultato
|
||
parziale in silenzio.
|
||
|
||
## Vicoli ciechi e trappole, per non rifarli
|
||
|
||
- **Modello "pop = chiudi N livelli" puro**: non basta. I marcatori `f0`-`f4` compaiono solo
|
||
quando la chiusura reale è diversa da quella che il lettore farebbe da solo; `f0` vuol dire
|
||
"resta qui". Ci sono volute quattro riscritture per arrivarci.
|
||
- **Ordine dei campi per struttura** (primo/successivo per schema): sbagliato. La tabella è
|
||
piatta, per id, e vale l'ultima transizione vista.
|
||
- **`e4`-`e7` come "zeri"**: sono ripetizioni del valore precedente.
|
||
- **Lo stesso id in contenitori diversi**: succede con le varianti di unione
|
||
(`typed_data(...)`); il parser le accetta e le conta in `lenient`.
|
||
- **Tag `double` negativi lineari**: no, ci sono tre buchi. Non fidarsi di un'estrapolazione
|
||
fuori dagli intervalli verificati.
|
||
- **Heredoc in Bash con `\\0`**: lo strumento collassa le barre e scrive un NUL nel sorgente.
|
||
Per creare file Python usare lo strumento di scrittura, non un heredoc.
|
||
- **Git Bash e `cd`**: gli script vanno lanciati dalla root del repo; `dotnet run` dentro
|
||
`PrepStep/` avvia la GUI.
|
||
|
||
## Cronologia
|
||
|
||
| Data | Fatto |
|
||
|---|---|
|
||
| 2026-10-21 | Albero delle feature, quote e geometria degli schizzi letti a pattern (`formato-prt-feature.md`, `formato-prt-geometria.md`) |
|
||
| 2026-11-10 | Contenitore corretto; grammatica PSB completa; tabella dei `double`; sezioni `.Z`; catalogo delle sezioni; parser di riferimento in `scripts/psb/`; `formato-psb.md` |
|