From 4485bd9da83dd0db65d5ffd97170d6c95d69847f Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Tue, 4 Aug 2026 01:57:26 +0500 Subject: [PATCH] fix: make _safe_write_json actually atomic with mkstemp + os.replace Despite its name, _safe_write_json used write_text() which truncates the file before writing. A crash or power loss mid-write leaves a partial JSON file. Now uses tempfile.mkstemp + os.replace for atomic writes, matching the pattern used in _utils.py, shared_infra.py, and other safe-write utilities in the codebase. --- src/specify_cli/events.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/events.py b/src/specify_cli/events.py index d3002fe805..cbc0d1db4a 100644 --- a/src/specify_cli/events.py +++ b/src/specify_cli/events.py @@ -17,6 +17,7 @@ import sys import subprocess import platform +import tempfile from pathlib import Path from typing import TYPE_CHECKING, Any @@ -2010,10 +2011,23 @@ def _load_user_json(path: Path) -> dict | None: def _safe_write_json(dst: Path, data: dict) -> None: - """Write *data* as JSON to *dst* after validating the destination (#12).""" + """Write *data* as JSON to *dst* atomically after validating the destination (#12).""" _ensure_safe_destination(dst) dst.parent.mkdir(parents=True, exist_ok=True) - dst.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") + fd, tmp = tempfile.mkstemp( + dir=str(dst.parent), prefix=f".{dst.name}.", suffix=".tmp" + ) + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + f.write("\n") + os.replace(tmp, dst) + except BaseException: + try: + os.unlink(tmp) + except OSError: + pass + raise def _ensure_safe_destination(dst: Path) -> None: