"""Estrazione della tabella parametri dal corpo di un file nativo Creo. I parametri sono memorizzati in chiaro, con una struttura regolare che si ripete per ogni voce: f6 e3 00 88 00 e3 ... dove `tipo` e' 0x33 ('3') per le stringhe e 0x32 ('2') per i numerici. Ogni parametro compare due volte di seguito (valore corrente e valore di riferimento) e l'intera tabella e' duplicata in due sezioni del file (tipicamente `LargeText` e `NeuPrtSld`). Usiamo questa ridondanza per assegnare un livello di confidenza a ogni valore. I valori stringa sono leggibili direttamente. I numerici usano una codifica binaria a lunghezza variabile (7-8 byte osservati) che non e' un double IEEE lineare e che qui non viene decodificata: ne conserviamo i byte grezzi. """ from __future__ import annotations import re from dataclasses import dataclass REC_START = b"\xf6\xe3" VALUE_LEAD = b"\x88\x00\xe3" NUM_LEAD = b"\xe3\x32" KIND_STRING = 0x33 KIND_NUMERIC = 0x32 #: Marcatore di espressione: il valore non e' costante ma prodotto da una #: relazione del modello (es. PESO guidato da "value(d_val)"). EXPR_LEAD = b"\xe0\x02" MAX_STRING = 256 MAX_NUMERIC_SPAN = 48 # Byte ammessi in un valore stringa: ASCII stampabile piu' l'intervallo alto # per le lettere accentate, esclusi i byte usati come token dal formato. _TOKENS = {0xE1, 0xE3, 0xF6, 0xF7} _STRING_BYTES = frozenset( set(range(0x20, 0x7F)) | (set(range(0xA0, 0x100)) - _TOKENS) ) _NAME = rb"([A-Za-z_][A-Za-z0-9_.\-]{0,63})\x00" #: Forma estesa, nella tabella parametri principale (sezione LargeText). _RECORD_RE = re.compile( REC_START + _NAME + rb"(.)" # byte di slot/identificativo + re.escape(VALUE_LEAD) + rb"([\x32\x33])", # tipo del valore re.DOTALL, ) #: Forma compatta, nella rappresentazione neutra (sezione NeuPrtSld). Ripete #: gli stessi parametri e ci serve come verifica incrociata. Il prefisso che #: separa i record varia a seconda dei campi accessori presenti (ref_id, #: unit, designated, ...), quindi ci ancoriamo solo al nome seguito dal tipo. _NEUTRAL_RE = re.compile( rb"\xe3" + _NAME + rb"\xe2([\x32\x33])", re.DOTALL, ) @dataclass class Occurrence: """Una singola comparsa di un parametro nel file.""" name: str kind: str # "string" | "real" value: str | None # None se non decodificato raw: bytes | None # byte grezzi, per i numerici offset: int section: str = "" expression: str | None = None # relazione che pilota il valore form: str = "extended" # "extended" (tabella modello) | "neutral" @property def decoded(self) -> bool: return self.value is not None def _decode_text(raw: bytes) -> str: """I valori sono scritti in UTF-8; i file piu' vecchi possono usare la codepage Windows.""" try: return raw.decode("utf-8") except UnicodeDecodeError: return raw.decode("cp1252", errors="replace") def _read_string(data: bytes, pos: int) -> tuple[str, int] | None: """Legge un valore stringa terminato da NUL. Ritorna (valore, fine).""" # Un token f7 xx puo' precedere il valore (flag di stato del parametro). if data[pos : pos + 1] == b"\xf7": pos += 2 end = pos limit = min(len(data), pos + MAX_STRING) while end < limit and data[end] != 0x00: if data[end] not in _STRING_BYTES: return None end += 1 if end >= limit: return None # nessun terminatore: non e' un record valido return _decode_text(data[pos:end]), end + 1 def _read_numeric(data: bytes, pos: int) -> tuple[bytes, int] | None: """Isola i byte grezzi di un valore numerico. Sfrutta il fatto che il valore e' immediatamente ripetuto dopo un secondo marcatore `e3 32`: proviamo ogni posizione candidata e teniamo la prima in cui la ripetizione combacia davvero. """ start = pos # Un token f7 xx puo' precedere il valore. if data[start : start + 1] == b"\xf7": start += 2 search_end = min(len(data), start + MAX_NUMERIC_SPAN) cursor = start while True: repeat = data.find(NUM_LEAD, cursor, search_end) if repeat < 0: return None candidate = data[start:repeat] # Un token di separazione puo' chiudere il primo valore. if candidate[-2:-1] == b"\xf7": candidate = candidate[:-2] after = repeat + len(NUM_LEAD) if candidate and data[after : after + len(candidate)] == candidate: return candidate, after + len(candidate) cursor = repeat + 1 def _read_expression(data: bytes, pos: int) -> tuple[str | None, int]: """Se il valore e' pilotato da una relazione, ne legge il testo.""" if data[pos : pos + len(EXPR_LEAD)] != EXPR_LEAD: return None, pos parsed = _read_string(data, pos + len(EXPR_LEAD)) if parsed is None: return None, pos return parsed def iter_occurrences(data: bytes) -> "list[Occurrence]": """Scorre il file e restituisce ogni comparsa di parametro trovata. Vengono percorse entrambe le rappresentazioni presenti nel file: la tabella parametri estesa e la copia neutra. La ridondanza serve poi ad assegnare la confidenza in `extract`. """ found: list[Occurrence] = [] for regex, kind_group, form in ( (_RECORD_RE, 3, "extended"), (_NEUTRAL_RE, 2, "neutral"), ): for match in regex.finditer(data): name = match.group(1).decode("ascii") kind_byte = match.group(kind_group)[0] pos = match.end() if kind_byte == KIND_STRING: parsed = _read_string(data, pos) if parsed is None: continue found.append( Occurrence( name, "string", parsed[0], None, match.start(), form=form ) ) else: expression, pos = _read_expression(data, pos) parsed = _read_numeric(data, pos) found.append( Occurrence( name, "real", None, parsed[0] if parsed else None, match.start(), expression=expression, form=form, ) ) found.sort(key=lambda o: o.offset) return found