From 2f7ce8550a59bbf436b1e5705be5ab86550b366e Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:34:16 +0200 Subject: [PATCH] fix(presets): start fresh on a non-UTF-8 preset registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PresetRegistry._load() catches json.JSONDecodeError and FileNotFoundError to start fresh on a corrupted or missing registry, but a registry file with invalid UTF-8 bytes raised UnicodeDecodeError before JSON parsing began, crashing every preset command. Catch UnicodeDecodeError in the same clause: undecodable bytes are the same corruption class as unparseable JSON. OSError stays uncaught on purpose — the data may be intact on disk, and starting fresh would let a later _save() wipe it (same fail-closed reasoning as the workflow catalog cache loader). Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/presets/__init__.py | 7 ++++++- tests/test_presets.py | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index cc5308f3fc..3362a2a19e 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -531,7 +531,12 @@ def _load(self) -> dict: if not isinstance(data.get("presets"), dict): data["presets"] = {} return data - except (json.JSONDecodeError, FileNotFoundError): + except (json.JSONDecodeError, UnicodeDecodeError, FileNotFoundError): + # Corrupted or missing registry, start fresh. A registry whose + # bytes cannot be decoded as UTF-8 is the same corruption class + # as malformed JSON — only the exception type differs. OSError is + # deliberately not caught: the data may be intact on disk, and + # starting fresh would let a later _save() wipe it. return { "schema_version": self.SCHEMA_VERSION, "presets": {} diff --git a/tests/test_presets.py b/tests/test_presets.py index 243d13ab55..7207a75173 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -484,6 +484,27 @@ def test_empty_registry(self, temp_dir): assert registry.list() == {} assert not registry.is_installed("test-pack") + def test_load_starts_fresh_for_non_utf8_registry(self, temp_dir): + """A registry file with undecodable bytes must start fresh, not raise. + + ``_load()`` already treats malformed JSON as "corrupted registry, + start fresh", but a registry whose *bytes* cannot be decoded as UTF-8 + raised a raw ``UnicodeDecodeError`` from the same boundary — the same + corruption class reaching a different exception type. + """ + packs_dir = temp_dir / "packs" + packs_dir.mkdir() + (packs_dir / PresetRegistry.REGISTRY_FILE).write_bytes( + b"\xff\xfe not utf-8 \xc3\x28" + ) + + registry = PresetRegistry(packs_dir) + + assert registry.data == { + "schema_version": PresetRegistry.SCHEMA_VERSION, + "presets": {}, + } + def test_add_and_get(self, temp_dir): """Test adding and retrieving a pack.""" packs_dir = temp_dir / "packs"