# Decodifica strutturale del .asm Creo: record, membri, family table Questo documento descrive il formato **record per record** del `.asm` Creo, ricavato per reverse engineering su un campione di assiemi reali e verificato confrontando l'output con le BOM esportate da Creo per gli stessi file. Sostituisce, per l'estrazione dei componenti, l'euristica testuale ("codice che compare almeno due volte") descritta in [`formato-asm.md`](formato-asm.md): quella era una statistica sui nomi, questa legge la struttura effettiva dei record. Implementazione: [`scripts/CreoBom.cs`](../scripts/CreoBom.cs). **Stato della validazione.** Durante il reverse engineering, il livello 1 estratto da 3 assiemi con BOM Creo di riferimento coincideva **esattamente**: stesse voci, stesso nome istanza, stesse quantità, stesso ordine. Altri 5 `.asm`, mai usati per ricavare le regole, parsavano con il conteggio dichiarato uguale a quello trovato. Tutto ciò che segue oltre questo perimetro (§7) è segnalato esplicitamente come ipotesi non dimostrata. --- ## 1. Intestazione Il file si apre con un header ASCII a righe terminate da `\` + newline: ``` #UGC:2 ASSEMBLY 2038 1760 805 1 1 15 4000 2022414 000002d0 \ #- VERS 0 0 \ #- HOST \ #- LINK \ #- DBID \ #- REVS 0, \ ``` `ASSEMBLY` distingue il tipo di modello. Gli altri campi numerici non sono stati decodificati e non servono per la BOM. Dopo l'header inizia il corpo binario. **Il corpo non è compresso**: i nomi dei modelli sono in chiaro, 1 byte per carattere (Latin1 — `Encoding.Latin1.GetString`, stesso metodo già usato in `AssemblyComponentScanner.cs` per leggere il file come testo senza alterarne i byte). ## 2. Il formato dei record Il corpo è una serializzazione a marcatori. I byte `\xe0`-`\xff` sono marcatori di struttura, tutti gli altri sono dati. I marcatori osservati: | byte | significato dedotto | |------|---------------------| | `\xe0` | inizio campo: `\xe0 \0 ` | | `\xe1` | valore nullo / vuoto | | `\xe2` | inizio struttura o unione (per i parametri: il byte dopo è il discriminante) | | `\xe3` | inizio struttura, usato per gli elementi di un array | | `\xf1` | apre un contenitore (visto prima di un array) | | `\xf7` | id di schema: `\xf7 ` identifica il layout dei record che seguono | | `\xf8` | array: `\xf8 ... ` | | `\xfb` | lo schema è definito in linea nel primo elemento dell'array | | `\xf4` | chiude una sezione (seguito da `#\n#\n`) | Il byte `` dopo `\xe0` codifica il tipo del valore. Osservati: `\x00` struttura/nullable, `\x01` intero, `\x02` array numerico, `\x0a` stringa terminata da `\0`. ### 2.1 Interi Gli interi sono big-endian con la **lunghezza codificata nei bit alti del primo byte**, per non collidere con i marcatori `\xe0`+: | primo byte | byte totali | valore | |------------|-------------|--------| | `0x00`-`0x7f` | 1 | il byte stesso | | `0x80`-`0xbf` | 2 | `(b0 & 0x3f) << 8 \| b1` | | `0xc0`-`0xdf` | 3 | `(b0 & 0x1f) << 16 \| b1 << 8 \| b2` | Esempi verificati: `\x2e` = 46; `\x80\x81` = 129; `\xc0\x4d\x51` = 19793. Questa regola è stata **ricavata** dalla necessità di far quadrare il conteggio dei membri: con la lettura a 1 byte fisso i campioni grandi si disallineavano. Non è confermata da una specifica; regge sui campioni disponibili al momento del reverse engineering. In `CreoBom.cs` è `CreoAsm.AsInt`, applicata al testo decodificato Latin1 (ogni `char` della stringa corrisponde 1:1 a un byte, quindi indicizzare la stringa o l'array di byte originale dà lo stesso offset). ### 2.2 Compressione per schema Un array `\xf8 \xf7 \xfb` scrive il **primo** elemento con tutti i nomi di campo per esteso; gli elementi successivi sono **posizionali**, senza nomi. È il motivo per cui nel file si vedono i nomi dei componenti "nudi", uno dopo l'altro, senza etichetta. Per leggere l'elemento N bisogna conoscere lo schema definito dall'elemento 1. --- ## 3. L'elenco dei componenti: la chiave `members` La chiave `members` è l'elenco dei **componenti diretti** dell'assieme (livello 1), nell'ordine di montaggio. ``` \xe0 \x00 "members" \0 \xf1 \xf8 \xf7 \xfb \xe3 \xe0\x01 "id"\0 \xe0\x01 "type"\0 \xe0\x0a "name"\0 \0 \xe0\x0a "family_name"\0 \0 \xe0\x00 "tha_ref"\0 \xe1 \xe0\x02 "e1"\0 ... "e2" ... "e3" ... "origin" ... \xe3 \0 \0 \xe3 \0 \0 ... ``` Campi utili: - **`id`** — identificatore del componente **dentro questo assieme**. È lo stesso numero che Creo mostra come "Componente ID N". Serve da chiave per collegare il membro ad altre sezioni (vedi family table, §4). - **`type`** — `1` = sottoassieme (`.asm`), `2` = parte (`.prt`). Sui campioni osservati compaiono solo questi due valori. - **`name`** — il **file su disco**, senza estensione. Per le family table è il **generico** (`UNI6592`), non l'istanza. - **`family_name`** — sui campioni disponibili è **sempre uguale a `name`**. Non porta quindi l'informazione sull'istanza; il nome è fuorviante. - `e1`, `e2`, `e3`, `origin` — la matrice di posizionamento (assi + origine). Non decodificati: non servono per la BOM, ma occupano il resto del record e hanno lunghezza variabile. `N` (il conteggio dichiarato) è il numero totale di **occorrenze**, non di codici distinti: una parte montata 4 volte conta 4 e compare in 4 record con id diversi. Il conteggio va usato come controllo di correttezza: se i record trovati non sono esattamente `N`, la lettura si è disallineata e va segnalata un'eccezione invece di restituire un risultato parziale silenzioso — vedi `CreoAsm.LeggiMembri`, che lancia `InvalidDataException` in questo caso. La sezione termina a `\xf4\x04#\n#MdlStatus\n`. ### 3.1 Come lo legge `CreoAsm.LeggiMembri` Il parser non ricostruisce lo schema completo (servirebbe decodificare le matrici). Si ancora invece al pattern `\xe3 \0 \0` (`CreoAsm.MemberRegex`, una regex .NET sul testo Latin1 del file) e salta il resto del record fino al `\xe3` successivo che soddisfa il pattern. La doppia stringa terminata da `\0` rende i falsi positivi improbabili, e il conteggio dichiarato `N` fa da rete di sicurezza. > `ponytail:` scansione a pattern invece del parser di schema — il punto debole > è che si appoggia al conteggio per accorgersi di un errore. Da sostituire con > la lettura dello schema se dovessero comparire campioni che non quadrano. --- ## 4. Family table: dove sta il nome dell'istanza Le parti normalizzate (viti, rondelle, spine, boccole) sono **istanze di family table**. In Creo esiste un solo file, il **generico** (`UNI6592.prt`), che contiene una tabella di varianti; ogni variante è un'**istanza** (`R8--_-_U6592_AISI304`). La BOM elenca le istanze, il disco contiene il generico. La sezione `members` cita **solo il generico**: senza risolvere `ConfigName` (sotto), il livello 1 produrrebbe `UNI6592` dove la BOM dice `R8--_-_U6592_AISI304`. Il collegamento sta in un record di **parametro** applicato al membro: ``` \xe3 "ConfigName"\0 \xe2 \x33 [\xf7 ] \0 \x82\x7b \xe1\xe1 ``` - `\xe2\x33` — parametro di tipo stringa (`\x33` = `'3'`). Confronto: `\xe2\x32` è un parametro numerico, es. `DENSITA`. - `` — il nome esatto che la BOM stampa. - `` — l'`id` del record `members` a cui il parametro si applica, con la codifica intera di §2.1. **È questo il collegamento.** - `\xf7 ` compare solo sulla prima occorrenza (schema definito in linea), poi è omesso. - `\x82\x7b` — costante (= 635) identica su tutte le occorrenze di tutti i campioni. Significato ignoto, presumibilmente attributi/flag del parametro. `CreoAsm.ConfigNameRegex` la usa come ancora di fine record; se un campione futuro avesse un valore diverso, il match salterebbe silenziosamente (nessun contatore di sicurezza analogo a quello di `members` copre questo caso). `ConfigName` compare **esattamente** per i membri che sono istanze, e per nessun altro. L'algoritmo è quindi: leggere `members`, costruire la mappa `id membro → nome istanza` dai record `ConfigName` (`CreoAsm.LeggiMembri`, dizionario `istanze`), e sostituire il nome dove la mappa ha una voce. Il generico resta disponibile come `Membro.File`. > **Avvertenza.** `ConfigName` sta in mezzo a parametri palesemente aziendali > (`TIPO_PARTE`, `CLASSE_RICAR`, `GR_PRODOTTO`, `TECN_RIORDINO`), quindi > potrebbe non essere un meccanismo standard di Creo ma una convenzione > interna. Regge sui campioni disponibili, ma su file provenienti da > un'altra installazione va riverificato prima di fidarsene. ### 4.1 Una seconda pista, che NON va usata per la BOM Altrove nel file compaiono stringhe nella forma `istanza`: ``` \xe2\x02 "R8--_-_U6592_AISI304"\0 ``` Documenta la stessa relazione istanza→generico in modo autonomo ed è utile come riscontro incrociato (è lo stesso pattern `` già sfruttato dall'euristica testuale in `formato-asm.md` per le family table non-UNI). **Non è però una lista dei componenti**: su un campione produceva 26 voci contro 17 membri reali, includeva alcuni nipoti (figli dei sottoassiemi) ma non tutti, e ripeteva le stesse voci più volte. È la lista dei riferimenti geometrici dei vincoli di accoppiamento, non una distinta. Usarla per la BOM dà risultati sbagliati sia in eccesso sia in difetto. --- ## 5. Il nome proprio del modello La BOM intesta i blocchi col nome **del modello**, non del file: il file `9244248550b.asm.11` produce `Assieme 9244248550B contiene:`. Il nome sta nel primo record `objname` del file: ``` \x00 "objname"\0 \xe2 \xe0\x01 "object_type"\0 \xe0\x0a "name"\0 \0 ``` La **prima** occorrenza è il modello stesso; quelle successive sono altri oggetti referenziati (disegni, modelli citati). `CreoAsm.NomeProprio` cerca solo la prima occorrenza (`Regex.Match`, non `Matches`), verificato su tutti i campioni disponibili durante il reverse engineering. --- ## 6. Generare la BOM: ricorsione e formato di output `members` dà un solo livello. La BOM di Creo è ricorsiva: apre anche i `.asm` dei sottoassiemi. Implementato in `CreoAsm.GeneraBom`/`Render` in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs). ### 6.1 Struttura del file BOM - Codifica **UTF-8 con BOM** (`EF BB BF`), fine riga **CRLF**. - Due righe vuote in testa, poi il primo blocco. - **Tre** righe vuote fra un blocco e il successivo, **due** prima del riepilogo. Il file termina con un solo CRLF. - Riga voce: `{qta:6} {tipo:>12} {nome}` — cioè quantità allineata a destra su 6 caratteri, spazio, tipo allineato a destra su 12, due spazi, nome. - Intestazioni: `Assieme contiene:` per la radice, `Sottoassieme contiene:` per gli altri. ### 6.2 Ordine dei blocchi Visita **depth-first pre-order**: si emette il blocco di un sottoassieme e subito dopo, ricorsivamente, quelli dei suoi sottoassiemi, prima di passare al fratello successivo. Ogni modello compare **una volta sola** anche se montato più volte (deduplica per nome, `CreoAsm.Blocchi`). > La deduplica è un'**assunzione**: nessuno dei campioni disponibili durante il > reverse engineering montava lo stesso sottoassieme due volte, quindi non è > dimostrata su un caso reale. ### 6.3 Riepilogo finale `Summary of items for :` elenca **solo le parti**, con la quantità totale moltiplicata lungo l'albero (una parte in 4 esemplari dentro un sottoassieme montato 2 volte conta 8), nell'ordine di **prima comparsa** nella visita depth-first (`CreoAsm.Sommario`). Una parte che compare a più livelli occupa la posizione della sua prima comparsa e somma le quantità. ### 6.4 Risoluzione dei file dei sottoassiemi È il punto che il formato **non** risolve: il `.asm` registra il nome del modello, non il percorso. Servono quindi cartelle di ricerca fornite dall'esterno (`-p`/`--path`). `CreoAsm.CreaFinder` indicizza `.asm.` senza distinzione di maiuscole e, a parità di nome, prende la **revisione più alta** presente. > Scelta discutibile: la revisione più alta in cartella non è > necessariamente quella con cui l'assieme padre è stato salvato. Il `.asm` > padre non sembra registrare la revisione attesa del figlio, o non è stata > trovata. Vedi `formato-asm.md` §"Risoluzione ricorsiva dell'albero" per le > trappole già incontrate storicamente su questo punto (con l'euristica > testuale, ma il problema della revisione attesa è indipendente dal metodo > di estrazione). Se un sottoassieme non si trova, il blocco viene emesso con `*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal riepilogo. Il risultato è dichiaratamente incompleto invece che silenziosamente sbagliato. --- ## 7. Cosa resta sconosciuto Elencato esplicitamente per non far passare per dimostrato ciò che non lo è: - **Stato soppresso / escluso.** Nessuna evidenza cercata o trovata. Non si sa se un componente soppresso compaia in `members`, né con quale marcatore. Sui campioni disponibili il totale coincideva con la BOM, il che *suggerisce* che non ce ne fossero, non che siano gestiti. - **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato numerico non decodificato. - **`family_name`**: sempre uguale a `name` sui campioni; non si sa in quale situazione divergerebbe. - **La costante `\x82\x7b`** nei record `ConfigName`. - **I campi numerici dell'header** dopo `ASSEMBLY`. - **Occorrenze multiple di `members`.** `CreoAsm.LeggiMembri` prende la prima. Sui campioni disponibili funziona, ma non è stato verificato se le rappresentazioni semplificate (simplified rep) generino sezioni `members` aggiuntive. - **La deduplica dei blocchi** e **la revisione attesa dei figli** (§6.2, §6.4). - **Quale revisione del figlio** il padre si aspetta: non trovata nel `.asm`. ## 8. Uso ```powershell dotnet run scripts/CreoBom.cs -- raw/9244228558.asm.20 -p M:\modelli -o out\9244228558.bom.1 dotnet run scripts/CreoBom.cs -- raw/9244228558.asm.20 -p M:\modelli # stampa a video dotnet run scripts/Analyze.cs -- raw/9244228558.asm.20 "ConfigName" # dump dei byte per investigare a mano ``` Senza `-p`/`--path`, la ricerca dei sottoassiemi usa la cartella del file di partenza. `-p` è ripetibile per più cartelle. `-o` scrive il file col BOM UTF-8 e CRLF descritti al §6.1; senza `-o` il testo va su stdout (con `\n` al posto di `\r\n`, per leggibilità in console).