"""Decoder for exported 1C ``ParentConfigurations.bin`` support rules. The SQL representation of these data is platform-private and may differ from the exported representation. Callers must therefore pass bytes from a positively identified source; this module deliberately does not discover a source or infer that missing data means "not on support". """ from __future__ import annotations from dataclasses import asdict, dataclass from pathlib import Path import re from typing import Any from .payload import parse_brace_text, payload_to_text, scalar GUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.I) OBJECT_RULES = { 0: "not_editable", 1: "editable_support_preserved", 2: "not_supported", } @dataclass(frozen=True) class SupportRule: object_guid: str rule_code: int rule: str def to_dict(self) -> dict[str, Any]: return asdict(self) @dataclass(frozen=True) class SupplierSupport: configuration_guid: str general_mode_code: int general_mode: str version: str producer: str name: str declared_object_count: int rules: tuple[SupportRule, ...] def to_dict(self) -> dict[str, Any]: result = asdict(self) result["rules"] = [rule.to_dict() for rule in self.rules] return result def _root_items(data: bytes, source: str) -> tuple[list[dict[str, Any]], dict[str, Any]]: decoded = payload_to_text(data) text = decoded.get("text") if text is None: raise ValueError(f"{source}: cannot decode support rules text") root = parse_brace_text(text) if not (isinstance(root, dict) and root.get("type") == "list"): raise ValueError(f"{source}: expected root list") return list(root.get("items") or []), decoded def _integer(items: list[dict[str, Any]], index: int, source: str, field: str) -> int: try: return int(scalar(items[index])) except (IndexError, TypeError, ValueError) as exc: raise ValueError(f"{source}: invalid {field} at item {index}") from exc def parse_parent_configurations_bytes( data: bytes, *, source: str = "ParentConfigurations.bin", ) -> dict[str, Any]: """Decode a positively identified exported support-rules payload.""" items, decoded = _root_items(data, source) if len(items) < 3: raise ValueError(f"{source}: support rules header is incomplete") format_marker = _integer(items, 0, source, "format marker") if format_marker != 6: raise ValueError(f"{source}: expected format marker 6, got {format_marker}") supplier_count = _integer(items, 2, source, "supplier count") if supplier_count < 0: raise ValueError(f"{source}: supplier count must not be negative") suppliers: list[SupplierSupport] = [] position = 3 for supplier_index in range(supplier_count): if position + 6 >= len(items): raise ValueError(f"{source}: supplier {supplier_index} header is incomplete") configuration_guid = scalar(items[position]).lower() if not GUID_RE.fullmatch(configuration_guid): raise ValueError(f"{source}: supplier {supplier_index} configuration GUID is invalid") general_code = _integer(items, position + 1, source, "general support mode") object_count = _integer(items, position + 6, source, "object count") if object_count < 0: raise ValueError(f"{source}: supplier {supplier_index} object count must not be negative") object_position = position + 7 rules: list[SupportRule] = [] for object_index in range(object_count): current = object_position + object_index * 4 if current + 3 >= len(items): raise ValueError(f"{source}: supplier {supplier_index} object {object_index} is incomplete") rule_code = _integer(items, current, source, "object support rule") object_guid = scalar(items[current + 2]).lower() if rule_code not in OBJECT_RULES: raise ValueError(f"{source}: unsupported object rule code {rule_code}") if not GUID_RE.fullmatch(object_guid): raise ValueError(f"{source}: supplier {supplier_index} object {object_index} GUID is invalid") effective_code = 0 if general_code != 0 else rule_code rules.append(SupportRule(object_guid, effective_code, OBJECT_RULES[effective_code])) suppliers.append( SupplierSupport( configuration_guid=configuration_guid, general_mode_code=general_code, general_mode="editable" if general_code == 0 else "locked", version=scalar(items[position + 3]), producer=scalar(items[position + 4]), name=scalar(items[position + 5]), declared_object_count=object_count, rules=tuple(rules), ) ) position = object_position + object_count * 4 + 2 return { "source": source, "compression": decoded["compression"], "encoding": decoded["encoding"], "format_marker": format_marker, "supplier_count": supplier_count, "suppliers": suppliers, } def parse_parent_configurations_file(path: Path) -> dict[str, Any]: return parse_parent_configurations_bytes(path.read_bytes(), source=path.name)