AsmRecordReader.LeggiMembri ora lancia InvalidDataException quando un assieme e' esso stesso un'istanza di family table (members descriverebbe solo una rappresentazione, non le istanze sorelle) invece di restituire un risultato sbagliato in silenzio - il chiamante ripiega sull'euristica testuale, che risolve gia' correttamente il caso. Aggiunti due controlli di coerenza analoghi (conteggio grezzo vs interpretato dei record ConfigName; occorrenze multiple di "members"), verificati senza falsi positivi su tutti i 350 .asm reali della share. Documentazione riorganizzata per distinguere cio' che e' verificato contro una BOM Creo di riferimento da cio' che e' verificato solo su scala (conteggi), e cio' che resta genuinamente non risolto. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
180 lines
9.8 KiB
C#
180 lines
9.8 KiB
C#
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);
|
|
|
|
// Un componente creato da un pattern/array feature ha un ConfigName con un tag interno
|
|
// "<codice>#AF<n>" (Array Feature, n = indice nel pattern), non un'istanza di family
|
|
// table vera: la BOM reale di Creo lo ignora e mostra il codice pulito - osservato su
|
|
// 9244250136.asm (4 dei suoi 8 membri, due dei quali stesso codice "_C03" con "#AF0" e
|
|
// "#AF1" diversi, che la BOM unisce in una sola riga con quantita' 2). Senza questa
|
|
// normalizzazione ogni istanza del pattern risulterebbe un componente diverso.
|
|
private static readonly Regex SuffissoArrayFeatureRegex = new(@"#AF\d+$", RegexOptions.Compiled);
|
|
|
|
// record di identita' del modello, seguito da "generic_name" (vuoto): compare su una
|
|
// minoranza di assiemi (~7% dei campioni sulla share) per motivi diversi e nella
|
|
// maggioranza dei casi "members" resta corretto - la sola presenza di "generic_name" NON
|
|
// e' un segnale affidabile (verificato: su 144 file che lo contengono, solo 25 hanno
|
|
// anche il sintomo sotto; per gli altri 119 "members" letto normalmente coincide con
|
|
// l'euristica testuale, quindi e' un contesto diverso, non lo stesso problema).
|
|
private static readonly Regex OwnModelIdentityRegex = new(
|
|
@"\xe0\x01model_t_type\x00" + Int + @"\xe0\x0amodel_name\x00([\x20-\x7e]+)\x00\xe0\x0ageneric_name\x00\x00",
|
|
RegexOptions.Compiled);
|
|
|
|
// sintomo vero e proprio (docs/formato-asm-record.md §7): il file e' esso stesso
|
|
// un'istanza di family table (il suo "model_name" ha il suffisso "_C0n" delle istanze,
|
|
// stessa convenzione di SuffissoArrayFeatureRegex/ConfigName sopra) - "members" descrive
|
|
// solo questa rappresentazione, non le istanze sorelle elencate in "stamp". Verificato su
|
|
// 2546220101.asm: la BOM Creo reale mostra 4 istanze diverse, "members" ne restituisce
|
|
// silenziosamente solo una.
|
|
private static readonly Regex SuffissoIstanzaFamilyTableRegex = new(@"_C\d{2,}$", 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 ContaOccorrenze(string testo, string marcatore)
|
|
{
|
|
int conteggio = 0;
|
|
for (int i = testo.IndexOf(marcatore, StringComparison.Ordinal); i >= 0; i = testo.IndexOf(marcatore, i + 1, StringComparison.Ordinal))
|
|
conteggio++;
|
|
return conteggio;
|
|
}
|
|
|
|
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 identita = OwnModelIdentityRegex.Match(testo);
|
|
if (identita.Success && SuffissoIstanzaFamilyTableRegex.IsMatch(identita.Groups[2].Value))
|
|
throw new InvalidDataException(
|
|
$"assieme con family table: il file e' esso stesso l'istanza '{identita.Groups[2].Value}', " +
|
|
"'members' descriverebbe solo questa rappresentazione, non le istanze sorelle - vedi " +
|
|
"docs/formato-asm-record.md §7");
|
|
|
|
var configNameMatches = ConfigNameRegex.Matches(testo);
|
|
int configNameGrezzi = ContaOccorrenze(testo, (char)0xe3 + "ConfigName\0");
|
|
if (configNameMatches.Count != configNameGrezzi)
|
|
throw new InvalidDataException(
|
|
$"trovati {configNameGrezzi} record 'ConfigName' ma solo {configNameMatches.Count} " +
|
|
"interpretati: formato del parametro diverso da quello atteso (vedi §4, costante " +
|
|
"\\x82\\x7b), lettura non affidabile");
|
|
|
|
var istanze = new Dictionary<int, string>();
|
|
foreach (Match m in configNameMatches)
|
|
istanze[AsInt(m.Groups[2].Value)] = SuffissoArrayFeatureRegex.Replace(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");
|
|
if (testo.IndexOf(chiaveMembers, start + chiaveMembers.Length, StringComparison.Ordinal) >= 0)
|
|
throw new InvalidDataException(
|
|
"trovate piu' occorrenze di 'members': probabile rappresentazione semplificata, " +
|
|
"mai verificata (vedi §7) - non e' chiaro quale sezione descriva la BOM");
|
|
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);
|
|
}
|
|
}
|