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>
10 KiB
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: 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 aCHANGELOG.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 inraw/.scripts/psb/__pycache__/va ignorato, non committato.docs/formato-prt-feature.mdcontiene byte NUL: gli strumenti di modifica lo rifiutano. Le correzioni che lo riguardano sono informato-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):
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
- 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".
- Tabella dei
float(tag50-df). È una tabella di prefissi scelti per frequenza, non una formula. Il parser usaFBASElineare, sbagliato per quei tag (lunghezza giusta, valore no). Senza, la mesh diSolidPrimdata(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 diraw/(ognuno haSolidPrimdata), poi sostituireFBASEcon un dizionario e far fallire il parser sui tag ignoti invece di inventare. - La pila dei contenitori. I valori sono giusti, l'annidamento no in alcuni punti:
LargeTextdi 24 file diraw/: alberi bilanciati (lesser_child,greater_child,balance_factor,backup_size). La regolapost(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_closeelocateinpsb.py, con la traccia (- t).
- Sezioni tipo
b(IndentTag,FeatRefData) e tipo11Wildfire: illeggibili. Stessa lunghezza del chiaro, primi 4 byte in chiaro. Già escluso (vediformato-prt-feature.md§1.3): XOR a chiave fissa, zlib, LZW. Non riprovare senza una coppia chiaro/cifrato dello stesso modello. DispDataTable(tipoc): comincia con1f 9d 10ma l'LZW standard fallisce al secondo codice. Non indagato.PSB_Tokens(ND:0:*): letti i quattro array, non il significato delle lettere ditype_har. Ipotesi da cui partire:Iconsuma un intero,Fun double, una lettera per le stringhe;M/Oaprono oggetto e campo. Contare le occorrenze contro le lunghezze degli array la conferma o la smentisce in pochi minuti.- Costanti
doubleignote:04(vista come componente di tangente e come estremo di inviluppo),0d(−0,5 da un solo caso), bucobffecontrobffdnon deciso. - 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.
- 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. - 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_arrdeve essere 0x14" (contato a mano: lo era). - 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.
- 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.
- 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. - 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ì".
- Allargare il campione appena una regola regge. Un file solo nasconde le tabelle: i buchi
nei prefissi negativi e la tabella dei
floatsono emersi solo confrontando più file. Dopo ogni modifica:norms.pysul campione, poibatch.pysuraw/. - Quando due spiegazioni reggono entrambe, cercare il dato che le distingue invece di
scegliere la più elegante (esempio: i tag
91-a0sembravano negativi; li ha decisi una curva i cui parametri dovevano crescere da 3π/2). - Scrivere subito nel doc, con il sintomo e i byte. Una scoperta non scritta si rifà. Scrivere anche ciò che è stato escluso e perché.
- 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-f4compaiono solo quando la chiusura reale è diversa da quella che il lettore farebbe da solo;f0vuol 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-e7come "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 inlenient. - Tag
doublenegativi 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 rundentroPrepStep/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 |