Integra il parser esatto del .asm nella GUI (fase 4 e 5)

AsmRecordReader (spostato da scripts/CreoBom.cs nel progetto principale,
unica fonte di verita' condivisa con lo script via #:project) sostituisce
AssemblyComponentScanner in Form1.RisolviAlberoComponenti (fase "Trova
componenti referenziati") e AssiemeCompleto (fase "Verifica completezza"):
legge esattamente la sezione "members" del binario invece di contare
occorrenze testuali, eliminando i falsi positivi/negativi noti dell'euristica
(riferimenti fantasma, placement senza suffisso, ecc.).

Se un file non rispetta il formato validato (InvalidDataException, coperto
solo dagli 8 campioni usati per il reverse engineering) Form1 ripiega
sull'euristica di AssemblyComponentScanner, che resta quindi come fallback
e come fonte di verita' per gli script BomViaDb.cs/ExtractCreoStrings.cs.

Aggiunti test per AsmRecordReader (fixture binarie costruite a mano: lettura
membri, risoluzione ConfigName, conteggio disallineato, sezione assente).
Aggiornata la documentazione (CLAUDE.md, docs/architettura.md,
docs/formato-asm.md, docs/formato-asm-record.md) di conseguenza.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5 committed 2026-09-21 16:14:00 +02:00
1 parent 60dfd26e8b
commit cce377a091
8 files changed
+363 -163

No files matched your search

+23 -113
View File
@@ -1,18 +1,17 @@
#:project ../PrepStep/PrepStep.csproj
#:property TargetFramework=net10.0-windows
// 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.
// Genera la distinta base (BOM) di un assieme Creo, dato un elenco di cartelle dove
// cercare i sottoassiemi (-p). La lettura del formato binario (sezione "members" +
// parametro "ConfigName", vedi docs/formato-asm-record.md) vive in
// PrepStep.AsmRecordReader, via "#:project" - stessa fonte usata da Form1 nella GUI (fase
// 4/5, con fallback automatico all'euristica di AssemblyComponentScanner.cs per i file che
// non rispettano il formato validato): qui solo la ricorsione sui sottoassiemi, il
// riepilogo delle quantita' e la resa nel formato esatto della BOM Creo.
using System.Text;
using System.Text.RegularExpressions;
using PrepStep;
if (args.Length == 0)
{
@@ -45,106 +44,17 @@ else
}
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>
/// <summary>Ricorsione sui sottoassiemi e resa nel formato della BOM esportata da Creo. La
/// lettura del formato binario vive in <see cref="AsmRecordReader"/> (PrepStep/, condivisa
/// con la GUI). 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)
public static List<((int Tipo, string Nome) Chiave, int Qta)> Rollup(IEnumerable<AsmRecordReader.Membro> membri)
{
var ordine = new List<(int, string)>();
var qty = new Dictionary<(int, string), int>();
@@ -163,7 +73,7 @@ public static class CreoAsm
/// <summary>nome modello -> membri, cercando &lt;nome&gt;.asm.&lt;rev&gt; 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)
private static Func<string, List<AsmRecordReader.Membro>?> CreaFinder(IEnumerable<string> cartelle)
{
var indice = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var revisioni = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
@@ -184,12 +94,12 @@ public static class CreoAsm
}
}
var cache = new Dictionary<string, List<Membro>?>();
var cache = new Dictionary<string, List<AsmRecordReader.Membro>?>();
return nome =>
{
if (!cache.TryGetValue(nome, out var mem))
{
mem = indice.TryGetValue(nome, out var percorso) ? LeggiMembri(File.ReadAllBytes(percorso)) : null;
mem = indice.TryGetValue(nome, out var percorso) ? AsmRecordReader.LeggiMembri(File.ReadAllBytes(percorso)) : null;
cache[nome] = mem;
}
return mem;
@@ -202,7 +112,7 @@ public static class CreoAsm
/// <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)
private static List<Blocco> Blocchi(string root, Func<string, List<AsmRecordReader.Membro>?> get)
{
var risultato = new List<Blocco>();
var visti = new HashSet<string>();
@@ -231,7 +141,7 @@ public static class CreoAsm
/// <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,
private static void Sommario(string root, Func<string, List<AsmRecordReader.Membro>?> get, int mult,
List<string> ordine, Dictionary<string, int> acc)
{
var mem = get(root);
@@ -258,7 +168,7 @@ public static class CreoAsm
/// <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)
private static string Render(string root, Func<string, List<AsmRecordReader.Membro>?> get)
{
var pezzi = new List<string>();
foreach (var blocco in Blocchi(root, get))
@@ -286,6 +196,6 @@ public static class CreoAsm
public static string GeneraBom(string percorsoAsm, IEnumerable<string> cartelle)
{
byte[] dati = File.ReadAllBytes(percorsoAsm);
return Render(NomeProprio(dati), CreaFinder(cartelle));
return Render(AsmRecordReader.NomeProprio(dati), CreaFinder(cartelle));
}
}