119 lines
5.5 KiB
Python
119 lines
5.5 KiB
Python
"""Read-only verification at the Windows Desktop handoff receipt boundary."""
|
|
import json
|
|
from pathlib import Path, PurePosixPath
|
|
import re
|
|
import struct
|
|
|
|
from hermes_cli.main_desktop import (
|
|
_HTML_TAG_WITH_URL,
|
|
_MODULE_TAG,
|
|
_desktop_build_needed,
|
|
_desktop_exe_integrity_error,
|
|
_desktop_packaged_executable,
|
|
)
|
|
|
|
|
|
def _verify_packaged_entry(resources: Path) -> None:
|
|
"""Read ASAR's Pickle header and the entry declared by packaged package.json.
|
|
|
|
dist/** is unpacked by electron-builder. No Node install or application
|
|
launch is needed at this boundary; this is not a full dependency audit.
|
|
"""
|
|
archive = resources / "app.asar"
|
|
try:
|
|
with archive.open("rb") as stream:
|
|
size, header_size, payload_size, json_size = struct.unpack("<4I", stream.read(16))
|
|
if (size != 4 or header_size != payload_size + 4
|
|
or payload_size != 4 + ((json_size + 3) // 4) * 4
|
|
or not 0 < json_size <= 64 * 1024 * 1024
|
|
or 8 + header_size > archive.stat().st_size):
|
|
raise ValueError("invalid ASAR header")
|
|
header = json.loads(stream.read(json_size))
|
|
|
|
def read_member(name: str) -> bytes:
|
|
path = PurePosixPath(name)
|
|
if not name or path.is_absolute() or ".." in path.parts or "\\" in name or ":" in name:
|
|
raise ValueError("invalid ASAR entry path")
|
|
node = header
|
|
for part in path.parts:
|
|
node = node["files"][part]
|
|
length = node["size"]
|
|
if not isinstance(length, int) or length <= 0:
|
|
raise ValueError(f"empty ASAR entry: {name}")
|
|
if node.get("unpacked"):
|
|
data = (resources / "app.asar.unpacked" / path).read_bytes()
|
|
else:
|
|
offset = int(node["offset"])
|
|
if offset < 0 or 8 + header_size + offset + length > archive.stat().st_size:
|
|
raise ValueError(f"truncated ASAR entry: {name}")
|
|
stream.seek(8 + header_size + offset)
|
|
data = stream.read(length)
|
|
if len(data) != length or not data.strip():
|
|
raise ValueError(f"incomplete ASAR entry: {name}")
|
|
return data
|
|
|
|
package = json.loads(read_member("package.json"))
|
|
read_member(package["main"]).decode("utf-8")
|
|
except (OSError, ValueError, KeyError, TypeError, struct.error) as exc:
|
|
raise RuntimeError(f"The updated Desktop archive or main entry is invalid: {exc}") from exc
|
|
|
|
index = resources / "app.asar.unpacked" / "dist" / "index.html"
|
|
try:
|
|
html = index.read_text(encoding="utf-8-sig")
|
|
if not any(_MODULE_TAG.search(match.group(0))
|
|
and match.group(0).lower().startswith("<script")
|
|
and not re.match(r"^[a-z]+:|^//", match.group(1), re.IGNORECASE)
|
|
for match in _HTML_TAG_WITH_URL.finditer(html)):
|
|
raise ValueError("renderer has no local module entry")
|
|
except (OSError, ValueError) as exc:
|
|
raise RuntimeError(f"The updated Desktop renderer entry is invalid: {exc}") from exc
|
|
|
|
|
|
def checkout_root() -> Path:
|
|
"""The checkout this module was imported from: the only root the receipt may describe."""
|
|
return Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def _root_to_verify(project_root: Path | None) -> Path:
|
|
"""The root the receipt may describe: a caller's root is a hint, not a contract.
|
|
|
|
The hand-off script is read before the pull, so an update that ships a fix to
|
|
the caller side cannot apply it to its own run: a pre-fix ``windows.ps1`` still
|
|
passes ``Path.cwd()``, which is HERMES_HOME and never holds a packaged Desktop
|
|
app. Honour a caller-supplied root only while it actually carries one, and fall
|
|
back to the checkout this module was imported from -- the tree the update just
|
|
wrote. A supplied root that does carry a packaged app is still verified as
|
|
given, so real damage there stays fail-closed.
|
|
"""
|
|
if project_root is None:
|
|
return checkout_root()
|
|
if _desktop_packaged_executable(project_root / "apps" / "desktop") is not None:
|
|
return project_root
|
|
return checkout_root()
|
|
|
|
|
|
def verify_windows_desktop_update(project_root: Path | None = None) -> None:
|
|
"""Raise when a zero-exit updater left an incomplete or stale packaged app.
|
|
|
|
The root defaults to the imported checkout, never the caller's cwd: the hand-off
|
|
is spawned from HERMES_HOME by the pre-update Desktop, and a cwd-derived root
|
|
reported a healthy install as "Desktop executable is missing" (Sep 2026). A
|
|
caller-supplied root with no packaged app falls back to that same checkout, so a
|
|
stale in-flight hand-off cannot fail a healthy install either.
|
|
"""
|
|
project_root = _root_to_verify(project_root)
|
|
desktop = project_root / "apps" / "desktop"
|
|
executable = _desktop_packaged_executable(desktop)
|
|
if executable is None:
|
|
raise RuntimeError("The updated Desktop executable is missing")
|
|
error = _desktop_exe_integrity_error(executable)
|
|
if error:
|
|
raise RuntimeError(f"The updated Desktop executable is invalid: {error}")
|
|
_verify_packaged_entry(executable.parent / "resources")
|
|
if _desktop_build_needed(desktop, project_root, source_mode=False):
|
|
raise RuntimeError("The updated Desktop build is stale, unstamped, or incomplete")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
verify_windows_desktop_update()
|