Skip to content

Commit 30ec922

Browse files
Pigbibicodex
andcommitted
fix: enforce canonical weekly manifest wire
Co-Authored-By: Codex <noreply@openai.com>
1 parent 31c3d8e commit 30ec922

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

src/political_event_tracking_research/weekly_manifest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,40 @@ def parse_weekly_manifest(value: Mapping[str, object]) -> WeeklySourceContract:
3030
return parse_weekly_contract(contract)
3131

3232

33+
def parse_weekly_manifest_bytes(wire: bytes) -> WeeklySourceContract:
34+
if type(wire) is not bytes:
35+
raise WeeklyContractError("manifest_wire_invalid")
36+
37+
def pairs(items: list[tuple[str, object]]) -> dict[str, object]:
38+
result: dict[str, object] = {}
39+
for key, item in items:
40+
if key in result:
41+
raise WeeklyContractError("manifest_duplicate_key")
42+
result[key] = item
43+
return result
44+
45+
try:
46+
value = json.loads(wire.decode("utf-8"), object_pairs_hook=pairs)
47+
except WeeklyContractError:
48+
raise
49+
except (UnicodeError, json.JSONDecodeError, TypeError, ValueError, RecursionError):
50+
raise WeeklyContractError("manifest_wire_invalid") from None
51+
if not isinstance(value, Mapping):
52+
raise WeeklyContractError("manifest_shape_invalid")
53+
contract = parse_weekly_manifest(value)
54+
if serialize_weekly_manifest(contract) != wire:
55+
raise WeeklyContractError("manifest_noncanonical")
56+
return contract
57+
58+
3359
def validate_weekly_manifest(value: Mapping[str, object], expected: WeeklySourceContract) -> WeeklySourceContract:
3460
if not isinstance(expected, WeeklySourceContract):
3561
raise WeeklyContractError("manifest_expected_invalid")
3662
parsed = parse_weekly_manifest(value)
3763
if parsed != expected:
3864
raise WeeklyContractError("manifest_contract_mismatch")
65+
if build_weekly_manifest(parsed) != dict(value):
66+
raise WeeklyContractError("manifest_noncanonical")
3967
return parsed
4068

4169

tests/test_weekly_manifest.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
MANIFEST_TYPE,
1111
build_weekly_manifest,
1212
parse_weekly_manifest,
13+
parse_weekly_manifest_bytes,
1314
serialize_weekly_manifest,
1415
validate_weekly_manifest,
1516
write_weekly_manifest,
@@ -36,6 +37,19 @@ def test_weekly_manifest_is_deterministic_and_round_trips():
3637
encoded = serialize_weekly_manifest(contract)
3738
assert encoded == serialize_weekly_manifest(parse_weekly_manifest(json.loads(encoded)))
3839
assert parse_weekly_manifest(manifest) == contract
40+
assert parse_weekly_manifest_bytes(encoded) == contract
41+
42+
43+
@pytest.mark.parametrize("wire", [
44+
lambda encoded: b" " + encoded,
45+
lambda encoded: encoded.replace(b'"contract":', b'"contract" :'),
46+
lambda encoded: encoded.replace(b'"manifest_type":', b'"manifest_type":"x","manifest_type":'),
47+
])
48+
def test_manifest_wire_must_be_exact_canonical_bytes(wire):
49+
contract = parse_weekly_contract(contract_payload())
50+
encoded = serialize_weekly_manifest(contract)
51+
with pytest.raises(WeeklyContractError):
52+
parse_weekly_manifest_bytes(wire(encoded))
3953

4054

4155
@pytest.mark.parametrize("field,value", [
@@ -53,6 +67,14 @@ def test_manifest_contract_tamper_is_rejected(field, value):
5367
validate_weekly_manifest(manifest, contract)
5468

5569

70+
def test_mapping_with_alias_or_unknown_shape_fails_closed():
71+
contract = parse_weekly_contract(contract_payload())
72+
manifest = build_weekly_manifest(contract)
73+
manifest["contract"]["generatedAt"] = manifest["contract"].pop("generated_at")
74+
with pytest.raises(WeeklyContractError):
75+
validate_weekly_manifest(manifest, contract)
76+
77+
5678
def test_manifest_feed_partial_and_shape_tamper_fail_closed():
5779
contract = parse_weekly_contract(contract_payload())
5880
manifest = build_weekly_manifest(contract)

0 commit comments

Comments
 (0)