diff --git a/docs/1c-sql-protocol/research/report-template-codec-program.md b/docs/1c-sql-protocol/research/report-template-codec-program.md index b89f6f7..41ce938 100644 --- a/docs/1c-sql-protocol/research/report-template-codec-program.md +++ b/docs/1c-sql-protocol/research/report-template-codec-program.md @@ -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, который умеет различать форму и макет, diff --git a/plugins/1c/parser/appearance_payload.py b/plugins/1c/parser/appearance_payload.py new file mode 100644 index 0000000..c943421 --- /dev/null +++ b/plugins/1c/parser/appearance_payload.py @@ -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"" + 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"}, + } diff --git a/tests/1c/test_payload_codec.py b/tests/1c/test_payload_codec.py index 86099a7..e4f04e0 100644 --- a/tests/1c/test_payload_codec.py +++ b/tests/1c/test_payload_codec.py @@ -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 = """ +Заголовокstyle:Title""" + + 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 = """