Skip to content

jq/yq: a #1194 structurally malformed key silently vanishes via keys_unsorted/materialize but raises via keys #1679

Description

@newhoggy

Severity: Low-Medium — reachable via the shipped sjq/syq CLIs (unlike #1678), but only
for the narrow #1194 shape (a key the format's grammar never allowed at all, e.g. a bare
non-string JSON key {123: 1}), not the far more common #1247/#1642 undecodable-key shape,
which this branch already fixed everywhere.

Summary

Found while reviewing/fixing #1642, which closed the "same document, different answer"
inconsistency for a key whose bytes won't decode (#1247's fault). That fix left a
structurally-analogous, pre-existing gap open on the adjacent #1194 axis: a key the format's
grammar never allowed at all still raises on some routes and silently vanishes on others.

keys() (src/jq/document.rs), effective_keys, to_owned_at_depth,
to_owned_cursor_at_depth, to_owned_with_comments_at_depth, and the ToEntries builtin
(src/jq/eval_generic.rs) all do:

let Some(key) = key_display_string(&field.key) else {
    return Err(f.malformed_member_error());
};

key_display_string returns None exactly for a #1194 key (see its doc comment), so all of
these correctly raise.

But lazy_keys_array_to_owned and cursor_to_owned_at_depth (src/jq/lazy.rs), and
stream_lazy_keys_json/stream_lazy_keys_yaml (src/jq/stream.rs), instead do:

if let Some(key) = key_display_string(&field.key) {
    // push / write
}

with no else — a None (the #1194 case) just silently drops the field, no error. This
predates #1642 (the old code had the identical asymmetry via field.key_str()/if let StandardJson::String(k) = key), so #1642 didn't introduce it — but its "one document, many
answers" framing makes this easy to mistake as already closed, since it's exactly that bug
class, just on the #1194 axis instead of #1247's.

Repro

$ echo '{123:1,"a":1}' | sjq -c 'keys'           # raises (correct)
$ echo '{123:1,"a":1}' | sjq -c 'keys_unsorted'  # ["a"]   <- silently one entry short, exit 0
$ echo '{123:1,"a":1}' | sjq -c 'length'         # 2

keys and length/keys_unsorted disagree with each other on the same document, depending
on which builtin is asked — the same failure mode #1642's own issue description named.

Suggested direction

Not attempted; same two-way choice #1642 itself faced (raise everywhere vs. skip/preserve
everywhere), and picking "raise everywhere" here is more invasive than #1642's fix was:
stream_lazy_keys_json/stream_lazy_keys_yaml write through a core::fmt::Write-style
signature with no error channel today, the same constraint that made #1247's original
design doc (docs/plan/decode-failure-routing.md) split out and defer the value-side
streaming fix as its own stage rather than attempt it inline. lazy_keys_array_to_owned's
signature already returns Result, so that one site at least is a smaller lift.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    SonnetSuitable for a Sonnet-class model to implement

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions