Skip to content

Commit 6fff638

Browse files
marcelsafinCopilot
andcommitted
fix(extensions): start fresh on a non-UTF-8 extension registry
ExtensionRegistry._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 extension 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 58f5d6e commit 6fff638

2 files changed

Lines changed: 28 additions & 2 deletions

File tree

‎src/specify_cli/extensions/__init__.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,8 +641,12 @@ def _load(self) -> dict:
641641
if not isinstance(data.get("extensions"), dict):
642642
data["extensions"] = {}
643643
return data
644-
except (json.JSONDecodeError, FileNotFoundError):
645-
# Corrupted or missing registry, start fresh
644+
except (json.JSONDecodeError, UnicodeDecodeError, FileNotFoundError):
645+
# Corrupted or missing registry, start fresh. A registry whose
646+
# bytes cannot be decoded as UTF-8 is the same corruption class
647+
# as malformed JSON — only the exception type differs. OSError is
648+
# deliberately not caught: the data may be intact on disk, and
649+
# starting fresh would let a later _save() wipe it.
646650
return {"schema_version": self.SCHEMA_VERSION, "extensions": {}}
647651

648652
def _save(self):

‎tests/test_extensions.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,6 +1241,28 @@ def test_list_returns_empty_dict_for_corrupted_registry(self, temp_dir):
12411241
result = registry.list()
12421242
assert result == {}
12431243

1244+
def test_load_starts_fresh_for_non_utf8_registry(self, temp_dir):
1245+
"""A registry file with undecodable bytes must start fresh, not raise.
1246+
1247+
``_load()`` documents "Corrupted or missing registry, start fresh" and
1248+
already treats malformed JSON that way, but a registry whose *bytes*
1249+
cannot be decoded as UTF-8 raised a raw ``UnicodeDecodeError`` from the
1250+
same boundary — the same corruption class reaching a different
1251+
exception type.
1252+
"""
1253+
extensions_dir = temp_dir / "extensions"
1254+
extensions_dir.mkdir()
1255+
(extensions_dir / ExtensionRegistry.REGISTRY_FILE).write_bytes(
1256+
b"\xff\xfe not utf-8 \xc3\x28"
1257+
)
1258+
1259+
registry = ExtensionRegistry(extensions_dir)
1260+
1261+
assert registry.data == {
1262+
"schema_version": ExtensionRegistry.SCHEMA_VERSION,
1263+
"extensions": {},
1264+
}
1265+
12441266

12451267
# ===== ExtensionManager Tests =====
12461268

0 commit comments

Comments
 (0)