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"