. "$BST_LIB" # BST-TIMEOUT: 300 # measured 40 s / 70 cases on a fresh guest, hz1 2026-08-18. # # 70 execs of a ~2.6 MB wasm module is what the time goes on, # # so this scales with guest load rather than with the work. # # The runner's own default is 60, which leaves 1.5x -- too # # little for that; 300 is also what the ticket gates and the # # nightly pass, so the marker costs them nothing and only # # takes effect for a caller that passes no bound at all. # # yajl 2.1.0 (meta-oe recipes-devtools/yajl) -- Yet Another JSON Library: an # event-driven (SAX-style) JSON parser and a validating generator, plus two CLI # front ends. json_verify validates stdin; json_reformat beautifies or minimises # it. A tree API (yajl_tree_parse) is layered on top of the event parser. # # EVERY EXPECTED VALUE BELOW IS PINNED FROM AN x86-64 CONTROL, not guessed: the # same SRCREV (a0ecdde0) with the recipe's three CVE patches applied, configured # with the recipe's own cmake switches, built natively, and driven through this # same sequence on 2026-08-18. Where a case says "control", that is what it means. # # WHY THIS SUITE HAS A PROBE AS WELL AS TWO PROGRAMS. # # json_verify and json_reformat between them reduce a parse to one bit and a byte # string, and neither can observe the interface yajl actually exports. yajl is a # SAX parser: its contract is which of eleven callbacks fires, in what order, # carrying what value. A build that emitted map keys after map values, routed # every number through yajl_double, or lost the high word of a long long would # print the same "JSON is valid" and reformat to the same bytes. So the callback # sequence is asserted in-process by yajl-probe, cross-linked against the shipped # libyajl.a at image build time -- and the ticket asks for exactly that. # # THREE MEASURED FACTS SHAPE THIS FILE, all reproduced on the control. # # 1. json_verify SPLITS ITS OUTPUT. The verdict ("JSON is valid" / "JSON is # invalid") goes to stdout, the diagnostic to stderr, and the exit status is # 0/1 independently of both. The ticket asks for "the documented diagnostic # AND exit code", so the three are captured separately rather than through # check_out, which merges them and would let a diagnostic on the wrong stream # pass unnoticed. # # 2. THE PARSER REFUSES AN INTEGER WIDER THAN A LONG LONG rather than falling # back to a double -- including LLONG_MIN itself, because # yajl_parse_integer() accumulates the magnitude and compares it against # LLONG_MAX before applying the sign. So -9223372036854775808 is refused on # the way IN while yajl_gen_integer() emits it happily on the way OUT. Both # are upstream's behaviour at this revision and both reproduce natively, so # they are pinned as facts, not filed as defects. # # 3. json_reformat -m EMITS NO TRAILING NEWLINE, while the beautifying default # does. Command substitution strips trailing newlines, so the two would read # identically through "$(...)" -- the byte-level cases below use wc -c to see # the difference. # # The suite generates every fixture inside the guest: the documents are small and # the point of each is a specific token boundary, so writing them here keeps the # expected value and the input that produces it in one place. require json_verify yajl/present require json_reformat yajl/reformat-present tmp=${TMPDIR:-/tmp}/bst-yajl.$$ mkdir -p "$tmp" || { bst_notok yajl/tmpdir "cannot create $tmp"; exit 0; } PROBE=/usr/libexec/yajl-tests/yajl-probe # ---------------------------------------------------------------- fixtures --- # NESTED is the document the callback-order case is about: three deep, both # container kinds at every level, and one of each scalar type. NESTED='{"n":null,"b":true,"arr":[1,{"k":"v"},[2.5]],"obj":{"i":-7}}' printf '%s' "$NESTED" > "$tmp/nested.json" printf '%s' '[1,2,3]' > "$tmp/array.json" printf '%s' '42' > "$tmp/scalar.json" printf '%s' '{"a":1,}' > "$tmp/trailing-comma.json" printf '%s' '{"a":1' > "$tmp/unclosed.json" printf '%s' '{"a":nul}' > "$tmp/bare-word.json" printf '%s' '{"i":3,"f":1.5,"e":1e3,"big":12345678901234567890,"neg":-0.25}' > "$tmp/numbers.json" printf '%s' '{"s":"\u00e9\u0041\ud83d\ude00\/\t"}' > "$tmp/escapes.json" printf '%s' '/*c*/{"a":1}' > "$tmp/comment.json" printf '%s' '{"a":1}{"b":2}' > "$tmp/stream.json" # run -- drive json_verify with stdout, stderr and rc kept # apart. Written as a function because every reject case needs all three, and # capturing them separately is fact 1 above. vout= ; verr= ; vrc= run_verify() { _f=$1; shift verr=$("$@" > "$tmp/out" 2> "$tmp/err" < "$_f"; echo "rc=$?"; cat "$tmp/err") vrc=${verr#rc=}; vrc=${vrc%% *} vout=$(cat "$tmp/out") verr=$(cat "$tmp/err") } # ------------------------------------------------- the tools are installed --- # Asserted as its own case: the recipe inherits lib_package, so the two programs # land in yajl-bin rather than in yajl, and an image that picked up the wrong # package would leave the suite reporting `skip yajl/present` -- which reads as # coverage. Naming the path makes that a red instead. check yajl/verify-installed test -x /usr/bin/json_verify check yajl/reformat-installed test -x /usr/bin/json_reformat # ------------------------------------------------------ json_verify: accept --- run_verify "$tmp/nested.json" json_verify check_eq yajl/verify-nested-rc 0 "$vrc" check_eq yajl/verify-nested-out "JSON is valid" "$vout" check_eq yajl/verify-nested-err "" "$verr" # A bare array and a bare scalar are complete JSON texts to yajl (RFC 7159), and # a parser that still required a top-level object would reject them. run_verify "$tmp/array.json" json_verify check_eq yajl/verify-array-rc 0 "$vrc" check_eq yajl/verify-array-out "JSON is valid" "$vout" run_verify "$tmp/scalar.json" json_verify check_eq yajl/verify-scalar-rc 0 "$vrc" check_eq yajl/verify-scalar-out "JSON is valid" "$vout" # -q suppresses the verdict AND the diagnostic, and changes neither exit status. run_verify "$tmp/nested.json" json_verify -q check_eq yajl/verify-quiet-rc 0 "$vrc" check_eq yajl/verify-quiet-out "" "$vout" # ------------------------------------------------------ json_verify: reject --- # Three malformations, three DIFFERENT documented diagnostics. Asserting the # exact text matters: a parser that reported every failure as one generic error # would pass a test that only checked the exit status, and the diagnostic is # what a user acts on. run_verify "$tmp/trailing-comma.json" json_verify check_eq yajl/reject-comma-rc 1 "$vrc" check_eq yajl/reject-comma-out "JSON is invalid" "$vout" case "$verr" in "parse error: invalid object key (must be a string)"*) bst_ok yajl/reject-comma-diag ;; *) bst_notok yajl/reject-comma-diag "want:'parse error: invalid object key (must be a string)' got:'$(_bst_1line "$verr")'" ;; esac # the diagnostic carries a caret pointing INTO the offending text -- the property # that makes it usable, and the one a rewritten error path silently drops case "$verr" in *"(right here) ------^"*) bst_ok yajl/reject-comma-caret ;; *) bst_notok yajl/reject-comma-caret "no caret in: $(_bst_1line "$verr")" ;; esac run_verify "$tmp/unclosed.json" json_verify check_eq yajl/reject-eof-rc 1 "$vrc" check_eq yajl/reject-eof-out "JSON is invalid" "$vout" case "$verr" in "parse error: premature EOF"*) bst_ok yajl/reject-eof-diag ;; *) bst_notok yajl/reject-eof-diag "want:'parse error: premature EOF' got:'$(_bst_1line "$verr")'" ;; esac run_verify "$tmp/bare-word.json" json_verify check_eq yajl/reject-word-rc 1 "$vrc" check_eq yajl/reject-word-out "JSON is invalid" "$vout" # a LEXICAL error, not a parse error -- yajl distinguishes the two and the word # "lexical" is the whole of the distinction case "$verr" in "lexical error: invalid string in json text."*) bst_ok yajl/reject-word-diag ;; *) bst_notok yajl/reject-word-diag "want:'lexical error: invalid string in json text.' got:'$(_bst_1line "$verr")'" ;; esac # -q still refuses, silently. A quiet mode that also swallowed the exit status # would make json_verify useless in a script, and nothing else here would notice. run_verify "$tmp/trailing-comma.json" json_verify -q check_eq yajl/reject-quiet-rc 1 "$vrc" check_eq yajl/reject-quiet-out "" "$vout" check_eq yajl/reject-quiet-err "" "$verr" # ------------------------------------------- json_verify: the option switches --- # Each of these is a REJECT that becomes an ACCEPT when its switch is given, so # both halves are asserted -- a switch that did nothing would pass the accept # half alone if the input were legal to begin with. run_verify "$tmp/comment.json" json_verify check_eq yajl/comment-default-rc 1 "$vrc" run_verify "$tmp/comment.json" json_verify -c check_eq yajl/comment-allowed-rc 0 "$vrc" run_verify "$tmp/stream.json" json_verify check_eq yajl/stream-default-rc 1 "$vrc" run_verify "$tmp/stream.json" json_verify -s check_eq yajl/stream-allowed-rc 0 "$vrc" # ------------------------------------------------ json_reformat: canonical --- # The beautified form is pinned WHOLE rather than by a substring: indentation # width, the space after the colon, and the placement of every brace are the # canonical form, and a substring check would miss all three. want='{ "a": 1, "b": [ 10, 20 ], "c": { "d": null, "e": true } }' got=$(printf '%s' '{"a":1,"b":[10,20],"c":{"d":null,"e":true}}' | json_reformat) check_eq yajl/reformat-beautify "$want" "$got" got=$(printf '%s' '{"a":1,"b":[10,20],"c":{"d":null,"e":true}}' | json_reformat -m) check_eq yajl/reformat-minimize '{"a":1,"b":[10,20],"c":{"d":null,"e":true}}' "$got" # fact 3: the two forms differ in their trailing byte, which "$(...)" cannot see n=$(printf '%s' '{"a":1}' | json_reformat | wc -c | tr -d ' ') check_eq yajl/reformat-beautify-bytes 15 "$n" n=$(printf '%s' '{"a":1}' | json_reformat -m | wc -c | tr -d ' ') check_eq yajl/reformat-minimize-bytes 7 "$n" # minimising an already-minimal document is a fixed point. A reformatter that # accumulated whitespace, or re-escaped an escape, would fail here and nowhere # else in this file. once=$(printf '%s' "$NESTED" | json_reformat -m) twice=$(printf '%s' "$once" | json_reformat -m) check_eq yajl/reformat-idempotent "$once" "$twice" check_eq yajl/reformat-nested '{"n":null,"b":true,"arr":[1,{"k":"v"},[2.5]],"obj":{"i":-7}}' "$once" # NUMBERS PASS THROUGH AS LEXED, which is the property that makes yajl safe for # documents holding integers wider than a double's mantissa: 1e3 does not become # 1000, and the 20-digit value survives exactly. Note this is the SAME token the # parser refuses through yajl_integer (fact 2) -- json_reformat installs the raw # number callback, which is why it can carry it. got=$(json_reformat -m < "$tmp/numbers.json") check_eq yajl/reformat-numbers '{"i":3,"f":1.5,"e":1e3,"big":12345678901234567890,"neg":-0.25}' "$got" # \u escapes decode to UTF-8 and \/ loses its backslash; -e puts the backslash # back. Compared as HEX so the assertion does not depend on the console encoding, # and because two of the five bytes at issue are invisible in a terminal. # # The fixture is {"s":"\u00e9\u0041\ud83d\ude00\/\t"}, and each escape is # there for a different reason: \u00e9 is a two-byte sequence, \u0041 is an # escape whose value is plain ASCII (a decoder that special-cased the low range # would drop it), \ud83d\ude00 is a surrogate PAIR that must combine into ONE # four-byte sequence rather than two three-byte ones, \/ must lose its backslash, # and \t must survive the round trip as an ESCAPE rather than as a raw 0x09 -- # the generator re-escapes it, which the raw byte would not show. got=$(json_reformat -m < "$tmp/escapes.json" | od -An -tx1 | tr -d ' \n') check_eq yajl/reformat-escapes '7b2273223a22c3a941f09f98802f5c74227d' "$got" got=$(json_reformat -e -m < "$tmp/escapes.json" | od -An -tx1 | tr -d ' \n') check_eq yajl/reformat-escapes-e '7b2273223a22c3a941f09f98805c2f5c74227d' "$got" got=$(printf '%s' '{"u":"a\/b"}' | json_reformat -m) check_eq yajl/reformat-escape-off '{"u":"a/b"}' "$got" got=$(printf '%s' '{"u":"a\/b"}' | json_reformat -e -m) check_eq yajl/reformat-escape-on '{"u":"a\/b"}' "$got" # a reformatter that cannot parse must refuse, not emit a partial document check_rc yajl/reformat-reject 1 sh -c "json_reformat -m < $tmp/trailing-comma.json" # ------------------------------------------------------------------- probe --- # The SAX interface, in-process. Everything above drives yajl through a program # that discards the event sequence; this is the only part of the suite that sees # it. Absent probe is a RED, not a skip: it ships with the library from the same # recipe, so its absence means the port did not install what it built. if [ ! -x "$PROBE" ]; then bst_notok yajl/probe-present "$PROBE is not installed -- yajl-tests missing from the image" else pout=$("$PROBE" 2>&1); prc=$? if bst_execfail "$pout"; then bst_unrun yajl/probe-run "$pout" else check_eq yajl/probe-rc 0 "$prc" case "$pout" in *ALL-YAJL-OK*) bst_ok yajl/probe-complete ;; *) bst_notok yajl/probe-complete "no ALL-YAJL-OK: $(_bst_1line "$pout")" ;; esac # field -- the value the probe printed for one tagged line. Selects on # an anchored tag so R2 cannot match R2b. field() { printf '%s\n' "$pout" | awk -v t="$1" '$1==t {print $2; exit}'; } check_eq yajl/probe-version 2.1.0 "$(field R0-version)" # THE TICKET'S ASK: the callback order on a nested document. Three deep, # both container kinds at every level. A parser that lost its container # stack could not reproduce this by accident. check_eq yajl/sax-order \ '{|k:n|null|k:b|bool:1|k:arr|[|int:1|{|k:k|str:v|}|[|dbl:2.5|]|]|k:obj|{|k:i|int:-7|}|}' \ "$(field R1-trace)" check_eq yajl/sax-status 'ok,' "$(field R1-status)" # the same document fed ONE BYTE PER yajl_parse() CALL must produce the # identical sequence -- the streaming property the library exists for, and # the one a buffering bug breaks without breaking anything a whole-document # test would notice check_eq yajl/sax-incremental "$(field R1-trace)" "$(field R8-trace)" check_eq yajl/sax-incremental-status 'ok,' "$(field R8-status)" # 64-bit values across this arm's calling convention, at both extremes. A # lost high word or a sign-extension error shows up here as a wrong VALUE. check_eq yajl/int64 \ '[|int:-9223372036854775807|int:9223372036854775807|int:0|int:-1|]' \ "$(field R2-int64)" # fact 2: the refusal, from three directions, each with the same diagnostic check_eq yajl/int-overflow-min 'parse_error:_integer_overflow_' "$(field R2b-err0)" check_eq yajl/int-overflow-max 'parse_error:_integer_overflow_' "$(field R2b-err1)" check_eq yajl/int-overflow-wide 'parse_error:_integer_overflow_' "$(field R2b-err2)" # the integer/double split is decided on the TOKEN, not the value: 1e3 is a # double here although it is a whole number, and that is the contract a # consumer's switch statement is written against check_eq yajl/number-dispatch \ '[|int:3|dbl:1.5|dbl:1000|dbl:-0.25|dbl:0.02|int:0|]' "$(field R3-dispatch)" # the raw-number callback suppresses both typed ones and hands back the # token as lexed -- including the 20-digit value the typed path refuses check_eq yajl/number-raw '3|1.5|1e3|12345678901234567890|-0.25' "$(field R4-raw)" # string decoding, as bytes: a two-byte sequence, a surrogate PAIR that must # combine into ONE four-byte sequence rather than two three-byte ones, an # unescaped solidus, an ASCII escape and a control escape. The surrogate # branch is the code CVE-2017-16516 rewrote, so this also establishes that # the recipe's patch is in the archive that shipped. check_eq yajl/string-decode 'c3a9f09f98802f4109' "$(field R5-strhex)" # the error path a LIBRARY consumer sees (terse), against the verbose form # json_verify prints check_eq yajl/err-terse 'parse_error:_invalid_object_key_(must_be_a_string)_' "$(field R6-err)" check_eq yajl/err-consumed 8 "$(field R6-consumed)" # a callback returning 0 CANCELS: the status is client_canceled rather than # error, and the trace stops where the callback refused rather than running # to the end of the document check_eq yajl/cancel-status 'client' "$(field R7-status)" check_eq yajl/cancel-trace '{|k:go|int:1' "$(field R7-trace)" # the generator, including the LLONG_MIN the parser refuses on the way in check_eq yajl/gen '{"a":[-9223372036854775808,2.5,1e3,null,true],"b":"x/y"}' "$(field R9-gen)" check_eq yajl/gen-callback '{"b":"x\/y"}' "$(field R9-cb)" check_eq yajl/gen-callback-bytes 12 "$(field R9-cbbytes)" # a generator error is reported rather than dropped: 1 is # yajl_gen_keys_must_be_strings check_eq yajl/gen-refuses-nonstring-key 1 "$(field R10-genstatus)" # the tree API -- a separate translation unit with its own allocation # discipline (the one CVE-2023-33460 patched), and nothing else on this # image exercises it check_eq yajl/tree-integer -7 "$(field R11-i)" check_eq yajl/tree-flags 3 "$(field R11-iflags)" check_eq yajl/tree-arraylen 3 "$(field R11-arrlen)" check_eq yajl/tree-type 3 "$(field R11-type)" check_eq yajl/tree-refuses-bad null "$(field R11-badtree)" # yajl_alloc(NULL, ...) is json_verify's own shape: a validate-only parse # that installs no callbacks. It must still validate, and still refuse. check_eq yajl/nullcb-accepts 'ok,' "$(field R12-nullcb-ok)" check_eq yajl/nullcb-refuses 'parse' "$(field R12-nullcb-bad)" check_eq yajl/nullcb-diag 'lexical_error:_invalid_string_in_json_text._' \ "$(field R12-nullcb-err)" fi fi rm -rf "$tmp"