Skip to content

Commit 1f9b0df

Browse files
cowork-bot: harden parser error-handling for malformed inputs
1 parent 0141589 commit 1f9b0df

4 files changed

Lines changed: 73 additions & 4 deletions

File tree

src/deploydiff/cloudformation_parser.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,19 @@ def parse_cloudformation_changeset(changeset_json: str | dict[str, Any]) -> Depl
5757

5858
changes: list[ResourceChange] = []
5959
changes_list = data.get("Changes", data.get("changes", []))
60+
if not isinstance(changes_list, list):
61+
raise ValueError("CloudFormation Changes must be a JSON array")
6062

61-
for change_entry in changes_list:
63+
for index, change_entry in enumerate(changes_list):
64+
if not isinstance(change_entry, dict):
65+
raise ValueError(f"CloudFormation Changes[{index}] must be a JSON object")
6266
resource_change_data = change_entry.get(
6367
"ResourceChange", change_entry.get("resource_change", {})
6468
)
69+
if not isinstance(resource_change_data, dict):
70+
raise ValueError(
71+
f"CloudFormation Changes[{index}].ResourceChange must be a JSON object"
72+
)
6573
action_str = change_entry.get(
6674
"Action", resource_change_data.get("Action", "Modify")
6775
)

src/deploydiff/pulumi_parser.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,16 @@ def parse_pulumi_preview(preview_json: str | dict[str, Any]) -> DeployPlan:
5757

5858
# Pulumi preview JSON has a "steps" array
5959
steps = data.get("steps", [])
60+
if not isinstance(steps, list):
61+
raise ValueError("Pulumi steps must be a JSON array")
6062

6163
# Also support the resource-oriented format
6264
resources = data.get("resourceChanges", data.get("resources", {}))
6365

6466
# Process steps-based format
65-
for step in steps:
67+
for index, step in enumerate(steps):
68+
if not isinstance(step, dict):
69+
raise ValueError(f"Pulumi steps[{index}] must be a JSON object")
6670
urn = step.get("urn", step.get("old", {}).get("urn", "unknown"))
6771
step_type = step.get("step", step.get("op", "same"))
6872

@@ -100,9 +104,19 @@ def parse_pulumi_preview(preview_json: str | dict[str, Any]) -> DeployPlan:
100104
changes.append(resource_change)
101105

102106
# Process resource-changes-based format (count-based)
103-
if not steps and isinstance(resources, dict):
107+
if not steps:
108+
if not isinstance(resources, dict):
109+
raise ValueError("Pulumi resourceChanges must be a JSON object")
104110
for resource_type, counts in resources.items():
111+
if not isinstance(counts, dict):
112+
raise ValueError(
113+
f"Pulumi resourceChanges[{resource_type!r}] must be a JSON object"
114+
)
105115
for action_str, count in counts.items():
116+
if not isinstance(count, int) or isinstance(count, bool) or count < 0:
117+
raise ValueError(
118+
f"Pulumi resourceChanges[{resource_type!r}][{action_str!r}] must be a non-negative integer"
119+
)
106120
action = PULUMI_STEP_MAP.get(action_str, ChangeAction.UPDATE)
107121
for i in range(count):
108122
resource_change = ResourceChange(

src/deploydiff/terraform_parser.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,24 @@ def parse_terraform_plan(plan_json: str | dict[str, Any]) -> DeployPlan:
5252

5353
# Parse planned changes
5454
resource_changes = data.get("resource_changes", [])
55+
if not isinstance(resource_changes, list):
56+
raise ValueError("Terraform resource_changes must be a JSON array")
5557

56-
for rc in resource_changes:
58+
for index, rc in enumerate(resource_changes):
59+
if not isinstance(rc, dict):
60+
raise ValueError(f"Terraform resource_changes[{index}] must be a JSON object")
5761
change = rc.get("change", {})
62+
if not isinstance(change, dict):
63+
raise ValueError(
64+
f"Terraform resource_changes[{index}].change must be a JSON object"
65+
)
5866
action_strs = change.get("actions", [])
67+
if not isinstance(action_strs, list) or not all(
68+
isinstance(action, str) for action in action_strs
69+
):
70+
raise ValueError(
71+
f"Terraform resource_changes[{index}].change.actions must be a JSON array of strings"
72+
)
5973

6074
# Use the primary action
6175
primary_action = _resolve_primary_action(action_strs)

tests/test_parse_errors.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,36 @@ def test_json_array_is_rejected_with_clear_error(self, parser, payload):
7272
"""A decoded JSON value must be an object before parser-specific access."""
7373
with pytest.raises(ValueError, match="JSON object"):
7474
parser(payload)
75+
76+
@pytest.mark.parametrize(
77+
("parser", "payload", "message"),
78+
[
79+
(
80+
parse_terraform_plan,
81+
{"resource_changes": {}},
82+
"resource_changes must be a JSON array",
83+
),
84+
(
85+
parse_cloudformation_changeset,
86+
{"Changes": {}},
87+
"Changes must be a JSON array",
88+
),
89+
(
90+
parse_pulumi_preview,
91+
{"steps": {}},
92+
"steps must be a JSON array",
93+
),
94+
],
95+
)
96+
def test_malformed_collections_raise_clear_error(self, parser, payload, message):
97+
"""Malformed collection fields must not be silently ignored."""
98+
with pytest.raises(ValueError, match=message):
99+
parser(payload)
100+
101+
def test_terraform_malformed_resource_entry_is_rejected(self):
102+
with pytest.raises(ValueError, match=r"resource_changes\[0\].*JSON object"):
103+
parse_terraform_plan({"resource_changes": ["not-an-object"]})
104+
105+
def test_pulumi_negative_resource_count_is_rejected(self):
106+
with pytest.raises(ValueError, match="non-negative integer"):
107+
parse_pulumi_preview({"resourceChanges": {"aws:s3/bucket:Bucket": {"create": -1}}})

0 commit comments

Comments
 (0)