"""Read 1C configuration identity from dump tree.""" from __future__ import annotations import re from dataclasses import dataclass from pathlib import Path _VERSION_TAG = re.compile(r"([^<]+)") _NAME_TAG = re.compile(r"([^<]+)") _VENDOR_TAG = re.compile(r"([^<]*)") _SYN_RE = re.compile( r"\s*\s*ru\s*(.*?)", re.DOTALL, ) @dataclass(frozen=True) class ConfigIdentity: """Configuration name / version / vendor for wiki footers.""" version: str name: str | None = None # technical Name synonym: str | None = None # Synonym ru vendor: str | None = None @property def display_name(self) -> str | None: return self.synonym or self.name def config_root_from_src(src_root: Path) -> Path: """src/ → configuration root (parent that usually has VERSION + Configuration.xml).""" src_root = Path(src_root).resolve() if src_root.name == "src": return src_root.parent return src_root def _configuration_xml(src_root: Path) -> Path | None: src_root = Path(src_root) root = config_root_from_src(src_root) for xml_path in ( root / "src" / "Configuration.xml", src_root / "Configuration.xml", root / "Configuration.xml", ): if xml_path.is_file(): return xml_path return None def _xml_unescape(s: str) -> str: return ( s.replace("<", "<") .replace(">", ">") .replace("&", "&") .replace(""", '"') .replace("'", "'") ) def read_config_version(src_root: Path) -> str: """ Resolve configuration version. Priority: 1. /VERSION 2. Configuration.xml → """ return read_config_identity(src_root).version def read_config_name(src_root: Path) -> str | None: """Technical configuration Name from Configuration.xml.""" return read_config_identity(src_root).name def _sanitize_text(s: str) -> str: """Drop NULs (UTF-16 mis-reads / dirty dumps) that break Windows APIs.""" return s.replace("\0", "").strip() def read_config_identity(src_root: Path) -> ConfigIdentity: """Version, Name, Synonym (ru), Vendor from dump.""" src_root = Path(src_root) root = config_root_from_src(src_root) version = "unknown" version_file = root / "VERSION" if version_file.is_file(): text = _sanitize_text( version_file.read_text(encoding="utf-8-sig", errors="ignore") ) if text: version = text.splitlines()[0].strip() name: str | None = None synonym: str | None = None vendor: str | None = None xml_path = _configuration_xml(src_root) if xml_path is not None: raw = xml_path.read_text(encoding="utf-8-sig", errors="ignore") # Prefer Properties block to avoid accidental matches deeper in the file props = raw if "" in raw and "" in raw: props = raw.split("", 1)[1].split("", 1)[0] if version == "unknown": m = _VERSION_TAG.search(props) if m: version = _sanitize_text(m.group(1)) or "unknown" m = _NAME_TAG.search(props) if m: name = _sanitize_text(m.group(1)) or None m = _SYN_RE.search(props) if m: synonym = _sanitize_text(_xml_unescape(m.group(1))) or None m = _VENDOR_TAG.search(props) if m: vendor = _sanitize_text(_xml_unescape(m.group(1))) or None return ConfigIdentity( version=_sanitize_text(version) or "unknown", name=name, synonym=synonym, vendor=vendor, )