Skip to content

Commit 349c6db

Browse files
fix: reject orphan attachments in generic decoder (SPEC 16.5)
A .field attachment whose name is neither a ^-marked column of its row nor a >-containing field name (SPEC 7.4.6.1.4) is now rejected as orphan_attachment, instead of being silently absorbed as an undeclared extra field. The old leniency (introduced in the v2.2.1 >-field work, which over-broadly disabled the v2.1.0 orphan reject) was a lossless-round-trip hole: a stray .field decoded to a record shape no encoder produces, injecting a field onto the last-parsed row. The legitimate >-named bare attachment still decodes. Verified against the shared conformance fixtures (016 now rejects; flatten/016 still round-trips).
1 parent dd9d7d8 commit 349c6db

2 files changed

Lines changed: 14 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66

7+
- Decoder: reject an orphan `.field` attachment (a `.field` whose name is neither a `^`-marked column of its row nor a `>`-containing field name, SPEC 7.4.6.1.4) instead of silently absorbing it as an undeclared extra field. Such a stray attachment previously decoded to a record no encoder produces, silently injecting a field onto the last-parsed row (a lossless round-trip hole); now rejected per SPEC 16.5 (`orphan_attachment`).
78
- Graph streaming trailer: the edge count is now always the last `counts` entry, even when the stream has no edges (positional `counts=2,1,0`; labeled `counts=…,edges:0`). A zero-edge stream previously dropped it, violating the SPEC §8.4 / §8.4.1 rule that the edge count is always present and last (the invariant that keeps the positional form unambiguous). The graph trailer is decoder-ignored, so this changes producer output only.
89

910
### Streaming: opt-in labeled trailer counts (SPEC §8.4.1)

src/gcf/decode_generic.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,11 @@ def _parse_tabular_body(
521521
if row_has_id:
522522
inline_idx = 0
523523

524+
# Columns that carry a `^` marker cell in this row legitimately expect
525+
# a `.field` body. Any other `.field` is an orphan (Section 16.5) unless
526+
# its name contains `>` (the flatten-fallback attachment, Section 7.4.6.1.4).
527+
expected_att = set(traditional_att_fields) | set(inline_att_fields)
528+
524529
while i < len(lines):
525530
a_line = lines[i]
526531
a_content: str | None = None
@@ -541,6 +546,14 @@ def _parse_tabular_body(
541546
att_name, after_name = _parse_attachment_name(rest)
542547
after_name_stripped = after_name.lstrip()
543548

549+
# Orphan attachment: a `.field` with no matching `^` cell in this
550+
# row is only legitimate for a `>`-named field (Section 7.4.6.1.4).
551+
# Any other unmatched attachment is rejected rather than silently
552+
# injected as an undeclared extra field, which would decode to a
553+
# record no encoder produces (Section 16.5, lossless round-trip).
554+
if att_name not in expected_att and ">" not in att_name:
555+
raise ValueError(f"orphan_attachment: {att_name}")
556+
544557
# Prefixed inline data.
545558
ifs = inline_schemas.get(att_name)
546559
if ifs and not after_name_stripped.startswith("{}") and not after_name_stripped.startswith("["):

0 commit comments

Comments
 (0)