. "$BST_LIB" # BST-TIMEOUT: 300 # ~90 cases; ONE probe start (a 2.4 MB wasm image) and a # # handful of greps over two archives are the whole cost. # # usbredir 0.13.0 (meta-networking recipes-support/spice) on wasm32-hwjs glibc. # # usbredir is two libraries and one tool. libusbredirparser is the WIRE CODEC -- # it serialises a usbredir packet to bytes and parses bytes back into a callback, # and it never opens a bus, an fd or a socket. libusbredirhost sits on top of it # and libusb-1.0 and is the half that does drive a device. usbredirect(1) is the # host-side agent that carries the stream over a socket. # # NO USB DEVICE IS ATTACHED AND NONE IS SIMULATED. That is a property of what is # being asserted rather than a gap worked around: the codec is exercised END TO # END, by running two parsers in one process and moving the bytes between them # by memcpy (data/usbredir-probe). The host half needs a bus, so it is asserted # as a shipped artifact and as archive symbols, and nothing here claims it was # driven. # # usbredirect(1) IS NOT ON THIS IMAGE, AND ITS ABSENCE IS ASSERTED RATHER THAN # ASSUMED. It is the only part of the package that binds glib: it takes # gio-unix-2.0 for its GSocket transport, and GIO's socket layer carries the # coroutinize refusals that also hold libproxy's proxy(1) and shared-mime-info's # tree-magic off this image (brintos/hwjs-cc#15). So -Dtools=disabled is set in # the bbappend, no runtime package is produced at all, and the two legs that # would have driven the tool are SKIPS naming that issue -- not reds, because the # tool is legitimately absent, and not silence, because a gap nobody records is # one nobody closes. # # BRINTOS-WORKAROUND: brintos/hwjs-cc#15 revisit -- when the pass can lower GIO's # socket frames the bbappend takes tools/ back, and the two skips below become # real cases against usbredirect --version and against a device that is not # there. # # --------------------------------------------------------------------------- # WHY THIS SUITE READS FILES ONCE AND MATCHES WITH `case` # # Files are read into shell variables and every text assertion is a `case` glob # against them -- no fork per case -- in the shape 99-hidapi established. On the # wasm32-hwjs runtime the forking IS the cost. # --------------------------------------------------------------------------- W=${TMPDIR:-/tmp}/bst-usbredir.$$ mkdir -p "$W" 2>/dev/null incd=/usr/include libd=/usr/lib pcd=/usr/lib/pkgconfig nl=' ' # hasfile -- ok iff is a non-empty regular file. # nofile -- ok iff does not exist. # has -- ok iff contains the literal . # hasline -- ok iff carries as a WHOLE line. # Substring matching would let # usbredirparser_init satisfy a lookup for # usbredirparser_ini. hasfile() { if [ -s "$2" ]; then bst_ok "$1"; else bst_notok "$1" "absent or empty: $2"; fi; } nofile() { if [ ! -e "$2" ]; then bst_ok "$1"; else bst_notok "$1" "present: $2"; fi; } has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "not found: $2" ;; esac; } hasline() { case "$nl$3$nl" in *"$nl$2$nl"*) bst_ok "$1" ;; *) bst_notok "$1" "not in archive: $2" ;; esac; } # kv : ok iff the probe printed exactly that line. kv() { case "$nl$probeout$nl" in *"$nl$2$nl"*) bst_ok "$1" ;; *) bst_notok "$1" "probe did not print $2 (got: $(_bst_1line "$(sed -n "s/^${2%%=*}=//p" "$W/probe.out" 2>/dev/null)"))" ;; esac; } # --- the four installed headers ----------------------------------------------- # # usbredirproto.h is the wire format itself; usbredirparser.h and # usbredirfilter.h are the codec's API; usbredirhost.h is the bus-driving half. # All four are in ${includedir} with no package subdirectory, which is what the # .pc files' bare `-I${includedir}` assumes. hasfile usbredir/header-proto "$incd/usbredirproto.h" hasfile usbredir/header-parser "$incd/usbredirparser.h" hasfile usbredir/header-filter "$incd/usbredirfilter.h" hasfile usbredir/header-host "$incd/usbredirhost.h" # The protocol version is pinned in the header the archives are compiled from, # so asserting it here pins the header to the wire format rather than to the .pc. protoh=$(cat "$incd/usbredirproto.h" 2>/dev/null) has usbredir/proto-version '#define USBREDIR_VERSION 0x000701' "$protoh" has usbredir/proto-caps-size '#define USB_REDIR_CAPS_SIZE 1' "$protoh" has usbredir/proto-cap-version 'usb_redir_cap_connect_device_version' "$protoh" parserh=$(cat "$incd/usbredirparser.h" 2>/dev/null) has usbredir/hdr-decl-create 'struct usbredirparser *usbredirparser_create(void);' "$parserh" has usbredir/hdr-decl-do-read 'int usbredirparser_do_read(struct usbredirparser *parser);' "$parserh" has usbredir/hdr-serialize-magic '#define USBREDIRPARSER_SERIALIZE_MAGIC 0x55525031' "$parserh" # --- pkg-config --------------------------------------------------------------- # # TWO .pc FILES AND TWO DIFFERENT SHAPES. The parser's carries no Requires at # all -- it depends on nothing -- while the host's names both libusbredirparser # and libusb-1.0, which is what a static consumer of the host half must link. # Asserting the parser's EMPTINESS is half the point: it is what says a consumer # of the codec alone drags in neither libusb nor glib. hasfile usbredir/pc-parser "$pcd/libusbredirparser-0.5.pc" hasfile usbredir/pc-host "$pcd/libusbredirhost.pc" pcp=$(cat "$pcd/libusbredirparser-0.5.pc" 2>/dev/null) has usbredir/pc-parser-name 'Name: libusbredirparser-0.5' "$pcp" has usbredir/pc-parser-version 'Version: 0.13.0' "$pcp" has usbredir/pc-parser-libs '-lusbredirparser' "$pcp" has usbredir/pc-parser-libdir 'libdir=${prefix}/lib' "$pcp" has usbredir/pc-parser-cflags 'Cflags: -I${includedir}' "$pcp" case "$pcp" in *Requires:*) bst_notok usbredir/pc-parser-no-requires "the codec's .pc grew a Requires line" ;; *) bst_ok usbredir/pc-parser-no-requires ;; esac pch=$(cat "$pcd/libusbredirhost.pc" 2>/dev/null) has usbredir/pc-host-name 'Name: libusbredirhost' "$pch" has usbredir/pc-host-version 'Version: 0.13.0' "$pch" has usbredir/pc-host-libs '-lusbredirhost' "$pch" has usbredir/pc-host-requires 'Requires: libusbredirparser-0.5, libusb-1.0' "$pch" # --- the two archives --------------------------------------------------------- pa=$libd/libusbredirparser.a ha=$libd/libusbredirhost.a hasfile usbredir/archive-parser "$pa" hasfile usbredir/archive-host "$ha" check_eq usbredir/archive-parser-format '!' "$(head -c 7 "$pa" 2>/dev/null)" check_eq usbredir/archive-host-format '!' "$(head -c 7 "$ha" 2>/dev/null)" # --- static-only guards ------------------------------------------------------- # # The distro builds this arm static-only and the bbappend sets # -Ddefault_library=static for it, because a SHARED build of the host half # FATALs on libusb1's one thread-local. A stray .so here would mean that # selection was lost and the link that follows it would not be reached at all. noglob_ok() { _id=$1; shift if [ ! -e "$1" ]; then bst_ok "$_id"; else bst_notok "$_id" "present: $1"; fi } set -- $libd/libusbredirparser.so*; noglob_ok usbredir/no-shared-parser "$@" set -- $libd/libusbredirhost.so*; noglob_ok usbredir/no-shared-host "$@" # --- the tool is absent, and so is its whole runtime package ------------------ nofile usbredir/no-usbredirect /usr/bin/usbredirect nofile usbredir/no-usbredirect-man /usr/share/man/man1/usbredirect.1 # usbredirtestclient is upstream `install: false` -- it is built and never # installed, here and on every other image -- so it must not reach this one # under any name either. nofile usbredir/no-testclient /usr/bin/usbredirtestclient bst_skip usbredir/usbredirect-version \ "usbredirect(1) is not on this image: it binds gio-unix-2.0 and GIO's socket layer is refused by the coroutinize pass -- brintos/hwjs-cc#15, see the bbappend" bst_skip usbredir/usbredirect-no-device \ "no usbredirect(1) to ask for an absent device -- brintos/hwjs-cc#15" # --- completeness: every declared entry point is in each archive -------------- # # Flatten each archive's printable runs to one token per line first: a raw # `grep -o` over binary data reports "Binary file matches" instead of the # matches, which would silently under-count and read as a partial compile. flatp=$W/parser-flat flath=$W/host-flat namep=$W/parser-names nameh=$W/host-names ( tr -c '[:print:]' '\n' < "$pa" > "$flatp" 2>/dev/null ) ( tr -c '[:print:]' '\n' < "$ha" > "$flath" 2>/dev/null ) # The declaration lines only: the callback typedefs spell the same prefix inside # `(*usbredirparser_hello)`, and taking those would count names no archive can # define. Requiring the `(` to follow the identifier directly is what separates # the two. grep -hE '^[[:space:]]*(int|void|char|uint64_t|struct)' \ "$incd/usbredirparser.h" "$incd/usbredirfilter.h" 2>/dev/null \ | grep -v typedef \ | grep -oE '\busbredir(parser|filter)_[a-z0-9_]+ *\(' \ | sed 's/ *(//' | sort -u > "$namep" grep -hE '^[[:space:]]*(int|void|char|uint64_t|struct)' \ "$incd/usbredirhost.h" 2>/dev/null \ | grep -v typedef \ | grep -oE '\busbredirhost_[a-z0-9_]+ *\(' \ | sed 's/ *(//' | sort -u > "$nameh" # END-OF-RUN ANCHORED, one batched pass -- and the anchor is the whole point. # The archive is an ar of LLVM IR bitcode whose string table concatenates symbol # names with NO separator, so a plain `grep -oFf` reports any needle that is # merely a PREFIX of a token that is present. Anchoring each needle at # end-of-run is the form 99-hidapi measured correct on two such archives. patp=$W/parser-pats path=$W/host-pats sed 's/$/$/' "$namep" > "$patp" sed 's/$/$/' "$nameh" > "$path" foundp=$(grep -oEf "$patp" "$flatp" 2>/dev/null | sort -u) foundh=$(grep -oEf "$path" "$flath" 2>/dev/null | sort -u) ndeclp=$(grep -c . "$namep"); nfoundp=$(printf '%s' "$foundp" | grep -c .) ndeclh=$(grep -c . "$nameh"); nfoundh=$(printf '%s' "$foundh" | grep -c .) # The floors guard the check itself: headers that lost their declarations, or an # extraction that silently matched nothing, would otherwise pass vacuously with # zero names to look up. 0.13.0 declares 52 across the codec's two headers and # 11 in the host's. if [ "$ndeclp" -ge 45 ] && [ "$nfoundp" -eq "$ndeclp" ]; then bst_ok usbredir/parser-sym-complete else bst_notok usbredir/parser-sym-complete "declared=$ndeclp found=$nfoundp"; fi if [ "$ndeclh" -ge 10 ] && [ "$nfoundh" -eq "$ndeclh" ]; then bst_ok usbredir/host-sym-complete else bst_notok usbredir/host-sym-complete "declared=$ndeclh found=$nfoundh"; fi # THE OVER-REPORT CONTROL, against the same archive and the same matcher. Six # names that do not exist, each a prefix of one that does. Plain -oFf reports # all six; the anchored form must report none. Without this case the count above # could be inflated by the matcher and nothing here would say so. fakef=$W/parser-fake printf '%s\n' 'usbredirparser_creat$' 'usbredirparser_ini$' 'usbredirparser_destro$' \ 'usbredirparser_do_rea$' 'usbredirfilter_chec$' 'usbredirfilter_verif$' > "$fakef" nfake=$(grep -oEf "$fakef" "$flatp" 2>/dev/null | sort -u | grep -c .) if [ "$nfake" -eq 0 ]; then bst_ok usbredir/parser-sym-no-prefix-inflation else bst_notok usbredir/parser-sym-no-prefix-inflation "matcher claimed $nfake non-existent names"; fi # A named sample across the layers as well, so a failure names the layer that # broke rather than only reporting a count. for s in usbredirparser_create usbredirparser_init usbredirparser_destroy \ usbredirparser_do_read usbredirparser_do_write \ usbredirparser_has_data_to_write usbredirparser_have_cap \ usbredirparser_peer_has_cap usbredirparser_caps_set_cap \ usbredirparser_send_device_connect usbredirparser_send_device_disconnect \ usbredirparser_send_reset usbredirparser_send_bulk_packet \ usbredirparser_serialize usbredirparser_unserialize \ usbredirfilter_string_to_rules usbredirfilter_rules_to_string \ usbredirfilter_check usbredirfilter_verify usbredirfilter_free; do hasline "usbredir/parser-sym-$s" "$s" "$foundp" done for s in usbredirhost_open usbredirhost_open_full usbredirhost_close \ usbredirhost_set_device usbredirhost_read_guest_data \ usbredirhost_write_guest_data usbredirhost_check_device_filter; do hasline "usbredir/host-sym-$s" "$s" "$foundh" done # The host half's whole reason for existing is that it binds libusb; finding # libusb's entry points in its archive is what says the two were compiled # against each other rather than merely built beside each other. # # AND THE ONES IT MUST NOT REFERENCE ARE ASSERTED TOO. usbredirhost_open takes a # libusb_context and an already-open libusb_device_handle from its caller # (usbredirhost.h), so it never initialises the library and never opens a # device: libusb_init and libusb_open are absent from this archive BY DESIGN. # Their appearance would mean the host half had grown an owner's relationship to # the bus that its API says the caller keeps -- which is also why nothing on # this image, with no bus to open, could call it. hasnot() { case "$3" in *"$2"*) bst_notok "$1" "unexpectedly present: $2" ;; *) bst_ok "$1" ;; esac; } ah=$(cat "$flath" 2>/dev/null) has usbredir/host-binds-libusb-claim 'libusb_claim_interface' "$ah" has usbredir/host-binds-libusb-xfer 'libusb_submit_transfer' "$ah" has usbredir/host-binds-libusb-desc 'libusb_get_device_descriptor' "$ah" hasnot usbredir/host-does-not-init-libusb 'libusb_init' "$ah" hasnot usbredir/host-does-not-open-libusb 'libusb_open' "$ah" # --- the probe ---------------------------------------------------------------- # # The one thing the shipped artifact cannot answer is whether the codec still # ENCODES AND DECODES correctly after the coroutinize pass has rewritten every # function in it. That needs a consumer, and this image has no C compiler, so # the consumer is cross-linked at port time against this very archive and # shipped in data/. See src/usbredir_probe.c. check usbredir/probe-shipped test -f "$BST_DATA/usbredir-probe" probe=$W/usbredir-probe if cp "$BST_DATA/usbredir-probe" "$probe" 2>/dev/null && chmod +x "$probe" 2>/dev/null; then "$probe" > "$W/probe.out" 2>&1 prc=$? if [ $prc -eq 0 ]; then bst_ok usbredir/probe-rc else bst_notok usbredir/probe-rc "probe exited $prc: $(_bst_1line "$(tail -n 1 "$W/probe.out" 2>/dev/null)")"; fi probeout=$(cat "$W/probe.out" 2>/dev/null) # The compiled-in constants, read from C rather than from the header text. kv usbredir/probe-proto-version 'proto-version=0x000701' kv usbredir/probe-serialize-magic 'serialize-magic=0x55525031' kv usbredir/probe-caps-size 'caps-size=1' # 10 bytes: speed, class, subclass, protocol (1 each) + vendor, product, # bcdDevice (2 each). ATTR_PACKED, so this is the wire size and not the # compiler's preferred layout -- a padded struct would report 12. kv usbredir/probe-dc-header-size 'device-connect-header-size=10' kv usbredir/probe-create-a 'create-a=1' kv usbredir/probe-create-b 'create-b=1' # The caps this side set, and the one it deliberately did not. kv usbredir/probe-cap-version 'a-have-cap-version=1' kv usbredir/probe-cap-filter 'a-have-cap-filter=1' kv usbredir/probe-cap-unset 'a-have-cap-bulk-streams=0' # The hello exchange, both ways. Before it, neither side knows the other's # caps; after it, both do -- and each recovered the OTHER's version string, # which is what says the bytes crossed rather than each side reading its own. kv usbredir/probe-peer-caps-before 'a-peer-caps-before=0' kv usbredir/probe-read-hello-a-b 'read-hello-a-to-b=0' kv usbredir/probe-read-hello-b-a 'read-hello-b-to-a=0' kv usbredir/probe-peer-caps-a 'a-peer-caps-after=1' kv usbredir/probe-peer-caps-b 'b-peer-caps-after=1' kv usbredir/probe-peer-cap-a 'a-peer-has-cap-version=1' kv usbredir/probe-peer-cap-b 'b-peer-has-cap-version=1' kv usbredir/probe-hello-seen-a 'a-hello-seen=1' kv usbredir/probe-hello-seen-b 'b-hello-seen=1' kv usbredir/probe-hello-version-a 'a-hello-version=usbredir-probe-B 0.13.0' kv usbredir/probe-hello-version-b 'b-hello-version=usbredir-probe-A 0.13.0' # The device-connect packet, field by field. Every value is asymmetric in # both bytes, so a byte-swapped or truncated round trip cannot pass. kv usbredir/probe-queued 'a-queued=1' kv usbredir/probe-read-dc 'read-device-connect=0' kv usbredir/probe-dc-seen 'b-device-connect-seen=1' kv usbredir/probe-dc-speed 'b-speed=2' kv usbredir/probe-dc-class 'b-device-class=0x09' kv usbredir/probe-dc-subclass 'b-device-subclass=0x01' kv usbredir/probe-dc-protocol 'b-device-protocol=0x02' kv usbredir/probe-dc-vendor 'b-vendor-id=0x1d6b' kv usbredir/probe-dc-product 'b-product-id=0x0002' # device_version_bcd rides in the packet ONLY because both sides advertised # usb_redir_cap_connect_device_version; without the cap usbredirparser sends # the shorter _no_device_version header and this field would not exist. kv usbredir/probe-dc-bcd 'b-device-version-bcd=0x0413' # One packet each way beyond the one that carries fields, so the type # dispatch is asserted on more than a single case -- and so the # guest-to-host direction is exercised at all. kv usbredir/probe-read-disconnect 'read-device-disconnect=0' kv usbredir/probe-other-b 'b-other-packets=1' kv usbredir/probe-read-reset 'read-reset=0' kv usbredir/probe-other-a 'a-other-packets=1' # THE ROLE FLAG IS ENFORCED, AND THAT IS ASSERTED RATHER THAN ASSUMED. # reset travels guest-to-host, so the side init'd with # usbredirparser_fl_usb_host must be REFUSED it: a logged error, and nothing # queued. Without this pair the flag could be ignored entirely and every # case above would still pass. kv usbredir/probe-wrong-direction-logged 'a-wrong-direction-log-errors=1' kv usbredir/probe-wrong-direction-queued 'a-wrong-direction-queued=0' # A read with nothing to read reports "would block" (0), not an error -- # the return a caller's poll loop is written against. kv usbredir/probe-read-empty 'read-empty=0' # No side logged an error outside the one the direction case provoked, and # neither write path ran out of memory. A probe that quietly OOM'd would # otherwise round-trip nothing and still print every field as zero. kv usbredir/probe-no-log-errors-b 'b-log-errors=0' kv usbredir/probe-no-oom-a 'a-write-oom=0' kv usbredir/probe-no-oom-b 'b-write-oom=0' # --- the filter arm, which is pure string and integer work --------------- kv usbredir/probe-filter-parse 'filter-parse-rc=0' kv usbredir/probe-filter-count 'filter-rule-count=2' kv usbredir/probe-filter-r0class 'filter-rule0-class=3' kv usbredir/probe-filter-r0allow 'filter-rule0-allow=0' kv usbredir/probe-filter-r1class 'filter-rule1-class=-1' kv usbredir/probe-filter-r1allow 'filter-rule1-allow=1' kv usbredir/probe-filter-verify 'filter-verify-rc=0' # Both verdicts, from the same rule set: the hub is allowed by the catch-all # and the keyboard is denied by the class rule ahead of it. Asserting only # the allow would pass just as well against a filter that allows everything. kv usbredir/probe-filter-allow 'filter-check-hub=0' kv usbredir/probe-filter-deny 'filter-check-hid-is-eperm=1' kv usbredir/probe-filter-string 'filter-roundtrip=0x03,-1,-1,-1,0|-1,-1,-1,-1,1' kv usbredir/probe-filter-ident 'filter-roundtrip-identical=1' # The probe reached its last line: without this, a probe killed halfway # would leave every case above it green and say nothing. kv usbredir/probe-done 'done=1' else bst_notok usbredir/probe-stage "cannot stage $BST_DATA/usbredir-probe" fi rm -rf "$W"