"""Install a desktop entry that does not depend on the GUI session's PATH.""" from __future__ import annotations import argparse import os from pathlib import Path import sys from typing import Sequence EXECUTABLE_TOKEN = "@PTLINUX_EXECUTABLE@" ICON_TOKEN = "@PTLINUX_ICON@" class DesktopEntryError(ValueError): """Raised when a safe desktop entry cannot be rendered.""" def quote_exec_argument(value: str) -> str: """Quote one Desktop Entry ``Exec`` argument without field-code expansion.""" if not value or any(character in value for character in ("\x00", "\n", "\r")): raise DesktopEntryError("desktop executable path contains invalid characters") escaped = value.replace("\\", "\\\\\\\\") escaped = escaped.replace('"', '\\\\"') escaped = escaped.replace("`", "\\\\`") escaped = escaped.replace("$", "\\\\$") escaped = escaped.replace("%", "%%") return f'"{escaped}"' def escape_icon_path(value: str) -> str: """Escape one absolute Desktop Entry ``Icon`` path as a string value.""" if not value or any(character in value for character in ("\x00", "\n", "\r")): raise DesktopEntryError("desktop icon path contains invalid characters") return value.replace("\\", "\\\\") def render_desktop_entry(template: str, executable: Path, icon: Path) -> str: """Replace launcher and icon tokens with validated absolute paths.""" if template.count(EXECUTABLE_TOKEN) != 1: raise DesktopEntryError("desktop template must contain exactly one executable token") if template.count(ICON_TOKEN) != 1: raise DesktopEntryError("desktop template must contain exactly one icon token") launcher = executable.expanduser() if not launcher.is_absolute(): raise DesktopEntryError("desktop launcher path must be absolute") if not launcher.is_file() or not os.access(launcher, os.X_OK): raise DesktopEntryError(f"desktop launcher is missing or not executable: {launcher}") icon_path = icon.expanduser() if not icon_path.is_absolute(): raise DesktopEntryError("desktop icon path must be absolute") if not icon_path.is_file(): raise DesktopEntryError(f"desktop icon is missing: {icon_path}") rendered = template.replace(EXECUTABLE_TOKEN, quote_exec_argument(str(launcher))) rendered = rendered.replace(ICON_TOKEN, escape_icon_path(str(icon_path))) return rendered if rendered.endswith("\n") else rendered + "\n" def install_desktop_entry( template: Path, executable: Path, icon: Path, destination: Path, ) -> None: """Render and atomically install one user-level desktop entry.""" rendered = render_desktop_entry( template.read_text(encoding="utf-8"), executable, icon, ) destination.parent.mkdir(parents=True, exist_ok=True) temporary = destination.with_name(f".{destination.name}.{os.getpid()}.tmp") try: temporary.write_text(rendered, encoding="utf-8") temporary.chmod(0o644) temporary.replace(destination) except BaseException: temporary.unlink(missing_ok=True) raise def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser(description="install the PristonTale EU desktop entry") parser.add_argument("template", type=Path) parser.add_argument("executable", type=Path) parser.add_argument("icon", type=Path) parser.add_argument("destination", type=Path) arguments = parser.parse_args(argv) try: install_desktop_entry( arguments.template, arguments.executable, arguments.icon, arguments.destination, ) except (DesktopEntryError, OSError) as exc: print(f"Could not install desktop shortcut: {exc}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())