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>
21 KiB
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 e
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 (parser),
scripts/psb/prt.py (contenitore),
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):
- 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). - Su scala: 2152 sezioni su 2226 lette fino in fondo negli 89 file di
raw/(tutte le generazioni, da1322a2063). Le 74 che falliscono sono in §8. - 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_ENTRYpunta (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
f7e 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:
- Valore nudo senza
f7: va nel contenitore più vicino alla cima che possiede quel campo; quelli sopra si chiudono in silenzio. - Campo con nome o
f7, con marcatoref0-f4: si chiudono esattamente quel numero di contenitori (f0= nessuno). Il marcatore sta dopo il nome del campo. - 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>coniddi 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:
nextinref_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:
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 costanteKnon 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 sezioniND:0:*decompresse non sono XML maPSB_Tokens(§7); i nomi tipo SOAP sono le stringhe distring_xar.formato-prt-geometria.md§1: la tabella dei tag è sostituita da §3.2. In particolaree4-e7non sono "zeri" ma ripetizioni del valore precedente, e i tag49-df("non decodificato") sono i prefissi a 2 byte.formato-prt-geometria.md§1:UnitSystemDefnon serve per le unità, stanno inBasicText.
10. Come proseguire
- Confrontare con Creo un file piccolo: è l'unica prova che manca a tutto il doc.
- Completare la tabella dei
floatraccogliendo le normali su più file (stesso metodo di §3.3): sblocca l'esportazione della mesh senza Creo. - Sistemare la pila sugli alberi di
LargeText(§8), poi portare il parser in C# accanto adAsmRecordReader, con la stessa regola: se la sezione non si legge fino in fondo,InvalidDataException. - Sezioni tipo
b: servono due salvataggi dello stesso modello che differiscono di poco.