Decode DCS appearance template XML

This commit is contained in:
2026-08-14 12:57:32 +03:00
parent 5615e262a8
commit 8a6610f30c
3 changed files with 79 additions and 0 deletions
@@ -111,6 +111,11 @@ Read-only diagnostic inventory `CommonTemplate` в `upo_test` уже дал тр
нужен собственный root-aware classifier и отдельный XML decoder; обратная
запись по-прежнему запрещена до controlled before/after fixture.
Reader реализован в `plugins/1c/parser/appearance_payload.py`: он возвращает
контейнер, все верхнеуровневые правила `item(parameter, value)` и точный
outline XML-тегов. Он не разбирает строковое значение style как произвольный
формат и всегда сообщает `unsupported_until_reverse_codec_fixture` для write.
## Ближайшая реализация
Первый кодовый результат — resolver, который умеет различать форму и макет,
+61
View File
@@ -0,0 +1,61 @@
"""Read-only decoder for 1C DCS appearance-template payloads.
Appearance templates are a distinct XML family (``AppearanceTemplate``), not
DataCompositionSchema and not a spreadsheet document. This decoder returns
only XML-backed rules and preserves the original carrier for any future
lossless writer work.
"""
from __future__ import annotations
import xml.etree.ElementTree as ET
from collections import Counter
from typing import Any
from .payload import decode_payload_lossless
def _local_name(tag: str) -> str:
return tag.rsplit("}", 1)[-1]
def _child_text(node: ET.Element, name: str) -> str:
child = next((item for item in node if _local_name(item.tag) == name), None)
return "".join(child.itertext()).strip() if child is not None else ""
def inspect_appearance_payload(data: bytes) -> dict[str, Any]:
"""Decode a compressed appearance-template carrier without changing it."""
decoded = decode_payload_lossless(data)
payload = bytes(decoded.get("payload") or b"")
start = payload.find(b"<?xml")
if start < 0:
start = payload.find(b"<AppearanceTemplate")
end_marker = b"</AppearanceTemplate>"
end = payload.find(end_marker, start) if start >= 0 else -1
if start < 0 or end < 0:
return {
"status": "undecodable",
"container": {"compression": decoded.get("compression"), "raw_bytes": decoded.get("raw_bytes"), "payload_bytes": decoded.get("payload_bytes")},
"diagnostics": {"code": "APPEARANCE_XML_NOT_FOUND"},
}
xml_bytes = payload[start : end + len(end_marker)]
try:
root = ET.fromstring(xml_bytes.decode("utf-8-sig"))
except (UnicodeDecodeError, ET.ParseError) as exc:
return {"status": "undecodable", "diagnostics": {"code": "APPEARANCE_XML_INVALID", "message": str(exc)}}
if _local_name(root.tag) != "AppearanceTemplate":
return {"status": "unsupported", "diagnostics": {"code": "APPEARANCE_ROOT_NOT_CONFIRMED", "xml_root": _local_name(root.tag)}}
items = []
for ordinal, item in enumerate((node for node in root if _local_name(node.tag) == "item"), start=1):
parameter, value = _child_text(item, "parameter"), _child_text(item, "value")
items.append({"ordinal": ordinal, "parameter": parameter, "value": value})
tag_counts = Counter(_local_name(node.tag) for node in root.iter())
return {
"status": "ok",
"container": {"compression": decoded.get("compression"), "raw_bytes": decoded.get("raw_bytes"), "payload_bytes": decoded.get("payload_bytes"), "xml_offset": start, "xml_bytes": len(xml_bytes), "xml_root": "AppearanceTemplate"},
"items": items,
"outline": {"tag_counts": dict(sorted(tag_counts.items(), key=lambda item: item[0].casefold()))},
"diagnostics": {"write": "unsupported_until_reverse_codec_fixture"},
}
+13
View File
@@ -38,6 +38,7 @@ from parser.bsl_validation import replace_routine_text, routine_blocks, text_sha
from parser.form_payload import command_handler_links, data_path_reference_from_node, decode_form_payload, dynamic_list_fields, dynamic_list_settings, embedded_table_addition_row, enrich_form_common_semantic, enrich_item_data_paths, enrich_item_reference_semantics, event_handlers, event_handlers_from_node, form_common_semantic, form_item_coverage_summary, section_record_semantic_properties # noqa: E402
from parser.form_xml import decode_form_xml # noqa: E402
from parser.scd_payload import compare_scd_semantics, inspect_scd_payload, plan_scd_scalar_patch # noqa: E402
from parser.appearance_payload import inspect_appearance_payload # noqa: E402
from parser.dbnames import parse_dbnames_bytes, parse_dbnames_version_bytes # noqa: E402
sys.path.insert(0, str(ROOT / "plugins" / "1c" / "connector"))
import adapter_1c_server as adapter_server # noqa: E402
@@ -252,6 +253,18 @@ def test_scd_payload_decoder_reads_xml_after_platform_prefix() -> None:
assert result["analysis"]["settings_context"] == {"status": "not_present", "sections": {}}
def test_appearance_payload_decoder_reads_xml_rules() -> None:
xml = """<?xml version="1.0" encoding="UTF-8"?>
<AppearanceTemplate><item><parameter>Заголовок</parameter><value>style:Title</value></item><style><name>Title</name></style></AppearanceTemplate>"""
result = inspect_appearance_payload(compress_payload(b"\x00\x01" + xml.encode("utf-8"), "raw_deflate"))
assert result["status"] == "ok"
assert result["items"] == [{"ordinal": 1, "parameter": "Заголовок", "value": "style:Title"}]
assert result["outline"]["tag_counts"] == {"AppearanceTemplate": 1, "item": 1, "name": 1, "parameter": 1, "style": 1, "value": 1}
assert result["diagnostics"]["write"] == "unsupported_until_reverse_codec_fixture"
def test_scd_scalar_patch_preserves_platform_prefix_and_trailer() -> None:
xml = """<?xml version="1.0" encoding="UTF-8"?>
<SchemaFile><dataCompositionSchema>