101 lines
2.7 KiB
Python
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
|