mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
fix(extensions): start fresh on a non-UTF-8 extension registry (#3998)
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 from the text-mode read before JSON parsing began. Because the registry is loaded in __init__, that bare traceback broke every extension command -- `specify extension list` on such a project exits with a raw UnicodeDecodeError instead of the module's clean path. Catch UnicodeDecodeError in the same clause: undecodable bytes are the same corruption class as unparseable JSON, only the exception type differs. OSError stays uncaught on purpose -- the data may be intact on disk, and starting fresh would let a later _save() wipe it. This is the exact twin of the PresetRegistry._load() fix in #3955; the two registries are parallel implementations and only the preset side was corrected. _get_installed_sibling_ids() already worked around this gap locally by catching UnicodeError at its own call site; its comment is updated to reflect that _load() now handles the case itself, with the local catch kept as belt-and-braces against regression. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f31b2b45eb
commit
3d4f71c90e
@@ -660,8 +660,13 @@ class ExtensionRegistry:
|
||||
if not isinstance(data.get("extensions"), dict):
|
||||
data["extensions"] = {}
|
||||
return data
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
# Corrupted or missing registry, start fresh
|
||||
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, and it is
|
||||
# raised by the text-mode read before JSON parsing begins. 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, "extensions": {}}
|
||||
|
||||
def _save(self):
|
||||
@@ -4310,11 +4315,10 @@ class ConfigManager:
|
||||
Returns an empty list if the registry is missing or corrupted
|
||||
(fresh project, ad-hoc test harness) so ``_get_env_config`` degrades
|
||||
to its pre-fix behaviour rather than crashing. ``UnicodeError`` is
|
||||
caught alongside ``OSError`` because ``ExtensionRegistry._load()``
|
||||
opens the file in text mode and only handles ``JSONDecodeError`` /
|
||||
``FileNotFoundError``, so a registry file with non-UTF-8 bytes would
|
||||
otherwise surface a ``UnicodeDecodeError`` here and break *every*
|
||||
config read instead of degrading gracefully.
|
||||
kept alongside ``OSError`` as belt-and-braces: ``_load()`` now starts
|
||||
fresh on non-UTF-8 registry bytes itself, but catching it here too
|
||||
keeps this call site degrading gracefully rather than breaking *every*
|
||||
config read if that handling ever regresses.
|
||||
|
||||
Used by ``_get_env_config`` to detect env vars whose remainder claims
|
||||
a longer, sibling-owned prefix (e.g. ``SPECKIT_GIT_HOOKS_URL`` is
|
||||
|
||||
@@ -1291,6 +1291,31 @@ class TestExtensionRegistry:
|
||||
result = registry.list()
|
||||
assert result == {}
|
||||
|
||||
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 text-mode read before
|
||||
JSON parsing began — the same corruption class reaching a different
|
||||
exception type. Because the registry is loaded in ``__init__``, that
|
||||
traceback broke *every* extension command on the project.
|
||||
"""
|
||||
extensions_dir = temp_dir / "extensions"
|
||||
extensions_dir.mkdir()
|
||||
(extensions_dir / ExtensionRegistry.REGISTRY_FILE).write_bytes(
|
||||
b"\xff\xfe not utf-8 \xc3\x28"
|
||||
)
|
||||
|
||||
registry = ExtensionRegistry(extensions_dir)
|
||||
|
||||
assert registry.data == {
|
||||
"schema_version": ExtensionRegistry.SCHEMA_VERSION,
|
||||
"extensions": {},
|
||||
}
|
||||
assert registry.list() == {}
|
||||
assert not registry.is_installed("test-ext")
|
||||
|
||||
|
||||
# ===== ExtensionManager Tests =====
|
||||
|
||||
|
||||
Reference in New Issue
Block a user