141 lines
5.3 KiB
Python
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)
|