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:
1 parent
52483177f0
commit
f28cb818bd
5 files changed
+231
No files matched your search
@@ -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
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user