Author SHA1 Message Date
davideandClaude 2d7ace07e2 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>
2026-11-10 14:34:29 +01:00
davideandClaude Sonnet 5.5 1eac3ccaa7 Documenta posizioni, fori e facce dei .prt (9239902104)
Matrici 4x3 di fori e feature (regola degli zeri corretta), foro a punti
di schizzo con facce piane di Estrusione 1, profilo a L, quote per feature
e tipi 928/1104/1052. Corregge l'ipotesi errata sul codice 12266.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 16:21:18 +02:00
davideandClaude Sonnet 5.5 5b11b3e8ca Documenta la decodifica dei .prt: albero delle feature, quote e geometria
Reverse engineering di test.prt.1, 9252000507.prt e 9239901907.prt contro
il model tree di Creo: contenitore e sezioni, ricostruzione dell'albero
(nomi per contatore, gruppi, sezioni), numeri compatti, quote, foro, smusso
e vincoli dei piani, con la tabella di cosa resta da decodificare.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 15:58:15 +02:00
16 changed files with 1853 additions and 0 deletions

No files matched your search

+1
View File
@@ -43,3 +43,4 @@ publish/
# Setup generato da Inno Setup (installer/PrepStep.iss)
installer/Output/
__pycache__/
+6
View File
@@ -7,6 +7,12 @@ aderisce a [Semantic Versioning](https://semver.org/lang/it/).
## [Non rilasciato]
### Aggiunto
- `docs/formato-prt-feature.md`: decodifica (da reverse engineering) dell'albero delle feature di un `.prt`.
- `docs/formato-prt-geometria.md`: numeri, quote e geometria degli schizzi di un `.prt`, con la tabella di cosa resta da decodificare.
- `docs/formato-psb.md` e `scripts/psb/`: formato di serializzazione completo delle sezioni dei file Creo (reverse engineering su `9253000117a.prt.35`) e parser Python di riferimento.
## [1.1.0] - 2026-10-07
### Aggiunto
+7
View File
@@ -20,6 +20,10 @@ e cambia solo quando cambia il programma.
- [`db-cad-index.md`](db-cad-index.md) — schema, vista e query di `cad_index.db`.
- [`formato-asm-record.md`](formato-asm-record.md) — parser esatto dei record `.asm`, casi limite (§7).
- [`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.
@@ -37,6 +41,9 @@ Ogni informazione vive in **un** posto; altrove si mette un link, non una copia.
| Versione e nome prodotto | `PrepStep/PrepStep.csproj` (`<Version>`, `<Product>`) |
| Schema e vista del DB | [`db-cad-index.md`](db-cad-index.md) |
| 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):
Binary file not shown.
+538
View File
@@ -0,0 +1,538 @@
# Decodifica del `.prt` Creo: numeri, quote e geometria degli schizzi
Seguito di [`formato-prt-feature.md`](formato-prt-feature.md), che spiega come trovare le sezioni
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
significato certo di alcuni codici (§7 elenca cosa manca, dove sta nel file, e come finire il
lavoro). Nessun codice del progetto lo usa: è reverse engineering fatto con script di prova.
Livelli di prova, come negli altri doc:
1. **Contro Creo** (schermate del modello aperto): `9252000507.prt.11`, feature *Rivoluzione 1*
(§5). Quote e coordinate dello schizzo coincidono con quelle mostrate da Creo.
2. **Coerenza e plausibilità**: `test.prt.1` e le altre feature di `9252000507.prt.11`. I valori
sono numeri "tondi" e coerenti fra sezioni diverse, ma nessuno li ha confrontati con Creo.
## 1. Come sono scritti i numeri reali
Nelle sezioni a marcatori ([`formato-asm-record.md`](formato-asm-record.md) §2) i `double` sono
scritti in forma compatta. Si distinguono per il primo byte, `tag`:
| Tag (hex) | Byte dopo il tag | Valore | Prova |
|---|---|---|---|
| `18` | 0 | `0.0` | in ogni sezione |
| `0f` | 0 | `1.0` | diagonale delle matrici identità |
| `0e` | 0 | `0.5` | **livello 1**: la quota di *Smusso 1* (D = 0.500) è il solo byte `0e` |
| `10` | 0 | `-1.0` | ipotesi dal contesto |
| `2f` | 2 | `40 b1 b2 00 00 00 00 00` | **livello 1** (12,5; 69,5; 100; 139) e 2 (8, 20, 25, 75) |
| `2e` | 2 | `40 b1 b2 b2 b2 b2 b2 b2` (l'ultimo byte si ripete) | **livello 1**: `2e 1b 33` = 6,8, il diametro della punta mostrato da Creo |
| `2d` | 7 | `40 b1 .. b7` (precisione piena) | livello 2: `2d 39 7f ff ff ff ff ff` = 25,5 |
| `2a` | 2 | `3f b1 b2 00...` | livello 2: `2a f4 00` = 1,25, il passo di M8x1.25 |
| `48` | 2 | `c0 b1 b2 00...` (negativo) | **livello 1** (−12,5; −69,5) |
| `47` / `46` | 2 / 7 | stessa famiglia di `48`, con ripetizione / precisione piena | non isolati |
| `e4`-`e7` | 0 | `K − 3` zeri di seguito, `K` = byte − `e0` (solo dentro le matrici, §6.1) | **livello 1** (matrici di `9239902104`) |
| `ed`, `32`... | 8-9 | altri byte alti | non decodificato |
**La regola, ricavata dai dati** (non da una specifica): a ogni *byte alto* `T` del `double`
corrispondono **tre tag consecutivi**, e la base si sposta di 5 per ogni passo di `T`:
```
tag_zero(T) = 0x2f + 5 * (T - 0x40) per i positivi (negativi: 0x48 + 5 * (T - 0xc0))
tag_zero -> T, b1, b2 e 5 byte a zero (valori "tondi")
tag_zero - 1 -> T, b1, b2 e b2 ripetuto 5 volte (es. 6,8 = 40 1b 33 33 33 33 33 33)
tag_zero - 2 -> T e 7 byte pieni (precisione piena)
```
Verifica: `T = 0x40` dà `2f`, `2e`, `2d`; `T = 0x3f` dà `2a` (1,25 con `f4 00`); `T = 0xc0` dà
`48`. Funziona solo finché i tag non si sovrappongono: i byte alti `0x41`-`0x43` e `0xb8`-`0xbf`
non li ho incontrati e non so se la regola regge (i tag di `0x45` collidono con quelli dei
negativi). Per i valori "non tondi" con ripetizione (come 6,8 o 3,4) serve il tag `2e`: senza
quello si leggerebbe `6.7998`.
Come si ricostruisce un valore a 3 byte: si prende il `double` big-endian `40 b1 b2 00...` (per
`2f`) oppure `c0 b1 b2 00...` (per `48`). Esempi verificati (`struct.unpack('>d', ...)`):
| Byte | Valore | Dove |
|---|---|---|
| `2f 59 00` | `100.0` | quota |
| `2f 52 c0` | `75.0` | quota |
| `2f 51 60` | `69.5` | coordinata y dello schizzo di Rivoluzione 1 (= 139 / 2) |
| `2f 29 00` | `12.5` | coordinata x (= 25 / 2) |
| `48 29 00` | `-12.5` | idem, lato opposto all'asse |
| `48 51 60` | `-69.5` | idem |
| `2f 61 60` | `139.0` | quota (lunghezza) |
| `48 59 00` | `-100.0` | estremo della linea d'asse |
Le unità non sono scritte nel record. Nei campioni sono millimetri, ma il file ha una sezione
`UnitSystemDef` (nel contenitore compresso, §1.2 di `formato-prt-feature.md`) che non ho letto:
l'unità va presa da lì prima di dare un numero come "mm".
## 2. Le quote: `dim_array` in `BasFullData`
Ogni quota (dimensione guida) di ogni feature e di ogni schizzo è un record della sezione
`BasFullData`, array `dim_array`. Campi, nell'ordine in cui compaiono:
```
\xe1 nome della quota (qui sempre nullo: il nome "d12" si calcola altrove)
\x08 tipo del record
<dim_type> 1 byte: classe della quota (tabella sotto)
<feat_id> intero (formato asm-record §2.1): la feature a cui appartiene
<attributi> \xf8 \x02 ... flag
\xe3 <valore> <valore_di_backup> \xe3 ... due numeri uguali: value e bck_value
\xe3 \x09 \x01 ... <tolleranza> ... blocco della tolleranza (nominal_value, bound...)
```
Per leggere solo valore e feature basta ancorarsi a `\xe1 \x08 <dim_type> <feat_id>` e leggere il
primo numero che segue (§1). Il primo record dell'array è scritto con i nomi di campo per esteso
(layout diverso: non l'ho agganciato con lo stesso pattern, quindi **la prima quota dell'array può
mancare**).
`feat_id` è lo stesso id di `AllFeatur` e dello storico (`formato-prt-feature.md` §2 e §5): è il
modo per attribuire la quota a "Foro 1" o a "Estrusione 2".
### 2.1 Le quote lette sui due campioni
`test.prt.1` (id → feature come in `formato-prt-feature.md` §6):
| Feature | `dim_type` | Valore | Interpretazione |
|---|---|---|---|
| Estrusione 1 (79) | 62 | 75 | profondità (ipotesi) |
| Estrusione 1 (79) | 4 | 100 | diametro del cerchio dello schizzo: nello schizzo il raggio è 50 (`entity(circle)`, `radius`) |
| Foro 1 (100) | 4 | 19 | diametro (ipotesi) |
| Foro 1 (100) | 62 | 40 | profondità (ipotesi) |
| Arrotondamento 1 (125) | 3 | 3,5 | raggio (ipotesi) |
| Estrusione 2 (149) | 62 / 1 / 1 | 11 / 25 / 53 | profondità e due quote dello schizzo (ipotesi) |
| Smusso 1 (188) | 9 | 6 | distanza dello smusso (ipotesi) |
`9252000507.prt.11`:
| Feature | `dim_type` | Valore | Interpretazione |
|---|---|---|---|
| Rivoluzione 1 (73) | 2 | **25** | **diametro, confermato da Creo** (Ø25 h7) |
| Rivoluzione 1 (73) | 1 | **139** | **lunghezza, confermata da Creo** |
| Estrusione 1 / taglio (118) | 62 / 14 / 5 / 20 | 4 / 45 / 8 / 3 | non verificate |
| Foro 1 (157) | 4 / 9 / 2 / 10 / 2 | 8 / 20 / 6,8 / 118 / 25 | **confermate da Creo** (§6.4): 8 = diametro nominale, 20 = profondità filetto, 118 = angolo punta, 25 = profondità foro |
`9239902104.prt.1` (piastra con tre fori, §6.4.1):
| Feature | `dim_type` | Valore | Interpretazione |
|---|---|---|---|
| Sketch 1 / Estrusione 1 (84) | 1 / 5 / 1 | 21 / 54 / 125 (+ 46) | quote del profilo a L (§3.2) |
| Estrusione 1 (84) | 62 | 6 | profondità, coerente con y = 0 ... 6 delle facce |
| Sketch 2 / Foro 1 (124 / 131) | 8 / 2 / 2 / 7 | 38 / 8 / 8 / 30 | quote dei 4 punti (§6.4.1) |
| Foro 1 (131) | 4 | 5,5 | diametro |
| Sketch 3 / Foro 2 (1095 / 1099) | 2 / 2 | 4 / 5 | distanze del punto dai bordi: l'origine `(67, 6, -5)` = 71 − 4 e 5 |
| Foro 2 (1099) | 4 | 5 | diametro |
| Foro 3 (1132) | 4 / 9 / 2 / 2 | 3 / 6 / 2,5 / 137,5 | diametro nominale M3, profondità filetto, punta, lunghezza (distanza fino all'estremo, §6.1) |
| Sketch 4 / Estrusione 2 (1903 / 1911) | 1 / 62 | 3,5 / 3 | quota dello schizzo e profondità |
| Arrotondamento 1 (2777) | 3 | 3 | raggio, confermato dallo storico |
**Cosa è provato e cosa no.** I valori `25` e `139` di *Rivoluzione 1* sono confermati da Creo
(§5). Per tutte le altre righe il *numero* è decodificato in modo plausibile ma il *significato*
(profondità? diametro?) è dedotto dal codice `dim_type`, da cui non ho una tabella certa. I codici
visti: `1` e `2` quote lineari/diametrali di schizzo, `3` raggio, `4` diametro, `9` distanza
(smusso), `62` profondità/estensione, altri (`5`, `10`, `14`, `37`) non interpretati. Per
distinguere con certezza serve un confronto riga per riga con un modello noto, o leggere il
nome della quota (`d0`, `d1`... del Creo).
La *tolleranza* (es. `h7` di Ø25) sta nello stesso record, ma non l'ho decodificata: nel campo
`tol_class`/`tol_table_index` compaiono indici, non la stringa `h7`.
## 3. La geometria di uno schizzo: `p_saved_result`
Ogni schizzo (sezione) ha, dentro `BasFullData`, un blocco:
```
gsec_ptr ... name "Sezione 1" <- nome mostrato nel tree
gsec2d_ptr name "S2D0001"
segtab_ptr / dimtab_ptr / dim_ref <- segmenti, quote e a cosa si riferiscono
...
p_saved_result
entity(line) ... <- le linee dello schizzo con le coordinate
entity(circle) ... <- i cerchi (centro, raggio)
local_sys 4x3 <- sistema di coordinate dello schizzo
```
Le coordinate delle entità sono **nel sistema dello schizzo**, non assolute (§6).
Ogni `entity(line)` ha due estremi con tre coordinate ciascuno `(x, y, z)`; con z sempre `0` nello
schizzo. Le coordinate sono numeri come in §1.
### 3.1 Esempio verificato: *Sezione 1* di Rivoluzione 1
Decodificando i numeri del blocco `entity(line)` di `9252000507.prt.11` si ottengono quattro
linee chiuse e una linea d'asse:
| Linea | Da `(x, y)` | A `(x, y)` |
|---|---|---|
| 1 | (0, 69,5) | (−12,5, 69,5) |
| 2 | (−12,5, 69,5) | (−12,5, −69,5) |
| 3 | (−12,5, −69,5) | (0, −69,5) |
| 4 | (0, −69,5) | (0, 69,5) |
| asse | (0, −100) | (0, 100) |
È il rettangolo di larghezza 12,5 (= raggio) e altezza 139, centrato in y sul piano di
riferimento, con il lato destro sull'asse x = 0, e una linea d'asse più lunga della figura.
**Coincide con quello che Creo mostra** nella vista dello schizzo (rettangolo di 139 di altezza,
quotato 25 come diametro rispetto all'asse tratteggiato, simmetrico rispetto a `TOP`).
Nota: nel file la quota `25` è un **diametro** (Ø25), anche se in geometria il lato è 12,5. Per
questo non si può dedurre la forma dalle sole quote: servono anche le linee.
### 3.2 Secondo esempio: il profilo a L di *Estrusione 1* (`9239902104.prt.1`)
Le linee del profilo (`entity(line)`, estremi `(x, y)` nel sistema dello schizzo, `z = 0`):
| Linea | Da | A |
|---|---|---|
| 1 | (71, 0) | (−54, 0) |
| 2 | (−54, 0) | (−54, 46) |
| 3 | (−54, 46) | (0, 46) |
| 4 | (0, 46) | (0, 21) |
| 5 | (0, 21) | (71, 21) |
| 6 | (71, 21) | (71, 0) |
più una linea d'asse. Le quote 125 (= 54 + 71), 54, 46 e 21 sono tutte spiegate dal profilo. Qui le
linee stanno in `FeatDefs` (record della feature, sezione `S2D0006`), non in `BasFullData` come in
§3: la posizione del blocco `p_saved_result` dipende dal tipo di feature.
## 4. Da schizzo + quote alla forma: la rivoluzione
Con le due cose sopra, *Rivoluzione 1* è determinata così:
```
profilo (nel piano dello schizzo): x in [-12,5 ; 0], y in [-69,5 ; 69,5]
asse di rotazione: la linea x = 0 (la linea d'asse dello schizzo)
angolo: 360 (non decodificato, vedi §7)
risultato: cilindro Ø25 x 139, centrato sull'origine lungo y dello schizzo
```
Nell'albero di Creo il cilindro risulta con il fondo sul piano `DTM1` e l'asse come `A_1`: la
posizione assoluta (quale piano contiene lo schizzo, come è orientato `y`) **non è nello schizzo**
ma nella definizione della feature (§7).
## 5. Verifica di *Rivoluzione 1* contro Creo
`9252000507.prt` aperto in Creo (schermate del model tree e dello schizzo):
| Cosa | Creo | Letto dal file |
|---|---|---|
| Lunghezza dello schizzo | 139.000 | quota `dim_type 1` = 139 |
| Diametro | Ø25 (h7) | quota `dim_type 2` = 25 |
| Rettangolo, simmetrico rispetto a `TOP` | sì | y da −69,5 a 69,5 |
| Rettangolo a sinistra dell'asse | sì | x da −12,5 a 0 |
| Asse (linea di costruzione) | più lungo della figura | da y = −100 a 100 |
| Assi A_3, A_4, A_5, punto PNT0 | presenti nel tree | `AllFeatur` + storico (`formato-prt-feature.md`) |
## 6. Posizione nello spazio
### 6.1 Matrici 4×3
Piani di riferimento, assi, sistemi di coordinate, schizzi e feature hanno un `local_sys`: array
`f9 04 03` = 4 righe per 3 colonne = 12 numeri: **tre versori e, per ultima, l'origine**. Si leggono
con i numeri di §1 più un codice di ripetizione: un byte `e4`-`e7` vale **"altri `K − 3` zeri"**,
con `K` = byte − `e0` (`e4` = 1 zero, `e5` = 2, `e6` = 3, `e7` = 4). L'identità è quindi
`0f 18 e5 0f 18 e5 0f 18 e5` = `1,0,0,0, 1,0,0,0, 1,0,0,0`.
**Verificato su `9239902104.prt.1`** (livello 1: i valori coincidono con quelli mostrati da Creo,
sotto), con le 4 righe `(e1, e2, e3, origine)`:
| Feature | Matrice letta | Significato |
|---|---|---|
| Foro 1 | `(1,0,0) (0,0,-1) (0,1,0)`, origine `(-46, 6, -38)` | uno dei 4 punti dello schizzo `(-46, 38)`: x = x dello schizzo, z = **−**y dello schizzo, y = 6 = faccia superiore (la piastra è spessa 6) |
| Foro 2 | `(1,0,0) (0,0,-1) (0,1,0)`, origine `(67, 6, -5)` | 71 − 4 e 5 dal bordo: le due quote 4 e 5 della feature |
| Foro 3 | `(0,1,0) (-1,0,0) (0,0,1)`, origine `(-66.5, 3, -5)` | y = 3: l'**offset 3,00** dalla faccia superiore (y = 6) che Creo mostra; z = −5: allineato all'asse di Foro 2 (**"Allinea" con `A_7`**); x = −66,5 = 71 − 137,5, cioè il foro parte 137,5 prima dell'estremo destro (71) |
Quindi **la posizione di un foro si legge dall'origine della sua matrice**: le quote di
posizionamento (4, 5, 3,00) non servono, sono già il risultato. Nei tre fori l'`y` e la `z` tornano
esattamente con le quote e con i riferimenti mostrati da Creo.
Dalla stessa matrice: nel modello di questa piastra lo schizzo di Estrusione 1 sta nel piano `x–z`
(x dello schizzo = x, y dello schizzo = −z) e l'estrusione cresce lungo +y fino a 6.
**Resta incerta l'interpretazione dell'asse del foro.** Per Foro 1 e 2 il terzo versore è `(0,1,0)`
(perpendicolare alla piastra, come deve essere). Per Foro 3 è `(0,0,1)`, ma il foro in Creo entra
dalla faccia laterale: o la convenzione del versore dell'asse è diversa, o la direzione di foratura
sta in un altro campo. Non risolto.
### 6.2 Dove stanno
| Cosa | Dove nel file |
|---|---|
| Piani di riferimento (RIGHT, TOP, FRONT, DTM1) | `ActDatums` (`srf_prim_ptr(plane)` con `local_sys`), `feat_id` = id della feature; vincoli di posizionamento in `FeatDefsDtm` (§6.3) |
| Sistema di coordinate (`PRT_CSYS_DEF`) | `MdlRefInfo` (`ent_list(coord_sys)`) e `GeomDepen` |
| Assi, punti (A_3, PNT0...) | `ent_list(arc_z)`, `ent_list(point)` in `MdlRefInfo` |
| Piano che contiene lo schizzo | `gsec3d_ptr` -> `plane_id`, `ref_planes`; il piano è identificato da un id di geometria |
| Orientamento dello schizzo | `sec_orient_data`, `place_instruction_ptrs` (nella definizione di feature `feat_defs_<tipo>`) |
| Offset di un piano da un altro (DTM1) | quota di tipo "offset" nel `dim_array` della feature, se esiste |
### 6.3 Come è posizionato un piano: i vincoli di `FeatDefsDtm`
La finestra "Piano di riferimento → Posizionamento" di Creo (riferimenti con il loro vincolo,
es. *Tangente* a una superficie, *Parallelo* a un piano) **si legge dal file**. Sta nella sezione
`FeatDefsDtm`, record `feat_defs_923` (un record per piano; `feat_defs_926` assi, `feat_defs_931`
punti, `feat_defs_979` sistemi di assi). Ogni record ha:
```
id id della feature (come in AllFeatur)
local_sys matrice 4x3 del piano risultante (§6.1)
place_instruction_ptrs una istruzione per ogni riferimento della finestra:
type codice del vincolo (intero a 2 byte)
offset double, offset del vincolo
dim_id, ref_id quota dell'offset e riferimento, se esistono
geometry1_id id della geometria a cui ci si riferisce
geometry2_id id della geometria che si sta creando (il piano stesso)
```
Gli `id` di geometria (`geom_id`) si risolvono nelle sezioni `ActDatums` (piani e assi di
riferimento), `VisibGeom` e `NovisGeom` (superfici del solido, visibili e no). Ogni voce di
`srf_array` ha `geom_id`, `geom_type` (`"`, 0x22 = piano; `$`, 0x24 = cilindro) e **`feat_id`**: è il
legame con la feature che l'ha generata.
**Verifica sul caso che hai mostrato**: *DTM1* di `9252000507.prt` (feature 116), finestra di Creo
"Supfce:F5(RIVOLUZIONE_1) Tangente / FRONT:F3 Parallelo". Dal file:
| Istruzione | `type` | `geometry1_id` | Si risolve in | Corrisponde a |
|---|---|---|---|---|
| 1 | `81 fb` (507) | 88 | `VisibGeom`: cilindro (`$`), `feat_id` 73 = Rivoluzione 1 | "Supfce:F5(RIVOLUZIONE_1)", **Tangente** |
| 2 | `81 f9` (505) | 6 | `ActDatums`: piano (`"`), `feat_id` 5 = FRONT | "FRONT:F3", **Parallelo** |
`geometry2_id` è 117 in entrambe: `ActDatums` lo dà come piano con `feat_id` 116 = DTM1 (il piano
risultante). Nella sua matrice compare il valore `12.5` (`2f 29 00`): è il raggio del cilindro,
come deve essere per un piano tangente ad esso. Il campo `Traslazione` della finestra era vuoto.
Quindi **quali sono i riferimenti** (quale feature/superficie e quale piano) si legge con certezza.
Resta da fissare:
- **Il significato di `type`.** 507 ↔ Tangente e 505 ↔ Parallelo è dedotto da un solo esempio e
dall'ordine delle righe della finestra: non ho una tabella dei codici. Gli altri visti
(`82 01`, `82 02`, `82 03`, `81 fd`, `81 ff`) sono quelli dei piani di default e degli altri
piani del campione, non interpretati.
- **`Feature number` di Creo** (`F3`, `F5`): non è l'`id` ma la **posizione** nel tree
(RIGHT = F1, TOP = F2, FRONT = F3, PRT_CSYS_DEF = F4, Rivoluzione 1 = F5). I piani interni non
contano. Per tradurre un `id` in `F<n>` si numerano le feature visibili in ordine
(`formato-prt-feature.md` §3).
- **L'offset.** Se un vincolo ha un offset (quota di tipo offset) il valore sta nel `dim_array`
della feature (§2) con il `dim_id` dell'istruzione; non ho un esempio con offset non nullo.
### 6.4 Il foro: `feat_defs_911`
Riferimento: *Foro 1* di `9252000507.prt.11` (feature 157), confrontato con le schermate di Creo.
Creo mostra: tipo **Standard**, profilo **Diritto** con **Maschiato**, filetto ISO **M8x1.25**,
**profondità 25**, **Ø6,8**, **profondità del filetto 20**, **angolo della punta 118°**,
"Includi la superficie del filetto" attivo, posizionamento **Coassiale** con riferimenti
`Supfce:F5(RIVOLUZIONE_1)` e `A_1(ASSE):F5(RIVOLUZIONE_1)`.
La definizione del foro sta nella sezione `FeatDefs`, record `feat_defs_911`. Ha due parti.
**a) I parametri del filetto, in chiaro** (`data(ddt_plinfo_hole_fstn_param)`):
| Campo | Valore letto | In Creo |
|---|---|---|
| `name`, `series` | `ISO`, `ISO` | Tipo filetto: ISO |
| `scrsize` | `M8x1.25` | Dimensione vite: M8x1.25 |
| `scrsize_val` | `0x1d` (29) | indice di M8x1.25 nella tabella delle viti di Creo (nel segnaposto `M1x.25` vale 0) |
| `hole_class` | `H` | (classe del foro) |
| `drillnum` | `6.8` | Ø6,8 |
**b) Le istruzioni** (`place_instruction_ptrs`): una per ogni parametro numerico, nella forma
`type`, `offset` (un `double`, §1), `dim_id`, `ref_id`, `geometry1_id`, `geometry2_id`. Quelle
del foro filettato, con il valore atteso da Creo:
| `type` | `offset` letto | In Creo | Prova |
|---|---|---|---|
| 5104 (`0x13f0`) | 6,8 | Ø6,8 (anche nel foro semplice, vedi sotto) | **livello 1** |
| 5528 (`0x1598`) | 118 | angolo della punta | **livello 1** |
| 5538 (`0x15a2`) | 25 | profondità del foro | **livello 1** |
| 5536 (`0x15a0`) | 1,25 | passo del filetto (M8**x1.25**) | livello 2 |
| 6617 (`0x19d9`) | 11 | non corrisponde a nulla di mostrato | non interpretato |
| 5542, 10103, 5533, 5534 | 0 | non mostrati | non interpretati |
Il codice `type` identifica il parametro; il valore è `offset`. La profondità del filetto (20) e il
diametro nominale (8) **non** sono istruzioni ma quote del `dim_array` (§2), con `dim_type` 9 e 4
e `feat_id` 157. In tutto il `dim_array` ha per il foro cinque quote: 8 (`dim_type` 4), 20 (9),
Ø6,8 (2, scritto con il tag `2e`), 118 (10) e 25 (2). Tre di queste (6,8, 118, 25) compaiono
quindi due volte, come quota e come parametro di un'istruzione.
**Foro semplice e foro standard.** `test.prt.1` ha un foro **semplice**: il suo `feat_defs_911`
è lungo 862 byte e ha solo due istruzioni (`5104` con `dim_id` 7, e `1853`); i parametri del
filetto ci sono comunque, ma con i valori segnaposto di Creo (`M1x.25`, `drillnum 0.75`,
`scrsize_val 0`): **quindi le stringhe `scrsize`/`drillnum` da sole non dicono se il foro è
filettato**. Quello di Fata ha 3212 byte e 12 istruzioni, valori diversi dal segnaposto. Il segno
affidabile oggi è: **foro standard = presenza delle istruzioni 5536 (passo) e 5528 (angolo punta)**,
e il foro è filettato quando anche i valori del filetto non sono quelli segnaposto.
**Non individuato nel file** (per ognuno servono due salvataggi che differiscono solo per
quell'opzione):
- *Maschiato / Trapanato / Gioco*, *Diritto / Rastremato*: non so quale istruzione o flag le
codifichi. `plin_info_hole_flag.flags` vale `2, 2` in entrambi i fori.
- *Includi la superficie del filetto*: idem.
- Il tipo *Standard*/*Semplice* come campo esplicito: oggi si deduce solo dal numero di istruzioni.
**Posizionamento Coassiale.** Gli ingredienti ci sono: un blocco `data(plin_info_ref)` per ogni
riferimento, con gli id di geometria. Il piano del riferimento `Supfce:F5` dovrebbe essere la
faccia piana di *Rivoluzione 1* (feature 73, in `VisibGeom` con `feat_id` 73) e il secondo
riferimento l'asse `A_1`: non ho agganciato quegli id perché le coppie `id_type1`/`id_type2` non le
ho decifrate. La geometria generata dal foro (cilindro, cono, piani con `feat_id` 157) è invece
in `VisibGeom` e `NovisGeom`, e `geometry2_id` 175 delle istruzioni è un suo piano.
### 6.4.1 Foro a punti di schizzo e facce piane: `9239902104.prt.1`
*Foro 1* è definito da *Sketch 2* con **4 punti**. Cosa si legge, tutto da `local_sys` (§6.1):
**I punti.** Nello schizzo (`entity(point)`, campo `pnt`, un `[x, y, z]` per punto) sono
`(-46, 8)`, `(-8, 8)`, `(-8, 38)`, `(-46, 38)`, con quote 8, 8, 30 e 38. Nel modello ogni punto
diventa un'**istanza** del foro, e ogni istanza ha un piano proprio in `NovisGeom` (superficie
piana con `feat_id` 131, §6.3) con la matrice del punto: l'**origine** è la posizione assoluta.
| Piano (`geom_id`) | Origine nel modello |
|---|---|
| 135 | `(-46, 6, -8)` |
| 147 | `(-8, 6, -8)` |
| 159 | `(-8, 6, -38)` |
| 171 | `(-46, 6, -38)` |
Quindi `x` = `x` dello schizzo, `z` = **−**`y` dello schizzo (confermato dai quattro punti e da Foro
2, §6.1) e `y` = 6.
**La faccia.** La superficie su cui stanno è la faccia piana superiore di *Estrusione 1*: ha
`y = 6` e la stessa matrice del sistema di coordinate dello schizzo (origine `(0, 6, 0)`,
versori `(1,0,0)`, `(0,0,1)`, `(0,-1,0)`). Le facce piane di *Estrusione 1* (`feat_id` 84, in
`VisibGeom`) sono, con origine e normale (il terzo versore):
| `geom_id` | Origine | Normale | Faccia |
|---|---|---|---|
| 95 | `(0, 6, 0)` | `(0,-1,0)` | **superiore** (`y = 6`), dove sono i fori |
| 107 | `(71, 0, 0)` | `(-1,0,0)` | estremo destro (`x = 71`) |
| 113 | `(-54, 0, -46)` | `(1,0,0)` | estremo sinistro (`x = -54`) |
| 115 | `(-54, 0, 0)` | `(0,0,1)` | lato `z = 0` |
| 2999 | `(71, 0, -21)` | `(0,0,-1)` | lato del gradino (`z = -21`) |
| 3001 | `(0, 0, -21)` | `(-1,0,0)` | alzata del gradino (`x = 0`) |
| 3003 | `(0, 0, -46)` | `(0,0,-1)` | lato `z = -46` |
La piastra a L di Estrusione 1 (§3.2, profilo `x` da −54 a 71, `y` dello schizzo da 0 a 46, gradino
a 21) torna in coordinate modello con `z = −y` dello schizzo, e spessore 6 lungo `y`. Gli altri
tre fori cadono dentro il materiale (`x` −46 e −8, `z` −8 e −38).
**Non risolto:** quale `geom_id` il foro usi *formalmente* come faccia di riferimento (95, per
coincidenza di piano e di origine) non è scritto in un campo che io abbia identificato: è
un'identificazione per posizione, non una lettura.
### 6.5 Lo smusso: `feat_defs_914`
Riferimento: *Smusso 1* di `9252000507.prt.11` (feature 252), in modifica in Creo: modalità
**Insiemi**, schema quote **D x D** con **D = 0.500**, un insieme ("Insieme 1") con **due
riferimenti**, entrambi `Spigolo:F5(RIVOLUZIONE_1)`, e l'opzione "Superfici con offset".
| Cosa | Dove | Letto | Prova |
|---|---|---|---|
| Valore D | `dim_array`, `feat_id` 252, `dim_type` 9 | `0e` = **0,5** | **livello 1** |
| Valore in `test.prt.1` | `dim_array`, feat 188, `dim_type` 9 | `2f 18 00` = 6,0 | livello 2 |
| Superfici del bordo | `feat_defs_914`, istruzioni di tipo 4530 | `geometry1_id` = 88, 86, 106, 90 | vedi sotto |
| Superfici generate dallo smusso | `VisibGeom`, `feat_id` 252 | superfici coniche (`geom_id` 253, 258, 261, 266) | presenti |
Il `dim_type` 9 è lo stesso del foro (profondità del filetto): dice la *classe* della quota, non
cosa misura. Per lo smusso vale D, ma la stessa classe in un foro vale 20.
**I riferimenti.** Le istruzioni di tipo `4530` hanno nel campo `geometry1_id` gli id 88, 86, 106 e
90, ripetuti a coppie. Risolti con `VisibGeom`: **88** e **106** sono superfici cilindriche con
`feat_id` 73 (cioè *Rivoluzione 1*, F5), **86** è il piano di estremità della stessa rivoluzione
(visto in §3.1 e §6.3). Quindi i riferimenti `Spigolo:F5(RIVOLUZIONE_1)` sono spigoli ricavati dalle
superfici di Rivoluzione 1, come mostra Creo. Non so ancora quale coppia di id formi quale
spigolo, né a quale dei due "Spigolo" della lista corrisponda (le istruzioni non sono in un ordine
che io abbia decifrato), e **90** non l'ho risolto.
**Non decodificato:**
- lo schema quote (D x D, D1 x D2, angolo x D...): non ho trovato dove sta; compaiono vari tipi di
istruzione (`7350`, `7360`, `4549`, `4552`, `4557`, `4558`, `1125`, `7265`, `14741`) senza
corrispondenza con le voci mostrate;
- "Insieme 1" e la sua composizione: compare la struttura (`data(plin_info_chut_info)`, array di
riferimenti) ma non la ho letta;
- "Superfici con offset" e il tipo di transizione;
- l'istruzione `14741`, con un valore `11,476` in precisione piena: non corrisponde a niente di
mostrato.
**Foro filettato con posizionamento lineare** (*Foro 3* di `9239902104.prt.1`). Creo mostra: tipo
Standard, Diritto, Maschiato, ISO **M3x.5**, Ø2,50 (punta), campo profondità vuoto, posizionamento
**Lineare** con riferimento primario `Supfce:F6(ESTRUSIONE_1)` e due riferimenti di offset
(`Supfce:F6(ESTRUSIONE_1)` con offset 3,00 e `A_7(ASSE):F10(FORO 2)` allineato). Dal file:
| Informazione | Letto | Prova |
|---|---|---|
| Filetto | stringhe `ISO`, `M3x.5`, punta `2.5`; `scrsize_val` = 12 | concorda con Creo |
| Ø della punta | istruzione 5104, offset 2,5 | concorda con Creo (Ø2,50) |
| Quote del foro | 3 (`dim_type` 4), 6 (9), 2,5 (2), 137,5 (2) | numeri letti; 137,5 con campo "Profondità" vuoto in Creo: probabilmente un foro passante, valore non verificato |
| Posizione risultante (offset 3,00 e allineamento a `A_7`) | **letta dalla matrice** `local_sys` (§6.1): origine `(-66.5, 3, -5)`, con y = 3 e z = −5 uguali a quelli di Foro 2 | concorda con Creo; la quota 3,00 non è una quota a sé nel `dim_array` |
| Riferimenti `F6` e `A_7` | non risolti | gli id stanno in blocchi `plin_info_ref` come per DTM1 (§6.3) |
Correzione: il primo codice delle istruzioni è 5542 anche qui, come nel foro coassiale di
`9252000507`; il `12266` che avevo letto nella prima versione di questo doc veniva da un'istruzione
di un altro record. Il tipo di posizionamento (coassiale / lineare) **non** è quel codice. Quale sia
non lo so: la posizione finale si legge comunque dalla matrice (§6.1).
## 7. Cosa manca per determinare un `.prt` in modo univoco
Tabella di verità: ogni informazione necessaria, se l'ho decodificata e quanto è provata.
| Informazione | Stato | Dove si trova |
|---|---|---|
| Elenco e ordine delle feature | **fatto**, provato (`test.prt.1`, `9252000507.prt.11` contro Creo) | `AllFeatur` |
| Tipo di ogni feature | **fatto**; estrusione vs rivoluzione solo con lo storico | `AllFeatur`, storico |
| Nome di ogni feature | **fatto**, ma solo se esiste lo storico (10 file su 31 leggibili) | `MdlStatus` |
| Quote (valori numerici) | **fatto per i valori interi/semplici**, tag `2f`/`48` | `dim_array` |
| Significato di ogni quota | **parziale**: provato solo per Rivoluzione 1 | `dim_type` (§2.1) |
| Geometria dello schizzo (linee) | **fatto** per le linee, provato su Rivoluzione 1 | `p_saved_result` |
| Cerchi, archi, spline dello schizzo | non verificato (cerchio visto in `test.prt.1`, raggio 50) | `entity(circle)`, `entity(arc_z)`, `entity(spline)` |
| Valori non "tondi" | **non decodificato** (tag diversi da `2f`/`48`) | ovunque |
| Piano e orientamento dello schizzo | **non decodificato** | `gsec3d_ptr`, `sec_orient_data` |
| Posizione/rotazione di fori e feature | **fatto**: matrice 4×3 (§6.1) | `local_sys` |
| Posizione/rotazione di piani, assi, punti | stessa matrice, da verificare su un piano ruotato | `ActDatums`, `MdlRefInfo` |
| Vincoli e riferimenti di un piano (come DTM1) | **fatto per DTM1 di `9252000507`** (riferimenti sì, nome del vincolo da un esempio) | `FeatDefsDtm`, §6.3 |
| Offset di un piano da un altro | **non decodificato**: mai visto un offset non nullo | quota in `dim_array` (`dim_id`) |
| Direzione/lato e angolo di una rivoluzione, profondità e direzione di un'estrusione | **non decodificato** | `feat_defs_917`/`916` (`place_instruction_ptrs`, `plin_info_arr`) |
| Parametri del foro (misura, punta, passo, profondità, angolo) | **fatto**, provato contro Creo su `9252000507` | `feat_defs_911`, §6.4 |
| Opzioni del foro (maschiato, diritto, superficie del filetto) | **non individuato** | §6.4 |
| Posizione assoluta del foro (origine e versori) | **fatto**, provato contro i riferimenti di Creo su `9239902104` (§6.1) | `local_sys` del record |
| Riferimenti del posizionamento (quale faccia/asse) | **non decodificato** | `plin_info_ref` in `feat_defs_911` |
| Valore di raccordi e smussi | **fatto**, provato contro Creo su `9252000507` (D = 0,5) | `dim_array` |
| Spigoli a cui si applicano raccordi/smussi | **parziale**: si risolvono le superfici (feature di origine), non lo spigolo né l'insieme | `feat_defs_914`, §6.5 |
| Schema quote e insiemi dello smusso | **non decodificato** | `feat_defs_914` |
| Tolleranze (h7...) | **non decodificato** | blocco `tol_ptr` del `dim_array` |
| Unità di misura | **non letto** | `UnitSystemDef` (sezione compressa) |
| Feature nascoste / soppresse | **non decodificato** | non individuato nel file |
| Parametri e materiale | parziale, via `ND:0:Model_*` (XML) | sezione compressa `.Z` |
| File Wildfire (generazioni 1322/1375/1376) | **illeggibile** (sezioni di tipo `11`) | `formato-prt-feature.md` §1.3 |
**Conclusione onesta:** oggi si ricava con affidabilità *quali* feature ci sono, *che tipo* sono,
*come si chiamano* (se c'è lo storico), *quanto misurano* le quote semplici e *che forma ha lo
schizzo* se fatto di linee con numeri semplici. Non si ricava ancora *dove* stanno nello spazio né
*come* sono orientate, né si può ricostruire il solido completo.
## 8. Come finire il lavoro
Per ogni riga "non decodificato" della §7 il metodo è lo stesso, ed è quello che ha dato Rivoluzione
1:
1. Scegliere un `.prt` piccolo con **una sola feature** del tipo da decodificare, e annotare da
Creo i valori veri (quote, posizione dei piani, angolo, direzione).
2. Estrarre da `BasFullData`/`ActDatums` i record di quella feature e cercarvi quei numeri con il
decodificatore di §1: i valori noti ancorano i byte.
3. Variare **una cosa alla volta** nel modello (spostare il piano, cambiare l'angolo), risalvare e
confrontare i due file: i byte che cambiano sono il campo cercato.
4. Registrare in questo doc il campo, con il file di prova e il valore atteso, come in §5.
Tre modelli di prova che chiuderebbero la maggior parte della §7: un piano `DTM1` a offset non nullo
dal piano `TOP` (e con un vincolo *Attraverso*, *Normale*, *Offset*...); una estrusione di profondità nota su un piano ruotato; una rivoluzione di 90°.
Una volta chiusa la §7 il passo successivo è un `PrtModelReader` in C# accanto ad `AsmRecordReader`,
con le stesse regole: se un valore non si decodifica si solleva `InvalidDataException`, non si
restituisce un modello parziale in silenzio.
+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` |
+25
View File
@@ -0,0 +1,25 @@
import sys,os,glob,collections
sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
import prt,psb
files=sorted(glob.glob(sys.argv[1]))
tot=collections.Counter(); errs=collections.Counter(); bygen=collections.defaultdict(collections.Counter)
samples={}
for f in files:
try: d,secs,order=prt.load(f)
except Exception as e:
print('LOADERR',f,e); continue
gen=d[:60].split()[2].decode()
for n in order:
o,l,fl=secs[n]
typ=fl[2]
if typ!='9': bygen[gen]['type_'+typ]+=1; continue
b=d[o:o+l]
p,root,e=psb.parse(b)
full = p.p==len(b)
if e is None and full: bygen[gen]['ok']+=1; bygen[gen]['okbytes']+=l
else:
bygen[gen]['err']+=1; bygen[gen]['errbytes']+=l
key=(n.split('#')[0], str(e).split('@')[0][:50])
errs[key]+=1; samples.setdefault(key,(os.path.basename(f),str(e)))
for g,c in sorted(bygen.items()): print(g,dict(c))
for k,v in errs.most_common(40): print(v,k,samples[k])
+9
View File
@@ -0,0 +1,9 @@
import sys,os
sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
import prt
for a in sys.argv[1:]:
f,sec,off=a.split(',')
off=int(off,16)
d,secs,order=prt.load(f); o,l,_=secs[sec]; b=d[o:o+l]
print('==',a)
prt.hexdump(b,max(0,off-96),192)
+30
View File
@@ -0,0 +1,30 @@
import sys,os,math,collections
sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
import prt,psb
d,secs,order=prt.load(sys.argv[1])
bad=collections.Counter(); good=0; unk=collections.Counter()
def walk(v,name=''):
global good
if isinstance(v,psb.Node):
for n,x in v: walk(x,n)
elif isinstance(v,psb.Arr):
for x in v: walk(x,name)
elif isinstance(v,list) and v and isinstance(v[0],list) and len(v) in (3,4) and len(v[0])==3 and name in ('local_sys','orient','matrix'):
for r in v[:3]:
if any(isinstance(c,psb.Unk) for c in r):
u=[c for c in r if isinstance(c,psb.Unk)]
if len(u)==1:
rest=sum(c*c for c in r if not isinstance(c,psb.Unk))
unk[(u[0].code, round(math.sqrt(max(0,1-rest)),6))]+=1
continue
n=math.sqrt(sum(c*c for c in r))
if abs(n-1)<1e-6: good+=1
else: bad[(name,tuple(round(c,4) for c in r))]+=1
for nme in order:
o,l,f=secs[nme]
if f[2]!='9': continue
p,root,e=psb.parse(d[o:o+l])
if root: walk(root[1])
print('good rows',good,'bad',sum(bad.values()))
for k,v in bad.most_common(15): print(v,k)
print('unknown consts solved by norm:',unk.most_common(20))
+11
View File
@@ -0,0 +1,11 @@
import sys,os
sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
import prt,psb
for f in sys.argv[1:]:
b=open(f,'rb').read()
p,root,e=psb.parse(b)
print(os.path.basename(f),len(b),p.p,'fields',p.nid,'lenient',p.lenient,'OK' if e is None else 'ERR '+repr(e))
if e is not None: prt.hexdump(b,max(0,p.p-64),128)
if root:
out=[root[0]+':']; psb.fmt(root[1],1,out)
open(f[:-4]+'.txt','w',encoding='utf-8').write('\n'.join(out))
+40
View File
@@ -0,0 +1,40 @@
import sys,re
sys.stdout.reconfigure(encoding='utf-8',errors='replace')
def load(p):
d=open(p,'rb').read()
base=int(d[:d.find(b'\n')].split()[-2],16)
secs={}
order=[]
i=d.find(b'#UGC_TOC')
while True:
e=d.find(b'\n',i); pos=e+1; nxt=None
while True:
e2=d.find(b'\n',pos); line=d[pos:e2]
if line.startswith(b'#'): break
f=line.rstrip(b'#').split()
name=f[0].decode('latin1'); off=int(f[1],16); ln=int(f[2],16)
if name=='NEXT_TOC_ENTRY': nxt=base+off
else:
mk=len(name)+2
secs[name]=(base+off+mk,ln-mk,[x.decode() for x in f[3:]]); order.append(name)
pos=e2+1
if nxt is None: break
i=nxt
return d,secs,order
def sec(p,name):
d,secs,_=load(p); o,l,_=secs[name]; return d[o:o+l]
def hexdump(b,start=0,n=None,w=32):
b=b[start:start+n] if n else b[start:]
for i in range(0,len(b),w):
c=b[i:i+w]
print(f"{start+i:6x} "+' '.join(f"{x:02x}" for x in c).ljust(w*3)+' '+''.join(chr(x) if 32<=x<127 else '.' for x in c))
if __name__=='__main__':
d,secs,order=load(sys.argv[1])
if len(sys.argv)>2:
o,l,_=secs[sys.argv[2]]; b=d[o:o+l]
st=int(sys.argv[3],0) if len(sys.argv)>3 else 0
n=int(sys.argv[4],0) if len(sys.argv)>4 else None
hexdump(b,st,n)
else:
for nme in order:
o,l,f=secs[nme]; print(f"{nme:26} {o:8} {l:8} {' '.join(f):34} {d[o:o+20]!r}")
+529
View File
@@ -0,0 +1,529 @@
"""Parser generico delle sezioni a marcatori dei file Creo (.prt/.asm). Reverse engineering.
Modello: ogni campo dichiarato per nome (e0 <tipo> nome\\0) riceve un id progressivo.
In modalita' posizionale il campo atteso e' next[ultimo id letto]; f7 <id> lo forza.
"""
import struct, sys, os
class Err(Exception):
pass
class Node(list):
"""struct: lista di (nome, valore)"""
def get(s, k, d=None):
for n, v in s:
if n == k:
return v
return d
def all(s, k):
return [v for n, v in s if n == k]
class Arr(list):
pass
class Unk(float):
"""costante a un byte non ancora identificata"""
def __new__(cls, code):
o = float.__new__(cls, 'nan')
o.code = code
return o
def __repr__(s):
return f"<c{s.code:02x}>"
class P:
def __init__(s, b, trace=False):
s.b = b
s.p = 0
s.fields = {}
s.nid = 0
s.next = {}
s.alias = {}
s.stack = []
s.trace = trace
s.root = None
s.last = None
s.lenient = 0
s.lastc = None
s.popped = None
def u8(s):
v = s.b[s.p]
s.p += 1
return v
def peek(s):
return s.b[s.p] if s.p < len(s.b) else None
def rint(s):
b = s.u8()
if b < 0x80:
return b
if b < 0xc0:
return (b & 0x3f) << 8 | s.u8()
if b < 0xe0:
v = (b & 0x1f) << 16 | s.b[s.p] << 8 | s.b[s.p + 1]
s.p += 2
return v
if b == 0xe8:
return s.u8()
if b == 0xe9:
v = int.from_bytes(s.b[s.p:s.p + 2], 'big')
s.p += 2
return v
if b == 0xea:
v = int.from_bytes(s.b[s.p:s.p + 3], 'big')
s.p += 3
return v
if b == 0xeb:
v = int.from_bytes(s.b[s.p:s.p + 4], 'big')
s.p += 4
return v
if b == 0xf5:
return s.rint() | 0x80000000
if b == 0xf6:
return -1
raise Err(f"int tag {b:02x} @{s.p - 1:x}")
CONST = {0x17: -6.283185307179586, 0x14: 4.71238898038469, 0x0d: -0.5, 0x11: 3.141592653589793, 0x15: -3.141592653589793, 0x12: 1.5707963267948966, 0x16: -1.5707963267948966, 0x13: 6.283185307179586, 0x18: 0.0, 0x0f: 1.0, 0x0e: 0.5, 0x10: -1.0, 0x0c: 0.1, 0x0a: 0.01, 0x07: 0.001}
FBASE = 0x3f9d
FCONST = {0x0b: 1.0, 0x28: -1.0}
def rflt(s):
t = s.u8()
if 0x30 <= t <= 0x4f:
T = (0x37 + t - 0x30) if t < 0x40 else (0xb7 + t - 0x40)
raw = bytes([T]) + s.b[s.p:s.p + 3]
s.p += 3
elif t == 0xea:
raw = s.b[s.p:s.p + 3] + bytes(1)
s.p += 3
elif t == 0xeb:
raw = s.b[s.p:s.p + 4]
s.p += 4
elif t == 0xe9:
raw = s.b[s.p:s.p + 2] + bytes(2)
s.p += 2
elif t == 0xe8:
raw = s.b[s.p:s.p + 1] + bytes(3)
s.p += 1
elif t == 0x00:
return 0.0
elif t in s.FCONST:
return s.FCONST[t]
elif t < 0x30:
return Unk(t)
elif 0x50 <= t <= 0xdf:
raw = (s.FBASE + t).to_bytes(2, 'big') + s.b[s.p:s.p + 2]
s.p += 2
else:
raise Err(f"flt tag {t:02x} @{s.p - 1:x}")
return struct.unpack('>f', raw)[0]
def rbyte(s):
b = s.u8()
if b == 0xe8:
return s.u8()
return b
# prefissi negativi a 16 bit dei tag a5-df: bfd0..c00d meno tre buchi (bfd2, bfdf, bffe)
NEG = [x for x in range(0xbfd0, 0xc00e) if x not in (0xbfd2, 0xbfdf, 0xbffe)]
def rdbl(s):
t = s.u8()
if t in s.CONST:
return s.CONST[t]
if t <= 0x16 and t > 0x03:
return Unk(t)
if t <= 0x03:
c = s.u8()
k = t + 1
pl = s.b[s.p:s.p + k]
s.p += k
if c not in s.CONST:
if 0x04 <= c <= 0x16:
return Unk(c)
raise Err(f"dbl const {c:02x} after modifier @{s.p - 1:x}")
raw = bytearray(struct.pack('>d', s.CONST[c]))
raw[8 - k:] = pl
return struct.unpack('>d', bytes(raw))[0]
if t == 0xed:
v = struct.unpack('>d', s.b[s.p:s.p + 8])[0]
s.p += 8
return v
if t == 0xea:
v = struct.unpack('>d', s.b[s.p:s.p + 3] + bytes(5))[0]
s.p += 3
return v
if t == 0xec:
v = struct.unpack('>d', s.b[s.p:s.p + 7] + bytes(1))[0]
s.p += 7
return v
if 0x19 <= t <= 0x48:
if t <= 0x2f:
T = 0x3c + (t - 0x17) // 5
m = (t - 0x17) % 5
else:
T = 0xbc + (t - 0x30) // 5
m = (t - 0x30) % 5
if m == 4:
pl = s.b[s.p:s.p + 2]
s.p += 2
raw = bytes([T]) + pl + b'\x00' * 5
elif m == 3:
pl = s.b[s.p:s.p + 2]
s.p += 2
raw = bytes([T]) + pl + pl[1:] * 5
elif m == 2:
pl = s.b[s.p:s.p + 7]
s.p += 7
raw = bytes([T]) + pl
elif m == 1:
pl = s.b[s.p:s.p + 6]
s.p += 6
raw = bytes([T]) + pl + b'\x00'
else:
pl = s.b[s.p:s.p + 3]
s.p += 3
raw = bytes([T]) + pl + bytes(4)
return struct.unpack('>d', raw)[0]
if 0x49 <= t <= 0xdf:
hi = (0x3f75 + t) if t <= 0xa4 else s.NEG[t - 0xa5]
pl = s.b[s.p:s.p + 6]
s.p += 6
return struct.unpack('>d', hi.to_bytes(2, 'big') + pl)[0]
raise Err(f"dbl tag {t:02x} @{s.p - 1:x}")
def rstr(s):
if s.peek() == 0xe1:
s.p += 1
return None
e = s.b.index(0, s.p)
v = s.b[s.p:e].decode('latin1')
s.p = e + 1
return v
def rarr(s, n, rd):
out = []
while len(out) < n:
b = s.peek()
if 0xe4 <= b <= 0xe6 and out:
s.p += 1
out += [out[-1]] * (b - 0xe3)
elif b == 0xe7 and out:
s.p += 1
k = s.rint()
out += [out[-1]] * k
else:
out.append(rd())
if len(out) != n:
raise Err(f"array overrun {len(out)}>{n} @{s.p:x}")
return out
def scalar(s, f):
t = f['t']
if t in (0x0a, 0x03):
if s.peek() == 0xf8:
s.p += 1
f['dim'] = (s.rint(),)
return [s.rstr() for _ in range(f['dim'][0])]
if f.get('dim'):
out = []
while len(out) < f['dim'][0] and 0x20 <= s.peek() < 0xe0:
out.append(s.rstr())
return out
return s.rstr()
rd = s.rdbl if t == 0x02 else s.rbyte if t in (0x07, 0x04) else s.rflt if t == 0x06 else s.rint
b = s.peek()
if b == 0xe1:
s.p += 1
return None
if b == 0xf8:
s.p += 1
f['dim'] = (s.rint(),)
elif b == 0xf9:
s.p += 1
f['dim'] = (s.rint(), s.rint())
elif b == 0xfc:
s.p += 1
f['dim'] = (s.rint(),) + tuple(f['dim'][1:])
elif b == 0xfa:
s.p += 1
f['dim'] = tuple(s.rint() for _ in range(s.rint()))
d = f.get('dim')
if d is None:
return rd()
n = 1
for k in d:
n *= k
v = s.rarr(n, rd)
if len(d) == 2:
v = [v[i * d[1]:(i + 1) * d[1]] for i in range(d[0])]
return v
def sch(s, fid):
return s.alias.get(fid, fid)
# --- contenitori -------------------------------------------------
def owner_index(s, E, post=False):
own = s.fields[E]['own']
for i in range(len(s.stack) - 1, -1, -1):
c = s.stack[i]
if c['kind'] == 'struct':
if c['schema'] in own:
if post and s.fields[E].get('post') and i > 0:
return i - 1
return i
elif c['occ'] == E or ('A', c['occ']) in own:
return i
return None
def default_close(s):
"""chiusura implicita prima di un campo con nome o di un salto senza marcatore f0-f4:
se il campo atteso appartiene al contenitore padre di quello in cui e' stato scritto
l'ultimo campo, quest'ultimo contenitore si considera chiuso."""
if s.last is None or not s.stack or s.lastc is None:
return
E = s.next.get(s.last)
if E is None:
return
p = None
for k in range(len(s.stack) - 1, -1, -1):
if s.stack[k] is s.lastc:
p = k
break
if not p:
return
i = s.owner_index(E, True)
if i == p - 1:
del s.stack[p:]
def marker(s):
b = s.peek()
if b is None or not (0xf0 <= b <= 0xf4):
return None
s.p += 1
if b == 0xf4:
return s.rint()
return b - 0xf0
def explicit(s, n):
for _ in range(n):
if not s.stack:
raise Err(f"pop underflow @{s.p:x}")
s.popped = s.stack.pop()
def trans(s, E):
if s.last is not None:
s.next[s.last] = E
s.last = E
def locate(s, E, jump, stay):
"""porta in cima allo stack il contenitore che riceve il campo E. Ritorna 'elem' o 'field'."""
f = s.fields[E]
hdr = s.peek() in (0xf8, 0xf9)
top = s.stack[-1]
if not hdr and f['t'] == 0:
for i in range(len(s.stack) - 1, -1, -1):
c = s.stack[i]
if c['kind'] == 'array' and c['occ'] == E:
del s.stack[i + 1:]
return 'elem'
par = f['parent']
i = s.owner_index(E, not jump)
if i is None and jump and top['kind'] == 'struct' and top['node']:
# variante di unione riusata in un altro contenitore: resta dove siamo
f['own'].add(top['schema'])
s.lenient += 1
return 'field'
fresh = top['kind'] == 'struct' and not top['node']
if jump and fresh and i != len(s.stack) - 1:
s.alias[top['field']] = par
top['schema'] = par
return 'field'
if i is not None:
if jump and i != len(s.stack) - 1:
s.lenient += 1
del s.stack[i + 1:]
return 'field'
if jump and top['kind'] == 'array' and not top['node'] and f['t'] == 0 and not hdr:
top['occ'] = E
return 'elem'
raise Err(f"field {E} {f['name']}: container {par} not on stack @{s.p:x}")
def element(s, E):
c = s.stack[-1]
s.trans(E)
s.lastc = c
if s.peek() == 0xfb:
s.p += 1
b = s.u8()
if len(c['node']) >= c['n']:
raise Err(f"too many elements in {s.fields[c['field']]['name']} @{s.p:x}")
if b == 0xe1:
c['node'].append(None)
elif b in (0xe2, 0xe3):
v = Node()
c['node'].append(v)
s.stack.append(dict(kind='struct', field=c['field'], schema=s.sch(c['occ']), node=v, elem=True, occ=None, occ0=c['occ']))
else:
raise Err(f"array elem tag {b:02x} @{s.p - 1:x} in {s.fields[c['field']]['name']}")
def value(s, fid):
c = s.stack[-1] if s.stack else None
f = s.fields[fid]
s.trans(fid)
s.lastc = c
if f['t'] != 0:
v = s.scalar(f)
else:
b = s.u8()
if b == 0xe1:
v = None
elif b in (0xe2, 0xe3):
v = Node()
elif b == 0xf8:
v = Arr()
n = s.rint()
elif b == 0xf9:
v = Arr()
n = s.rint()
s.rint()
else:
raise Err(f"ptr tag {b:02x} @{s.p - 1:x} field {f['name']}")
if c is None:
s.root = (f['name'], v)
elif c['kind'] == 'array':
c.setdefault('tail', []).append((f['name'], v))
else:
c['node'].append((f['name'], v))
if isinstance(v, Arr):
s.stack.append(dict(kind='array', field=fid, occ=fid, n=n, node=v))
elif isinstance(v, Node):
s.stack.append(dict(kind='struct', field=fid, schema=s.sch(fid), node=v, occ=None))
def run(s):
b = s.b
while s.p < len(b):
m = s.marker()
x = s.peek()
if x is None:
s.explicit(m or 0)
break
if x == 0xe0:
s.p += 1
t = s.u8()
e = b.index(0, s.p)
name = b[s.p:e].decode('latin1')
s.p = e + 1
m2 = s.marker()
if m2 is not None:
m = (m or 0) + m2
s.popped = None
if m is None:
s.default_close()
else:
s.explicit(m)
c = s.stack[-1] if s.stack else None
s.nid += 1
fid = s.nid
if c is None:
par = 0
elif c['kind'] == 'array':
par = ('A', c['occ'])
else:
par = c['schema']
s.fields[fid] = dict(name=name, t=t, parent=par, own={par})
pp = s.popped
if pp is not None and c is not None and pp['kind'] == 'struct' and c['kind'] == 'struct' and pp['schema'] == c['schema'] and t == 0:
s.fields[fid]['post'] = True
if s.trace:
print(f"{s.p:6x} {' ' * len(s.stack)}#{fid} {name} t={t}")
s.value(fid)
continue
if m:
s.explicit(m)
jump = False
if x == 0xf7:
s.p += 1
E = s.rint()
jump = True
if m is None:
s.default_close()
if E not in s.fields:
raise Err(f"jump to unknown field {E} @{s.p:x}")
else:
E = s.next.get(s.last)
if E is None:
raise Err(f"no expected field @{s.p:x} after {s.last} "
f"{s.fields[s.last]['name'] if s.last else ''}")
kind = s.locate(E, jump or m is not None, m == 0)
if s.trace:
print(f"{s.p:6x} {' ' * len(s.stack)}{'>' if jump else '='}{E} {s.fields[E]['name']} {kind}")
if kind == 'elem':
s.element(E)
else:
s.value(E)
return s.root
def short(x, maxarr=40):
if isinstance(x, Unk):
return repr(x)
if isinstance(x, float):
return repr(round(x, 9))
if isinstance(x, list):
r = ', '.join(short(y) for y in x[:maxarr])
return f"[{r}{', ...' if len(x) > maxarr else ''}] (n={len(x)})"
return repr(x)
def fmt(v, ind=0, out=None, maxarr=40):
pad = ' ' * ind
if isinstance(v, Node):
for n, x in v:
if isinstance(x, (Node, Arr)):
out.append(f"{pad}{n}:")
fmt(x, ind + 1, out, maxarr)
else:
out.append(f"{pad}{n} = {short(x, maxarr)}")
elif isinstance(v, Arr):
for i, x in enumerate(v):
out.append(f"{pad}[{i}]")
fmt(x, ind + 1, out, maxarr)
def parse(b, trace=False):
p = P(b, trace)
try:
root = p.run()
return p, root, None
except (Err, IndexError, ValueError, KeyError) as e:
return p, p.root, e
if __name__ == '__main__':
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import prt
d, secs, order = prt.load(sys.argv[1])
names = sys.argv[2].split(',') if len(sys.argv) > 2 and sys.argv[2] != '*' else [n for n in order if secs[n][2][2] == '9']
outdir = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] != '-' else None
for nme in names:
o, l, f = secs[nme]
b = d[o:o + l]
p, root, e = parse(b, trace=len(sys.argv) > 4)
ok = e is None and not p.stack
print(f"{nme:22} len={l:7} parsed={p.p:7} fields={p.nid:5} lenient={p.lenient} {'OK' if ok else 'ERR ' + repr(e) + ' stack=' + str(len(p.stack))}")
if e is not None:
prt.hexdump(b, max(0, p.p - 64), 128)
if outdir and root:
out = [root[0] + ':']
fmt(root[1], 1, out)
open(f"{outdir}/{nme.replace(':', '_').replace('#', '_')}.txt", 'w', encoding='utf-8').write('\n'.join(out))
+47
View File
@@ -0,0 +1,47 @@
import sys,os,math,collections,struct,glob,itertools
sys.path.insert(0,os.path.dirname(os.path.abspath(__file__)))
import prt,psb
class TF(float):
pass
orig=psb.P.rdbl
def rdbl(s):
p0=s.p; t=s.b[p0]
v=orig(s)
if 0x49<=t<=0xdf and not isinstance(v,psb.Unk):
o=TF(v); o.tag=t; o.pl=bytes(s.b[p0+1:p0+7]); return o
return v
psb.P.rdbl=rdbl
OFFS=[0]+list(range(0x44,0x4d))
def mag(tf,off):
return struct.unpack('>d',(0x3f75+tf.tag-off).to_bytes(2,'big')+tf.pl)[0]
votes=collections.defaultdict(collections.Counter)
def walk(v,name=''):
if isinstance(v,psb.Node):
for n,x in v: walk(x,n)
elif isinstance(v,psb.Arr):
for x in v: walk(x,name)
elif isinstance(v,list) and v and isinstance(v[0],list) and len(v) in (3,4) and len(v[0])==3 and name in ('local_sys','orient','matrix','transf_to_local'):
rows=v[:3]
if any(isinstance(c,psb.Unk) for r in rows for c in r): return
vecs=rows+[[rows[i][j] for i in range(3)] for j in range(3)]
for r in vecs:
tf=[c for c in r if isinstance(c,TF)]
if not tf or len(tf)>2: continue
rest=sum(c*c for c in r if not isinstance(c,TF))
sols=[]
for offs in itertools.product(OFFS,repeat=len(tf)):
n=rest+sum(mag(c,o)**2 for c,o in zip(tf,offs))
if abs(n-1)<1e-9: sols.append(offs)
if len(sols)==1:
for c,o in zip(tf,sols[0]): votes[c.tag][o]+=1
for f in sys.argv[1:]:
for g in glob.glob(f):
try: d,secs,order=prt.load(g)
except Exception: continue
for nme in order:
o,l,fl=secs[nme]
if fl[2]!='9' or l<200: continue
p,root,e=psb.parse(d[o:o+l])
if root: walk(root[1])
for t in sorted(votes):
print(hex(t), {hex(o):n for o,n in votes[t].items()}, ' -> prefix', [hex((0x3f75+t-o) | (0x8000 if o else 0)) for o in votes[t]])
+75
View File
@@ -0,0 +1,75 @@
"""Decompressore Unix compress (.Z, LZW) minimale, come ncompress."""
import sys, os
def unz(data):
assert data[0] == 0x1f and data[1] == 0x9d, "non e' un flusso .Z"
flags = data[2]
maxbits = flags & 0x1f
block = bool(flags & 0x80)
out = bytearray()
n_bits = 9
maxcode = (1 << n_bits) - 1
table = {i: bytes([i]) for i in range(256)}
free = 257 if block else 256
pos = 3 * 8 # posizione in bit
total = len(data) * 8
start = pos # inizio del gruppo corrente di codici (per il riallineamento)
old = None
while pos + n_bits <= total:
byte = pos >> 3
chunk = int.from_bytes(data[byte:byte + 4], 'little')
code = (chunk >> (pos & 7)) & ((1 << n_bits) - 1)
pos += n_bits
if old is None:
old = table[code]
out += old
continue
if code == 256 and block:
table = {i: bytes([i]) for i in range(256)}
free = 256
# riallinea a multipli di 8 codici
grp = n_bits * 8
pos = start + ((pos - start + grp - 1) // grp) * grp
n_bits = 9
maxcode = (1 << n_bits) - 1
start = pos
old = None
free = 257
continue
if code in table:
entry = table[code]
elif code == free:
entry = old + old[:1]
else:
raise ValueError(f"codice LZW non valido {code} (free={free}) a bit {pos}")
out += entry
if free <= (1 << maxbits) - 1:
table[free] = old + entry[:1]
free += 1
old = entry
if free > maxcode and n_bits < maxbits:
grp = n_bits * 8
pos = start + ((pos - start + grp - 1) // grp) * grp
n_bits += 1
maxcode = (1 << n_bits) - 1
start = pos
return bytes(out)
if __name__ == '__main__':
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import prt
d, secs, order = prt.load(sys.argv[1])
outdir = sys.argv[2]
for n in order:
o, l, f = secs[n]
b = d[o:o + l]
if b[:2] == b'\x1f\x9d':
try:
u = unz(b)
except Exception as e:
print(n, 'ERR', e)
continue
print(n, 'compressa', l, '->', len(u), 'dichiarata', int(f[0], 16), u[:60])
open(os.path.join(outdir, n.replace(':', '_') + '.bin'), 'wb').write(u)