2026-09-21 16:14:00 +02:00
|
|
|
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);
|
|
|
|
|
|
2026-09-21 16:27:15 +02:00
|
|
|
// 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);
|
|
|
|
|
|
2026-09-22 09:20:56 +02:00
|
|
|
// 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);
|
|
|
|
|
|
2026-09-22 09:57:48 +02:00
|
|
|
// Alcuni membri di family table non-UNI (es. viti/dadi commerciali) non passano da
|
|
|
|
|
// ConfigName: il campo "name" del record members porta gia' la forma "<istanza><generico>"
|
|
|
|
|
// (stessa notazione di §4.1, li' sconsigliata per la BOM perche' e' un elenco di
|
|
|
|
|
// riferimenti geometrici - qui invece e' il valore vero del componente, va solo separato).
|
|
|
|
|
// Osservato su 9244248577a_c01.asm: "V6-12--_-_U5931_GC<UNI5931>" - senza questo split il
|
|
|
|
|
// codice cercato nell'indice sarebbe la stringa intera con le parentesi, mai trovata.
|
|
|
|
|
private static readonly Regex IstanzaConGenericoRegex = new(@"^([\x20-\x7e]+?)<([\x20-\x7e]+)>$", RegexOptions.Compiled);
|
|
|
|
|
|
2026-09-22 10:47:08 +02:00
|
|
|
// Suffisso "_C0n" delle istanze di family table (stessa convenzione di
|
|
|
|
|
// SuffissoArrayFeatureRegex/ConfigName sopra). Usato insieme a OwnModelIdentityRegex per
|
|
|
|
|
// riconoscere un file che e' esso stesso un'istanza, il cui "members" descriverebbe solo
|
|
|
|
|
// quella rappresentazione invece delle istanze sorelle (docs/formato-asm-record.md §7).
|
|
|
|
|
// Da solo NON basta: su file reali quel record nomina spesso un altro modello (un membro
|
|
|
|
|
// del file, o un codice imparentato), non il file stesso - per quello serve la conferma
|
|
|
|
|
// del nome proprio (objname).
|
2026-09-22 09:20:56 +02:00
|
|
|
private static readonly Regex SuffissoIstanzaFamilyTableRegex = new(@"_C\d{2,}$", RegexOptions.Compiled);
|
|
|
|
|
|
2026-09-21 16:14:00 +02:00
|
|
|
// 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);
|
|
|
|
|
|
2026-09-22 09:20:56 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 16:14:00 +02:00
|
|
|
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);
|
|
|
|
|
|
2026-09-22 09:20:56 +02:00
|
|
|
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");
|
|
|
|
|
|
2026-09-21 16:14:00 +02:00
|
|
|
var istanze = new Dictionary<int, string>();
|
2026-09-22 09:20:56 +02:00
|
|
|
foreach (Match m in configNameMatches)
|
2026-09-21 16:27:15 +02:00
|
|
|
istanze[AsInt(m.Groups[2].Value)] = SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, "");
|
2026-09-21 16:14:00 +02:00
|
|
|
|
|
|
|
|
const string chiaveMembers = "\0members\0";
|
|
|
|
|
int start = testo.IndexOf(chiaveMembers, StringComparison.Ordinal);
|
|
|
|
|
if (start < 0)
|
|
|
|
|
throw new InvalidDataException("sezione 'members' assente");
|
2026-09-22 09:20:56 +02:00
|
|
|
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");
|
2026-09-21 16:14:00 +02:00
|
|
|
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");
|
|
|
|
|
|
2026-09-22 10:47:08 +02:00
|
|
|
// Il record "model_name" e' l'identita' del file solo se il nome proprio del modello
|
|
|
|
|
// (objname, §5) lo conferma: da solo nomina spesso un altro modello - un membro del
|
|
|
|
|
// file, o un modello imparentato - e in quei casi "members" e' gia' corretto. Vedi
|
|
|
|
|
// docs/formato-asm-record.md §7.
|
|
|
|
|
var identita = OwnModelIdentityRegex.Match(testo);
|
|
|
|
|
var nomeProprio = ObjNameRegex.Match(testo);
|
|
|
|
|
if (identita.Success && nomeProprio.Success
|
|
|
|
|
&& SuffissoIstanzaFamilyTableRegex.IsMatch(identita.Groups[2].Value)
|
|
|
|
|
&& string.Equals(SuffissoIstanzaFamilyTableRegex.Replace(identita.Groups[2].Value, ""),
|
|
|
|
|
nomeProprio.Groups[1].Value, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
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");
|
|
|
|
|
|
2026-09-22 09:57:48 +02:00
|
|
|
return grezzi.Select(g =>
|
|
|
|
|
{
|
|
|
|
|
if (istanze.TryGetValue(g.Id, out var nomeIstanza))
|
|
|
|
|
return new Membro(g.Id, g.Tipo, nomeIstanza, g.Nome);
|
|
|
|
|
var bracket = IstanzaConGenericoRegex.Match(g.Nome);
|
|
|
|
|
return bracket.Success
|
|
|
|
|
? new Membro(g.Id, g.Tipo, bracket.Groups[1].Value, bracket.Groups[2].Value)
|
|
|
|
|
: new Membro(g.Id, g.Tipo, g.Nome, g.Nome);
|
|
|
|
|
}).ToList();
|
2026-09-21 16:14:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <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);
|
|
|
|
|
}
|
2026-09-22 11:52:16 +02:00
|
|
|
|
|
|
|
|
/// <summary>Codici che compaiono come piazzamento ma non fra i membri: sono i componenti
|
|
|
|
|
/// **soppressi**, che spariscono da "members" senza lasciare traccia (§7.1) ma restano
|
|
|
|
|
/// citati dalla feature che li posiziona.
|
|
|
|
|
///
|
|
|
|
|
/// Non fanno parte della BOM - Creo non li elenca - ma il loro file serve comunque per
|
|
|
|
|
/// **aprire** l'assieme: senza, il caricamento fallisce con "dati di input non completi".
|
|
|
|
|
/// Sono quindi due domande diverse: cosa compare nella distinta (<see
|
|
|
|
|
/// cref="EstraiCodiciReferenziati"/>) e cosa serve avere su disco (questo metodo in
|
|
|
|
|
/// aggiunta), e la seconda lista e' piu' lunga della prima.</summary>
|
|
|
|
|
public static HashSet<string> EstraiCodiciSoppressi(string percorsoAsm)
|
|
|
|
|
{
|
|
|
|
|
byte[] dati = File.ReadAllBytes(percorsoAsm);
|
|
|
|
|
var membri = LeggiMembri(dati);
|
|
|
|
|
var noti = new HashSet<string>(
|
|
|
|
|
membri.Select(m => m.Generico).Concat(membri.Select(m => m.Nome)), StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
|
|
|
|
var soppressi = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
foreach (Match m in PlacementRegex.Matches(Encoding.Latin1.GetString(dati)))
|
|
|
|
|
if (!noti.Contains(m.Groups[1].Value))
|
|
|
|
|
soppressi.Add(m.Groups[1].Value);
|
|
|
|
|
return soppressi;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 14:09:48 +02:00
|
|
|
/// <summary>Modelli che l'assieme cita nella tabella dei riferimenti (§7.5): oltre ai propri
|
|
|
|
|
/// membri comprende quelli legati da **riferimenti esterni** - assembly feature, datum
|
|
|
|
|
/// copiati - che non compaiono né in "members" né come piazzamento, ma che Creo carica
|
|
|
|
|
/// comunque per aprire l'assieme.
|
|
|
|
|
///
|
|
|
|
|
/// Terza lista dopo <see cref="EstraiCodiciReferenziati"/> ("cosa c'è in distinta") e <see
|
|
|
|
|
/// cref="EstraiCodiciSoppressi"/>: come quest'ultima risponde a "cosa serve su disco", non a
|
|
|
|
|
/// "cosa va nella BOM. L'array contiene anche nomi di feature (MIRROR3, CHAMFER1...) e non
|
|
|
|
|
/// solo di modelli: non si riesce a distinguerli dal record, ma non danno fastidio perché il
|
|
|
|
|
/// chiamante li cerca nell'indice e ignora quelli che non ci sono.</summary>
|
|
|
|
|
public static HashSet<string> EstraiCodiciRiferimentiEsterni(string percorsoAsm)
|
|
|
|
|
{
|
|
|
|
|
string testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm));
|
|
|
|
|
var codici = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
for (int i = testo.IndexOf(InizioRiferimenti, StringComparison.Ordinal); i >= 0;
|
|
|
|
|
i = testo.IndexOf(InizioRiferimenti, i + 1, StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
int max = Math.Min(LunghezzaMassimaRiferimenti, testo.Length - i);
|
|
|
|
|
int fine = testo.IndexOf(FineRiferimenti, i, max);
|
|
|
|
|
foreach (Match m in RiferimentoRegex.Matches(testo.Substring(i, fine >= 0 ? fine - i : max)))
|
|
|
|
|
{
|
|
|
|
|
var bracket = IstanzaConGenericoRegex.Match(m.Groups[1].Value);
|
|
|
|
|
codici.Add(bracket.Success
|
|
|
|
|
? bracket.Groups[2].Value
|
|
|
|
|
: SuffissoArrayFeatureRegex.Replace(m.Groups[1].Value, ""));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return codici;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Array dei modelli referenziati: "\xf2\xf7\x05" lo apre, "\xf3" lo chiude, ogni elemento e'
|
|
|
|
|
// "\xe3\xe2<tipo><nome>\x00" (§7.5). Il tetto di lunghezza limita il danno se il terminatore
|
|
|
|
|
// non si trova dove atteso: si perde qualche riferimento, non si raccoglie mezzo file.
|
|
|
|
|
private const string InizioRiferimenti = "ò÷\u0005ãâ";
|
|
|
|
|
private const char FineRiferimenti = 'ó';
|
|
|
|
|
private const int LunghezzaMassimaRiferimenti = 4096;
|
|
|
|
|
private static readonly Regex RiferimentoRegex = new(
|
|
|
|
|
@"\xe3\xe2[\x01\x02]([\x20-\x7e]{3,})\x00", RegexOptions.Compiled);
|
|
|
|
|
|
2026-09-22 11:52:16 +02:00
|
|
|
// "Componente ID 52 (9230100043.PRT)" - la stringa cambia con la localizzazione Creo del
|
|
|
|
|
// file, come in AssemblyComponentScanner.HaPlacement. Minimo 3 caratteri per lo stesso
|
|
|
|
|
// motivo per cui li' i token generici partono da 3: sotto quella soglia si raccoglie
|
|
|
|
|
// rumore (sulla share esistono davvero modelli chiamati "1.prt", "2.prt"...).
|
|
|
|
|
private static readonly Regex PlacementRegex = new(
|
|
|
|
|
@"Component(?:e)? [Ii][Dd] \d+ \(([A-Za-z0-9_\-]{3,})\.(?:PRT|ASM)\)", RegexOptions.Compiled);
|
2026-09-21 16:14:00 +02:00
|
|
|
}
|