Skip to content

Commit 71dd58a

Browse files
iamaeroplaneclaude
andcommitted
fix: handle corrupted registry in list() methods
- Add defensive handling to list() when presets/extensions is not a dict - Return empty dict instead of crashing on corrupted registry - Apply same fix to both PresetRegistry and ExtensionRegistry for parity - Add tests for corrupted registry handling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6978238 commit 71dd58a

4 files changed

Lines changed: 38 additions & 4 deletions

File tree

src/specify_cli/extensions.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,12 @@ def list(self) -> Dict[str, dict]:
340340
from accidentally mutating nested internal registry state.
341341
342342
Returns:
343-
Dictionary of extension_id -> metadata (deep copies)
343+
Dictionary of extension_id -> metadata (deep copies), empty dict if corrupted
344344
"""
345-
return copy.deepcopy(self.data["extensions"])
345+
extensions = self.data.get("extensions", {}) or {}
346+
if not isinstance(extensions, dict):
347+
return {}
348+
return copy.deepcopy(extensions)
346349

347350
def is_installed(self, extension_id: str) -> bool:
348351
"""Check if extension is installed.

src/specify_cli/presets.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,9 +350,12 @@ def list(self) -> Dict[str, dict]:
350350
from accidentally mutating nested internal registry state.
351351
352352
Returns:
353-
Dictionary of pack_id -> metadata (deep copies)
353+
Dictionary of pack_id -> metadata (deep copies), empty dict if corrupted
354354
"""
355-
return copy.deepcopy(self.data["presets"])
355+
packs = self.data.get("presets", {}) or {}
356+
if not isinstance(packs, dict):
357+
return {}
358+
return copy.deepcopy(packs)
356359

357360
def list_by_priority(self, include_disabled: bool = False) -> List[tuple]:
358361
"""Get all installed presets sorted by priority.

tests/test_extensions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,20 @@ def test_list_returns_deep_copy(self, temp_dir):
520520
internal = registry.data["extensions"]["test-ext"]
521521
assert internal["registered_commands"] == {"claude": ["cmd1"]}
522522

523+
def test_list_returns_empty_dict_for_corrupted_registry(self, temp_dir):
524+
"""Test that list() returns empty dict when extensions is not a dict."""
525+
extensions_dir = temp_dir / "extensions"
526+
extensions_dir.mkdir()
527+
registry = ExtensionRegistry(extensions_dir)
528+
529+
# Corrupt the registry - extensions is a list instead of dict
530+
registry.data["extensions"] = ["not", "a", "dict"]
531+
registry._save()
532+
533+
# list() should return empty dict, not crash
534+
result = registry.list()
535+
assert result == {}
536+
523537

524538
# ===== ExtensionManager Tests =====
525539

tests/test_presets.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,20 @@ def test_list_returns_deep_copy(self, temp_dir):
487487
assert fresh["version"] == "1.0.0"
488488
assert fresh["nested"]["key"] == "original"
489489

490+
def test_list_returns_empty_dict_for_corrupted_registry(self, temp_dir):
491+
"""Test that list() returns empty dict when presets is not a dict."""
492+
packs_dir = temp_dir / "packs"
493+
packs_dir.mkdir()
494+
registry = PresetRegistry(packs_dir)
495+
496+
# Corrupt the registry - presets is a list instead of dict
497+
registry.data["presets"] = ["not", "a", "dict"]
498+
registry._save()
499+
500+
# list() should return empty dict, not crash
501+
result = registry.list()
502+
assert result == {}
503+
490504
def test_list_by_priority_excludes_disabled(self, temp_dir):
491505
"""Test that list_by_priority excludes disabled presets by default."""
492506
packs_dir = temp_dir / "packs"

0 commit comments

Comments
 (0)