using System.Text; using System.Text.RegularExpressions; namespace PrepStep; /// /// Estrae i codici componente referenziati dentro un file .asm Pro-E/Creo. /// Il formato è per lo più binario, ma i codici dei sotto-componenti compaiono /// come stringhe ASCII leggibili al suo interno. /// /// Due famiglie di codici, due strategie: /// /// 1) La maggioranza (>90%) sono codici numerici Pro-E del gestionale, con una forma /// nota (stesso stile usato nei nomi file, vedi Indexer.FilenameRegex): si riconoscono /// da soli, senza bisogno del DB, e possono quindi essere segnalati come "referenziati /// ma non trovati nell'indice" quando un codice risulta rinominato/spostato - il /// chiamante fa la ricerca nel DB e riporta i mancanti. /// /// 2) Una minoranza sono parti commerciali scaricate come STEP da cataloghi online /// (viti, anelli, boccole...), che in Creo hanno nomi alfabetici senza una forma /// riconoscibile a priori (es. "ANELLO_ESTERNO-1-SOLID1", non un numero). Per questi /// non esiste un pattern da indovinare: si tokenizza in modo generico (qualunque run /// di lettere/cifre/underscore/trattino) e si lascia decidere al DB, già caricato dal /// chiamante, se il token è un codice vero - altrimenti il rumore binario sarebbe /// ingestibile. Un token così "convalidato" è per costruzione già nell'indice, quindi /// non contribuisce mai ai "non trovati": per le parti commerciali mancanti dall'indice /// non c'è modo di distinguerle dal rumore, restano semplicemente invisibili come lo /// erano prima. /// /// Verificato su un caso reale confrontando con l'albero componenti di Creo: i /// riferimenti veri compaiono più volte nel file (una volta per feature/placement), /// mentre il rumore binario casuale che assomiglia a un codice compare quasi sempre /// una sola volta. Si scartano quindi, per entrambe le famiglie, i candidati "nudi" (senza /// un suffisso di revisione/variante o una forma family-table nota) che non abbiano anche /// un vero placement testuale nel binario. /// /// Falso positivo noto ed escluso (famiglia 1): gli ID interni di viste salvate, nel /// formato "ModelView#<numero>", hanno la stessa forma di un codice (8-12 cifre) e /// compaiono più volte come un riferimento vero. Vedi docs/formato-asm.md. /// /// Viti/dadi/rosette normalizzati UNI (e cuscinetti CUSCI_...) sono family table Creo: il /// nome che compare nel binario (es. "R6--_-_U6592<UNI6592>", /// "2510060303<CUSCI_ACBB7>") è la singola istanza, non un file separato su disco - il /// file reale indicizzato è il generico "uni6592.prt.<versione>"/ /// "cusci_acbb7.prt.<versione>". Con la tokenizzazione generica "UNI6592"/ /// "CUSCI_ACBB7" vengono catturati da soli: "<" e ">" non fanno parte del charset del /// token e ne delimitano naturalmente i confini. Vedi docs/formato-asm.md. /// /// Lettera di revisione e suffisso "_..." possono comparire insieme sullo stesso codice /// (es. "9284200366A_C01"), e il suffisso dopo "_" può essere puramente numerico (es. /// "2702220182_8188", una variante dimensionale, non lettera+cifre come "_C01"/"_D100"). /// /// Un codice "nudo" (senza suffisso "_..." e senza forma family-table nota) ripetuto più /// volte non basta: può trattarsi del riferimento al nome di un altro assieme che lo /// contiene (non noto a priori, quindi non escludibile per confronto diretto come il /// proprio codice) o di un residuo di feature soppresse/superate, oppure - per la /// tokenizzazione generica - di puro rumore binario che coincide con un codice DB corto o /// generico (vedi docs/formato-asm.md). Per questi si richiede anche un vero placement, in /// una delle quattro forme osservate nel binario: "Componente ID N (<codice>...)"/ /// "Component id N (<codice>...)" (la stringa cambia con la localizzazione Creo del /// file); il codice ripetuto due volte di seguito separato da un singolo byte NUL /// ("<codice>\0<codice>\0", osservato per componenti di family table non-UNI, /// es. cuscinetti "CUSCI_..."); il campo "family_name\0<codice>\0" (osservato una /// sola volta per file, su un sotto-assieme referenziato come family table generica invece /// che con un placement testuale esplicito); oppure il codice seguito immediatamente da un /// bracket "<codice><GENERICO>" - l'istanza di una family table non-UNI vista /// nel nome (es. "2510060303<CUSCI_ACBB7>", un cuscinetto): il codice dell'istanza /// non è mai indicizzato a sé (solo il generico tra "<" e ">" lo è), ma questo /// segnale lo fa comunque sopravvivere fino alla ricerca nel DB invece di scomparire in /// silenzio come "nudo senza placement" - lì verrà comunque scartato come "non in indice", /// ma in modo tracciabile. /// static class AssemblyComponentScanner { private static readonly Regex CodiceNumericoRegex = new( @"(?Codice radice (parte prima del primo "_") di un codice componente. Un /// assieme con suffisso "_D.../_C..." referenzia sempre al suo interno anche l'assieme /// generale a cui appartiene (codice radice, senza suffisso) - riferimento fantasma /// verso l'alto, non un componente vero (vedi docs/formato-asm.md). Chi fa una /// risoluzione ricorsiva dell'albero deve seminare l'insieme dei codici già visitati /// anche con la radice del codice di partenza, non solo col codice stesso, altrimenti /// risale fino all'assieme generale e riporta dentro l'intero albero del macchinario. public static string CodiceRadice(string codice) => codice.Split('_')[0]; /// Tutti i codici prt/asm noti all'indice (caricati una sola /// volta dal chiamante, non per-file): usati per validare i token generici della /// famiglia 2 (parti commerciali). Non serve per la famiglia 1 (codici numerici), che /// si riconosce dalla forma senza bisogno del DB. public static HashSet EstraiCodiciReferenziati(string percorsoAsm, ICollection? codiciNoti = null) { var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm)); bool HaPlacement(string codice) => Regex.IsMatch(testo, $@"Component(?:e)? id \d+ \([^()]*{Regex.Escape(codice)}[^()]*\)", RegexOptions.IgnoreCase) || testo.Contains(codice + "\0" + codice + "\0") || testo.Contains("family_name\0" + codice + "\0") || testo.Contains(codice + "<"); var risultato = new HashSet(StringComparer.OrdinalIgnoreCase); var occorrenzeNumeriche = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (Match m in CodiceNumericoRegex.Matches(testo)) occorrenzeNumeriche[m.Value] = occorrenzeNumeriche.GetValueOrDefault(m.Value) + 1; foreach (var (codice, occorrenze) in occorrenzeNumeriche) { if (occorrenze < OccorrenzeMinime) continue; if (CodiceNudoRegex.IsMatch(codice) && !HaPlacement(codice)) continue; risultato.Add(codice); } if (codiciNoti is { Count: > 0 }) { var occorrenzeGeneriche = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (Match m in TokenGenericoRegex.Matches(testo)) { if (codiciNoti.Contains(m.Value)) occorrenzeGeneriche[m.Value] = occorrenzeGeneriche.GetValueOrDefault(m.Value) + 1; } foreach (var (codice, occorrenze) in occorrenzeGeneriche) { if (risultato.Contains(codice)) continue; bool formaNota = SuffissoRegex.IsMatch(codice) || codice.StartsWith("UNI", StringComparison.OrdinalIgnoreCase) || codice.StartsWith("CUSCI_", StringComparison.OrdinalIgnoreCase); if (formaNota ? occorrenze >= OccorrenzeMinime : HaPlacement(codice)) risultato.Add(codice); } } return risultato; } }