Aggiungi CreoBom.cs: parser esatto del formato .asm via record binari
Porting C# del reverse engineering della struttura a record del .asm Creo (sezione "members" + parametro "ConfigName" per le family table): legge esattamente i componenti diretti e genera la stessa BOM esportata da Creo, senza le euristiche testuali (conteggio occorrenze) di AssemblyComponentScanner. Resta uno script di analisi indipendente, non integrato nella GUI. Aggiorna anche formato-asm.md (non e' piu' vero che "non esiste un parser strutturale") e architettura.md (script mancante nell'elenco). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3cfc040fb0
commit
35009b5cd3
4 files changed
+631
-4
No files matched your search
@@ -151,6 +151,11 @@ dipendenze extra con `#:package Nome@versione` in testa al file):
|
||||
Pro-E).
|
||||
- **BomViaDb.cs** — come sopra ma con la tokenizzazione generica validata contro il DB, per
|
||||
intercettare anche i nomi alfabetici delle parti commerciali.
|
||||
- **CreoBom.cs** — indipendente dagli altri: legge esattamente la struttura a record del
|
||||
binario `.asm` (sezione `members` + parametro `ConfigName`, vedi
|
||||
`docs/formato-asm-record.md`) invece di contare occorrenze di codici nel testo. Risolve i
|
||||
sottoassiemi cercandoli in cartelle locali (`-p`), non contro `cad_index.db`. Non sostituisce
|
||||
l'euristica di `AssemblyComponentScanner` nella GUI né negli altri script.
|
||||
- **Analyze.cs** — dump del contesto testo/hex attorno a un pattern in un file binario, per
|
||||
investigare a mano nuovi falsi positivi prima di toccare la regex di
|
||||
`AssemblyComponentScanner`.
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
# 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).
|
||||
+17
-4
@@ -1,10 +1,23 @@
|
||||
# Come leggere un .asm Creo per dedurne i componenti
|
||||
|
||||
Il file `.asm` di Pro/ENGINEER-Creo è un formato binario proprietario, non
|
||||
documentato pubblicamente. Non esiste un parser strutturale completo in questo
|
||||
progetto: l'estrazione dei componenti si basa su un'euristica testuale,
|
||||
validata a mano confrontando l'output con l'albero reale mostrato da Creo.
|
||||
Questo documento riassume cosa si è osservato e i suoi limiti.
|
||||
documentato pubblicamente. Questo documento descrive l'euristica testuale usata da
|
||||
`AssemblyComponentScanner.cs` (GUI, fase "Trova componenti referenziati"/"Verifica
|
||||
completezza") e dagli script che risolvono l'albero componenti contro `cad_index.db`
|
||||
(`ExtractCreoStrings.cs`, `BomViaDb.cs`): conta le occorrenze di stringhe candidate nel
|
||||
testo binario, validata a mano confrontando l'output con l'albero reale mostrato da Creo.
|
||||
Riassume cosa si è osservato e i suoi limiti, in particolare i falsi positivi che
|
||||
un conteggio testuale non può escludere con certezza (riferimenti fantasma, componenti
|
||||
soppressi con placement residuo — vedi sotto).
|
||||
|
||||
Un reverse engineering successivo ha invece decodificato la **struttura a record** del
|
||||
file (non solo pattern testuali): legge esattamente la sezione `members` di un assieme,
|
||||
senza contare occorrenze né rischiare i falsi positivi descritti in questo documento.
|
||||
Implementato in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs), documentato in
|
||||
[`formato-asm-record.md`](formato-asm-record.md). Resta però uno script di analisi
|
||||
indipendente: legge solo `.asm` locali dato un percorso di ricerca esplicito (non
|
||||
`cad_index.db`), e non è integrato in `AssemblyComponentScanner`/nella GUI, che
|
||||
continuano a usare l'euristica descritta qui.
|
||||
|
||||
## Struttura osservata
|
||||
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
// dotnet run scripts/CreoBom.cs -- <file.asm.rev> [-p cartella]... [-o out.bom.1]
|
||||
//
|
||||
// Parser esatto del formato binario .asm di Creo, ricavato per reverse engineering (vedi
|
||||
// docs/formato-asm-record.md per la spiegazione byte-per-byte dei record, verificata
|
||||
// confrontando l'output con le BOM esportate da Creo). Legge la sezione "members" di un
|
||||
// assieme (id/tipo/nome dei componenti diretti, con risoluzione del nome istanza di family
|
||||
// table dal parametro "ConfigName") e, dato un elenco di cartelle dove cercare i
|
||||
// sottoassiemi, genera la stessa distinta base (BOM) esportata da Creo.
|
||||
//
|
||||
// Alternativa esatta all'euristica testuale di PrepStep/AssemblyComponentScanner.cs (docs/
|
||||
// formato-asm.md): quella conta le occorrenze dei codici nel testo, questa legge la
|
||||
// struttura dei record. Per ora resta uno script di analisi indipendente: non sostituisce
|
||||
// AssemblyComponentScanner nella GUI (fase 4) ne' negli altri script.
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
Console.WriteLine("Uso: dotnet run scripts/CreoBom.cs -- <file.asm.rev> [-p cartella]... [-o out.bom.1]");
|
||||
return 1;
|
||||
}
|
||||
|
||||
string fileIniziale = args[0];
|
||||
var cartelle = new List<string>();
|
||||
string? outPath = null;
|
||||
for (int i = 1; i < args.Length; i++)
|
||||
{
|
||||
if ((args[i] == "-p" || args[i] == "--path") && i + 1 < args.Length)
|
||||
cartelle.Add(args[++i]);
|
||||
else if (args[i] == "-o" && i + 1 < args.Length)
|
||||
outPath = args[++i];
|
||||
}
|
||||
if (cartelle.Count == 0)
|
||||
cartelle.Add(Path.GetDirectoryName(Path.GetFullPath(fileIniziale)) ?? ".");
|
||||
|
||||
string testoBom = CreoAsm.GeneraBom(fileIniziale, cartelle);
|
||||
if (outPath is not null)
|
||||
{
|
||||
File.WriteAllBytes(outPath, new byte[] { 0xEF, 0xBB, 0xBF }.Concat(Encoding.UTF8.GetBytes(testoBom)).ToArray());
|
||||
Console.WriteLine($"scritto {outPath} ({testoBom.Split("\r\n").Length} righe)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Write(testoBom.Replace("\r\n", "\n"));
|
||||
}
|
||||
return 0;
|
||||
|
||||
/// <summary>Lettura del formato .asm Creo (sezione "members" + parametro "ConfigName") e resa
|
||||
/// nel formato della BOM esportata da Creo. Vedi docs/formato-asm-record.md per il dettaglio
|
||||
/// dei record e i limiti noti (stato soppresso non gestito, matrice di posizionamento non
|
||||
/// decodificata, deduplica dei blocchi non dimostrata su assiemi che montano lo stesso
|
||||
/// sottoassieme due volte).</summary>
|
||||
public static class CreoAsm
|
||||
{
|
||||
public sealed record Membro(int Id, int Tipo, string Nome, string File);
|
||||
|
||||
// interi big-endian, lunghezza nei bit alti del primo byte (0xe0+ e' riservato ai
|
||||
// marcatori di struttura): 0x00-0x7f 1 byte, 0x80-0xbf 2 byte, 0xc0-0xdf 3.
|
||||
private const string Int = @"([\x00-\x7f]|[\x80-\xbf][\x00-\xff]|[\xc0-\xdf][\x00-\xff][\x00-\xff])";
|
||||
|
||||
// \xe3 id type name\0 family_name\0
|
||||
private static readonly Regex MemberRegex = new(
|
||||
@"\xe3" + Int + Int + @"([\x20-\x7e]+)\x00([\x20-\x7e]+)\x00", RegexOptions.Compiled);
|
||||
|
||||
// parametro ConfigName: nome istanza family table + id del membro a cui si applica.
|
||||
private static readonly Regex ConfigNameRegex = new(
|
||||
@"\xe3ConfigName\x00\xe23(?:\xf7[\x00-\xff])?([\x20-\x7e]+)\x00" + Int + @"\x82\x7b",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
// nome proprio del modello (quello che la BOM stampa in intestazione).
|
||||
private static readonly Regex ObjNameRegex = new(
|
||||
@"\x00objname\x00\xe2\xe0\x01object_type\x00[\x00-\x7f]\xe0\x0aname\x00([^\x00]+)\x00",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
// primo elemento dell'array "members": porta le chiavi per esteso.
|
||||
private static readonly Regex FirstMemberRegex = new(
|
||||
@"\x01id\x00" + Int + @"\xe0\x01type\x00" + Int
|
||||
+ @"\xe0\x0aname\x00([^\x00]+)\x00\xe0\x0afamily_name\x00([^\x00]+)\x00",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Dictionary<int, string> TipoNome = new() { [1] = "Sottoassieme", [2] = "Parte" };
|
||||
|
||||
private static int AsInt(string bytes)
|
||||
{
|
||||
int mask = bytes.Length switch { 1 => 0x7F, 2 => 0x3F, 3 => 0x1F, _ => throw new ArgumentException("lunghezza intero non valida") };
|
||||
int valore = bytes[0] & mask;
|
||||
for (int i = 1; i < bytes.Length; i++)
|
||||
valore = (valore << 8) | bytes[i];
|
||||
return valore;
|
||||
}
|
||||
|
||||
/// <summary>Nome proprio del modello (quello che Creo stampa in intestazione BOM),
|
||||
/// dalla prima occorrenza del record "objname".</summary>
|
||||
public static string NomeProprio(byte[] dati)
|
||||
{
|
||||
var m = ObjNameRegex.Match(Encoding.Latin1.GetString(dati));
|
||||
if (!m.Success)
|
||||
throw new InvalidDataException("record 'objname' non trovato");
|
||||
return m.Groups[1].Value;
|
||||
}
|
||||
|
||||
/// <summary>Componenti diretti di un assieme, in ordine di montaggio. "Nome" e' il nome
|
||||
/// che mostrerebbe la BOM (istanza di family table se presente), "File" e' il generico
|
||||
/// realmente su disco (coincide con "Nome" per i componenti che non sono istanze).</summary>
|
||||
public static List<Membro> LeggiMembri(byte[] dati)
|
||||
{
|
||||
string testo = Encoding.Latin1.GetString(dati);
|
||||
|
||||
var istanze = new Dictionary<int, string>();
|
||||
foreach (Match m in ConfigNameRegex.Matches(testo))
|
||||
istanze[AsInt(m.Groups[2].Value)] = m.Groups[1].Value;
|
||||
|
||||
const string chiaveMembers = "\0members\0";
|
||||
int start = testo.IndexOf(chiaveMembers, StringComparison.Ordinal);
|
||||
if (start < 0)
|
||||
throw new InvalidDataException("sezione 'members' assente");
|
||||
start += chiaveMembers.Length;
|
||||
|
||||
int conteggio = dati[start + 2]; // \xf1 \xf8 <conteggio>
|
||||
int end = testo.IndexOf("#MdlStatus", start, StringComparison.Ordinal);
|
||||
string region = testo.Substring(start, (end > 0 ? end : testo.Length) - start);
|
||||
|
||||
var grezzi = new List<(int Id, int Tipo, string Nome)>();
|
||||
|
||||
// il primo record e' esteso: ricava i campi dalle chiavi, i successivi sono posizionali
|
||||
var primo = FirstMemberRegex.Match(region);
|
||||
if (primo.Success)
|
||||
{
|
||||
grezzi.Add((AsInt(primo.Groups[1].Value), AsInt(primo.Groups[2].Value), primo.Groups[3].Value));
|
||||
region = region.Substring(primo.Index + primo.Length);
|
||||
}
|
||||
|
||||
foreach (Match m in MemberRegex.Matches(region))
|
||||
{
|
||||
grezzi.Add((AsInt(m.Groups[1].Value), AsInt(m.Groups[2].Value), m.Groups[3].Value));
|
||||
if (grezzi.Count == conteggio)
|
||||
break;
|
||||
}
|
||||
|
||||
if (grezzi.Count != conteggio)
|
||||
throw new InvalidDataException($"attesi {conteggio} membri, trovati {grezzi.Count}: lettura disallineata");
|
||||
|
||||
return grezzi.Select(g => new Membro(g.Id, g.Tipo, istanze.GetValueOrDefault(g.Id, g.Nome), g.Nome)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>Voci distinte con quantita', nell'ordine di prima comparsa (come la BOM).</summary>
|
||||
public static List<((int Tipo, string Nome) Chiave, int Qta)> Rollup(IEnumerable<Membro> membri)
|
||||
{
|
||||
var ordine = new List<(int, string)>();
|
||||
var qty = new Dictionary<(int, string), int>();
|
||||
foreach (var m in membri)
|
||||
{
|
||||
var chiave = (m.Tipo, m.Nome);
|
||||
if (!qty.ContainsKey(chiave))
|
||||
ordine.Add(chiave);
|
||||
qty[chiave] = qty.GetValueOrDefault(chiave) + 1;
|
||||
}
|
||||
return ordine.Select(c => (c, qty[c])).ToList();
|
||||
}
|
||||
|
||||
// --- risoluzione dei file -------------------------------------------
|
||||
|
||||
/// <summary>nome modello -> membri, cercando <nome>.asm.<rev> nelle cartelle
|
||||
/// date. A parita' di nome vince la revisione piu' alta presente. Nome null se il file
|
||||
/// non c'e' in nessuna delle cartelle.</summary>
|
||||
private static Func<string, List<Membro>?> CreaFinder(IEnumerable<string> cartelle)
|
||||
{
|
||||
var indice = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var revisioni = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var dir in cartelle)
|
||||
{
|
||||
if (!Directory.Exists(dir))
|
||||
continue;
|
||||
foreach (var f in Directory.GetFiles(dir, "*.asm.*"))
|
||||
{
|
||||
if (!int.TryParse(Path.GetExtension(f).TrimStart('.'), out int rev))
|
||||
continue;
|
||||
string chiave = Path.GetFileName(f).Split('.')[0];
|
||||
if (!revisioni.TryGetValue(chiave, out int attuale) || rev > attuale)
|
||||
{
|
||||
indice[chiave] = f;
|
||||
revisioni[chiave] = rev;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cache = new Dictionary<string, List<Membro>?>();
|
||||
return nome =>
|
||||
{
|
||||
if (!cache.TryGetValue(nome, out var mem))
|
||||
{
|
||||
mem = indice.TryGetValue(nome, out var percorso) ? LeggiMembri(File.ReadAllBytes(percorso)) : null;
|
||||
cache[nome] = mem;
|
||||
}
|
||||
return mem;
|
||||
};
|
||||
}
|
||||
|
||||
// --- costruzione dell'albero -----------------------------------------
|
||||
|
||||
private sealed record Blocco(string Tipo, string Nome, List<((int Tipo, string Nome) Chiave, int Qta)>? Righe);
|
||||
|
||||
/// <summary>Blocchi in ordine depth-first pre-order. Ogni modello compare una volta sola
|
||||
/// (deduplica per nome) anche se montato piu' volte.</summary>
|
||||
private static List<Blocco> Blocchi(string root, Func<string, List<Membro>?> get)
|
||||
{
|
||||
var risultato = new List<Blocco>();
|
||||
var visti = new HashSet<string>();
|
||||
|
||||
void Visita(string nome, string tipo)
|
||||
{
|
||||
if (!visti.Add(nome))
|
||||
return; // Creo stampa ogni modello una volta sola
|
||||
var mem = get(nome);
|
||||
if (mem is null)
|
||||
{
|
||||
risultato.Add(new Blocco(tipo, nome, null));
|
||||
return;
|
||||
}
|
||||
var righe = Rollup(mem);
|
||||
risultato.Add(new Blocco(tipo, nome, righe));
|
||||
foreach (var (chiave, _) in righe)
|
||||
if (chiave.Tipo == 1)
|
||||
Visita(chiave.Nome, "Sottoassieme");
|
||||
}
|
||||
|
||||
Visita(root, "Assieme");
|
||||
return risultato;
|
||||
}
|
||||
|
||||
/// <summary>Totale per parte, moltiplicato lungo l'albero, in ordine di prima comparsa: una
|
||||
/// parte che compare a piu' livelli occupa la posizione della sua prima comparsa e somma
|
||||
/// le quantita'.</summary>
|
||||
private static void Sommario(string root, Func<string, List<Membro>?> get, int mult,
|
||||
List<string> ordine, Dictionary<string, int> acc)
|
||||
{
|
||||
var mem = get(root);
|
||||
if (mem is null)
|
||||
return;
|
||||
foreach (var (chiave, qta) in Rollup(mem))
|
||||
{
|
||||
if (chiave.Tipo == 2)
|
||||
{
|
||||
if (!acc.ContainsKey(chiave.Nome))
|
||||
ordine.Add(chiave.Nome);
|
||||
acc[chiave.Nome] = acc.GetValueOrDefault(chiave.Nome) + qta * mult;
|
||||
}
|
||||
else
|
||||
{
|
||||
Sommario(chiave.Nome, get, mult * qta, ordine, acc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- resa nel formato di Creo ------------------------------------------
|
||||
|
||||
private static string RigaVoce(int qta, string tipo, string nome) => $"{qta,6} {tipo,12} {nome}";
|
||||
|
||||
/// <summary>Testo della BOM, identico byte per byte a quello esportato da Creo (UTF-8 con
|
||||
/// BOM, CRLF - vedi GeneraBom per la scrittura su file).</summary>
|
||||
private static string Render(string root, Func<string, List<Membro>?> get)
|
||||
{
|
||||
var pezzi = new List<string>();
|
||||
foreach (var blocco in Blocchi(root, get))
|
||||
{
|
||||
string testa = $"{blocco.Tipo} {blocco.Nome} contiene:";
|
||||
if (blocco.Righe is null)
|
||||
{
|
||||
pezzi.Add(testa + "\r\n" + new string(' ', 5) + "*** NON RISOLTO: file .asm non trovato ***");
|
||||
continue;
|
||||
}
|
||||
var righe = blocco.Righe.Select(r => RigaVoce(r.Qta, TipoNome[r.Chiave.Tipo], r.Chiave.Nome));
|
||||
pezzi.Add(string.Join("\r\n", new[] { testa }.Concat(righe)));
|
||||
}
|
||||
|
||||
var ordine = new List<string>();
|
||||
var acc = new Dictionary<string, int>();
|
||||
Sommario(root, get, 1, ordine, acc);
|
||||
var totale = ordine.Select(n => RigaVoce(acc[n], "Parte", n));
|
||||
pezzi.Add(string.Join("\r\n", new[] { $"Summary of items for {root}:" }.Concat(totale)));
|
||||
|
||||
// due righe vuote iniziali, tre fra i blocchi, due prima del riepilogo
|
||||
return "\r\n\r\n" + string.Join("\r\n\r\n\r\n\r\n", pezzi.Take(pezzi.Count - 1)) + "\r\n\r\n\r\n" + pezzi[^1] + "\r\n";
|
||||
}
|
||||
|
||||
public static string GeneraBom(string percorsoAsm, IEnumerable<string> cartelle)
|
||||
{
|
||||
byte[] dati = File.ReadAllBytes(percorsoAsm);
|
||||
return Render(NomeProprio(dati), CreaFinder(cartelle));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user