Complete name-first 1C adapter saved-state support
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user