Add standalone .asm BOM-extraction scripts and format documentation

Reverse-engineered the Creo .asm binary format enough to extract referenced
component codes via a text heuristic, mirroring AssemblyComponentScanner's
approach and Form1's recursive resolution against cad_index.db. Documents
known false-positive classes (ModelView# view IDs, self-references, and
phantom references to superseded/renamed codes).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5 committed 2026-09-17 09:01:45 +02:00
1 parent 52483177f0
commit f28cb818bd
5 files changed
+231

No files matched your search

+3
View File
@@ -33,3 +33,6 @@ publish/
# Report BOM generati manualmente da Creo per debug/validazione: mai una dipendenza
# della pipeline (che deve funzionare senza Creo), solo un aiuto occasionale.
*.bom.*
# Output di scripts/ExtractCreoStrings.cs
/BOM.txt
+14
View File
@@ -95,3 +95,17 @@ All'avvio la app cerca `cad_index.db` accanto all'eseguibile o nella working
directory corrente; se non lo trova, l'utente può selezionarlo con "Apri DB...".
Il bottone "Aggiorna indice" rilancia `Indexer.ScanAsync` sullo stesso path (radice
disco di default `X:\`, hardcoded in `Form1.RadiceDiscoDefault`).
## Script standalone (`scripts/`)
`scripts/` contiene C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`),
senza csproj — dipendenze NuGet extra si aggiungono con `#:package Nome@versione` in cima
al file. `ExtractCreoStrings.cs` estrae la BOM di un `.asm` risolvendo ricorsivamente i
sotto-assiemi contro `cad_index.db` (stessa logica di `Form1.AssiemeCompleto`); `Analyze.cs`
dumpa il contesto testo/hex attorno a un pattern in un file binario, per investigare a mano
nuovi falsi positivi prima di toccare la regex.
Il formato binario `.asm`, l'euristica di estrazione codici e i falsi positivi noti (ID vista
`ModelView#`, self-reference, riferimenti fantasma a codici superati/rinominati — sintatticamente
identici a un componente vero, non filtrabili in modo affidabile) sono documentati in
`docs/formato-asm.md`.
+102
View File
@@ -0,0 +1,102 @@
# 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.
## Struttura osservata
Il file contiene, frammentate tra dati binari, molte coppie testuali del tipo
```
<nome_campo>\x00<valore>\x00
```
in encoding a 1 byte per carattere (Latin1/ASCII), es.:
```
6d 6f 64 65 6c 5f 6e 61 6d 65 00 39 30 34 34 32 30 37 30 36 31 5f 43 30 31 23 41 46 30 00
m o d e l _ n a m e \0 9 0 4 4 2 0 7 0 6 1 _ C 0 1 # A F 0 \0
```
I codici dei componenti (propri e sotto-componenti) compaiono come stringhe
ASCII leggibili in diversi campi ricorrenti:
- `model_name`, `generic_name`, `inst_name`, `gen_name`, `feat_name`,
`family_name`, `name` — metadati associati a un modello referenziato.
- `Componente ID N (<codice>.PRT|.ASM)` — riga di placement: `N` è un ID
progressivo assegnato da Creo al componente posizionato nell'assieme.
- `<codice>#AF0` — riferimento con suffisso di versione/configurazione.
Un codice reale compare **più volte** nel file (una per feature/placement
diverso); rumore binario che assomiglia casualmente a un codice compare quasi
sempre una sola volta. Questa è la base del filtro in
[`AssemblyComponentScanner.cs`](../CadSearchGui/AssemblyComponentScanner.cs)
e in [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs):
```
regex: \b\d{8,12}(?:_[A-Za-z]\d{2,4}|[A-Za-z]{0,2})\b
occorrenze: >= 2
```
## Falsi positivi noti
**ID di viste salvate.** Il pattern `ModelView#<numero>` (es.
`ModelView#1073741828`) genera candidati a 10 cifre indistinguibili da un
codice reale per forma, ma sono ID interni di viste orientate salvate, non
componenti. Si escludono con un lookbehind negativo sulla regex:
`(?<!ModelView#)`.
**Il codice dell'assieme stesso.** Il proprio codice compare spesso decine di
volte nei metadati (`CODICE`, `part_table`, `STAMPIGLIATURA`, ecc.) e supera
quindi la soglia di occorrenze. Va escluso esplicitamente confrontandolo con
il codice derivato dal nome del file in ingresso (`nomefile.split('.')[0]`).
**Riferimenti fantasma a codici superati/rinominati.** Caso osservato:
`9244207061.asm` contiene un riferimento a `9044207061` (stesso numero, prima
di una rinumerazione) con la stessa identica struttura sintattica di un
componente vero — `Componente ID 46 (9044207061.ASM)`, più i campi
`model_name`/`generic_name` corrispondenti. Il codice `9044207061` esiste
davvero nell'indice CAD (il file vecchio non è stato eliminato dalla share),
quindi supera anche una validazione incrociata contro il DB. Non è però un
componente realmente presente nell'albero mostrato da Creo per quell'assieme
(confermato aprendo il file in Creo).
Questo caso **non è distinguibile a livello testuale**: la sintassi è
identica a quella di un placement reale. Nessuna euristica sulla stringa
riesce a discriminarlo in modo affidabile senza rischiare di scartare
componenti veri con pattern simili altrove. Se ricompare, l'unica verifica
sicura resta l'apertura manuale in Creo del sotto-assieme sospetto.
## Risoluzione ricorsiva dell'albero
Un `.asm` referenzia solo i propri sotto-componenti diretti, non l'intero
albero annidato. Per ricostruire la distinta base completa serve una BFS:
1. Estrarre i codici candidati dal file corrente (euristica sopra).
2. Per ogni codice, cercarlo nell'indice `cad_index.db`
(`files_ultima_versione`) per sapere se è un `.prt` (foglia) o un `.asm`
(da accodare alla coda per essere scansionato a sua volta).
3. Un codice non trovato nell'indice va scartato/segnalato: o è rumore
binario, o è un componente non ancora indicizzato.
Stessa logica in tre punti del codice, con scopi diversi:
- [`AssemblyComponentScanner.EstraiCodiciReferenziati`](../CadSearchGui/AssemblyComponentScanner.cs) — solo estrazione dei candidati da un file.
- [`Form1.AssiemeCompleto`](../CadSearchGui/Form1.cs) — BFS per verificare se un assieme è completo nell'indice (fase "Verifica completezza").
- [`scripts/ExtractCreoStrings.cs`](../scripts/ExtractCreoStrings.cs) — stesso BFS, standalone, per generare un `BOM.txt` da riga di comando.
## Strumento di analisi manuale
[`scripts/Analyze.cs`](../scripts/Analyze.cs) stampa il contesto testuale ed
esadecimale attorno a ogni occorrenza di un pattern in un file binario — usato
per tutte le osservazioni di questo documento:
```powershell
dotnet run scripts/Analyze.cs -- raw/9244207061.asm.1 "9044207061"
```
Utile per verificare a mano un nuovo falso positivo prima di modificare la
regex o l'esclusione nello script di estrazione.
+21
View File
@@ -0,0 +1,21 @@
// dotnet run scripts/Analyze.cs -- <file.asm> <pattern>
using System.Text;
using System.Text.RegularExpressions;
string path = args[0];
string pattern = args[1];
byte[] data = File.ReadAllBytes(path);
string testo = Encoding.Latin1.GetString(data);
foreach (Match m in Regex.Matches(testo, pattern))
{
int s = Math.Max(0, m.Index - 24);
int e = Math.Min(testo.Length, m.Index + m.Length + 24);
string ctx = testo.Substring(s, e - s).Replace("\r", "\\r").Replace("\n", "\\n");
string hexCtx = string.Join(" ", data.Skip(s).Take(e - s).Select(b => b.ToString("x2")));
Console.WriteLine($"offset={m.Index:x} val={m.Value}");
Console.WriteLine($" txt: {ctx}");
Console.WriteLine($" hex: {hexCtx}");
}
return 0;
+91
View File
@@ -0,0 +1,91 @@
#:package Microsoft.Data.Sqlite@10.0.12
// dotnet run scripts/ExtractCreoStrings.cs -- <file.asm> [cad_index.db] [output.txt]
//
// Estrae dal file .asm i codici dei componenti referenziati (stessa euristica di
// CadSearchGui/AssemblyComponentScanner.cs: i codici veri compaiono più volte nel
// file, il rumore binario quasi sempre una sola volta; gli ID interni
// "ModelView#<numero>" sono esclusi via lookbehind - vedi scripts/Analyze.cs) e poi
// risolve ricorsivamente i sotto-assiemi cercandoli nell'indice cad_index.db, con lo
// stesso BFS di Form1.AssiemeCompleto: un assieme può referenziare solo i propri
// sotto-assiemi diretti, non l'intero albero, quindi va camminato livello per livello.
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Data.Sqlite;
if (args.Length < 1)
{
Console.WriteLine("Uso: dotnet run ExtractCreoStrings.cs -- <file.asm> [cad_index.db] [output.txt]");
return 1;
}
string asmIniziale = args[0];
string dbPath = args.Length > 1 ? args[1] : "cad_index.db";
string outPath = args.Length > 2 ? args[2] : "BOM.txt";
var codiceRegex = new Regex(@"(?<!ModelView#)\b\d{8,12}(?:_[A-Za-z]\d{2,4}|[A-Za-z]{0,2})\b", RegexOptions.Compiled);
const int occorrenzeMinime = 2;
HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
{
var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
var occorrenze = new Dictionary<string, int>();
foreach (Match m in codiceRegex.Matches(testo))
occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1;
return occorrenze.Where(kv => kv.Value >= occorrenzeMinime).Select(kv => kv.Key).ToHashSet();
}
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT tipo_file, percorso_completo FROM files_ultima_versione "
+ "WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ('prt', 'asm')";
var pCodice = command.Parameters.Add("@codice", SqliteType.Text);
List<(string Tipo, string Percorso)> CercaComponenti(string codice)
{
pCodice.Value = codice;
var risultato = new List<(string, string)>();
var tipiVisti = new HashSet<string>();
using var reader = command.ExecuteReader();
while (reader.Read())
{
string tipo = reader.GetString(0).ToLowerInvariant();
if (tipiVisti.Add(tipo))
risultato.Add((tipo, reader.GetString(1)));
}
return risultato;
}
var codiciVisti = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { Path.GetFileName(asmIniziale).Split('.')[0] };
var daVisitare = new Queue<string>();
daVisitare.Enqueue(asmIniziale);
var componentiTrovati = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var nonTrovati = new List<string>();
while (daVisitare.Count > 0)
{
var file = daVisitare.Dequeue();
foreach (var codice in EstraiCodiciReferenziati(file))
{
if (!codiciVisti.Add(codice))
continue;
var componenti = CercaComponenti(codice);
if (componenti.Count == 0)
{
nonTrovati.Add(codice);
continue;
}
componentiTrovati.Add(codice);
var percorsoAsm = componenti.FirstOrDefault(c => c.Tipo == "asm").Percorso;
if (percorsoAsm is not null)
daVisitare.Enqueue(percorsoAsm);
}
}
File.WriteAllLines(outPath, componentiTrovati);
Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}");
if (nonTrovati.Count > 0)
Console.WriteLine($"Codici trovati nel file ma non nell'indice (scartati): {string.Join(", ", nonTrovati)}");
return 0;