Files
pdm/creoparams/params.py
T

196 lines
6.4 KiB
Python
Raw Normal View History

"""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 <NOME> 00 <slot> 88 00 e3 <tipo> <valore> ...
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