Skip to content

Commit 6e7818f

Browse files
marcelsafinCopilot
andauthored
fix(presets): start fresh on a non-UTF-8 preset registry (#3955)
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>
1 parent 0824a09 commit 6e7818f

2 files changed

Lines changed: 27 additions & 1 deletion

File tree

src/specify_cli/presets/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,12 @@ def _load(self) -> dict:
550550
if not isinstance(data.get("presets"), dict):
551551
data["presets"] = {}
552552
return data
553-
except (json.JSONDecodeError, FileNotFoundError):
553+
except (json.JSONDecodeError, UnicodeDecodeError, FileNotFoundError):
554+
# Corrupted or missing registry, start fresh. A registry whose
555+
# bytes cannot be decoded as UTF-8 is the same corruption class
556+
# as malformed JSON — only the exception type differs. OSError is
557+
# deliberately not caught: the data may be intact on disk, and
558+
# starting fresh would let a later _save() wipe it.
554559
return {
555560
"schema_version": self.SCHEMA_VERSION,
556561
"presets": {}

tests/test_presets.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,27 @@ def test_empty_registry(self, temp_dir):
515515
assert registry.list() == {}
516516
assert not registry.is_installed("test-pack")
517517

518+
def test_load_starts_fresh_for_non_utf8_registry(self, temp_dir):
519+
"""A registry file with undecodable bytes must start fresh, not raise.
520+
521+
``_load()`` already treats malformed JSON as "corrupted registry,
522+
start fresh", but a registry whose *bytes* cannot be decoded as UTF-8
523+
raised a raw ``UnicodeDecodeError`` from the same boundary — the same
524+
corruption class reaching a different exception type.
525+
"""
526+
packs_dir = temp_dir / "packs"
527+
packs_dir.mkdir()
528+
(packs_dir / PresetRegistry.REGISTRY_FILE).write_bytes(
529+
b"\xff\xfe not utf-8 \xc3\x28"
530+
)
531+
532+
registry = PresetRegistry(packs_dir)
533+
534+
assert registry.data == {
535+
"schema_version": PresetRegistry.SCHEMA_VERSION,
536+
"presets": {},
537+
}
538+
518539
def test_add_and_get(self, temp_dir):
519540
"""Test adding and retrieving a pack."""
520541
packs_dir = temp_dir / "packs"

0 commit comments

Comments
 (0)