Skip to content

Ignore unknown cbor map keys on meta item decode - #286

Merged
thedavidmeister merged 1 commit into
mainfrom
2026-08-25-issue-216
Aug 26, 2026
Merged

Ignore unknown cbor map keys on meta item decode#286
thedavidmeister merged 1 commit into
mainfrom
2026-08-25-issue-216

Conversation

@thedavidmeister

Copy link
Copy Markdown
Contributor

Closes #216.

The adjudication

#216 is a neutral flag. It reports that the RainMetaDocumentV1Item visitor
hard-errors on any cbor map key outside {0, 1, 2, 3, 4, OaSchema}, notes that
the spec's stated reason for the map shape is forward compatibility, and holds
back on a verdict because strictness might be protecting hash-addressed
round-tripping. This PR takes ignore the unknown key, on two grounds.

1. The spec text is one-sided. The map shape exists because "It would be
more difficult to represent new indexes that we MAY want to add in the future",
and "the map structure is chosen to facilitate future modifications to the
conventions in this document in a way that tooling can adopt (or not) in a
backwards compatible way". A decoder that rejects unknown keys makes "or not"
unreachable: every future index is a hard break for everything already
deployed. This repo has already run that experiment — OaSchema was added as
an extra map key under exactly this mechanism, and every build of this crate
from before that addition rejects, outright, every document the SFT frontend
writes today.

2. The counter-argument does not hold on main. "Strictness protects
hash-addressed round-tripping" would require decode -> re-encode to be
byte-preserving. It is not, and unknown keys are not what breaks it — key order
is. Measured on main's decode path (probe applied to the tree with the strict
arm in place, run, then reverted; it passes):

// {1: DotrainV1, 0: h'01'} — the same map, keys on the wire in the other order
let mut swapped: Vec<u8> = vec![0xa2, 0x01, 0x1b];
swapped.extend_from_slice(&KnownMagic::DotrainV1.to_prefix_bytes());
swapped.extend_from_slice(&[0x00, 0x41, 0x01]);
let decoded = RainMetaDocumentV1Item::cbor_decode(&swapped)?;
assert_ne!(decoded[0].cbor_encode()?, swapped);
assert_ne!(decoded[0].hash(false)?, keccak256(&swapped).0);

RainMetaDocumentV1Item is a canonicalising decoder already. An unknown key is
one more thing it cannot carry, not a new class of hazard.

That consequence is now explicit instead of implied.
ignored_map_key_is_absent_from_the_reencoding pins it: an item that decoded
from bytes carrying an unknown key re-encodes without it, and so hashes as what
this version can represent rather than as the bytes it read. The one caller that
re-encodes decoded items, Store::store_content, therefore caches such an item
under the hash of its re-encoding — which is strictly more than main does with
it, since on main the unknown key fails cbor_decode and the whole document
goes uncached.

What changed

The visitor's unknown-key arm consumes the value with serde::de::IgnoredAny
instead of raising found unexpected key in the map. Nothing else about item
validity moves: keys 0 and 1 are still mandatory, an unknown magic number value
under key 1 is still an error, and the OaSchema key still decodes into
schema.

Only unsigned integer keys are ignorable. A text key or a negative key still
errors. The spec rules the header names out as keys — "the HTTP string
representations of the keys such as 'Content-Encoding' are NOT supported as
this would allow encoders to produce data that decoders are explicitly trying to
avoid the complexity of reading" — and next_key::<u64>() is also what lets this
generic Deserialize impl work on formats other than cbor. non_integer_map_key_errors
pins that boundary, and mutation 3 below shows it is a decision rather than an
accident of the key type.

non_oa_schema_extra_map_key_errors is deleted: it pinned the behaviour this PR
changes. Its handwritten bytes live on in non_oa_schema_extra_map_key_is_ignored
with the opposite assertion.

Composition with the other in-flight cbor_decode work

QA

  • Discriminating tests (all in crates/cli/src/meta/mod.rs):
    unknown_map_key_index_is_ignored (the issue's own repro, byte for byte),
    non_oa_schema_extra_map_key_is_ignored,
    unknown_map_key_consumes_its_whole_value,
    ignored_map_key_is_absent_from_the_reencoding. All four fail on base —
    verified by mutation 1, which restores the base arm verbatim: 51 passed; 4 failed, exactly these four.
    Two further tests bound the new path rather than exercising it, and both pass
    on base by construction (base rejects those inputs too, for a different
    reason): non_integer_map_key_errors, killed by mutation 3;
    unknown_map_key_does_not_stand_in_for_a_mandatory_key, killed by no mutation
    of this diff — it is here because an item can now consist of ignorable keys
    plus nothing else, and the mandatory-key requirement has to survive that.
  • Mutations applied — each applied to the fixed tree, suite run, then
    reverted and the suite re-run green (55 passed; 0 failed):
    1. the _ arm -> base's other => Err(serde::de::Error::custom(...))?
      (never ignore) -> killed by all four discriminating tests (51 passed; 4 failed).
    2. _ => { map.next_value::<IgnoredAny>()?; } -> _ => {} (recognise the key
      as ignorable but never consume its value) -> killed by the same four
      (51 passed; 4 failed), so the consume is load-bearing and not decoration.
    3. next_key::<u64>() -> next_key::<serde_cbor::Value>() with the arms
      rewritten to Value::Integer(..), i.e. the alternative that ignores keys of
      every cbor type -> killed by non_integer_map_key_errors alone
      (54 passed; 1 failed), which is the evidence that the integer-only limit
      is a real decision and that nothing else in the suite depends on it.
  • Oracle: the metadata-v1 spec
    (https://github.com/rainlanguage/specs/blob/main/metadata-v1.md), quoted above,
    for both the ignore and the integer-only limit. Every fixture is handwritten
    byte by byte from RFC 8949 major types (0xa3 map(3), 0x40/0x41 bytes,
    0x1b u64, 0x61 text(1), 0x20 -1), never produced by cbor_encode, and
    the expected decoded items come from the existing plain_item helper. The one
    assertion that does compare against cbor_encode output is
    ignored_map_key_is_absent_from_the_reencoding, which pins the re-encoding
    against the independently handwritten handwritten_map() as well.
  • Category check: the issue reports one behaviour (any key outside the known
    set fails the decode of the whole sequence) and asks for one decision
    (strict vs forward-compatible), flagging one trade-off (hash round-tripping).
    All three are covered: the decision is taken and argued from the spec, the
    repro decodes, and the trade-off is measured on main, found already broken by
    key order, and its remaining edge pinned by a test.

Verification

cargo test -p rain-metadata --lib — 313 passed, 0 failed.
cargo clippy -p rain-metadata --all-targets -- -D warnings and
cargo fmt --all --check clean. The repo-wide suite was not run locally, by
request — this machine is running many concurrent agents; CI covers it.

🤖 Generated with Claude Code

The metadata-v1 map shape exists so later conventions can add indexes that
older tooling adopts "or not" backwards compatibly, which a decoder that
rejects unknown keys makes unreachable.

Closes #216

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 12 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1efa1dda-1ab5-4067-94bc-ca4218a9dbe3

📥 Commits

Reviewing files that changed from the base of the PR and between 3a2e0cf and b1ce330.

📒 Files selected for processing (1)
  • crates/cli/src/meta/mod.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thedavidmeister
thedavidmeister merged commit 116e5b1 into main Aug 26, 2026
11 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai assess this PR size classification for the totality of the PR with the following criterias and report it in your comment:

S/M/L PR Classification Guidelines:

This guide helps classify merged pull requests by effort and complexity rather than just line count. The goal is to assess the difficulty and scope of changes after they have been completed.

Small (S)

Characteristics:

  • Simple bug fixes, typos, or minor refactoring
  • Single-purpose changes affecting 1-2 files
  • Documentation updates
  • Configuration tweaks
  • Changes that require minimal context to review

Review Effort: Would have taken 5-10 minutes

Examples:

  • Fix typo in variable name
  • Update README with new instructions
  • Adjust configuration values
  • Simple one-line bug fixes
  • Import statement cleanup

Medium (M)

Characteristics:

  • Feature additions or enhancements
  • Refactoring that touches multiple files but maintains existing behavior
  • Breaking changes with backward compatibility
  • Changes requiring some domain knowledge to review

Review Effort: Would have taken 15-30 minutes

Examples:

  • Add new feature or component
  • Refactor common utility functions
  • Update dependencies with minor breaking changes
  • Add new component with tests
  • Performance optimizations
  • More complex bug fixes

Large (L)

Characteristics:

  • Major feature implementations
  • Breaking changes or API redesigns
  • Complex refactoring across multiple modules
  • New architectural patterns or significant design changes
  • Changes requiring deep context and multiple review rounds

Review Effort: Would have taken 45+ minutes

Examples:

  • Complete new feature with frontend/backend changes
  • Protocol upgrades or breaking changes
  • Major architectural refactoring
  • Framework or technology upgrades

Additional Factors to Consider

When deciding between sizes, also consider:

  • Test coverage impact: More comprehensive test changes lean toward larger classification
  • Risk level: Changes to critical systems bump up a size category
  • Team familiarity: Novel patterns or technologies increase complexity

Notes:

  • the assessment must be for the totality of the PR, that means comparing the base branch to the last commit of the PR
  • the assessment output must be exactly one of: S, M or L (single-line comment) in format of: SIZE={S/M/L}
  • do not include any additional text, only the size classification
  • your assessment comment must not include tips or additional sections
  • do NOT tag me or anyone else on your comment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RainMetaDocumentV1Item decode hard-errors on unknown map keys, defeating the spec's forward-compatible key extension mechanism

1 participant