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

+1
View File
@@ -43,3 +43,4 @@ publish/
# Setup generato da Inno Setup (installer/PrepStep.iss)
installer/Output/
__pycache__/
+1
View File
@@ -11,6 +11,7 @@ aderisce a [Semantic Versioning](https://semver.org/lang/it/).
- `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
+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` |
+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)