diff --git a/PrepStep.Tests/AsmRecordReaderTests.cs b/PrepStep.Tests/AsmRecordReaderTests.cs index 9579063..53305ab 100644 --- a/PrepStep.Tests/AsmRecordReaderTests.cs +++ b/PrepStep.Tests/AsmRecordReaderTests.cs @@ -111,6 +111,39 @@ public class AsmRecordReaderTests Assert.Throws(() => AsmRecordReader.LeggiMembri(dati)); } + [Fact] + public void Assieme_family_table_con_generic_name_lancia_eccezione() + { + // Regressione: 2546220101.asm (docs/formato-asm-record.md §7) e' esso stesso + // un'istanza di family table (model_name "2546220101_C01" + generic_name vuoto); il + // suo "members" descrive solo questa rappresentazione, non le 4 istanze reali della + // BOM Creo. Va segnalato invece di restituire silenziosamente il contenuto sbagliato. + byte[] dati = Asm(1, Membro(1, 2, "9244206098"), + extra: Bytes(0xe0, 0x01, "model_t_type", 0, 2, + 0xe0, 0x0a, "model_name", 0, "2546220101_C01", 0, + 0xe0, 0x0a, "generic_name", 0, 0)); + + Assert.Throws(() => AsmRecordReader.LeggiMembri(dati)); + } + + [Fact] + public void Generic_name_senza_suffisso_istanza_non_lancia_eccezione() + { + // Regressione: su ~7% dei file reali della share compare un record "model_name" + + // "generic_name" (vuoto) che NON e' il sintomo della family table di assiemi - il + // valore non ha il suffisso "_C0n" delle istanze (es. e' un codice qualunque non + // correlato al nome del file). In quei casi "members" e' letto correttamente e va + // usato normalmente, non bloccato per un falso positivo. + byte[] dati = Asm(1, Membro(1, 2, "9244206098"), + extra: Bytes(0xe0, 0x01, "model_t_type", 0, 2, + 0xe0, 0x0a, "model_name", 0, "9244230117", 0, + 0xe0, 0x0a, "generic_name", 0, 0)); + + var membri = AsmRecordReader.LeggiMembri(dati); + + Assert.Single(membri); + } + [Fact] public void Sezione_members_assente_lancia_eccezione() { diff --git a/PrepStep/AsmRecordReader.cs b/PrepStep/AsmRecordReader.cs index ccbe67e..b52ce6c 100644 --- a/PrepStep/AsmRecordReader.cs +++ b/PrepStep/AsmRecordReader.cs @@ -49,12 +49,38 @@ public static class AsmRecordReader // normalizzazione ogni istanza del pattern risulterebbe un componente diverso. private static readonly Regex SuffissoArrayFeatureRegex = new(@"#AF\d+$", RegexOptions.Compiled); + // 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); + + // sintomo vero e proprio (docs/formato-asm-record.md §7): il file e' esso stesso + // un'istanza di family table (il suo "model_name" ha il suffisso "_C0n" delle istanze, + // stessa convenzione di SuffissoArrayFeatureRegex/ConfigName sopra) - "members" descrive + // solo questa rappresentazione, non le istanze sorelle elencate in "stamp". Verificato su + // 2546220101.asm: la BOM Creo reale mostra 4 istanze diverse, "members" ne restituisce + // silenziosamente solo una. + private static readonly Regex SuffissoIstanzaFamilyTableRegex = new(@"_C\d{2,}$", 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 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; + } + private static int AsInt(string bytes) { int mask = bytes.Length switch { 1 => 0x7F, 2 => 0x3F, 3 => 0x1F, _ => throw new ArgumentException("lunghezza intero non valida") }; @@ -84,14 +110,33 @@ public static class AsmRecordReader { string testo = Encoding.Latin1.GetString(dati); + var identita = OwnModelIdentityRegex.Match(testo); + if (identita.Success && SuffissoIstanzaFamilyTableRegex.IsMatch(identita.Groups[2].Value)) + 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"); + + 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"); + var istanze = new Dictionary(); - foreach (Match m in ConfigNameRegex.Matches(testo)) + foreach (Match m in configNameMatches) istanze[AsInt(m.Groups[2].Value)] = SuffissoArrayFeatureRegex.Replace(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"); + 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"); start += chiaveMembers.Length; int conteggio = dati[start + 2]; // \xf1 \xf8 diff --git a/docs/formato-asm-record.md b/docs/formato-asm-record.md index 324005e..be53fac 100644 --- a/docs/formato-asm-record.md +++ b/docs/formato-asm-record.md @@ -13,11 +13,23 @@ referenziati"/"Verifica completezza" - con fallback su `AssemblyComponentScanner 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 -istanza, stesse quantità, stesso ordine. Altri 5 `.asm`, mai usati per ricavare le regole, -parsavano con il conteggio dichiarato uguale a quello trovato. Tutto ciò che segue oltre -questo perimetro (§7) è segnalato esplicitamente come ipotesi non dimostrata. +**Stato della validazione.** Due livelli di prova, da non confondere: + +1. **Byte-per-byte contro BOM Creo di riferimento** (la prova forte): 3 assiemi del + reverse engineering iniziale, più 5 casi reali aggiunti in seguito, ciascuno con la sua BOM + esportata da Creo messa a confronto riga per riga con l'output di `CreoBom.cs` — + `9284218024b.asm` (albero profondo, multi-livello), `2546220101.asm`/`2546220101_c04.asm.1` + (istanza di family table isolata), `2546220101.asm` risalvato con un componente soppresso, + `9244218080_c01.asm` (sottoassieme montato due volte), `9284208092_D110.asm` (revisione del + figlio). Ognuno di questi è citato per nome nella sezione del documento a cui si riferisce, + così resta verificabile quale affermazione poggia su quale file. +2. **Su tutti i 350 `.asm` reali di `raw/`**, solo conteggi interni e coerenza con l'euristica + testuale (`AssemblyComponentScanner`), **non** contro BOM di riferimento: usato per + calibrare le soglie dei controlli di sicurezza (§7) su scala, non per dimostrare la + correttezza dei valori estratti file per file. + +Un'affermazione verificata solo al livello 2 (conteggi su scala) è più debole di una +verificata al livello 1 (BOM reale) - il testo lo distingue sempre esplicitamente. --- @@ -271,9 +283,12 @@ subito dopo, ricorsivamente, quelli dei suoi sottoassiemi, prima di passare al fratello successivo. Ogni modello compare **una volta sola** anche se montato più volte (deduplica per nome, `CreoAsm.Blocchi`). -> La deduplica è un'**assunzione**: nessuno dei campioni disponibili durante il -> reverse engineering montava lo stesso sottoassieme due volte, quindi non è -> dimostrata su un caso reale. +> **Verificato.** `9244218080_C01.asm` monta `2605410010_C01` due volte +> (`scripts/9244218080_c01.asm.1` + `scripts/9244218080_c01.bom.2` di riferimento): la BOM +> Creo mostra un solo blocco `Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa, +> non due blocchi separati, e `CreoBom.cs` riproduce l'output esatto senza modifiche. Confermata +> anche la moltiplicazione delle quantità nel riepilogo (§6.3): le parti di `2605410010_C01` +> compaiono raddoppiate (1→2, 1→2, 16→32). ### 6.3 Riepilogo finale @@ -291,13 +306,17 @@ dall'esterno (`-p`/`--path`). `CreoAsm.CreaFinder` indicizza `.asm.` senza distinzione di maiuscole e, a parità di nome, prende la **revisione più alta** presente. -> Scelta discutibile: la revisione più alta in cartella non è -> necessariamente quella con cui l'assieme padre è stato salvato. Il `.asm` -> padre non sembra registrare la revisione attesa del figlio, o non è stata -> trovata. Vedi `formato-asm.md` §"Risoluzione ricorsiva dell'albero" per le -> trappole già incontrate storicamente su questo punto (con l'euristica -> testuale, ma il problema della revisione attesa è indipendente dal metodo -> di estrazione). +> **Verificato: è la scelta giusta.** Il `.asm` padre non registra la revisione attesa del +> figlio (cercata e non trovata, vedi §7), ma non serve: confermato che **Creo stesso apre +> sempre la revisione più alta disponibile** di un componente, indipendentemente da quale +> fosse presente quando l'assieme padre fu salvato l'ultima volta. Caso reale controllato: +> `9284208092_D110.asm` (salvato 2018-08-01) referenzia `9284202481.prt`, che sulla share ha +> revisioni sia del 2014 (v1-v3, presenti all'epoca) sia del 2024 (v4-v6, arrivate anni dopo) - +> Creo carica v6, non v3. `CreaFinder` che prende sempre la revisione più alta in cartella +> replica esattamente questo comportamento, non è un'assunzione azzardata. Vedi `formato- +> asm.md` §"Risoluzione ricorsiva dell'albero" per le trappole storiche su questo punto (con +> l'euristica testuale - restano valide, sono su un problema diverso: trovare la cartella +> giusta, non quale revisione preferire). Se un sottoassieme non si trova, il blocco viene emesso con `*** NON RISOLTO: file .asm non trovato ***` e le sue parti restano fuori dal @@ -306,44 +325,110 @@ sbagliato. --- -## 7. Cosa resta sconosciuto +## 7. Stato di ogni punto aperto -Elencato esplicitamente per non far passare per dimostrato ciò che non lo è: +Tabella di riferimento: cosa è provato, come, e cosa resta genuinamente ignoto. Il dettaglio +di ciascun punto segue sotto, in due gruppi. -- **⚠️ Family table di ASSIEMI (non solo di parti), risultato silenziosamente sbagliato.** - Non solo le parti normalizzate (§4) possono essere family table: un intero **assieme** può - esserlo. Osservato su `2546220101.asm` (dentro l'albero di `9284218024b.asm`): il file - generico ha una propria sezione `members` che elenca il contenuto di UNA rappresentazione - (nell'esempio, 3 cuscinetti + 1 sotto-assieme), ma la BOM Creo per lo stesso file mostra 4 - **istanze diverse** (`2546220101_C01`.."_C04", tre "Parte" e una "Sottoassieme" - tipi - diversi fra loro!), ciascuna con un proprio contenuto potenzialmente diverso. Il nome - istanza qui **non** passa da un record `ConfigName` (verificato: nessuna occorrenza - letterale di `ConfigName` nel file) ma da un meccanismo diverso, imparentato con la - "seconda pista" del §4.1 (`\xe3\xe2\x02\0`, senza il bracket - `` in questo caso) - non decodificato. **`AsmRecordReader.LeggiMembri` non lo - rileva e non lancia eccezione**: restituisce silenziosamente il contenuto della - rappresentazione di default sotto un solo nome, invece delle 4 istanze corrette - a - differenza degli altri limiti di questa sezione, qui il fallimento non è segnalato in - nessun modo al chiamante (né un conteggio che non torna, né un'eccezione). Scoperto sullo - stesso test end-to-end del punto `#AF` sopra, mai osservato negli 8 campioni originali. - **Prima di fidarsi del parser esatto su un albero con assiemi a family table, verificare a - mano contro la BOM Creo.** -- **Stato soppresso / escluso.** Nessuna evidenza cercata o trovata. Non si sa - se un componente soppresso compaia in `members`, né con quale marcatore. Sui - campioni disponibili il totale coincideva con la BOM, il che *suggerisce* che - non ce ne fossero, non che siano gestiti. -- **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato - numerico non decodificato. -- **`family_name`**: sempre uguale a `name` sui campioni; non si sa in quale - situazione divergerebbe. -- **La costante `\x82\x7b`** nei record `ConfigName`. -- **I campi numerici dell'header** dopo `ASSEMBLY`. -- **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. -- **La deduplica dei blocchi** e **la revisione attesa dei figli** (§6.2, §6.4). -- **Quale revisione del figlio** il padre si aspetta: non trovata nel `.asm`. +| Punto | Stato | Prova | +|---|---|---| +| Deduplica blocchi + moltiplicazione quantità (§6.2/§6.3) | Risolto | BOM reale (§7.1) | +| Revisione del figlio che il padre si aspetta (§6.4) | Risolto | BOM reale (§7.1) | +| Stato soppresso/escluso in `members` | Risolto | BOM reale (§7.1) | +| Family table di **assiemi** (rilevamento) | Risolto | BOM reale + scala 350 file (§7.1) | +| Family table di **assiemi** (contenuto di ogni istanza) | Non risolto | — (§7.2) | +| Costante `\x82\x7b` in `ConfigName` | Significato ignoto, rischio coperto | Scala 350 file (§7.2) | +| Occorrenze multiple di `members` (simplified rep) | Mai osservato, rischio coperto | Scala 350 file (§7.2) | +| Matrice di posizionamento (`e1`/`e2`/`e3`/`origin`) | Non letto, nessun impatto sulla BOM | — (§7.2) | +| Campi numerici dell'header dopo `ASSEMBLY` | Non letto, nessun impatto sulla BOM | — (§7.2) | +| Divergenza `family_name` ≠ `name` | Mai osservata, non letta | — (§7.2) | + +"Rischio coperto" vuol dire: il codice non sa cosa significhi il byte, ma verifica un +invariante che lo renderebbe visibile (eccezione) se mai smettesse di valere - non è una prova +che il byte non conti mai, è la garanzia che se contasse il programma lo segnalerebbe invece +di sbagliare in silenzio. + +### 7.1 Risolti con una BOM Creo di riferimento + +- **Deduplica blocchi e moltiplicazione quantità.** `9244218080_C01.asm` monta + `2605410010_C01` due volte (`scripts/9244218080_c01.asm.1` + + `scripts/9244218080_c01.bom.2`): la BOM Creo mostra un solo blocco + `Sottoassieme 2605410010_C01 contiene:` con quantità 2 in testa (non due blocchi separati), + e le sue parti compaiono raddoppiate nel riepilogo (1→2, 1→2, 16→32). `CreoBom.cs` riproduce + l'output esatto senza modifiche: la deduplica per nome (`CreoAsm.Blocchi`) e la + moltiplicazione lungo l'albero (`CreoAsm.Sommario`) sono il comportamento reale di Creo, non + solo un'assunzione. +- **Revisione del figlio.** `9284208092_D110.asm` (salvato 2018-08-01) referenzia + `9284202481.prt`, che sulla share ha revisioni sia del 2014 (v1-v3, presenti quando il padre + fu salvato) sia del 2024 (v4-v6, arrivate anni dopo). Confermato direttamente in Creo: apre + v6, non v3 - **sempre la revisione più alta disponibile**, indipendentemente da quale fosse + presente all'ultimo salvataggio del padre. `CreoAsm.CreaFinder`, che prende sempre la + revisione più alta trovata in cartella, replica esattamente questo comportamento. +- **Stato soppresso/escluso.** Stesso assieme `2546220101.asm` risalvato da Creo con + `2546220101_C02` soppresso (`scripts/2546220101.asm.1` + `scripts/2546220101.bom.1`, con le + parti/sottoassiemi correlati). Un componente soppresso **non produce alcun record** in + `members`: sparisce e basta, il conteggio dichiarato scende di conseguenza (`\xf8 03` invece + di `\xf8 04`). Nessun marcatore di stato da gestire: `CreoBom.cs` sul file risalvato + riproduce esattamente la BOM di riferimento senza modifiche al parser. +- **Family table di assiemi — rilevamento.** Non solo le parti normalizzate (§4) possono + essere family table: un intero **assieme** può esserlo. Osservato su `2546220101.asm` + (dentro l'albero di `9284218024b.asm`): il file generico ha una propria sezione `members` + che elenca il contenuto di UNA rappresentazione (3 cuscinetti + 1 sotto-assieme, dati + arbitrari), ma la BOM Creo per lo stesso file mostra 4 **istanze diverse** + (`2546220101_C01`.."_C04", tre "Parte" e una "Sottoassieme" - tipi diversi fra loro), + ciascuna con un contenuto diverso. Il nome istanza qui **non** passa da `ConfigName` + (verificato: nessuna occorrenza letterale nel file), ma da una coppia di record distinta + subito dopo l'header del modello: + + ``` + \xe0\x01model_t_type\0\xe0\x0amodel_name\0_C0n\0\xe0\x0ageneric_name\0\0 + ...stamp\0\xe2\xf2\xf7 + (\xe3\x02_C0n\0\0\xe2)* -- un'occorrenza per ogni istanza sorella + \xf4 + ``` + + Il file generico è già di per sé un'istanza (`model_name` porta il suffisso `_C0n`, non è + neutro), e `stamp` elenca le sorelle. **Segnale affidabile**: la combinazione + `model_t_type`+`model_name` (con suffisso `_C0n`)+`generic_name` vuoto + (`AsmRecordReader.OwnModelIdentityRegex` + `SuffissoIstanzaFamilyTableRegex`) - **non** la + sola presenza di `generic_name`, che su altri 119 file del corpus da 350 compare in un + contesto diverso e non correlato (`model_name` lì è un codice slegato dal nome del file, e + `members` in quei casi è già corretto). Verificato: solo 25/350 file rispettano il segnale + affidabile, e su quei 25 `AsmRecordReader.LeggiMembri` lancia `InvalidDataException` invece + di restituire il contenuto sbagliato della rappresentazione di default. + + Verificato anche che il fallback su `AssemblyComponentScanner` **risolve già correttamente** + il caso vero: i nomi delle istanze compaiono come testo puro nel campo `stamp`, e + l'euristica ("codice che compare almeno due volte") li trova senza bisogno di decodificare + `stamp`. E che una singola istanza, se esportata da Creo come file a sé (Family Table → + istanza → "Salva una copia"), è un `.asm`/`.prt` normale senza traccia di family table al suo + interno - letto correttamente dal parser esatto (`2546220101_c04.asm.1`, verificato contro + `2546220101.bom.1`). + +### 7.2 Non risolti + +- **Family table di assiemi — contenuto di ogni istanza.** Come recuperare il contenuto + specifico di un'istanza **senza** che esista già un file separato per essa sul disco (il + caso comune, se nessuno l'ha mai esportata a parte con "Salva una copia"): il meccanismo + dietro `stamp` non è stato decodificato oltre a nome+tipo di ciascuna istanza sorella. + Aperto anche cosa sia davvero il record `generic_name` sui 119 file dove non è questo + sintomo. +- **Costante `\x82\x7b`** nei record `ConfigName`: significato ignoto. Rischio coperto - + `LeggiMembri` conta le occorrenze grezze di `ConfigName` e le confronta col numero di match + del regex; se un campione avesse un valore diverso, il regex non farebbe più match (prima: + in silenzio) e ora lancia eccezione invece di restituire il nome generico al posto + dell'istanza senza dirlo. Verificato su tutti i 350 `.asm` di `raw/`: 215 usano davvero + `ConfigName`, 0 mismatch. +- **Occorrenze multiple di `members`** (rappresentazioni semplificate, mai osservate). Rischio + coperto - `LeggiMembri` lancia eccezione se ne trova più di una invece di prendere + silenziosamente la prima senza sapere se sia quella giusta. Verificato su tutti i 350 `.asm` + di `raw/`: sempre esattamente una occorrenza. +- **La matrice di posizionamento** (`e1`, `e2`, `e3`, `origin`): formato numerico non + decodificato, nessun impatto sulla BOM (non letta da nessuna parte del codice). +- **I campi numerici dell'header** dopo `ASSEMBLY`: non letti, nessun impatto sulla BOM. +- **`family_name`**: sempre uguale a `name` su tutti i campioni disponibili; non si sa in + quale situazione divergerebbe, né se il codice dovrebbe fare qualcosa se succedesse (oggi + non la usa in nessun modo, legge solo `name`). ## 8. Uso