Files
pristontale-eu-linux/ptdiag/registry.py
2026-08-27 17:50:36 +02:00

101 lines
2.7 KiB
Python

"""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<name>"(?:\\.|[^"\\])*")=(?P<value>.*)$')
_SECTION_RE = re.compile(r"^\[(?P<name>.*)\](?:\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