Integra il parser esatto del .asm nella GUI (fase 4 e 5)
AsmRecordReader (spostato da scripts/CreoBom.cs nel progetto principale, unica fonte di verita' condivisa con lo script via #:project) sostituisce AssemblyComponentScanner in Form1.RisolviAlberoComponenti (fase "Trova componenti referenziati") e AssiemeCompleto (fase "Verifica completezza"): legge esattamente la sezione "members" del binario invece di contare occorrenze testuali, eliminando i falsi positivi/negativi noti dell'euristica (riferimenti fantasma, placement senza suffisso, ecc.). Se un file non rispetta il formato validato (InvalidDataException, coperto solo dagli 8 campioni usati per il reverse engineering) Form1 ripiega sull'euristica di AssemblyComponentScanner, che resta quindi come fallback e come fonte di verita' per gli script BomViaDb.cs/ExtractCreoStrings.cs. Aggiunti test per AsmRecordReader (fixture binarie costruite a mano: lettura membri, risoluzione ConfigName, conteggio disallineato, sezione assente). Aggiornata la documentazione (CLAUDE.md, docs/architettura.md, docs/formato-asm.md, docs/formato-asm-record.md) di conseguenza. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60dfd26e8b
commit
cce377a091
8 files changed
+363
-163
No files matched your search
@@ -59,13 +59,16 @@ designer visuale):
|
||||
3. **Crea cartella raw** — `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela, vedi
|
||||
`docs/copia-raw-parallela.md`) copiano in `raw/` i file CAD dei componenti del CSV trovati
|
||||
nell'indice.
|
||||
4. **Trova componenti referenziati** — `AssemblyComponentScanner` legge euristicamente i
|
||||
`.asm` in `raw/` per risolvere ricorsivamente sotto-assiemi/parti (`RisolviAlberoComponenti`),
|
||||
copiandoli anch'essi in `raw/`. Euristica e falsi positivi noti in `docs/formato-asm.md`.
|
||||
4. **Trova componenti referenziati** — legge i `.asm` in `raw/` per risolvere ricorsivamente
|
||||
sotto-assiemi/parti (`RisolviAlberoComponenti`), copiandoli anch'essi in `raw/`. Usa
|
||||
`AsmRecordReader` (parser esatto dei record binari, `docs/formato-asm-record.md`) e ripiega
|
||||
su `AssemblyComponentScanner` (euristica testuale, `docs/formato-asm.md`) solo se il file
|
||||
non rispetta il formato validato (`InvalidDataException`).
|
||||
5. **Verifica completezza** — mostra i `.asm` della distinta già in `raw/`, trascinabili su
|
||||
Creo per l'esportazione STEP (Distributed Batch, manuale, fuori dall'app); "Verifica
|
||||
completezza assiemi" controlla ricorsivamente sull'indice (non su `raw/`) se tutti i
|
||||
sotto-componenti di ogni assieme sono presenti.
|
||||
completezza assiemi" controlla ricorsivamente sull'indice (non su `raw/`, stessa
|
||||
`AsmRecordReader`/fallback della fase 4) se tutti i sotto-componenti di ogni assieme sono
|
||||
presenti.
|
||||
|
||||
## Script standalone (`scripts/`)
|
||||
|
||||
@@ -74,10 +77,13 @@ lanciati dalla root del repo — **non** da dentro `PrepStep/`, che ha un `.cspr
|
||||
`dotnet run` lì avvia la GUI vera invece dello script). Elenco e dettaglio di ciascuno in
|
||||
`docs/architettura.md`.
|
||||
|
||||
Il pattern da preservare: `ExtractCreoStrings.cs`/`BomViaDb.cs` referenziano
|
||||
`PrepStep/PrepStep.csproj` via `#:project` invece di duplicare l'euristica di
|
||||
estrazione/placement, che vive **solo** in `PrepStep/AssemblyComponentScanner.cs` — un'unica
|
||||
fonte di verità condivisa da GUI e script (vedi `docs/formato-asm.md` per l'euristica e i
|
||||
falsi positivi noti). `CreoBom.cs` è l'eccezione deliberata: legge la struttura a record del
|
||||
binario invece di contare occorrenze testuali (`docs/formato-asm-record.md`), non passa da
|
||||
`AssemblyComponentScanner`/`cad_index.db` e non la sostituisce nella GUI.
|
||||
Il pattern da preservare: gli script referenziano `PrepStep/PrepStep.csproj` via `#:project`
|
||||
invece di duplicare la logica di lettura dei `.asm`, che vive **solo** nel progetto
|
||||
principale — un'unica fonte di verità condivisa da GUI e script. Due famiglie distinte, non
|
||||
intercambiabili:
|
||||
|
||||
- `ExtractCreoStrings.cs`/`BomViaDb.cs` usano `AssemblyComponentScanner` (euristica testuale,
|
||||
`docs/formato-asm.md`) e risolvono i sottoassiemi contro `cad_index.db`.
|
||||
- `CreoBom.cs` usa `AsmRecordReader` (parser esatto dei record binari,
|
||||
`docs/formato-asm-record.md`, condiviso con la GUI in fase 4/5) e risolve i sottoassiemi
|
||||
cercandoli in cartelle locali (`-p`), non contro `cad_index.db`.
|
||||
@@ -0,0 +1,123 @@
|
||||
using System.Text;
|
||||
using PrepStep;
|
||||
|
||||
namespace PrepStep.Tests;
|
||||
|
||||
public class AsmRecordReaderTests
|
||||
{
|
||||
private static string ScriviFile(byte[] dati)
|
||||
{
|
||||
string percorso = Path.GetTempFileName();
|
||||
File.WriteAllBytes(percorso, dati);
|
||||
return percorso;
|
||||
}
|
||||
|
||||
private static byte[] Bytes(params object[] parti)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
foreach (var p in parti)
|
||||
{
|
||||
switch (p)
|
||||
{
|
||||
case byte b: ms.WriteByte(b); break;
|
||||
case int i: ms.WriteByte((byte)i); break;
|
||||
case string s: var buf = Encoding.Latin1.GetBytes(s); ms.Write(buf); break;
|
||||
case byte[] arr: ms.Write(arr); break;
|
||||
default: throw new ArgumentException($"tipo non gestito: {p.GetType()}");
|
||||
}
|
||||
}
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
// \xe3 id type name\0 family_name\0 - id/type come singolo byte (0x00-0x7f, un byte)
|
||||
private static byte[] Membro(int id, int tipo, string nome, string? nomeFamiglia = null) =>
|
||||
Bytes(0xe3, id, tipo, nome, 0, nomeFamiglia ?? nome, 0);
|
||||
|
||||
// \xe3 ConfigName\0 \xe23 <istanza>\0 <idMembro> \x82\x7b
|
||||
private static byte[] ConfigName(string istanza, int idMembro) =>
|
||||
Bytes(0xe3, "ConfigName", 0, 0xe2, (byte)'3', istanza, 0, idMembro, 0x82, 0x7b);
|
||||
|
||||
private static byte[] Asm(int conteggioDichiarato, byte[] membri, byte[]? extra = null) =>
|
||||
Bytes("header ", extra ?? Array.Empty<byte>(), "\0members\0", 0xf1, 0xf8, conteggioDichiarato, membri, "#MdlStatus");
|
||||
|
||||
[Fact]
|
||||
public void Legge_membri_semplici_senza_family_table()
|
||||
{
|
||||
byte[] dati = Asm(2, Bytes(
|
||||
Membro(1, 2, "9244206098"),
|
||||
Membro(2, 1, "9244228558_C01")));
|
||||
|
||||
var membri = AsmRecordReader.LeggiMembri(dati);
|
||||
|
||||
Assert.Equal(2, membri.Count);
|
||||
Assert.Equal((1, 2, "9244206098", "9244206098"), (membri[0].Id, membri[0].Tipo, membri[0].Nome, membri[0].Generico));
|
||||
Assert.Equal((2, 1, "9244228558_C01", "9244228558_C01"), (membri[1].Id, membri[1].Tipo, membri[1].Nome, membri[1].Generico));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Risolve_il_nome_istanza_di_una_family_table_da_ConfigName()
|
||||
{
|
||||
// Il membro cita solo il generico (UNI6592); il nome che la BOM mostra
|
||||
// (R8--_-_U6592) sta nel parametro ConfigName, collegato per id membro.
|
||||
byte[] dati = Asm(1, Membro(1, 2, "UNI6592"), extra: ConfigName("R8--_-_U6592", idMembro: 1));
|
||||
|
||||
var membri = AsmRecordReader.LeggiMembri(dati);
|
||||
|
||||
Assert.Single(membri);
|
||||
Assert.Equal("R8--_-_U6592", membri[0].Nome);
|
||||
Assert.Equal("UNI6592", membri[0].Generico);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Componente_senza_ConfigName_non_e_toccato_da_istanze_di_altri_membri()
|
||||
{
|
||||
byte[] dati = Asm(2, Bytes(
|
||||
Membro(1, 2, "UNI6592"),
|
||||
Membro(2, 2, "9244206098")),
|
||||
extra: ConfigName("R8--_-_U6592", idMembro: 1));
|
||||
|
||||
var membri = AsmRecordReader.LeggiMembri(dati);
|
||||
|
||||
Assert.Equal("R8--_-_U6592", membri.Single(m => m.Id == 1).Nome);
|
||||
Assert.Equal("9244206098", membri.Single(m => m.Id == 2).Nome);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Conteggio_dichiarato_diverso_da_quello_letto_lancia_eccezione()
|
||||
{
|
||||
// Dichiara 2 membri ma ne fornisce solo 1: lettura disallineata, va segnalata invece
|
||||
// di restituire un risultato parziale silenzioso.
|
||||
byte[] dati = Asm(2, Membro(1, 2, "9244206098"));
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sezione_members_assente_lancia_eccezione()
|
||||
{
|
||||
byte[] dati = Encoding.Latin1.GetBytes("un file qualunque senza la sezione attesa");
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => AsmRecordReader.LeggiMembri(dati));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EstraiCodiciReferenziati_restituisce_i_generici_distinti()
|
||||
{
|
||||
byte[] dati = Asm(3, Bytes(
|
||||
Membro(1, 2, "9144210327"),
|
||||
Membro(2, 2, "9144210327"), // stesso codice montato due volte: un solo codice distinto
|
||||
Membro(3, 1, "9244238563_D100")));
|
||||
string percorso = ScriviFile(dati);
|
||||
try
|
||||
{
|
||||
var codici = AsmRecordReader.EstraiCodiciReferenziati(percorso);
|
||||
Assert.Equal(new[] { "9144210327", "9244238563_D100" }, codici.OrderBy(c => c));
|
||||
}
|
||||
finally { File.Delete(percorso); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void File_inesistente_lancia_eccezione_IO() =>
|
||||
Assert.Throws<DirectoryNotFoundException>(() =>
|
||||
AsmRecordReader.EstraiCodiciReferenziati(@"C:\non\esiste\pippo.asm.1"));
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
var istanze = new Dictionary<int, string>();
|
||||
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 <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");
|
||||
|
||||
return grezzi.Select(g => new Membro(g.Id, g.Tipo, istanze.GetValueOrDefault(g.Id, g.Nome), g.Nome)).ToList();
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
+22
-2
@@ -743,6 +743,26 @@ public partial class Form1 : Form
|
||||
return codiciNoti;
|
||||
}
|
||||
|
||||
/// <summary>Codici referenziati direttamente in un assieme: prova prima il parser esatto
|
||||
/// dei record binari (<see cref="AsmRecordReader"/>, vedi docs/formato-asm-record.md), che
|
||||
/// non ha i falsi positivi/negativi noti dell'euristica testuale (docs/formato-asm.md); se
|
||||
/// il file non rispetta il formato validato (<see cref="InvalidDataException"/> - capita su
|
||||
/// .asm di versioni Creo mai riscontrate nei campioni usati per il reverse engineering)
|
||||
/// ripiega su <see cref="AssemblyComponentScanner"/>, che ha copertura più ampia anche se
|
||||
/// meno precisa. Un'eventuale IOException (file irraggiungibile) non viene intercettata qui:
|
||||
/// resta responsabilità del chiamante, come prima di questo cambiamento.</summary>
|
||||
private static IEnumerable<string> EstraiCodiciReferenziati(string percorsoAsm, ICollection<string> codiciNoti)
|
||||
{
|
||||
try
|
||||
{
|
||||
return AsmRecordReader.EstraiCodiciReferenziati(percorsoAsm);
|
||||
}
|
||||
catch (InvalidDataException)
|
||||
{
|
||||
return AssemblyComponentScanner.EstraiCodiciReferenziati(percorsoAsm, codiciNoti);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
|
||||
/// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
|
||||
/// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
|
||||
@@ -769,7 +789,7 @@ public partial class Form1 : Form
|
||||
{
|
||||
var asm = daScansionare.Dequeue();
|
||||
processati++;
|
||||
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
||||
foreach (var codice in EstraiCodiciReferenziati(asm, codiciNoti.Keys))
|
||||
{
|
||||
if (!codiciVisti.Add(codice))
|
||||
continue;
|
||||
@@ -873,7 +893,7 @@ public partial class Form1 : Form
|
||||
{
|
||||
try
|
||||
{
|
||||
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file, codiciNoti.Keys);
|
||||
codiciReferenziati = EstraiCodiciReferenziati(file, codiciNoti.Keys).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
|
||||
+21
-11
@@ -79,14 +79,26 @@ Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati
|
||||
`ROW_NUMBER() OVER (...)`, non self-join+`GROUP BY` per evitare tempi quasi quadratici su
|
||||
~200k righe).
|
||||
|
||||
## Scansione binaria dei `.asm`
|
||||
## Lettura dei componenti referenziati in un `.asm`
|
||||
|
||||
`Form1.EstraiCodiciReferenziati` (usata da `RisolviAlberoComponenti`, fase 4, e da
|
||||
`AssiemeCompleto` dentro `VerificaCompletezzaAssiemi`, fase 5) prova prima
|
||||
`AsmRecordReader.EstraiCodiciReferenziati` e ripiega su
|
||||
`AssemblyComponentScanner.EstraiCodiciReferenziati` solo se il primo lancia
|
||||
`InvalidDataException` (file che non rispetta il formato validato dal reverse engineering).
|
||||
|
||||
- **AsmRecordReader.cs** — legge esattamente la sezione `members` del binario `.asm` (id/tipo/
|
||||
nome dei componenti diretti, con risoluzione del nome istanza di family table dal parametro
|
||||
`ConfigName`): nessuna euristica, struttura a record. Lancia `InvalidDataException` se il
|
||||
file non ha una sezione `members` o se il conteggio dichiarato non torna. Dettagli in
|
||||
`docs/formato-asm-record.md`. Condivisa con `scripts/CreoBom.cs` via `#:project`.
|
||||
- **AssemblyComponentScanner.cs** — estrae euristicamente i codici componente referenziati
|
||||
dentro un `.asm` (formato per lo più binario, codici come stringhe ASCII leggibili al suo
|
||||
interno). Due strategie: codici numerici Pro-E riconosciuti da soli via regex (come nei nomi
|
||||
file), e nomi alfabetici di parti commerciali tokenizzati genericamente e validati contro
|
||||
l'elenco di codici noti del DB (altrimenti il rumore binario sarebbe ingestibile). Dettagli
|
||||
sul formato, l'euristica e i falsi positivi noti in `docs/formato-asm.md`.
|
||||
contando le occorrenze di stringhe candidate nel testo binario. Due strategie: codici
|
||||
numerici Pro-E riconosciuti da soli via regex (come nei nomi file), e nomi alfabetici di
|
||||
parti commerciali tokenizzati genericamente e validati contro l'elenco di codici noti del DB
|
||||
(altrimenti il rumore binario sarebbe ingestibile). Dettagli sull'euristica e i falsi
|
||||
positivi noti in `docs/formato-asm.md`. Resta il fallback per i file che il parser esatto
|
||||
non copre, e la fonte di verità per `ExtractCreoStrings.cs`/`BomViaDb.cs`.
|
||||
|
||||
## Import distinta base
|
||||
|
||||
@@ -151,11 +163,9 @@ dipendenze extra con `#:package Nome@versione` in testa al file):
|
||||
Pro-E).
|
||||
- **BomViaDb.cs** — come sopra ma con la tokenizzazione generica validata contro il DB, per
|
||||
intercettare anche i nomi alfabetici delle parti commerciali.
|
||||
- **CreoBom.cs** — indipendente dagli altri: legge esattamente la struttura a record del
|
||||
binario `.asm` (sezione `members` + parametro `ConfigName`, vedi
|
||||
`docs/formato-asm-record.md`) invece di contare occorrenze di codici nel testo. Risolve i
|
||||
sottoassiemi cercandoli in cartelle locali (`-p`), non contro `cad_index.db`. Non sostituisce
|
||||
l'euristica di `AssemblyComponentScanner` nella GUI né negli altri script.
|
||||
- **CreoBom.cs** — usa `AsmRecordReader` (stesso lettore esatto della GUI, fase 4/5, via
|
||||
`#:project`) invece dell'euristica testuale. Risolve i sottoassiemi cercandoli in cartelle
|
||||
locali (`-p`), non contro `cad_index.db` come `ExtractCreoStrings.cs`/`BomViaDb.cs`.
|
||||
- **Analyze.cs** — dump del contesto testo/hex attorno a un pattern in un file binario, per
|
||||
investigare a mano nuovi falsi positivi prima di toccare la regex di
|
||||
`AssemblyComponentScanner`.
|
||||
|
||||
@@ -7,7 +7,11 @@ l'euristica testuale ("codice che compare almeno due volte") descritta in
|
||||
[`formato-asm.md`](formato-asm.md): quella era una statistica sui nomi, questa legge la
|
||||
struttura effettiva dei record.
|
||||
|
||||
Implementazione: [`scripts/CreoBom.cs`](../scripts/CreoBom.cs).
|
||||
Implementazione: lettura in [`PrepStep/AsmRecordReader.cs`](../PrepStep/AsmRecordReader.cs)
|
||||
(condivisa con la GUI, `Form1.EstraiCodiciReferenziati` in fase "Trova componenti
|
||||
referenziati"/"Verifica completezza" - con fallback su `AssemblyComponentScanner` per i file
|
||||
che non rispettano il formato validato), resa in formato BOM e ricorsione sui sottoassiemi
|
||||
in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs) (via `#:project`).
|
||||
|
||||
**Stato della validazione.** Durante il reverse engineering, il livello 1 estratto da 3
|
||||
assiemi con BOM Creo di riferimento coincideva **esattamente**: stesse voci, stesso nome
|
||||
@@ -75,7 +79,7 @@ conteggio dei membri: con la lettura a 1 byte fisso i campioni grandi si
|
||||
disallineavano. Non è confermata da una specifica; regge sui campioni
|
||||
disponibili al momento del reverse engineering.
|
||||
|
||||
In `CreoBom.cs` è `CreoAsm.AsInt`, applicata al testo decodificato Latin1 (ogni `char`
|
||||
In `CreoBom.cs` è `AsmRecordReader.AsInt`, applicata al testo decodificato Latin1 (ogni `char`
|
||||
della stringa corrisponde 1:1 a un byte, quindi indicizzare la stringa o l'array di byte
|
||||
originale dà lo stesso offset).
|
||||
|
||||
@@ -128,15 +132,15 @@ codici distinti: una parte montata 4 volte conta 4 e compare in 4 record con id
|
||||
diversi. Il conteggio va usato come controllo di correttezza: se i record
|
||||
trovati non sono esattamente `N`, la lettura si è disallineata e va segnalata
|
||||
un'eccezione invece di restituire un risultato parziale silenzioso — vedi
|
||||
`CreoAsm.LeggiMembri`, che lancia `InvalidDataException` in questo caso.
|
||||
`AsmRecordReader.LeggiMembri`, che lancia `InvalidDataException` in questo caso.
|
||||
|
||||
La sezione termina a `\xf4\x04#\n#MdlStatus\n`.
|
||||
|
||||
### 3.1 Come lo legge `CreoAsm.LeggiMembri`
|
||||
### 3.1 Come lo legge `AsmRecordReader.LeggiMembri`
|
||||
|
||||
Il parser non ricostruisce lo schema completo (servirebbe decodificare le
|
||||
matrici). Si ancora invece al pattern `\xe3 <int> <int> <stringa>\0 <stringa>\0`
|
||||
(`CreoAsm.MemberRegex`, una regex .NET sul testo Latin1 del file) e salta il
|
||||
(`AsmRecordReader.MemberRegex`, una regex .NET sul testo Latin1 del file) e salta il
|
||||
resto del record fino al `\xe3` successivo che soddisfa il pattern.
|
||||
La doppia stringa terminata da `\0` rende i falsi positivi improbabili, e il
|
||||
conteggio dichiarato `N` fa da rete di sicurezza.
|
||||
@@ -172,14 +176,14 @@ Il collegamento sta in un record di **parametro** applicato al membro:
|
||||
linea), poi è omesso.
|
||||
- `\x82\x7b` — costante (= 635) identica su tutte le occorrenze di tutti i
|
||||
campioni. Significato ignoto, presumibilmente attributi/flag del parametro.
|
||||
`CreoAsm.ConfigNameRegex` la usa come ancora di fine record; se un campione
|
||||
`AsmRecordReader.ConfigNameRegex` la usa come ancora di fine record; se un campione
|
||||
futuro avesse un valore diverso, il match salterebbe silenziosamente (nessun
|
||||
contatore di sicurezza analogo a quello di `members` copre questo caso).
|
||||
|
||||
`ConfigName` compare **esattamente** per i membri che sono istanze, e per
|
||||
nessun altro. L'algoritmo è quindi: leggere `members`, costruire la mappa
|
||||
`id membro → nome istanza` dai record `ConfigName`
|
||||
(`CreoAsm.LeggiMembri`, dizionario `istanze`), e sostituire il nome dove la
|
||||
(`AsmRecordReader.LeggiMembri`, dizionario `istanze`), e sostituire il nome dove la
|
||||
mappa ha una voce. Il generico resta disponibile come `Membro.File`.
|
||||
|
||||
> **Avvertenza.** `ConfigName` sta in mezzo a parametri palesemente aziendali
|
||||
@@ -218,7 +222,7 @@ primo record `objname` del file:
|
||||
```
|
||||
|
||||
La **prima** occorrenza è il modello stesso; quelle successive sono altri
|
||||
oggetti referenziati (disegni, modelli citati). `CreoAsm.NomeProprio` cerca
|
||||
oggetti referenziati (disegni, modelli citati). `AsmRecordReader.NomeProprio` cerca
|
||||
solo la prima occorrenza (`Regex.Match`, non `Matches`), verificato su tutti i
|
||||
campioni disponibili durante il reverse engineering.
|
||||
|
||||
@@ -297,7 +301,7 @@ Elencato esplicitamente per non far passare per dimostrato ciò che non lo è:
|
||||
situazione divergerebbe.
|
||||
- **La costante `\x82\x7b`** nei record `ConfigName`.
|
||||
- **I campi numerici dell'header** dopo `ASSEMBLY`.
|
||||
- **Occorrenze multiple di `members`.** `CreoAsm.LeggiMembri` prende la prima.
|
||||
- **Occorrenze multiple di `members`.** `AsmRecordReader.LeggiMembri` prende la prima.
|
||||
Sui campioni disponibili funziona, ma non è stato verificato se le
|
||||
rappresentazioni semplificate (simplified rep) generino sezioni `members`
|
||||
aggiuntive.
|
||||
|
||||
+17
-16
@@ -1,23 +1,24 @@
|
||||
# Come leggere un .asm Creo per dedurne i componenti
|
||||
|
||||
Il file `.asm` di Pro/ENGINEER-Creo è un formato binario proprietario, non
|
||||
documentato pubblicamente. Questo documento descrive l'euristica testuale usata da
|
||||
`AssemblyComponentScanner.cs` (GUI, fase "Trova componenti referenziati"/"Verifica
|
||||
completezza") e dagli script che risolvono l'albero componenti contro `cad_index.db`
|
||||
(`ExtractCreoStrings.cs`, `BomViaDb.cs`): conta le occorrenze di stringhe candidate nel
|
||||
testo binario, validata a mano confrontando l'output con l'albero reale mostrato da Creo.
|
||||
Riassume cosa si è osservato e i suoi limiti, in particolare i falsi positivi che
|
||||
un conteggio testuale non può escludere con certezza (riferimenti fantasma, componenti
|
||||
soppressi con placement residuo — vedi sotto).
|
||||
documentato pubblicamente. Questo documento descrive l'euristica testuale di
|
||||
`AssemblyComponentScanner.cs`: conta le occorrenze di stringhe candidate nel testo binario,
|
||||
validata a mano confrontando l'output con l'albero reale mostrato da Creo. Riassume cosa si
|
||||
è osservato e i suoi limiti, in particolare i falsi positivi che un conteggio testuale non
|
||||
può escludere con certezza (riferimenti fantasma, componenti soppressi con placement
|
||||
residuo — vedi sotto).
|
||||
|
||||
Un reverse engineering successivo ha invece decodificato la **struttura a record** del
|
||||
file (non solo pattern testuali): legge esattamente la sezione `members` di un assieme,
|
||||
senza contare occorrenze né rischiare i falsi positivi descritti in questo documento.
|
||||
Implementato in [`scripts/CreoBom.cs`](../scripts/CreoBom.cs), documentato in
|
||||
[`formato-asm-record.md`](formato-asm-record.md). Resta però uno script di analisi
|
||||
indipendente: legge solo `.asm` locali dato un percorso di ricerca esplicito (non
|
||||
`cad_index.db`), e non è integrato in `AssemblyComponentScanner`/nella GUI, che
|
||||
continuano a usare l'euristica descritta qui.
|
||||
Un reverse engineering successivo ha decodificato la **struttura a record** del file (non
|
||||
solo pattern testuali): legge esattamente la sezione `members` di un assieme, senza contare
|
||||
occorrenze né rischiare i falsi positivi descritti in questo documento. Implementato in
|
||||
`PrepStep/AsmRecordReader.cs`, documentato in [`formato-asm-record.md`](formato-asm-record.md).
|
||||
È il metodo **preferito** in GUI (fase "Trova componenti referenziati"/"Verifica
|
||||
completezza", `Form1.EstraiCodiciReferenziati`) e nello script `CreoBom.cs`:
|
||||
`AssemblyComponentScanner` (l'euristica di questo documento) resta come **fallback**, usato
|
||||
solo quando `AsmRecordReader` lancia `InvalidDataException` (file che non rispetta il
|
||||
formato validato dal reverse engineering — copertura limitata agli 8 campioni disponibili al
|
||||
momento), e come fonte di verità per gli script `ExtractCreoStrings.cs`/`BomViaDb.cs`, che
|
||||
risolvono l'albero contro `cad_index.db` invece che tramite un percorso di ricerca locale.
|
||||
|
||||
## Struttura osservata
|
||||
|
||||
|
||||
+23
-113
@@ -1,18 +1,17 @@
|
||||
#:project ../PrepStep/PrepStep.csproj
|
||||
#:property TargetFramework=net10.0-windows
|
||||
// dotnet run scripts/CreoBom.cs -- <file.asm.rev> [-p cartella]... [-o out.bom.1]
|
||||
//
|
||||
// Parser esatto del formato binario .asm di Creo, ricavato per reverse engineering (vedi
|
||||
// docs/formato-asm-record.md per la spiegazione byte-per-byte dei record, verificata
|
||||
// confrontando l'output con le BOM esportate da Creo). Legge la sezione "members" di un
|
||||
// assieme (id/tipo/nome dei componenti diretti, con risoluzione del nome istanza di family
|
||||
// table dal parametro "ConfigName") e, dato un elenco di cartelle dove cercare i
|
||||
// sottoassiemi, genera la stessa distinta base (BOM) esportata da Creo.
|
||||
//
|
||||
// Alternativa esatta all'euristica testuale di PrepStep/AssemblyComponentScanner.cs (docs/
|
||||
// formato-asm.md): quella conta le occorrenze dei codici nel testo, questa legge la
|
||||
// struttura dei record. Per ora resta uno script di analisi indipendente: non sostituisce
|
||||
// AssemblyComponentScanner nella GUI (fase 4) ne' negli altri script.
|
||||
// Genera la distinta base (BOM) di un assieme Creo, dato un elenco di cartelle dove
|
||||
// cercare i sottoassiemi (-p). La lettura del formato binario (sezione "members" +
|
||||
// parametro "ConfigName", vedi docs/formato-asm-record.md) vive in
|
||||
// PrepStep.AsmRecordReader, via "#:project" - stessa fonte usata da Form1 nella GUI (fase
|
||||
// 4/5, con fallback automatico all'euristica di AssemblyComponentScanner.cs per i file che
|
||||
// non rispettano il formato validato): qui solo la ricorsione sui sottoassiemi, il
|
||||
// riepilogo delle quantita' e la resa nel formato esatto della BOM Creo.
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using PrepStep;
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
@@ -45,106 +44,17 @@ else
|
||||
}
|
||||
return 0;
|
||||
|
||||
/// <summary>Lettura del formato .asm Creo (sezione "members" + parametro "ConfigName") e resa
|
||||
/// nel formato della BOM esportata da Creo. Vedi docs/formato-asm-record.md per il dettaglio
|
||||
/// dei record e i limiti noti (stato soppresso non gestito, matrice di posizionamento non
|
||||
/// decodificata, deduplica dei blocchi non dimostrata su assiemi che montano lo stesso
|
||||
/// sottoassieme due volte).</summary>
|
||||
/// <summary>Ricorsione sui sottoassiemi e resa nel formato della BOM esportata da Creo. La
|
||||
/// lettura del formato binario vive in <see cref="AsmRecordReader"/> (PrepStep/, condivisa
|
||||
/// con la GUI). Vedi docs/formato-asm-record.md per il dettaglio dei record e i limiti noti
|
||||
/// (stato soppresso non gestito, matrice di posizionamento non decodificata, deduplica dei
|
||||
/// blocchi non dimostrata su assiemi che montano lo stesso sottoassieme due volte).</summary>
|
||||
public static class CreoAsm
|
||||
{
|
||||
public sealed record Membro(int Id, int Tipo, string Nome, string File);
|
||||
|
||||
// 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 readonly Dictionary<int, string> TipoNome = new() { [1] = "Sottoassieme", [2] = "Parte" };
|
||||
|
||||
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), "File" e' il generico
|
||||
/// realmente su disco (coincide con "Nome" per i componenti che non sono istanze).</summary>
|
||||
public static List<Membro> LeggiMembri(byte[] dati)
|
||||
{
|
||||
string testo = Encoding.Latin1.GetString(dati);
|
||||
|
||||
var istanze = new Dictionary<int, string>();
|
||||
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 <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");
|
||||
|
||||
return grezzi.Select(g => new Membro(g.Id, g.Tipo, istanze.GetValueOrDefault(g.Id, g.Nome), g.Nome)).ToList();
|
||||
}
|
||||
|
||||
/// <summary>Voci distinte con quantita', nell'ordine di prima comparsa (come la BOM).</summary>
|
||||
public static List<((int Tipo, string Nome) Chiave, int Qta)> Rollup(IEnumerable<Membro> membri)
|
||||
public static List<((int Tipo, string Nome) Chiave, int Qta)> Rollup(IEnumerable<AsmRecordReader.Membro> membri)
|
||||
{
|
||||
var ordine = new List<(int, string)>();
|
||||
var qty = new Dictionary<(int, string), int>();
|
||||
@@ -163,7 +73,7 @@ public static class CreoAsm
|
||||
/// <summary>nome modello -> membri, cercando <nome>.asm.<rev> nelle cartelle
|
||||
/// date. A parita' di nome vince la revisione piu' alta presente. Nome null se il file
|
||||
/// non c'e' in nessuna delle cartelle.</summary>
|
||||
private static Func<string, List<Membro>?> CreaFinder(IEnumerable<string> cartelle)
|
||||
private static Func<string, List<AsmRecordReader.Membro>?> CreaFinder(IEnumerable<string> cartelle)
|
||||
{
|
||||
var indice = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var revisioni = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -184,12 +94,12 @@ public static class CreoAsm
|
||||
}
|
||||
}
|
||||
|
||||
var cache = new Dictionary<string, List<Membro>?>();
|
||||
var cache = new Dictionary<string, List<AsmRecordReader.Membro>?>();
|
||||
return nome =>
|
||||
{
|
||||
if (!cache.TryGetValue(nome, out var mem))
|
||||
{
|
||||
mem = indice.TryGetValue(nome, out var percorso) ? LeggiMembri(File.ReadAllBytes(percorso)) : null;
|
||||
mem = indice.TryGetValue(nome, out var percorso) ? AsmRecordReader.LeggiMembri(File.ReadAllBytes(percorso)) : null;
|
||||
cache[nome] = mem;
|
||||
}
|
||||
return mem;
|
||||
@@ -202,7 +112,7 @@ public static class CreoAsm
|
||||
|
||||
/// <summary>Blocchi in ordine depth-first pre-order. Ogni modello compare una volta sola
|
||||
/// (deduplica per nome) anche se montato piu' volte.</summary>
|
||||
private static List<Blocco> Blocchi(string root, Func<string, List<Membro>?> get)
|
||||
private static List<Blocco> Blocchi(string root, Func<string, List<AsmRecordReader.Membro>?> get)
|
||||
{
|
||||
var risultato = new List<Blocco>();
|
||||
var visti = new HashSet<string>();
|
||||
@@ -231,7 +141,7 @@ public static class CreoAsm
|
||||
/// <summary>Totale per parte, moltiplicato lungo l'albero, in ordine di prima comparsa: una
|
||||
/// parte che compare a piu' livelli occupa la posizione della sua prima comparsa e somma
|
||||
/// le quantita'.</summary>
|
||||
private static void Sommario(string root, Func<string, List<Membro>?> get, int mult,
|
||||
private static void Sommario(string root, Func<string, List<AsmRecordReader.Membro>?> get, int mult,
|
||||
List<string> ordine, Dictionary<string, int> acc)
|
||||
{
|
||||
var mem = get(root);
|
||||
@@ -258,7 +168,7 @@ public static class CreoAsm
|
||||
|
||||
/// <summary>Testo della BOM, identico byte per byte a quello esportato da Creo (UTF-8 con
|
||||
/// BOM, CRLF - vedi GeneraBom per la scrittura su file).</summary>
|
||||
private static string Render(string root, Func<string, List<Membro>?> get)
|
||||
private static string Render(string root, Func<string, List<AsmRecordReader.Membro>?> get)
|
||||
{
|
||||
var pezzi = new List<string>();
|
||||
foreach (var blocco in Blocchi(root, get))
|
||||
@@ -286,6 +196,6 @@ public static class CreoAsm
|
||||
public static string GeneraBom(string percorsoAsm, IEnumerable<string> cartelle)
|
||||
{
|
||||
byte[] dati = File.ReadAllBytes(percorsoAsm);
|
||||
return Render(NomeProprio(dati), CreaFinder(cartelle));
|
||||
return Render(AsmRecordReader.NomeProprio(dati), CreaFinder(cartelle));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user