#:package Microsoft.Data.Sqlite@10.0.12 // dotnet run scripts/BomViaDb.cs -- [cad_index.db] [output.txt] // // PROVA: a differenza di ExtractCreoStrings.cs (che restringe i candidati a un solo // pattern di codice, tarato sui codici numerici Pro-E), qui l'estrazione è generica - // qualunque run di lettere/cifre/underscore/trattino - e il DB decide se è un codice // vero, così si intercettano anche i nomi alfabetici delle parti commerciali scaricate // come STEP (es. "ANELLO_ESTERNO-1-SOLID1"). L'essere nel DB da solo non basta però: // codici corti o generici (es. "907", "NONE") comparirebbero per puro rumore binario, // quindi si richiede comunque il placement esplicito nel file, come in // ExtractCreoStrings.cs. Stessa espansione ricorsiva sui sotto-assiemi. using System.Text; using System.Text.RegularExpressions; using Microsoft.Data.Sqlite; if (args.Length < 1) { Console.WriteLine("Uso: dotnet run BomViaDb.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"; // Oltre il 90% dei codici in indice sono numerici Pro-E, ma una minoranza (parti // commerciali scaricate come STEP, es. "ANELLO_ESTERNO-1-SOLID1") ha nomi alfabetici // con trattini/underscore. Niente forma di codice ipotizzata a mano: si tokenizza in // modo generico (run di lettere/cifre/underscore/trattino) e si lascia al DB decidere // cos'è un codice vero - stesso principio già usato per il filtro, esteso anche // all'estrazione dei candidati. Cattura anche "UNI6592"/"CUSCI_ACBB7" dentro // "" gratis: '<' e '>' non fanno parte del charset e delimitano il token. var codiceRegex = new Regex(@"[A-Za-z0-9_-]{3,}", RegexOptions.Compiled); using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); connection.Open(); // Tutti i codici prt/asm noti all'indice, caricati una volta sola: il lookup per ogni // candidato estratto dal binario diventa O(1) invece di una query per file. var codiciNoti = new Dictionary>(StringComparer.OrdinalIgnoreCase); using (var command = connection.CreateCommand()) { command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione " + "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC"; using var reader = command.ExecuteReader(); while (reader.Read()) { string codice = reader.GetString(0); string tipo = reader.GetString(1).ToLowerInvariant(); var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new(); if (!lista.Any(x => x.Tipo == tipo)) lista.Add((tipo, reader.GetString(2))); } } Console.WriteLine($"Codici prt/asm noti nell'indice: {codiciNoti.Count}"); // Stesso controllo di placement di ExtractCreoStrings.cs/AssemblyComponentScanner.cs: // la sola presenza del codice nel DB non basta, perché codici corti o generici // (es. "907", "NONE", "start_part") possono comparire per puro rumore binario. Un // componente vero è sempre referenziato in una di queste forme (vedi // docs/formato-asm.md): "Componente ID N (...)"/"Component id N (...)", // il codice ripetuto due volte separato da un byte NUL, il campo // "family_name\0\0", o il codice seguito da un bracket "". bool HaPlacement(string testo, 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 codiceProprio = Path.GetFileName(asmIniziale).Split('.')[0]; // Stessa esclusione di ExtractCreoStrings.cs: un assieme "_D..." referenzia sempre al // suo interno anche l'assieme generale a cui appartiene (riferimento fantasma verso // l'alto), da escludere subito o si risale tutto l'albero del macchinario. var codiceRadice = codiceProprio.Split('_')[0]; var componentiTrovati = new SortedSet(StringComparer.OrdinalIgnoreCase); var scartatiNonInIndice = new SortedSet(StringComparer.OrdinalIgnoreCase); var scartatiSenzaPlacement = new SortedSet(StringComparer.OrdinalIgnoreCase); var codiciVisti = new HashSet(StringComparer.OrdinalIgnoreCase) { codiceProprio, codiceRadice }; var daScansionare = new Queue(new[] { asmIniziale }); while (daScansionare.Count > 0) { var asm = daScansionare.Dequeue(); var testo = Encoding.Latin1.GetString(File.ReadAllBytes(asm)); var candidati = codiceRegex.Matches(testo).Select(m => m.Value).ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var candidato in candidati) { if (!codiciVisti.Add(candidato)) continue; if (!codiciNoti.TryGetValue(candidato, out var componenti)) { scartatiNonInIndice.Add(candidato); continue; } if (!HaPlacement(testo, candidato)) { scartatiSenzaPlacement.Add(candidato); continue; } componentiTrovati.Add(candidato); foreach (var (tipo, percorso) in componenti) if (tipo == "asm") daScansionare.Enqueue(percorso); } } File.WriteAllLines(outPath, componentiTrovati); Console.WriteLine($"{componentiTrovati.Count} componenti scritti in {outPath}"); Console.WriteLine($"Candidati nell'indice ma senza placement (scartati, {scartatiSenzaPlacement.Count}): {string.Join(", ", scartatiSenzaPlacement.Take(30))}" + (scartatiSenzaPlacement.Count > 30 ? ", ..." : "")); Console.WriteLine($"Candidati non nell'indice (rumore binario): {scartatiNonInIndice.Count}"); return 0;