Files

141 lines
5.3 KiB
Python

"""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)