Files
prep-step/docs/formato-asm-record.md
T

318 lines
15 KiB
Markdown
Raw Normal View History

# 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 <tipo> <nome campo>\0 <valore>` |
| `\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 <n>` identifica il layout dei record che seguono |
| `\xf8` | array: `\xf8 <conteggio> ... <elementi>` |
| `\xfb` | lo schema è definito in linea nel primo elemento dell'array |
| `\xf4` | chiude una sezione (seguito da `#\n#<NomeSezione>\n`) |
Il byte `<tipo>` 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 <n> \xf7 <idSchema> \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 <N> \xf7 <idSchema> \xfb
\xe3 \xe0\x01 "id"\0 <int>
\xe0\x01 "type"\0 <int>
\xe0\x0a "name"\0 <stringa>\0
\xe0\x0a "family_name"\0 <stringa>\0
\xe0\x00 "tha_ref"\0 \xe1
\xe0\x02 "e1"\0 ... "e2" ... "e3" ... "origin" ...
\xe3 <id> <type> <name>\0 <family_name>\0 <resto posizionale>
\xe3 <id> <type> <name>\0 <family_name>\0 <resto posizionale>
...
```
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 <int> <int> <stringa>\0 <stringa>\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 <idSchema>] <nome istanza>\0 <id membro> \x82\x7b \xe1\xe1
```
- `\xe2\x33` — parametro di tipo stringa (`\x33` = `'3'`). Confronto: `\xe2\x32`
è un parametro numerico, es. `DENSITA`.
- `<nome istanza>` — il nome esatto che la BOM stampa.
- `<id membro>` — l'`id` del record `members` a cui il parametro si applica,
con la codifica intera di §2.1. **È questo il collegamento.**
- `\xf7 <idSchema>` 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<generico>`:
```
\xe2\x02 "R8--_-_U6592_AISI304<UNI6592>"\0
```
Documenta la stessa relazione istanza→generico in modo autonomo ed è utile come
riscontro incrociato (è lo stesso pattern `<codice><GENERICO>` 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 <int> \xe0\x0a "name"\0 <nome>\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 <nome> contiene:` per la radice,
`Sottoassieme <nome> 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 <radice>:` 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 `<nome>.asm.<rev>`
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).