Documenta il formato PSB dei file Creo e aggiungi il parser di riferimento

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>
This commit is contained in:
davideandClaude committed 2026-11-10 14:34:29 +01:00
1 parent 1eac3ccaa7
commit 2d7ace07e2
15 files changed
+1309

No files matched your search

+3
View File
@@ -22,6 +22,8 @@ e cambia solo quando cambia il programma.
- [`formato-asm.md`](formato-asm.md) — euristica testuale di fallback.
- [`formato-prt-feature.md`](formato-prt-feature.md) — come leggere l'albero delle feature di un `.prt` (reverse engineering, non ancora nel codice).
- [`formato-prt-geometria.md`](formato-prt-geometria.md) — numeri, quote e geometria degli schizzi di un `.prt`, con la tabella di cosa manca per determinarlo per intero.
- [`formato-psb.md`](formato-psb.md) — il formato di serializzazione di tutte le sezioni (`.prt` e `.asm`): contenitore, grammatica, numeri, cosa c'è in ogni sezione, con un parser di riferimento in `scripts/psb/`.
- [`history.md`](history.md) — stato del reverse engineering dei file Creo: cosa è provato, cosa manca, da dove ripartire.
- [`glossario.md`](glossario.md) — i termini usati nei doc e nell'app.
- [`../CHANGELOG.md`](../CHANGELOG.md) — storico delle versioni.
@@ -41,6 +43,7 @@ Ogni informazione vive in **un** posto; altrove si mette un link, non una copia.
| Formato dei record `.asm` e casi limite | [`formato-asm-record.md`](formato-asm-record.md) |
| Albero delle feature di un `.prt` | [`formato-prt-feature.md`](formato-prt-feature.md) |
| Quote, coordinate e geometria degli schizzi di un `.prt` | [`formato-prt-geometria.md`](formato-prt-geometria.md) |
| Come è scritta una sezione qualsiasi di un `.prt`/`.asm` | [`formato-psb.md`](formato-psb.md) |
| Processo delle 5 fasi, mappa dei file | [`architettura.md`](architettura.md) |
| Passi dell'utente | [`guida-utente.md`](guida-utente.md) |
| Build, test, regole sui doc | [`sviluppo.md`](sviluppo.md) |
+4
View File
@@ -286,6 +286,10 @@ Comandi di build del setup in [`rilascio.md`](rilascio.md).
## Script standalone (`scripts/`)
`scripts/psb/` fa eccezione: sono tre file Python (`prt.py`, `psb.py`, `unz.py`), lo strumento
del reverse engineering descritto in [`formato-psb.md`](formato-psb.md). Non referenziano il
progetto e l'app non li usa.
C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`, nessun `.csproj`,
dipendenze extra con `#:package Nome@versione` in testa al file):
+3
View File
@@ -4,6 +4,9 @@ Seguito di [`formato-prt-feature.md`](formato-prt-feature.md), che spiega come t
del file e leggere l'**albero** delle feature (ordine, tipo, nome). Qui: come leggere i **numeri**
(quote, coordinate) e come ricostruire da quelli la forma di una feature.
> **Aggiornamento.** La lettura campo per campo di tutte le sezioni, e la tabella completa dei
> numeri, sono in [`formato-psb.md`](formato-psb.md), che corregge la §1 di questo doc (vedi lì §9).
**Stato, da leggere prima di fidarsi.** Questo doc contiene *tutto quello che è stato decodificato
e verificato*, e dice per ogni cosa quanto è provata. **Non basta ancora a determinare in modo
univoco com'è fatto un `.prt`**: manca il posizionamento delle feature nello spazio e il
+371
View File
@@ -0,0 +1,371 @@
# Il formato PSB: leggere tutte le sezioni di un `.prt`/`.asm` senza Creo
Come è scritto **per intero** il contenuto di un file Creo, e come leggerlo campo per campo con
nome e valore, senza àncore a pattern. Ricavato per reverse engineering su
`9253000117a.prt.35` (Creo 2.0 M200, generazione `1632`, 2 MB) e poi provato sugli 89 file di
`raw/`. Il nome "PSB" è quello che il formato dà a se stesso (`PSB_Tokens`, §7).
Questo doc **sostituisce** le parti di [`formato-prt-feature.md`](formato-prt-feature.md) e
[`formato-prt-geometria.md`](formato-prt-geometria.md) che leggevano i record cercando sequenze
di byte: quelle àncore restano valide come esempi, ma il modo giusto di leggere è quello qui
sotto. Le correzioni puntuali a quei due doc sono in §9.
Implementazione di riferimento: [`scripts/psb/psb.py`](../scripts/psb/psb.py) (parser),
[`scripts/psb/prt.py`](../scripts/psb/prt.py) (contenitore),
[`scripts/psb/unz.py`](../scripts/psb/unz.py) (sezioni compresse). È Python (solo libreria
standard), non C#: è lo strumento con cui è stato fatto il reverse engineering, non codice
dell'app. Nessun codice di `PrepStep/` lo usa.
**Livelli di prova** (come negli altri doc):
1. **Verifica interna forte**: la sezione si legge fino all'ultimo byte, con i conteggi degli
array rispettati; i numeri reali sono controllati su vincoli geometrici (righe delle matrici
di rotazione a norma 1: 5397 su 5397 in `9253000117a.prt.35`).
2. **Su scala**: 2152 sezioni su 2226 lette fino in fondo negli 89 file di `raw/` (tutte le
generazioni, da `1322` a `2063`). Le 74 che falliscono sono in §8.
3. **Contro Creo**: nessuna. Nessun valore di questo doc è stato confrontato con il modello
aperto in Creo. I nomi dei campi sono quelli scritti nel file; il loro *significato* è
dedotto dal nome.
## 1. Il contenitore
```
#UGC:2 PART 1632 1060 805 1 1 15 3300 2015440 000002cc \
...
#-END_OF_UGC_HEADER
#Creo TM 2.0 (c) 2020 by PTC Inc. All Rights Reserved. M200
#UGC_TOC 2 32 81 17####...
DEPDB_DATA ab1 19 d 1632 9 -1 05e1 260f####
...
NEXT_TOC_ENTRY 1f8679 3cc 0 1632 1 -1 0000 0000####
```
- **L'ultimo campo della prima riga** (`000002cc`) è la lunghezza dell'intestazione: è la
costante da sommare a tutti gli offset dell'indice. `inizio = 0x2cc + offset`.
- Ogni sezione comincia con una riga `#<Nome>\n`, **compresa** nell'offset e nella lunghezza.
Il corpo è quindi `[0x2cc + offset + len(nome) + 2, 0x2cc + offset + lunghezza)`.
- **L'indice è a catena**: la voce `NEXT_TOC_ENTRY` punta (stessa regola) a un secondo
`#UGC_TOC`, in fondo al file, con altre sezioni. Vanno seguiti tutti fino a `#END_OF_UGC`.
- Campi di una riga: `nome offset lunghezza dimensione generazione tipo metodo crc crc`.
`dimensione` è la lunghezza del corpo **decompresso** (per le sezioni in chiaro è
`lunghezza − len(nome) − 2`).
Il campo `tipo` in questo file:
| Tipo | Contenuto | Sezioni |
|---|---|---|
| `9` | PSB in chiaro | quasi tutte |
| `a` | PSB compresso Unix `compress` (`1f 9d 10`, LZW a 16 bit): si decomprime e si legge come le altre; la dimensione ottenuta coincide con quella dichiarata | `ND:0:*`, `SolidPersistTable`, `SolidPrimdata` |
| `c` | comincia con `1f 9d 10` ma **non** è un LZW standard | `DispDataTable` (non letto) |
| `11` | JPEG (`ff d8 ff e0 ... JFIF`): la miniatura del modello | `THMB_IMG_MAIN` |
| `b` | non decodificato: primi 4 byte in chiaro (`e0 00 Fi`, `e0 00 Fe`), poi entropia massima, stessa lunghezza del testo in chiaro | `IndentTag`, `FeatRefData` |
Il tipo `b` ha lo stesso aspetto delle sezioni "tipo `11`" dei file Wildfire descritte in
`formato-prt-feature.md` §1.3. Resta il blocco principale.
## 2. La grammatica
Una sezione è un albero di campi. I byte `e0`-`ff` sono marcatori, gli altri dati.
| Byte | Significato |
|---|---|
| `e0 <tipo> <nome>\0` | **dichiara** un campo e ne dà subito il valore |
| `e1` | puntatore nullo |
| `e2`, `e3` | apre una struttura (non distinte dal lettore) |
| `e4`, `e5`, `e6` | dentro un array: ripeti il valore precedente 1, 2, 3 volte |
| `e7 <n>` | dentro un array: ripeti il valore precedente `n` volte |
| `e8`-`ed` | forme "grezze" dei numeri (§3) |
| `f0`-`f3` | chiudi 0, 1, 2, 3 contenitori (§2.3) |
| `f4 <n>` | chiudi `n` contenitori |
| `f5` | prefisso di un intero: bit 31 acceso |
| `f6` | intero −1 |
| `f7 <id>` | il prossimo valore è del campo numero `id` (§2.2) |
| `f8 <n>` | array a una dimensione di `n` elementi |
| `f9 <a> <b>` | array a due dimensioni (`a`×`b` numeri; per gli array di strutture `a` è il totale) |
| `fa <k> <d1>..<dk>` | array a `k` dimensioni |
| `fb` | segue l'intestazione di un array di strutture, prima del primo elemento |
| `fc <n>` | array a due dimensioni: cambia solo il numero di righe, colonne come l'ultima volta |
Tipi di campo (il byte dopo `e0`):
| Tipo | Valore |
|---|---|
| `00` | puntatore: `e1`, struttura (`e2`/`e3`), o array di strutture (`f8 n [f7 id] [fb]`) |
| `01`, `05`, `08`, `09` | intero (§3.1). `09` è senza segno (flag); `05`/`08` non distinti |
| `02` | `double` (§3.2) |
| `06` | `float` a 32 bit (§3.3) |
| `04`, `07` | array di byte: ogni byte vale se stesso; un byte ≥ `e0` è preceduto da `e8` |
| `03`, `0a` | stringa Latin1 terminata da `\0`, o `e1` |
Un campo numerico o stringa può essere un array: `f8`/`f9`/`fa` prima dei valori.
### 2.1 Gli id dei campi
Ogni `e0` riceve un **numero progressivo** (dal primo `e0` della sezione = 1). Lo stesso nome in
due strutture diverse ha due id diversi. È la chiave di tutto: `f7 <id>` si riferisce a quel
numero. Verifica: l'intestazione di ogni array di strutture ripete l'id del proprio campo
(`unit_arr` è il 20° campo di `BasicText` e l'array è `f8 03 f7 14 fb`).
### 2.2 Modalità posizionale: la tabella "dopo X viene Y"
Un campo è scritto col nome **una sola volta**. Dopo, i valori arrivano nudi. Per sapere di
quale campo è un valore nudo, chi scrive e chi legge tengono la stessa tabella:
```
next[id] = l'id del campo scritto subito dopo, l'ultima volta che id è stato scritto
```
- Se il prossimo campo è `next[ultimo]`, il valore è scritto e basta.
- Altrimenti è preceduto da `f7 <id>`.
- La tabella è **piatta** (non tiene conto dell'annidamento) e si aggiorna a ogni campo, anche
dopo un `f7` e dopo un campo dichiarato per nome.
- Ogni elemento di un array di strutture è una nuova occorrenza del campo-array: per questo
dopo il primo elemento si trova `f1 f7 <id array>` (insegna che "dopo l'ultimo campo
dell'elemento viene un altro elemento") e dopo gli altri non c'è nulla.
- Un campo mai dichiarato e nullo **non viene scritto**: compare col nome la prima volta che ha
un valore, anche in mezzo a un record posizionale.
**Lunghezza degli array in modalità posizionale**: `f8 <n>` compare solo se `n` è diverso
dall'ultima volta che quel campo è stato scritto. Eccezione osservata: un array di stringhe più
corto del precedente arriva senza `f8` (`edit_header` in `BasicData`: 6 stringhe, poi 5); il
lettore si ferma al primo byte < `20`. È un'euristica, non una regola capita.
### 2.3 Dove va un campo: la pila dei contenitori
Strutture, array e loro elementi sono contenitori su una pila. Tre regole:
1. **Valore nudo senza `f7`**: va nel contenitore più vicino alla cima che possiede quel campo;
quelli sopra si chiudono in silenzio.
2. **Campo con nome o `f7`, con marcatore `f0`-`f4`**: si chiudono esattamente quel numero di
contenitori (`f0` = nessuno). Il marcatore sta **dopo** il nome del campo.
3. **Campo con nome o `f7`, senza marcatore**: se il campo atteso (`next[ultimo]`) appartiene al
contenitore **padre** di quello in cui è stato scritto l'ultimo campo, quest'ultimo si chiude;
altrimenti non si chiude nulla.
Casi particolari, tutti osservati:
- **Alias di tipo**: `e3 f7 <id>` con `id` di un'altra struttura vuol dire "questa struttura ha
lo stesso schema di quella" (`next_feat_ptr 00 f2 e3 f7 03`: il record successivo usa i campi
del primo).
- **Puntatori "al successivo"** (`next_feat_ptr`, `next_cntr_ptr`, `relcode_tree_next`): sono
scritti **dopo aver chiuso** la struttura a cui appartengono, quindi al livello del padre. Le
liste concatenate risultano così sequenze piatte, non annidamenti.
- **Campi a livello di array**: `next` in `ref_part_table` è dichiarato dopo aver chiuso il
primo elemento e ricompare (`e1`) dopo ogni elemento.
- **Varianti di unione**: il nome porta la variante tra parentesi (`data(sld_trace)`,
`srf_prim_ptr(cylinder)`, `typed_data(MTTyped_CreateData)`). Lo stesso id può comparire in
contenitori diversi.
**Cosa è solido e cosa no.** La sequenza *campo → valore* dipende solo da `next[]` e dai tipi:
è la parte provata (tutte le sezioni lette fino all'ultimo byte). L'annidamento dipende dalle
regole 1-3, che sono ricavate per tentativi: in `AllFeatur`, `MdlStatus` e `FeatDefs` di
questo file la pila alla fine non torna a zero (un contenitore in più o in meno). I valori sono
giusti, il livello di qualche campo può non esserlo.
## 3. I numeri
### 3.1 Interi
| Primo byte | Valore |
|---|---|
| `00`-`7f` | il byte |
| `80`-`bf` | 2 byte, 14 bit |
| `c0`-`df` | 3 byte, 21 bit |
| `e8` / `e9` / `ea` / `eb` | 1 / 2 / 3 / 4 byte grezzi (`e9` ipotizzato) |
| `f5 <intero>` | intero con il bit 31 acceso (`hostid` `f5 eb 2c 14 fd cf` = `ac14fdcf` = 172.20.253.207) |
| `f6` | −1 |
### 3.2 `double`
Il primo byte è un tag. `T` è il byte alto del `double` IEEE big-endian.
| Tag | Byte dopo | Valore |
|---|---|---|
| `00`-`03` | 1 costante + (tag+1) byte | la costante, con gli ultimi (tag+1) byte sostituiti: `00 0c 9a` = 0,1 esatto; `02 11 66 89 30` = π con rumore |
| `04`-`18` | 0 | costante (tabella sotto) |
| `19`-`2f` | vedi modi | positivi, 5 tag per ogni `T` da `3c` a `40`: `T = 0x3c + (tag − 0x17) / 5` |
| `30`-`48` | vedi modi | negativi, `T` da `bc` a `c0`: `T = 0xbc + (tag − 0x30) / 5` |
| `49`-`a4` | 6 | positivi: i primi 2 byte sono `0x3f75 + tag` (da `3fbe` a `4019`) |
| `a5`-`df` | 6 | negativi: i primi 2 byte sono, in ordine, `bfd0`...`c00d` **saltando** `bfd2`, `bfdf` e `bffe` |
| `ea` | 3 | 3 byte alti, il resto zero |
| `ec` | 7 | 7 byte alti, l'ultimo zero |
| `ed` | 8 | il `double` intero |
Modi dei tag a famiglie di 5 (resto di `(tag − base) / 5`):
| Resto | Byte dopo | Composizione |
|---|---|---|
| 4 | 2 | `T b1 b2` e 5 zeri (`2f 59 00` = 100) |
| 3 | 2 | `T b1 b2` e `b2` ripetuto (`2e 1b 33` = 6,8) |
| 2 | 7 | `T` e 7 byte |
| 1 | 6 | `T`, 6 byte, uno zero |
| 0 | 3 | `T`, 3 byte, 4 zeri |
La famiglia di `T = 3c` ha solo i tag `19`, `1a`, `1b`: `17` e `18` sono costanti.
Costanti a un byte:
| Tag | Valore | Prova |
|---|---|---|
| `07` / `0a` / `0c` | 0,001 / 0,01 / 0,1 | tolleranze di default per numero di decimali (`BasicData`), ISO 2768 sulle quote |
| `0d` | −0,5 | **ipotesi** (un solo caso) |
| `0e` / `0f` / `10` | 0,5 / 1 / −1 | matrici ortonormali |
| `11` / `12` / `13` / `14` | π / π/2 / 2π / 3π/2 | estremi angolari dei cilindri; stesso rumore (`+0x112e0c` sulla mantissa) nelle forme `02 xx ...` |
| `15` / `16` / `17` | −π / −π/2 / −2π | idem |
| `18` | 0 | ovunque |
| `04` | **ignoto** | compare come componente di tangente e come estremo di un inviluppo |
| `05`, `06`, `08`, `09`, `0b` | mai visti | |
Prove: le righe e colonne di tutte le matrici `local_sys` hanno norma 1 con questa tabella
(5397 su 5397 in questo file); i prefissi `3fd0`-`3fef` e `bfd0`-`bfef` sono verificati uno a
uno sugli 89 file risolvendo il modulo dalla norma. Fuori da quell'intervallo valgono solo
alcuni punti fermi: `86` = `3ffb` (√3), `8c` = `4001` (2,2046 lb/kg), `8f` = `4004` (5π/6),
`ce` = `bffb`, `d9` = `c007` (−3 + ε). Il buco `bffe` (e non `bffd`) è una **scelta non
provata**: uno dei due manca, non so quale.
### 3.3 `float` a 32 bit (tipo `06`)
Usati nei dati di visualizzazione (`SolidPrimdata`, `SolidPersistTable`).
| Tag | Byte dopo | Valore |
|---|---|---|
| `00` / `0b` / `28` | 0 | 0 / +1 / −1 (segno ricavato dalle normali delle facce estreme) |
| `30`-`3f` | 3 | byte alto `0x37 + (tag − 0x30)`, positivi |
| `40`-`4f` | 3 | byte alto `0xb7 + (tag − 0x40)`, negativi |
| `50`-`a3` | 2 | positivi: i primi 2 byte da una **tabella non lineare** |
| `a4`-`df` | 2 | negativi: idem |
| `e8` / `e9` / `ea` / `eb` | 1 / 2 / 3 / 4 | byte alti grezzi, il resto zero |
La tabella dei tag `50`-`df` **non è ricostruita**: sono prefissi scelti per frequenza, non una
formula. Voci ricavate dalle normali unitarie: `63`=`3e1d`, `67`=`3e46`, `68`=`3e80`,
`69`=`3e84`, `6a`=`3ea6`, `6d`=`3f08`, `6e`=`3f0a`, `6f`=`3f0b`, `70`=`3f0c`, `71`=`3f0d`,
`72`=`3f0e`, `73`=`3f0f`, `74`=`3f10`, `75`=`3f17`, `76`=`3f21`, `77`=`3f57`; `bc`=`be80`,
`bd`=`be84`, `bf`=`becf`, `c0`=`bee4`, `c1`=`beea`, `c4`=`bf0a`, `c5`=`bf1c`, `c6`=`bf20`,
`c7`=`bf27`, `c8`=`bf32`, `ca`=`bf60`, `cb`=`bf66`, `cc`=`bf7a`. Il parser di riferimento usa
una formula lineare **sbagliata** per questi tag: la lunghezza è giusta (la lettura prosegue),
il valore no. Finché la tabella non è completa la mesh non si può esportare.
## 4. Algoritmo di lettura
```
pila = [], ultimo = nessuno, next = {}, campi = {}
finché ci sono byte:
m = marcatore f0-f4 se presente
se il byte è e0: # campo con nome
leggi tipo e nome; m = marcatore dopo il nome, se presente
applica la regola 2 (m presente) o 3 (m assente) di §2.3
id = nuovo numero; il campo appartiene al contenitore in cima
altrimenti:
se m presente: chiudi m contenitori
se il byte è f7: id = intero che segue (poi regola 3 se m assente)
altrimenti: id = next[ultimo]
porta in cima il contenitore di id (regola 1; alias se la cima è una struttura vuota)
next[ultimo] = id; ultimo = id
leggi il valore secondo il tipo di id; strutture e array vanno sulla pila
```
Uso:
```powershell
python scripts/psb/prt.py 9253000117a.prt.35 # indice delle sezioni
python scripts/psb/psb.py 9253000117a.prt.35 '*' out # ogni sezione in out/<Nome>.txt
python scripts/psb/psb.py 9253000117a.prt.35 FullMData out
python scripts/psb/unz.py 9253000117a.prt.35 out # decomprime le sezioni .Z in out/*.bin
```
## 5. Cosa c'è in ogni sezione
Nomi di radice e campi come nel file. "Letto" = arriva all'ultimo byte.
| Sezione | Radice | Contenuto |
|---|---|---|
| `BasicText` | `Sld_BasicText` | proprietà di massa (`density`, `volume`, `cg`, `inertia`, `surfarea`), unità (`unit_arr`: `MM`, `KILOGRAM`, `SECS`; `principal_sys_units` = `millimeter Kilogram Sec (mmKs)`) |
| `BasicData` | `Sld_BasicData` | tolleranze di default, tabelle ISO (`iso_tol_tbls_ptr`, classi `Fine`...`Very Coarse`), tabelle dei fori (`holes_arr`) |
| `FullMData` | `Sld_FullData` | **quote** (`dim_array`, 788 voci: `dim_type`, `feat_id`, `value`, `tol_ptr.value`), modelli referenziati (`ref_part_table`) |
| `AllFeatur` | `Sld_Features` | lista delle feature (`first_feat_ptr`/`next_feat_ptr`): `id`, `feat_type_ptr.type`, `sect_name`, tabelle delle geometrie create, date |
| `FeatDefs` | `Sld_FeatDefs` | definizione di ogni feature (`feat_defs_<tipo>`), schizzi, istruzioni di posizionamento |
| `FeatDefsIndex` | `Sld_FeatDefsIndex` | i tipi di feature presenti (`feat_defs_index`: 923, 979, 1084, 917, 916, 914, 926, 1104, 1232, 911, 949, 1070, 913, 1052) |
| `MdlStatus` | `Sld_MdlStatus` | storico: un record per azione, con utente, data e nome (`Extrude 9`, `Specchiare 4`) |
| `VisibGeom` / `NovisGeom` | `Sld_VisGeom` / `Sld_NonVisGeom` | **B-rep**: `srf_array` (superfici: `geom_id`, `geom_type`, `feat_id`, `srf_prim_ptr(plane/cylinder/...)` con `local_sys` e raggio), contorni (`next_cntr_ptr`), `crv_array` (spigoli con i punti) |
| `ActEntity` | `Sld_Entities` | entità di riferimento: punti (`PNT0`...), linee, sistemi di coordinate |
| `GeomDepen` | `Sld_GeomDepend` | ultima feature rigenerata, data dell'ultima rigenerazione, proprietà delle geometrie |
| `LargeText` | `Sld_LargeText` | **nomi** (`names_table`), **parametri** (`symtbl`: `DENOMINAZIONE`, `PRO_MP_*`...), **relazioni** come albero (`relset_code_tree`) con il testo sorgente e i commenti |
| `DispCntrl` | `Sld_DispCntrl` | layer (`01___PRT_ALL_DTM_PLN`, ...) con gli id contenuti e le regole |
| `FamilyInf` | `Sld_FamilyInfo` | family table (`gen_name`, `info_items`) |
| `Xsections` | `Sld_Xsections` | sezioni trasversali, ognuna con la propria geometria (`xsec_geom`) |
| `ModelView#n` | `View` | viste salvate: matrici `orient`/`local_sys` 4×4, nome |
| `NeuPrtSld` | `Sld_NeutPart` | dati "neutral" della parte |
| `ColorSchemeInfo*`, `DispDataColor`, `AssMrgPrt`, `PipeInfos`, `NeuAsmSld`, `DEPDB_DATA`, `THMB_IMG_INDX` | | piccole o vuote |
| `SolidPrimdata` (compressa) | `pgl_primdata_group_table` | **geometria di visualizzazione**: archi (`prim_arc`: raggio, angoli, centro, piano), linee e la **mesh** (`mv_p_NxNyNzxyz`: normale + vertice, `float`) |
| `SolidPersistTable` (compressa) | `pgl_persist_table` | ingombro (`xyz_min_max`: x ±45,57, y −74,07..90, z −807..0), `bounding_radius` 414,27 |
| `ND:0:Model_*`, `ND:0:UnitSystemDef_*`, `ND:0:PDMTrail_*` (compresse) | `PSB_Tokens` | §7 |
Valori letti da questo file, come esempio (nessuno confrontato con Creo): densità
7,85·10⁻⁶ kg/mm³, volume 3,11·10⁶ mm³, dipendenze `9053004005` e `9253018001`.
## 6. Cosa cambia rispetto alla lettura a pattern
- **Il primo record non è più un caso speciale**: è solo quello che dichiara i campi. "La prima
quota dell'array può mancare" (`formato-prt-geometria.md` §2) non vale più.
- **I record posizionali si leggono con i nomi**: ogni valore ha il nome del suo campo.
- **Le quote**: `dim_array` → `dim_dat_ptr.value`, `tol_ptr.value` (i due scostamenti: 0,1 per
la quota 2; 0,2 per 20; 0,3 per 59, cioè ISO 2768-m), `dim_cosm_ptr`.
- **Le unità** si leggono (`BasicText`), non vanno più assunte.
## 7. Le sezioni `ND:0:*`: `PSB_Tokens`
Decompresse sono un PSB con una sola struttura:
```
PSB_Tokens:
format = 0
type_har = "MOMOIOIOIOIOFCOFOM..." un carattere per token
int_har = [0, 1, 2, 3, 4, 5, -1, ...]
double_xar = [-1.0, 0.001, 1e-09, ...]
string_xar = ['ndmodel_l05::Model_L05', 'Descriptor', 'Name', '9253000117a', 'Type', 'prt', ...]
```
È un secondo livello di serializzazione: `type_har` è la sequenza dei token, gli altri tre array
sono i valori a cui i token attingono in ordine. **Il significato delle lettere non è
decodificato** (`M`, `O`, `I`, `F`, `C`, `P`, `Q`, `N`, `H`, `R`, `S`, `U`). Le stringhe però si
leggono già: nome e tipo del modello, dipendenze, nomi dei parametri.
## 8. Dove non regge
| Problema | Dove | Stato |
|---|---|---|
| Sezioni tipo `b` (`IndentTag`, `FeatRefData`) e tipo `11` dei file Wildfire | tutti i file | **illeggibili** |
| `DispDataTable` (tipo `c`) | file recenti | non letto |
| Tabella dei prefissi `float` `50`-`df` | `SolidPrimdata` | parziale: la mesh non è esportabile |
| Costante `double` `04` | geometria | valore ignoto |
| Annidamento | `AllFeatur`, `MdlStatus`, `FeatDefs` | la pila non torna a zero a fine sezione |
| Alberi bilanciati (`lesser_child`/`greater_child`/`balance_factor`) | `LargeText` di 24 file di `raw/` | il lettore si ferma: la regola dei puntatori "al successivo" non basta |
| `srf_flip_dat`, `end2`, `backup_size` "contenitore non in pila" | 26 sezioni di `raw/` | stesso tipo di errore: è la pila, non i valori |
| Array di stringhe senza `f8` | `BasicData` | euristica (§2.2) |
| `PSB_Tokens` | `ND:0:*` | solo gli array, non la struttura |
| Significato dei campi | ovunque | dedotto dal nome, mai verificato in Creo |
Sugli 89 file di `raw/`: 2152 sezioni lette per intero, 74 no. Tutti i fallimenti sono del tipo
"contenitore non in pila" o "pila vuota": la sequenza dei valori fino a quel punto è corretta.
## 9. Correzioni agli altri doc
- `formato-prt-feature.md` §1.1: la costante `K` non va ricavata dall'ultima sezione: è
l'ultimo campo della prima riga del file, e la sezione comincia con la riga `#Nome`. L'indice
può essere a catena (`NEXT_TOC_ENTRY`). La formula data lì funziona solo per caso.
- `formato-prt-feature.md` §1.2: le sezioni `ND:0:*` decompresse non sono XML ma `PSB_Tokens`
(§7); i nomi tipo SOAP sono le stringhe di `string_xar`.
- `formato-prt-geometria.md` §1: la tabella dei tag è sostituita da §3.2. In particolare
`e4`-`e7` non sono "zeri" ma **ripetizioni del valore precedente**, e i tag `49`-`df`
("non decodificato") sono i prefissi a 2 byte.
- `formato-prt-geometria.md` §1: `UnitSystemDef` non serve per le unità, stanno in `BasicText`.
## 10. Come proseguire
1. **Confrontare con Creo** un file piccolo: è l'unica prova che manca a tutto il doc.
2. Completare la tabella dei `float` raccogliendo le normali su più file (stesso metodo di
§3.3): sblocca l'esportazione della mesh senza Creo.
3. Sistemare la pila sugli alberi di `LargeText` (§8), poi portare il parser in C# accanto ad
`AsmRecordReader`, con la stessa regola: se la sezione non si legge fino in fondo,
`InvalidDataException`.
4. Sezioni tipo `b`: servono due salvataggi dello stesso modello che differiscono di poco.
+160
View File
@@ -0,0 +1,160 @@
# 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` |