style: increase ReadoutPanel font sizes and add CSV test fixture for data logging
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"""C#-compatible wafer license file parsing.
|
||||
|
||||
License files are AES-128-CBC encrypted `.bin` blobs named
|
||||
``X00000-00.bin`` where ``X`` is the wafer family code, ``00000`` the
|
||||
5-digit serial number, and ``00`` the 2-digit license level. The blob is
|
||||
``ciphertext || iv`` (IV is the trailing 16 bytes). The decrypted text is
|
||||
a fixed-offset record::
|
||||
|
||||
[0:6] serial e.g. "A00001"
|
||||
[7:15] mfg date 8 hex chars
|
||||
[16:18] level 2 decimal digits ("00" basic, "01"+ adds replay)
|
||||
[19:27] license date 8 hex chars
|
||||
|
||||
The key is hardcoded to match the C# application exactly
|
||||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from pygui.backend.crypto.crypto_helper import CryptoHelper
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
LICENSE_KEY = b"ThisIsA16ByteKey"
|
||||
LICENSE_FILENAME_RE = re.compile(r"^[A-Z]\d{5}-\d{2}\.bin$")
|
||||
|
||||
_MIN_TEXT_LEN = 27
|
||||
|
||||
|
||||
# ===== License Record =====
|
||||
@dataclass(frozen=True)
|
||||
class License:
|
||||
"""One parsed license file (all fields as stored in the file)."""
|
||||
|
||||
serial: str # "A00001"
|
||||
level: str # "00".."03"
|
||||
mfg_date_hex: str # 8 hex chars
|
||||
lic_date_hex: str # 8 hex chars
|
||||
|
||||
@property
|
||||
def level_int(self) -> int:
|
||||
"""Level as int; -1 if unparseable."""
|
||||
try:
|
||||
return int(self.level, 10)
|
||||
except ValueError:
|
||||
return -1
|
||||
|
||||
@property
|
||||
def mfg_date_decimal(self) -> str:
|
||||
"""Mfg date shown as decimal (C# grid does Convert.ToInt64(hex, 16))."""
|
||||
return _hex_to_decimal(self.mfg_date_hex)
|
||||
|
||||
@property
|
||||
def lic_date_decimal(self) -> str:
|
||||
"""License date shown as decimal, same conversion as the C# grid."""
|
||||
return _hex_to_decimal(self.lic_date_hex)
|
||||
|
||||
|
||||
def _hex_to_decimal(hex_str: str) -> str:
|
||||
try:
|
||||
return str(int(hex_str, 16))
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
|
||||
# ===== Blob Parsing =====
|
||||
def split_data_iv(blob: bytes) -> tuple[bytes, bytes]:
|
||||
"""Split a license blob into (ciphertext, iv) — IV is the last 16 bytes."""
|
||||
if len(blob) < 32:
|
||||
raise ValueError("License blob too short")
|
||||
return blob[:-16], blob[-16:]
|
||||
|
||||
|
||||
def parse_license_blob(blob: bytes) -> License | None:
|
||||
"""Decrypt and parse one license blob. None if malformed."""
|
||||
try:
|
||||
data, iv = split_data_iv(blob)
|
||||
message = CryptoHelper.decrypt_aes(data, LICENSE_KEY, iv)
|
||||
text = message.decode("utf-8")
|
||||
except (ValueError, UnicodeDecodeError) as exc:
|
||||
log.warning("Undecryptable license blob: %s", exc)
|
||||
return None
|
||||
|
||||
if len(text) < _MIN_TEXT_LEN:
|
||||
log.warning("License text too short (%d chars)", len(text))
|
||||
return None
|
||||
|
||||
return License(
|
||||
serial=text[0:6],
|
||||
mfg_date_hex=text[7:15],
|
||||
level=text[16:18],
|
||||
lic_date_hex=text[19:27],
|
||||
)
|
||||
|
||||
|
||||
# ===== Directory Scan =====
|
||||
def read_license_files(licenses_dir: str | Path) -> list[License]:
|
||||
"""Parse every valid license .bin in the directory.
|
||||
|
||||
A file counts only if its name matches ``X00000-00.bin`` AND equals
|
||||
``{serial}-{level}.bin`` from its own decrypted payload — same
|
||||
tamper check as the C# app.
|
||||
"""
|
||||
directory = Path(licenses_dir)
|
||||
if not directory.is_dir():
|
||||
return []
|
||||
|
||||
licenses: list[License] = []
|
||||
for path in sorted(directory.glob("*.bin")):
|
||||
if not LICENSE_FILENAME_RE.match(path.name):
|
||||
continue
|
||||
try:
|
||||
blob = path.read_bytes()
|
||||
except OSError as exc:
|
||||
log.warning("Cannot read license file %s: %s", path, exc)
|
||||
continue
|
||||
lic = parse_license_blob(blob)
|
||||
if lic is None:
|
||||
continue
|
||||
if path.name != f"{lic.serial}-{lic.level}.bin":
|
||||
log.warning("License filename mismatch: %s", path.name)
|
||||
continue
|
||||
licenses.append(lic)
|
||||
return licenses
|
||||
Reference in New Issue
Block a user