using System.Text; using System.Text.RegularExpressions; namespace PrepStep; /// 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 /// (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 /// invece di restituire un risultato parziale silenzioso - il chiamante () 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. 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; } /// Nome proprio del modello (quello che Creo stampa in intestazione BOM), /// dalla prima occorrenza del record "objname". 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; } /// 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 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). public static List LeggiMembri(byte[] dati) { string testo = Encoding.Latin1.GetString(dati); var istanze = new Dictionary(); 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 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(); } /// Codici dei componenti diretti (il generico su disco, non il nome istanza), /// distinti, nell'ordine in cui compaiono. Equivalente esatto di /// per i file coperti dal /// formato: stesso contratto di ritorno (un insieme di codici da cercare nell'indice), /// lettura strutturale invece di euristica testuale. public static HashSet EstraiCodiciReferenziati(string percorsoAsm) { var membri = LeggiMembri(File.ReadAllBytes(percorsoAsm)); return new HashSet(membri.Select(m => m.Generico), StringComparer.OrdinalIgnoreCase); } }