From f28cb818bd784f82b8bea18a61d3b26a2f721c56 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 17 Sep 2026 09:01:45 +0200 Subject: [PATCH] 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 --- .gitignore | 3 + CLAUDE.md | 14 +++++ docs/formato-asm.md | 102 ++++++++++++++++++++++++++++++++++ scripts/Analyze.cs | 21 +++++++ scripts/ExtractCreoStrings.cs | 91 ++++++++++++++++++++++++++++++ 5 files changed, 231 insertions(+) create mode 100644 docs/formato-asm.md create mode 100644 scripts/Analyze.cs create mode 100644 scripts/ExtractCreoStrings.cs diff --git a/.gitignore b/.gitignore index 9d30acd..dc97586 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 4933ae0..00f6106 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`. diff --git a/docs/formato-asm.md b/docs/formato-asm.md new file mode 100644 index 0000000..8038d62 --- /dev/null +++ b/docs/formato-asm.md @@ -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 + +``` +\x00\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 (.PRT|.ASM)` — riga di placement: `N` è un ID + progressivo assegnato da Creo al componente posizionato nell'assieme. +- `#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#` (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: +`(? +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; diff --git a/scripts/ExtractCreoStrings.cs b/scripts/ExtractCreoStrings.cs new file mode 100644 index 0000000..e154380 --- /dev/null +++ b/scripts/ExtractCreoStrings.cs @@ -0,0 +1,91 @@ +#:package Microsoft.Data.Sqlite@10.0.12 +// dotnet run scripts/ExtractCreoStrings.cs -- [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#" 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 -- [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(@"(? EstraiCodiciReferenziati(string percorsoAsm) +{ + var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm)); + var occorrenze = new Dictionary(); + 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(); + 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(StringComparer.OrdinalIgnoreCase) { Path.GetFileName(asmIniziale).Split('.')[0] }; +var daVisitare = new Queue(); +daVisitare.Enqueue(asmIniziale); +var componentiTrovati = new SortedSet(StringComparer.OrdinalIgnoreCase); +var nonTrovati = new List(); + +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;