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:
1 parent
60dfd26e8b
commit
cce377a091
8 files changed
+363
-163
No files matched your search
@@ -0,0 +1,126 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace PrepStep;
|
||||
|
||||
/// <summary>Lettura esatta della struttura a record del binario .asm Creo (sezione
|
||||
/// "members" + parametro "ConfigName" per le family table), ricavata per reverse
|
||||
/// engineering e verificata confrontando l'output con le BOM esportate da Creo - vedi
|
||||
/// docs/formato-asm-record.md per il dettaglio byte-per-byte.
|
||||
///
|
||||
/// Alternativa esatta all'euristica testuale di <see cref="AssemblyComponentScanner"/>
|
||||
/// (docs/formato-asm.md): quella conta le occorrenze dei codici nel testo, questa legge
|
||||
/// la struttura dei record. Copre solo gli 8 campioni usati per il reverse engineering: su
|
||||
/// un file che non rispetta il formato atteso lancia <see cref="InvalidDataException"/>
|
||||
/// invece di restituire un risultato parziale silenzioso - il chiamante (<see
|
||||
/// cref="Form1"/>) ripiega sull'euristica in quel caso, non essendoci garanzia di
|
||||
/// copertura completa su tutte le versioni Creo mai incontrate sulla share.
|
||||
///
|
||||
/// Unica fonte di verità condivisa da GUI e script (stesso pattern di
|
||||
/// AssemblyComponentScanner): scripts/CreoBom.cs referenzia questa classe via
|
||||
/// "#:project" per la parte di lettura, aggiungendo solo la resa in formato BOM.</summary>
|
||||
public static class AsmRecordReader
|
||||
{
|
||||
public sealed record Membro(int Id, int Tipo, string Nome, string Generico);
|
||||
|
||||
// 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 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), "Generico" e' il file
|
||||
/// realmente su disco (coincide con "Nome" per i componenti che non sono istanze).
|
||||
/// Lancia <see cref="InvalidDataException"/> se il file non ha una sezione "members" o
|
||||
/// se il conteggio dichiarato non coincide con i record letti (lettura disallineata:
|
||||
/// il formato del file non e' quello coperto dal reverse engineering).</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>Codici dei componenti diretti (il generico su disco, non il nome istanza),
|
||||
/// distinti, nell'ordine in cui compaiono. Equivalente esatto di
|
||||
/// <see cref="AssemblyComponentScanner.EstraiCodiciReferenziati"/> per i file coperti dal
|
||||
/// formato: stesso contratto di ritorno (un insieme di codici da cercare nell'indice),
|
||||
/// lettura strutturale invece di euristica testuale.</summary>
|
||||
public static HashSet<string> EstraiCodiciReferenziati(string percorsoAsm)
|
||||
{
|
||||
var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm));
|
||||
return new HashSet<string>(membri.Select(m => m.Generico), StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
+22
-2
@@ -743,6 +743,26 @@ public partial class Form1 : Form
|
||||
return codiciNoti;
|
||||
}
|
||||
|
||||
/// <summary>Codici referenziati direttamente in un assieme: prova prima il parser esatto
|
||||
/// dei record binari (<see cref="AsmRecordReader"/>, vedi docs/formato-asm-record.md), che
|
||||
/// non ha i falsi positivi/negativi noti dell'euristica testuale (docs/formato-asm.md); se
|
||||
/// il file non rispetta il formato validato (<see cref="InvalidDataException"/> - capita su
|
||||
/// .asm di versioni Creo mai riscontrate nei campioni usati per il reverse engineering)
|
||||
/// ripiega su <see cref="AssemblyComponentScanner"/>, che ha copertura più ampia anche se
|
||||
/// meno precisa. Un'eventuale IOException (file irraggiungibile) non viene intercettata qui:
|
||||
/// resta responsabilità del chiamante, come prima di questo cambiamento.</summary>
|
||||
private static IEnumerable<string> EstraiCodiciReferenziati(string percorsoAsm, ICollection<string> codiciNoti)
|
||||
{
|
||||
try
|
||||
{
|
||||
return AsmRecordReader.EstraiCodiciReferenziati(percorsoAsm);
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
return AssemblyComponentScanner.EstraiCodiciReferenziati(percorsoAsm, codiciNoti);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
|
||||
/// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
|
||||
/// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
|
||||
@@ -769,7 +789,7 @@ public partial class Form1 : Form
|
||||
{
|
||||
var asm = daScansionare.Dequeue();
|
||||
processati++;
|
||||
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
||||
foreach (var codice in EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
||||
{
|
||||
if (!codiciVisti.Add(codice))
|
||||
continue;
|
||||
@@ -873,7 +893,7 @@ public partial class Form1 : Form
|
||||
{
|
||||
try
|
||||
{
|
||||
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file, codiciNoti.Keys);
|
||||
codiciReferenziati = EstraiCodiciReferenziati(file, codiciNoti.Keys).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user