fix(bundler): reject falsy non-mapping requires/provides in CatalogEntry.from_dict (#3667)

CatalogEntry.from_dict used `data.get("requires") or {}` and
`data.get("provides") or {}`, so a FALSY non-mapping ([], '', 0, false) was
coerced to {} before the isinstance guard — a corrupt catalog entry passed
silently. Only a truthy non-mapping was rejected.

Handle None explicitly and reject every other non-mapping, mirroring the
merged manifest requires/provides/integration guards (#3629, #3661).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali jawwad
2026-07-23 19:33:19 +05:00
committed by GitHub
parent 34bbaafbf3
commit f5be0fffc8
2 changed files with 25 additions and 4 deletions

View File

@@ -152,14 +152,21 @@ class CatalogEntry:
if not isinstance(data, dict):
raise BundlerError("Each catalog entry must be a mapping.")
entry_id = str(data.get("id", "")).strip()
requires = data.get("requires") or {}
if not isinstance(requires, dict):
# `or {}` would coerce a FALSY non-mapping (0, '', False, []) to {} before
# the isinstance guard, silently accepting a corrupt catalog entry; only
# an absent/None value means "not present".
requires = data.get("requires")
if requires is None:
requires = {}
elif not isinstance(requires, dict):
raise BundlerError(
f"Catalog entry '{entry_id or '<unknown>'}': 'requires' must be a "
"mapping when present."
)
provides_raw = data.get("provides") or {}
if not isinstance(provides_raw, dict):
provides_raw = data.get("provides")
if provides_raw is None:
provides_raw = {}
elif not isinstance(provides_raw, dict):
raise BundlerError(
f"Catalog entry '{entry_id or '<unknown>'}': 'provides' must be a "
"mapping when present."

View File

@@ -207,3 +207,17 @@ def test_catalog_entry_rejects_non_mapping_provides():
data["provides"] = "extensions"
with pytest.raises(BundlerError, match="'provides' must be a mapping"):
CatalogEntry.from_dict(data)
@pytest.mark.parametrize("field", ["requires", "provides"])
@pytest.mark.parametrize("bad", [[], "", 0, False])
def test_catalog_entry_rejects_falsy_non_mapping(field, bad):
# `or {}` coerced a FALSY non-mapping ([], '', 0, False) to {} before the
# isinstance guard, silently accepting a corrupt entry; only absent/None
# means "not present". Mirrors the manifest requires/provides guard.
from specify_cli.bundler.models.catalog import CatalogEntry
data = catalog_entry_dict("demo")
data[field] = bad
with pytest.raises(BundlerError, match=f"'{field}' must be a mapping"):
CatalogEntry.from_dict(data)