. "$BST_LIB" # QCBOR is a CBOR (RFC 8949) encoder/decoder in portable C: it encodes into a # buffer the caller supplies, allocating nothing, and decodes either item by item # or through the "spiffy" map/array-oriented interface that fetches a named # member by type and reports a type mismatch as an error rather than a wrong # value. CBOR is the binary format the IETF standardised for what JSON cannot # express -- exact 64-bit integers of either sign, byte strings as a type rather # than base64 in a string, tags, indefinite lengths, and real floats including # half-precision -- and it is what COSE, CWT, EAT and SUIT are written in. # # WHAT MAKES THIS SUITE DIFFERENT FROM THE OTHER LIBRARY-ONLY PORTS. cjson, # nlohmann-json and the header-only libraries before them ship no executable, so # on an image with no C compiler every case they can make is about the artifact. # QCBOR ships one: upstream's Makefile builds qcbortest as half of its default # target and this port installs it (the stock recipe discards it), so the core of # this suite is UPSTREAM'S OWN CONFORMANCE CORPUS RUN ON THE TARGET -- 71 test # groups over the encoder, the decoder, tags, big numbers, exponent-and-mantissa, # indefinite lengths, the not-well-formed corpus, the string allocators, and # half-precision floats checked against the RFC 7049 reference implementation. # Every group is asserted BY NAME below, so a red names the mechanism. # # It is also the proof that the archive is well-formed. QCBOR's Makefile archived # with a hardcoded `ar` until this port patched it to $(AR); with host binutils # indexing bitcode the archive looks fine and the LINK fails. qcbortest links # against libqcbor.a, so a runner that starts at all is that patch working. # # --------------------------------------------------------------------------- # WHY THIS SUITE MATCHES WITH `case` INSTEAD OF ONE check_out PER CASE # # 81-cjson.test learned this the hard way: each check_out costs a command # substitution plus an exec, and ~200 cases is ~400 process spawns, which on the # wasm32-hwjs runtime is a minute of forking on its own -- past the runner's 60s # per-suite bound (BST_SUITE_TIMEOUT). So qcbortest is run ONCE into a shell # variable, the files are read once each, and every text assertion is a `case` # glob against what is already in memory. ~130 cases, ~15 spawns. # --------------------------------------------------------------------------- require qcbortest qcbor/present bin=/usr/bin/qcbortest lib=/usr/lib/libqcbor.a inc=/usr/include/qcbor compat=/usr/include/UsefulBuf.h nl=' ' # hasfile -- ok iff is a non-empty regular file. # has -- ok iff contains the literal . # hasnt -- ok iff it does not. # hasline -- ok iff has as a WHOLE line, so # ShortBufferParseTest cannot be satisfied by # ShortBufferParseTest2 (both exist upstream). hasfile() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "absent or empty: $2"; fi; } has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "not found: $2" ;; esac; } hasnt() { case "$3" in *"$2"*) bst_notok "$1" "unexpectedly present: $2" ;; *) bst_ok "$1" ;; esac; } hasline() { case "$nl$3$nl" in *"$nl$2$nl"*) bst_ok "$1" ;; *) bst_notok "$1" "no such line: $2" ;; esac; } # ============================================================================= # 1. Upstream's conformance corpus, run once on the target. # ============================================================================= out=$(qcbortest 2>&1); rc=$? # qcbortest's exit status IS the failed-test count. One test fails here and it # is a known upstream 32-bit defect, pinned by its own case further down, so the # expected status is 1 -- and pinning the NUMBER is what keeps this from being a # blanket "non-zero is fine": a second failure moves it to 2 and lands here. if [ "$rc" -eq 1 ]; then bst_ok qcbor/runner-rc elif bst_execfail "$out"; then bst_unrun qcbor/runner-rc "$out" else bst_notok qcbor/runner-rc "rc=$rc want 1 (rc is the failed-test count; 1 = the known BstrWrapErrorTest ILP32 defect) $(_bst_1line "$out")"; fi has qcbor/summary "SUMMARY: 71 tests run; 1 tests failed" "$out" # The RUN COUNT is pinned at 71 separately from the summary line above, because # 71 is exactly what distinguishes the shipped float configuration from the two # it is not: -DUSEFULBUF_DISABLE_ALL_FLOAT compiles five groups out and reports # 66, and upstream's full-float default does not build on this arm at all (wasm # defines no FE_* exception macros). See the bbappend. A run reporting 66 with no # failures would otherwise read as a better result than this one. hasnt qcbor/not-66-tests "SUMMARY: 66 tests run" "$out" # --- the example, which runs before the tests and is its own end-to-end case -- # # cmd_line_main calls RunQCborExample() first: it encodes a fixed engine-status # document with the definite-length API and decodes it back with the spiffy # interface. Both numbers are properties of the document, not of the machine -- # CBOR's encoding is deterministic -- so 154 bytes is an assertable constant and # Result 0 is QCBOR_SUCCESS. has qcbor/example-encode "Example: Definite Length Engine Encoded in 154 bytes" "$out" has qcbor/example-decode "Example: Spiffy Engine Decode Result: 0" "$out" # --- every test group, by name ------------------------------------------------ # # run_tests.c's table, in its own order. 71 entries, 70 distinct: upstream lists # EncodeLengthThirtyoneTest twice, which is why the summary says 71 and this list # has 70 names. BstrWrapErrorTest is the one absentee -- it has its own case # below. grp() { hasline "qcbor/t-$1" "$1 PASSED" "$out"; } # UsefulBuf: the buffer abstraction everything else is built on -- a pointer and # a length carried together, with the copy/compare/advance operations that make # QCBOR's no-allocation promise possible. for t in UBUTest_CopyUtil UOBTest_NonAdversarial TestBasicSanity \ UOBTest_BoundaryConditionsTest UBMacroConversionsTest UBUtilTests \ UIBTest_IntegerFormat; do grp "$t"; done # The encoder: integers, simple values, dates, raw items, arrays and maps to # depth, the AddX methods, bstr wrapping (a byte string whose content is itself # CBOR -- what COSE signs), and the error paths. for t in EncodeRawTest RTICResultsTest MapEncodeTest ArrayNestingTest1 \ ArrayNestingTest2 ArrayNestingTest3 EncodeDateTest SimpleValuesTest1 \ IntegerValuesTest1 AllAddMethodsTest BasicEncodeTest NestedMapTest \ BstrWrapTest BstrWrapNestTest CoseSign1TBSTest \ EncodeErrorTests SimpleValuesIndefiniteLengthTest1 \ EncodeLengthThirtyoneTest QCBORHeadTest; do grp "$t"; done # The decoder: the item walk, maps as arrays, deep and too-deep nesting, short # buffers, the failure corpus, sequences, peek and rewind, and the tag machinery. for t in EnterBstrTest IntegerConvertTest EnterMapTest EmptyMapsAndArraysTest \ NotWellFormedTests ParseMapAsArrayTest ParseSimpleTest \ DecodeFailureTests ParseTooDeepArrayTest ComprehensiveInputTest \ ParseMapTest ShortBufferParseTest ShortBufferParseTest2 \ ParseDeepArrayTest SimpleArrayTest IntegerValuesParseTest \ StringDecoderModeFailTest CBORSequenceDecodeTests IntToTests \ DecodeTaggedTypeTests PeekAndRewindTest ParseEmptyMapInMapTest \ BoolTest; do grp "$t"; done # Indefinite lengths -- CBOR's streaming shape, where an array/map/string is # opened without a count and closed with a break. Encoders that only handle the # definite form silently mis-parse these. for t in IndefiniteLengthNestTest IndefiniteLengthArrayMapTest \ NestedMapTestIndefLen IndefiniteLengthStringTest \ SpiffyIndefiniteLengthStringsTests; do grp "$t"; done # Tags and the tagged types built on them: big numbers (arbitrary-precision # integers as byte strings), epoch and text dates, and decimal fractions / # big floats, i.e. the exponent-and-mantissa pair. for t in BignumParseTest OptTagParseTest DateParseTest SpiffyDateDecodeTest \ ExponentAndMantissaDecodeTests ExponentAndMantissaDecodeFailTests \ ExponentAndMantissaEncodeTests; do grp "$t"; done # The string allocators: QCBOR can decode indefinite-length strings without # malloc by using a caller-supplied memory pool, which is the mechanism that # lets it run in the constrained places CBOR is chosen for. for t in AllocAllStringsTest MemPoolTest SetUpAllocatorTest; do grp "$t"; done # Floats. These five are the port's central claim: with # -DQCBOR_DISABLE_FLOAT_HW_USE the library keeps ALL of RFC 8949's float # support, because QCBOR's half-precision is shifts and masks rather than libm. # HalfPrecisionAgainstRFCCodeTest is the strongest of them -- it checks QCBOR's # encoder against the reference implementation printed in RFC 7049 itself -- and # DoubleAsSmallestTest is preferred serialization, the rule that a double must # be emitted in the shortest form that round-trips exactly. # # Worth knowing when reading a red here: HalfPrecisionDecodeBasicTests FAILS on # an i386 control build of this same source (returned -16, x87 excess precision) # and PASSES here. wasm32's f32/f64 are exactly IEEE-754 with no wider # intermediate format, so this arm is the more correct of the two. for t in HalfPrecisionDecodeBasicTests DoubleAsSmallestTest \ HalfPrecisionAgainstRFCCodeTest GeneralFloatEncodeTests \ GeneralFloatDecodeTests; do grp "$t"; done # --- the one red, pinned rather than worked around --------------------------- # # BstrWrapErrorTest fails, and it is an UPSTREAM QCBOR DEFECT ON 32-BIT TARGETS # rather than anything about this arm: an i386 control build of the identical # SRCREV, built with stock gcc -m32 on ordinary x86 Linux, fails it identically # and with the identical return code 301. The x86_64 control passes. # # What it is. The test opens 17 bstr wraps against a stack whose limit is # QCBOR_MAX_ARRAY_NESTING1 = 15, expecting QCBOR_ERR_ARRAY_NESTING_TOO_DEEP (4), # and gets QCBOR_ERR_BUFFER_TOO_SMALL (1) -- 300 + 1. The error IS produced # correctly: traced on the i386 control, uError is 4 from open 16 all the way # through close 16. The problem is what the 17 closes do to the nesting stack. # InsertCBORHead's usage guard is `if(me->uError == QCBOR_SUCCESS)`, so once an # error is latched the guard is SKIPPED and every close still runs # Nesting_Decrease -- and 17 closes against 15 pushes walk pCurrentNesting to # index -1 and then -2, below pArrays[0] and into the enclosing # QCBOREncodeContext. Nesting_GetStartPos then reads an insertion position out # of bounds, UsefulOutBuf latches its own error, and # QCBOREncode_GetErrorState converts that to QCBOR_ERR_BUFFER_TOO_SMALL -- # which its own comment says outright masks other errors. The read is out of # bounds on 64-bit too; there it happens to land on a benign value, so the test # passes by luck rather than by correctness. # # It is pinned in the inverted form 78-liboop.test established for an upstream # defect: green while the known-bad state holds, and LOUD the moment upstream # fixes it, because that is when this file needs editing. Nothing is worked # around -- the port ships upstream's code as upstream wrote it. case "$nl$out$nl" in *"${nl}BstrWrapErrorTest FAILED (returned 301)$nl"*) bst_ok qcbor/upstream-bstrwraperror-ilp32 ;; *"${nl}BstrWrapErrorTest PASSED$nl"*) bst_notok qcbor/upstream-bstrwraperror-ilp32 \ "BstrWrapErrorTest now PASSES -- upstream fixed the 32-bit nesting-stack under-run; move it into the encoder group list, expect rc=0 and '0 tests failed'" ;; *) bst_notok qcbor/upstream-bstrwraperror-ilp32 \ "BstrWrapErrorTest failed differently -- expected 'returned 301'; a new failure mode, not the known ILP32 defect" ;; esac # ...and nothing ELSE failed. The per-group cases above cannot see a group # upstream ADDS in a later SRCREV, and the summary only counts; this bounds the # whole run to the single known red. fails=$(printf '%s\n' "$out" | grep -c "FAILED") check_eq qcbor/exactly-one-red 1 "$fails" # --- the struct sizes qcbortest prints before the tests ----------------------- # # PrintSizesQCBOR exists upstream as a "keep them small" reminder, and on a port # it is free ABI evidence. Every value here is MEASURED on this target, and each # is smaller than the x86_64 control's because this is an ILP32 machine whose # structs carry pointers (136/176/208/312/56/16/16/9 there). All eight match an # i386 -m32 control of the same source exactly, so the wasm32 ABI agrees with # the ordinary ILP32 model for these types rather than having its own layout. # Pinned so a toolchain change to struct layout or pointer width is a red rather # than a silent ABI shift under callers that embed a QCBORDecodeContext by value. sz() { hasline "qcbor/size-$1" "sizeof($2) $3" "$out"; } sz nesting QCBORTrackNesting 132 sz enc-ctx QCBOREncodeContext 152 sz dec-nest QCBORDecodeNesting 200 sz dec-ctx QCBORDecodeContext 280 sz item QCBORItem 40 sz taglist-in QCBORTagListIn 8 sz taglist-out QCBORTagListOut 8 sz tagspec TagSpecification 9 # ============================================================================= # 2. The shipped payload. # ============================================================================= check qcbor/bin-executable test -x "$bin" hasfile qcbor/lib-present "$lib" check_out qcbor/lib-archive "!" head -c 8 "$lib" # The seven public headers, plus the one-line compatibility shim upstream keeps # at the top of inc/ (#include "qcbor/UsefulBuf.h") for callers written against # the pre-1.0 layout. The stock do_install ships both levels and this asserts it # stayed that way -- a -dev package that dropped qcbor_spiffy_decode.h would # still compile every consumer that uses only the item-at-a-time API, so the gap # would surface later, in someone else's build. for h in qcbor.h qcbor_common.h qcbor_private.h qcbor_encode.h qcbor_decode.h \ qcbor_spiffy_decode.h UsefulBuf.h; do hasfile "qcbor/hdr-$h" "$inc/$h" done hasfile qcbor/hdr-compat-shim "$compat" has qcbor/compat-shim-body '#include "qcbor/UsefulBuf.h"' "$(cat "$compat" 2>/dev/null)" # Static-only distro: wasmld has no stable -shared ABI on the HWJS-3 coro arm. # Upstream's Makefile does carry a libqcbor.so rule and an install_so target -- # they are simply not the default -- so a stray .so here would mean something # invoked them. noglob_ok() { _id=$1; shift; if [ ! -e "$1" ]; then bst_ok "$_id"; else bst_notok "$_id" "present: $1"; fi; } set -- /usr/lib/libqcbor.so*; noglob_ok qcbor/no-shared "$@" set -- /usr/lib/libqcbor.la; noglob_ok qcbor/no-libtool "$@" # ============================================================================= # 3. The archive: what is in it, and which float configuration built it. # ============================================================================= # # Flattened to printable text once. GNU grep suppresses matches inside data it # classifies as binary and busybox tr does not handle [:print:] under -c, so the # octal range is the formulation both greps and both trs agree on -- derived # empirically by 69-libb64.test and reused since. t=${TMPDIR:-/tmp}/bst-qcbor.$$ flat=$t.a tr -c '\40-\176\n' ' ' < "$lib" > "$flat" 2>/dev/null a=$(cat "$flat" 2>/dev/null) # Five translation units, one per source file the Makefile lists in QCBOR_OBJ. # A missing member is a partial compile that the link would only notice if # something referenced it. for m in UsefulBuf qcbor_encode qcbor_decode ieee754 qcbor_err_to_str; do has "qcbor/member-$m" "$m.o" "$a" done # --- the float configuration, read off the artifact --------------------------- # # The three configurations are distinguishable here and nowhere else on the # image, so this is where the port's central decision is pinned: # # full float (upstream default) IEEE754_* present, fenv/libm refs present # USEFULBUF_DISABLE_ALL_FLOAT IEEE754_* ABSENT, no fenv/libm refs # QCBOR_DISABLE_FLOAT_HW_USE IEEE754_* present, no fenv/libm refs <-- ours # # So the pair of assertions below is the whole statement: floats are compiled in, # and nothing in the library reaches for a floating-point environment this # machine does not have. (The first configuration cannot even be built here, but # a future toolchain that grew FE_* macros could make it buildable and it would # then be selected silently by dropping one line of the bbappend.) for s in IEEE754_HalfToDouble IEEE754_DoubleToHalf IEEE754_FloatToHalf \ IEEE754_FloatToSmallest IEEE754_DoubleToSmallestInternal; do has "qcbor/float-$s" "$s" "$a" done for s in fetestexcept feclearexcept; do hasnt "qcbor/no-fenv-$s" "$s" "$a" done # --- the public entry points -------------------------------------------------- # # A named sample across every layer of the API rather than a count: QCBOR # declares a great many of its functions `static inline` in the headers, so a # header-derived completeness check of the cjson kind would demand symbols that # correctly are not in the archive. These are the out-of-line ones, and between # them they cover encode, decode, spiffy decode, tags, big numbers, the # allocators, and the error-string table. for s in QCBOREncode_Init QCBOREncode_Finish QCBOREncode_FinishGetSize \ QCBOREncode_AddInt64 QCBOREncode_AddUInt64 QCBOREncode_AddBuffer \ QCBOREncode_AddDouble QCBOREncode_AddFloat QCBOREncode_AddType7 \ QCBOREncode_AddTag QCBOREncode_AddExponentAndMantissa \ QCBOREncode_OpenMapOrArray QCBOREncode_CloseMapOrArray \ QCBOREncode_OpenMapOrArrayIndefiniteLength \ QCBOREncode_CloseBstrWrap2 QCBOREncode_CancelBstrWrap \ QCBOREncode_EncodeHead \ QCBORDecode_Init QCBORDecode_Finish QCBORDecode_GetNext \ QCBORDecode_PeekNext QCBORDecode_Rewind QCBORDecode_VGetNext \ QCBORDecode_EnterBoundedMapOrArray QCBORDecode_ExitBoundedMapOrArray \ QCBORDecode_EnterBstrWrapped QCBORDecode_ExitBstrWrapped \ QCBORDecode_GetItemsInMap QCBORDecode_GetBool \ QCBORDecode_GetEpochDate QCBORDecode_GetEpochDays \ QCBORDecode_GetBignum QCBORDecode_GetDecimalFraction \ QCBORDecode_GetBigFloat QCBORDecode_GetNthTag \ QCBORDecode_SetMemPool QCBORDecode_SetUpAllocator \ qcbor_err_to_str \ UsefulBuf_Compare UsefulBuf_CopyOffset UsefulBuf_FindBytes \ UsefulBuf_IsValue UsefulOutBuf_Init UsefulOutBuf_InsertUsefulBuf \ UsefulOutBuf_OutUBuf UsefulOutBuf_CopyOut UsefulInputBuf_GetBytes; do has "qcbor/sym-$s" "$s" "$a" done rm -f "$flat"