Files
prep-step/docs/formato-psb.md
davideandClaude 2d7ace07e2 Documenta il formato PSB dei file Creo e aggiungi il parser di riferimento
Reverse engineering di 9253000117a.prt.35: contenitore (offset dall'ultimo
campo dell'intestazione, indice a catena), grammatica completa delle sezioni
(id progressivi dei campi, tabella "dopo X viene Y", marcatori di chiusura),
tabella dei double verificata sulla norma delle matrici, sezioni compresse e
catalogo del contenuto. Parser Python in scripts/psb/. docs/history.md
raccoglie stato, metodo e punti aperti per riprendere il lavoro.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-10 14:34:29 +01:00

21 KiB
Raw Permalink Blame History

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):

  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:

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.