Skip to content

Commit 187ab66

Browse files
jawwad-aliclaude
andcommitted
fix(bundler): reject falsy non-mapping requires/provides in CatalogEntry.from_dict
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>
1 parent 3356161 commit 187ab66

2 files changed

Lines changed: 25 additions & 4 deletions

File tree

src/specify_cli/bundler/models/catalog.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,21 @@ def from_dict(cls, data: Any) -> "CatalogEntry":
152152
if not isinstance(data, dict):
153153
raise BundlerError("Each catalog entry must be a mapping.")
154154
entry_id = str(data.get("id", "")).strip()
155-
requires = data.get("requires") or {}
156-
if not isinstance(requires, dict):
155+
# `or {}` would coerce a FALSY non-mapping (0, '', False, []) to {} before
156+
# the isinstance guard, silently accepting a corrupt catalog entry; only
157+
# an absent/None value means "not present".
158+
requires = data.get("requires")
159+
if requires is None:
160+
requires = {}
161+
elif not isinstance(requires, dict):
157162
raise BundlerError(
158163
f"Catalog entry '{entry_id or '<unknown>'}': 'requires' must be a "
159164
"mapping when present."
160165
)
161-
provides_raw = data.get("provides") or {}
162-
if not isinstance(provides_raw, dict):
166+
provides_raw = data.get("provides")
167+
if provides_raw is None:
168+
provides_raw = {}
169+
elif not isinstance(provides_raw, dict):
163170
raise BundlerError(
164171
f"Catalog entry '{entry_id or '<unknown>'}': 'provides' must be a "
165172
"mapping when present."

tests/contract/test_catalog_schema.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,17 @@ def test_catalog_entry_rejects_non_mapping_provides():
207207
data["provides"] = "extensions"
208208
with pytest.raises(BundlerError, match="'provides' must be a mapping"):
209209
CatalogEntry.from_dict(data)
210+
211+
212+
@pytest.mark.parametrize("field", ["requires", "provides"])
213+
@pytest.mark.parametrize("bad", [[], "", 0, False])
214+
def test_catalog_entry_rejects_falsy_non_mapping(field, bad):
215+
# `or {}` coerced a FALSY non-mapping ([], '', 0, False) to {} before the
216+
# isinstance guard, silently accepting a corrupt entry; only absent/None
217+
# means "not present". Mirrors the manifest requires/provides guard.
218+
from specify_cli.bundler.models.catalog import CatalogEntry
219+
220+
data = catalog_entry_dict("demo")
221+
data[field] = bad
222+
with pytest.raises(BundlerError, match=f"'{field}' must be a mapping"):
223+
CatalogEntry.from_dict(data)

0 commit comments

Comments
 (0)