Cleanup and security updates.

This commit is contained in:
2026-08-30 11:24:02 +02:00
parent ae21f6c64a
commit 61700df17e
21 changed files with 374 additions and 1504 deletions

View File

@@ -9,6 +9,7 @@ import json
import os
from pathlib import Path
import shutil
import stat
from typing import Mapping
import uuid
@@ -43,6 +44,7 @@ class GameImportResult:
installed_build: int | None
started_at: str
completed_at: str
verification_required: bool
def import_existing_game(
@@ -87,10 +89,13 @@ def import_existing_game(
),
started_at=started_at,
completed_at=started_at,
verification_required=True,
)
destination.parent.mkdir(parents=True, exist_ok=True)
temporary = destination.with_name(f".{destination.name}.import-{uuid.uuid4().hex}")
committed = False
pending_recorded = False
try:
temporary.mkdir(parents=False, exist_ok=False)
runner.run(
@@ -111,8 +116,31 @@ def import_existing_game(
):
cache_compatibility_binaries(paths, source)
temporary.replace(destination)
committed = True
provisional = GameImportResult(
source=str(source),
destination=str(destination),
copy_mode="reflink-or-copy",
files=source_before.files,
bytes=source_before.bytes,
core_sha256=source_before.core_sha256,
depot_metadata_source=(
str(depot_metadata.path) if depot_metadata is not None else None
),
depot_metadata_destination=None,
installed_build=(
depot_metadata.installed_build if depot_metadata is not None else None
),
started_at=started_at,
completed_at=datetime.now().astimezone().isoformat(),
verification_required=True,
)
_write_import_manifest(paths, provisional)
pending_recorded = True
except BaseException:
_remove_staging_tree(temporary, destination.parent)
if committed and not pending_recorded:
_remove_staging_tree(destination, destination.parent)
raise
metadata_destination = None
@@ -140,8 +168,9 @@ def import_existing_game(
),
started_at=started_at,
completed_at=datetime.now().astimezone().isoformat(),
verification_required=True,
)
_write_json(paths.game_import_manifest, asdict(result))
_write_import_manifest(paths, result)
return result
@@ -202,22 +231,48 @@ def inventory_game_tree(source_game: Path) -> GameTreeInventory:
source = source_game.expanduser().resolve(strict=True)
for relative in REQUIRED_GAME_FILES:
path = source / relative
if not path.is_file():
try:
status = path.lstat()
except OSError:
raise GameImportError(f"game installation is incomplete: missing {relative}")
if stat.S_ISLNK(status.st_mode):
raise GameImportError(f"game installation contains a symbolic link: {relative}")
if not stat.S_ISREG(status.st_mode):
raise GameImportError(f"game installation contains a non-file: {relative}")
files = 0
total_bytes = 0
for root, _directories, filenames in os.walk(source, followlinks=False):
def walk_error(error: OSError) -> None:
raise GameImportError(f"cannot inventory game tree: {error}") from error
for root, directories, filenames in os.walk(
source,
followlinks=False,
onerror=walk_error,
):
root_path = Path(root)
for directory in directories:
path = root_path / directory
try:
status = path.lstat()
except OSError as exc:
raise GameImportError(f"cannot inventory game directory {path}: {exc}") from exc
if stat.S_ISLNK(status.st_mode):
raise GameImportError(f"game installation contains a symbolic link: {path}")
if not stat.S_ISDIR(status.st_mode):
raise GameImportError(f"game installation contains a special entry: {path}")
for filename in filenames:
path = root_path / filename
if path.is_symlink():
files += 1
continue
try:
total_bytes += path.stat().st_size
status = path.lstat()
except OSError as exc:
raise GameImportError(f"cannot inventory game file {path}: {exc}") from exc
if stat.S_ISLNK(status.st_mode):
raise GameImportError(f"game installation contains a symbolic link: {path}")
if not stat.S_ISREG(status.st_mode):
raise GameImportError(f"game installation contains a special entry: {path}")
total_bytes += status.st_size
files += 1
return GameTreeInventory(
files=files,
@@ -327,6 +382,21 @@ def _sha256(path: Path) -> str:
def _write_json(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
temporary.replace(path)
temporary = path.with_name(f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp")
try:
with temporary.open("x", encoding="utf-8") as handle:
json.dump(value, handle, indent=2, sort_keys=True)
handle.write("\n")
handle.flush()
os.fsync(handle.fileno())
temporary.replace(path)
except BaseException:
temporary.unlink(missing_ok=True)
raise
def _write_import_manifest(paths: AppPaths, result: GameImportResult) -> None:
_write_json(
paths.game_import_manifest,
{"schema": 2, **asdict(result)},
)