#:package Microsoft.Data.Sqlite@10.0.12 // dotnet run scripts/ExtractCreoStrings.cs -- [cad_index.db] [output.txt] // // Estrae dal file .asm i codici dei componenti referenziati, espandendo ricorsivamente // ogni sotto-assieme (.asm) trovato nell'indice, finché non restano solo PRT (foglie) o // codici non presenti nell'indice. Stessa euristica di CadSearchGui/AssemblyComponentScanner.cs: // i codici veri compaiono più volte nel file, il rumore binario quasi sempre una sola volta; // gli ID interni "ModelView#" sono esclusi via lookbehind - vedi scripts/Analyze.cs. // Ogni codice trovato viene validato contro l'indice cad_index.db. using System.Text; using System.Text.RegularExpressions; using Microsoft.Data.Sqlite; if (args.Length < 1) { Console.WriteLine("Uso: dotnet run ExtractCreoStrings.cs -- [cad_index.db] [output.txt]"); return 1; } string asmIniziale = args[0]; string dbPath = args.Length > 1 ? args[1] : "cad_index.db"; string outPath = args.Length > 2 ? args[2] : "BOM.txt"; // Seconda alternativa: componenti normalizzati definiti come family table in Creo (viti/ // dadi/rosette UNI, ma anche cuscinetti CUSCI_...). Compaiono come "Componente ID N // (R12--_-_U6592.PRT)": "R12--_-_U6592" è il nome della singola istanza (riga // della family table, non un file separato su disco), il file reale indicizzato è il // generico "uni6592.prt."/"cusci_acbb7.prt." - va quindi estratto solo // il contenuto tra "<" e ">". // Terza alternativa: varianti dimensionali con suffisso puramente numerico (es. cinghie // "2702220182_8188", dove "8188" è una misura, non una lettera di revisione/sotto-assieme // come "_C01"/"_D100") - la lettera dopo "_" è quindi opzionale, non obbligatoria. La // lettera di revisione e il suffisso "_" possono anche comparire insieme sullo stesso // codice (es. "9284200366A_C01", componente di un sotto-assieme già di per sé revisionato). var codiceRegex = new Regex( @"(?)", RegexOptions.Compiled); const int occorrenzeMinime = 2; HashSet EstraiCodiciReferenziati(string percorsoAsm) { var testo = Encoding.Latin1.GetString(File.ReadAllBytes(percorsoAsm)); var occorrenze = new Dictionary(); foreach (Match m in codiceRegex.Matches(testo)) occorrenze[m.Value] = occorrenze.GetValueOrDefault(m.Value) + 1; var candidati = occorrenze.Where(kv => kv.Value >= occorrenzeMinime).Select(kv => kv.Key).ToHashSet(); // Ripetersi >=2 volte nel file non basta per un codice "nudo" (senza suffisso "_..."): // riferimenti al proprio assieme padre (nome sessione/file) o a codici superati/ // rinominati (caso "fantasma", docs/formato-asm.md) ripetono comunque il codice più // volte pur non essendo mai un componente vero. Per questi si richiede un vero // placement, in una delle quattro forme osservate nel binario: "Componente ID N // (...)"/"Component id N (...)" (la stringa cambia con la // localizzazione Creo del file); il codice ripetuto due volte di seguito separato da // un singolo byte NUL ("\0\0", osservato per componenti di family // table non-UNI, es. cuscinetti "CUSCI_..."); il campo "family_name\0\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 "" - l'istanza di una family // table non-UNI vista nel nome (es. "2510060303", un cuscinetto): il // codice dell'istanza non è mai indicizzato a sé (solo il generico tra "<" e ">" lo è, // vedi sotto), 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. Non si applica alle varianti // con suffisso "_..." né alle istanze family table UNI: in alcuni file il placement // riporta solo il nome istanza/generico senza il dettaglio del suffisso o del bracket // "<...>", che vive altrove nei metadati - lì l'occorrenza ripetuta resta l'unico // segnale disponibile. 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 + "<"); candidati.RemoveWhere(c => Regex.IsMatch(c, @"^\d{8,12}[A-Za-z]{0,2}$") && !HaPlacement(c)); return candidati; } using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); connection.Open(); // "files_ultima_versione" è una VIEW con ROW_NUMBER() OVER (...), che SQLite non // materializza: ogni query la ricalcola sull'intera tabella "files" (~230ms anche per un // solo codice, su ~300k righe). Batchare tutti i codici trovati in un file in un'unica // query paga quel costo una volta per file invece che una volta per codice - decisivo // sugli assiemi con centinaia di sotto-componenti referenziati. Dictionary> CercaComponenti(IReadOnlyCollection codici) { var risultato = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var codice in codici) risultato[codice] = new List<(string, string)>(); if (codici.Count == 0) return risultato; using var command = connection.CreateCommand(); var nomiParametri = codici.Select((_, i) => $"@c{i}").ToList(); // Lo stesso codice+tipo può esistere in più cartelle radice (duplicati/copie storiche // sulla share, es. un generico family table ripetuto in 5 progetti scollegati): ordinato // per data_modifica DESC, si tiene solo il più recente per codice+tipo (stessa regola di // Form1.RisolviAlberoComponenti, vedi docs/formato-asm.md). command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione " + $"WHERE LOWER(codice) IN ({string.Join(",", nomiParametri)}) AND LOWER(tipo_file) IN ('prt', 'asm') " + "ORDER BY codice, tipo_file, data_modifica DESC"; int i = 0; foreach (var codice in codici) command.Parameters.AddWithValue(nomiParametri[i++], codice.ToLowerInvariant()); var tipiVisti = new Dictionary>(StringComparer.OrdinalIgnoreCase); using var reader = command.ExecuteReader(); while (reader.Read()) { string codiceDb = reader.GetString(0); string tipo = reader.GetString(1).ToLowerInvariant(); var visti = tipiVisti.TryGetValue(codiceDb, out var v) ? v : tipiVisti[codiceDb] = new HashSet(); if (visti.Add(tipo)) risultato[codiceDb].Add((tipo, reader.GetString(2))); } return risultato; } // Espansione ricorsiva: ogni sotto-assieme (.asm) trovato viene a sua volta letto e i suoi // componenti aggiunti alla coda, finché non restano solo PRT (foglie) o codici non presenti // nell'indice - stessa logica di Form1.RisolviAlberoComponenti. var codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0]; // Un assieme con suffisso "_D..." referenzia sempre al suo interno anche l'assieme // generale (codice nudo, senza suffisso) a cui appartiene - riferimento fantasma verso // l'alto, non un componente vero (vedi docs/formato-asm.md). Va escluso subito: se // seguito, riporta dentro l'intero albero del macchinario invece del solo sotto-assieme // richiesto (visto su 9284208188_D600 -> 9284208188, che da solo genera 598 falsi // positivi invece di ~90 componenti reali). var codiceRadice = codiceProprio.Split('_')[0]; var componentiTrovati = new SortedSet(StringComparer.OrdinalIgnoreCase); var nonTrovati = new List(); var codiciVisti = new HashSet(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice }; var daScansionare = new Queue(new[] { asmIniziale }); while (daScansionare.Count > 0) { var asm = daScansionare.Dequeue(); var candidati = EstraiCodiciReferenziati(asm) .Where(codiciVisti.Add) .ToList(); var trovatiPerCodice = CercaComponenti(candidati); foreach (var codice in candidati) { var componenti = trovatiPerCodice[codice]; if (componenti.Count == 0) { nonTrovati.Add(codice); continue; } componentiTrovati.Add(codice); foreach (var (tipo, percorso) in componenti) if (tipo == "asm") daScansionare.Enqueue(percorso); } } File.WriteAllLines(outPath, componentiTrovati); Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}"); if (nonTrovati.Count > 0) Console.WriteLine($"Codici trovati nel file ma non nell'indice (scartati): {string.Join(", ", nonTrovati)}"); return 0;