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:
1 parent
1eac3ccaa7
commit
2d7ace07e2
15 files changed
+1309
No files matched your search
@@ -43,3 +43,4 @@ publish/
|
||||
|
||||
# Setup generato da Inno Setup (installer/PrepStep.iss)
|
||||
installer/Output/
|
||||
__pycache__/
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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` |
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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}")
|
||||
@@ -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))
|
||||
@@ -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]])
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user