"""Read-only Wine registry snapshots for reversible desktop configuration.""" from __future__ import annotations import ast from dataclasses import asdict, dataclass from pathlib import Path import re from .filesystem import sha256_file _VALUE_RE = re.compile(r'^(?P"(?:\\.|[^"\\])*")=(?P.*)$') _SECTION_RE = re.compile(r"^\[(?P.*)](?:\s+[0-9]+)?$") @dataclass(frozen=True) class RegistryValue: exists: bool value: str | None def to_dict(self) -> dict[str, str | bool | None]: return asdict(self) @dataclass(frozen=True) class DesktopRegistrySnapshot: user_reg: Path sha256: str desktop_name: str selected_desktop: RegistryValue desktop_size: RegistryValue def to_dict(self) -> dict[str, object]: return { "user_reg": str(self.user_reg), "sha256": self.sha256, "desktop_name": self.desktop_name, "selected_desktop": self.selected_desktop.to_dict(), "desktop_size": self.desktop_size.to_dict(), } def snapshot_desktop_registry(prefix: Path, desktop_name: str) -> DesktopRegistrySnapshot: user_reg = prefix / "user.reg" sections = read_registry_string_values(user_reg) selected = sections.get(r"Software\Wine\Explorer", {}).get("Desktop") size = sections.get(r"Software\Wine\Explorer\Desktops", {}).get(desktop_name) return DesktopRegistrySnapshot( user_reg=user_reg, sha256=sha256_file(user_reg), desktop_name=desktop_name, selected_desktop=RegistryValue(selected is not None, selected), desktop_size=RegistryValue(size is not None, size), ) def desktop_registry_restored( before: DesktopRegistrySnapshot, after: DesktopRegistrySnapshot, ) -> bool: return ( before.desktop_name == after.desktop_name and before.selected_desktop == after.selected_desktop and before.desktop_size == after.desktop_size ) def read_registry_string_values(path: Path) -> dict[str, dict[str, str]]: sections: dict[str, dict[str, str]] = {} current: dict[str, str] | None = None with path.open("r", encoding="utf-8", errors="replace") as handle: for raw in handle: line = raw.rstrip("\r\n") section = _SECTION_RE.match(line) if section is not None: name = section.group("name").replace(r"\\", "\\") current = sections.setdefault(name, {}) continue if current is None: continue match = _VALUE_RE.match(line) if match is None: continue name = _decode_string(match.group("name")) value_text = match.group("value") if name is None or not value_text.startswith('"'): continue value = _decode_string(value_text) if value is not None: current[name] = value return sections def _decode_string(value: str) -> str | None: try: result = ast.literal_eval(value) except (SyntaxError, ValueError): return None return result if isinstance(result, str) else None