. "$BST_LIB" require sethdlc sethdlc/present # sethdlc 1.18 (meta-networking recipes-connectivity) -- Krzysztof Halasa's # configuration tool for the kernel's generic HDLC layer. It is one 705-line C # file and one ioctl: everything it does is SIOCWANDEV (plus SIOCDEVPRIVATE for # the `private` arm) on an AF_INET datagram socket, with argv parsed into the # if_settings union the call carries. # # THIS IMAGE EXPOSES NO HDLC DEVICE, and that is a property of the kernel rather # than of the port. wasm32_defconfig turns on CONFIG_NET, CONFIG_INET and # CONFIG_UNIX and stops there -- there is no CONFIG_NETDEVICES, so no # drivers/net/wan, no CONFIG_HDLC, and the only interface on the machine is the # loopback device the net core registers itself. A netdev with no # ndo_siocwandev is refused by net/core/dev_ioctl.c::dev_siocwandev() with # -EOPNOTSUPP, which is what every ioctl-reaching case below sees. # # So the suite splits three ways, exactly as the ticket asks: # # ASSERTED FOR REAL, needing no HDLC device -- the usage/version banner, the # argument validation that runs BEFORE any ioctl, the documented status for an # unknown interface, and the routing of every documented PROTOCOL, PHYSICAL, # CLOCK, ENCODING, PARITY and LMI keyword. The last of those is worth being # precise about: what proves the parser routed a mode correctly is WHICH arm's # ioctl it reached, and each arm names itself in its own refusal string # ("Unable to set Cisco HDLC protocol information", "Unable to create PVC", # ...). Those strings are produced by the parse, not by the hardware, so they # are a real assertion on a machine with no HDLC device -- a mode that fell # into the wrong arm, or fell through to usage, reads differently and reds. # # PROBED, then skipped -- reading and setting the modem port parameters, the # two things that genuinely need a device to talk to. The probe is sethdlc's # OWN operation (a bare `sethdlc IFACE`, which is the IF_GET_IFACE / # IF_GET_PROTO read), run over every interface the machine has, cross-read # against each interface's ARPHRD type in sysfs. A probe that fails IS the # port failing rather than a proxy for it, and the skip reason carries what # every reading answered rather than only the conclusion. # # PINNED AS BUILD FACTS -- three of sethdlc's features are #ifdef'd on uapi # macros (IF_PROTO_HDLC_ETH, IF_PROTO_FR_ETH_PVC, LMI_CISCO) and silently # vanish from both the banner and the parser when the headers this arm # compiles against do not carry them. That is a genuine per-arch question, and # the banner is where it is observable, so it is asserted there. # # Every kernel-independent expectation below was executed against sethdlc 1.18 # built from the pinned tarball on an x86-64 host before it was written down, rc # and output compared. The ioctl-reaching ones were too: lo on x86-64 has no # ndo_siocwandev either, so it takes the identical -EOPNOTSUPP path and produced # the identical strings. # --- the package ------------------------------------------------------------- check sethdlc/path test -x /usr/bin/sethdlc check_out sethdlc/on-path "/usr/bin/sethdlc" command -v sethdlc # _case : ok iff exits AND its # combined output contains . bst.sh has check_out for the rc-0 form and # check_rc for the status alone; almost every assertion here needs both halves, # because sethdlc answers nearly everything with status 1 and the whole of the # information is in the string. _case() { _cid=$1; _cwant=$2; _cn=$3; shift 3 _co=$("$@" 2>&1); _cr=$? case "$_co" in *"$_cn"*) _ch=1 ;; *) _ch=0 ;; esac if [ "$_cr" -eq "$_cwant" ] && [ "$_ch" -eq 1 ]; then bst_ok "$_cid" elif bst_execfail "$_co"; then bst_unrun "$_cid" "$_co" else bst_notok "$_cid" "rc=$_cr want=$_cwant needle:'$_cn' got:'$(_bst_1line "$_co")'"; fi } # --- the usage / version banner ---------------------------------------------- # There is NO --version flag and no --help: usage() is the whole of both, it is # reached by running sethdlc with no arguments at all, and it prints to STDERR # and exits 0. Each of those three is asserted separately -- a port that sent # the banner to stdout, or exited non-zero on no arguments, would still satisfy # a single "does it print its version" case. _case sethdlc/usage-rc 0 "Usage: sethdlc INTERFACE" sethdlc _case sethdlc/version-text 0 "sethdlc version 1.18" sethdlc _case sethdlc/usage-copyright 0 "Krzysztof Halasa" sethdlc _uo=$(sethdlc 2>/dev/null); _ue=$(sethdlc 2>&1 >/dev/null) if [ -z "$_uo" ]; then bst_ok sethdlc/usage-stdout-empty else bst_notok sethdlc/usage-stdout-empty "usage() writes to stdout: '$(_bst_1line "$_uo")'"; fi case "$_ue" in *"sethdlc version 1.18"*) bst_ok sethdlc/usage-on-stderr ;; *) bst_notok sethdlc/usage-on-stderr "banner not on stderr: '$(_bst_1line "$_ue")'" ;; esac # The four usage lines that are #ifdef'd on uapi macros. sethdlc.c compiles the # hdlc-eth protocol only under IF_PROTO_HDLC_ETH, the `ether` PVC arm only under # IF_PROTO_FR_ETH_PVC, and the cisco LMI only under LMI_CISCO -- all three from # linux/hdlc/ioctl.h and linux/if.h, pulled in through the sysroot's uapi # headers. A header set that dropped any of them would take the corresponding # arm out of the PARSER with no error anywhere, and the banner is the one place # that shows it. (linux/hdlc.h itself carries only HDLC_MAX_MTU/MRU; the macros # and the GENERIC_HDLC_VERSION == 4 compile-time gate arrive via linux/if.h, # which is why the source's include order matters and why this is worth pinning # from the outside.) _case sethdlc/proto-table-hdlc-eth 0 "hdlc-eth [ENCODING] [PARITY]" sethdlc _case sethdlc/pvc-table-ether 0 "create | delete [ether] DLCI" sethdlc _case sethdlc/lmi-table-cisco 0 "ansi | ccitt | cisco" sethdlc _case sethdlc/encoding-table 0 "nrz | nrzi | fm-mark | fm-space | manchester" sethdlc _case sethdlc/parity-table 0 "no-parity | crc16 | crc16-pr0 | crc16-itu | crc16-itu-pr0 | crc32-itu" sethdlc _case sethdlc/physical-table 0 "v24 | v35 | x21 | e1 | t1" sethdlc _case sethdlc/clock-table 0 "int [rate RATE] | ext | txint [rate RATE] | txfromrx" sethdlc # --- the documented status for an unknown interface -------------------------- # `sethdlc IFACE` with no further argument is show_port(), whose first act is the # IF_GET_IFACE read. __dev_get_by_name() fails for a name no interface carries, # so the ioctl returns -ENODEV, and show_port's non-EINVAL arm prints and # exits 1. # # THE MESSAGE IS ON STDOUT, not stderr, and that is not a slip in this suite: it # is printf(3) inside show_port(), while every OTHER diagnostic sethdlc emits # goes through error(), which is fprintf(stderr). A port that "fixed" the stream # would break every script that reads a port's settings. Both halves are pinned. _case sethdlc/unknown-iface-rc 1 "No such device" sethdlc bstnodev0 _case sethdlc/unknown-iface-msg 1 "bstnodev0: unable to get interface information:" sethdlc bstnodev0 _no=$(sethdlc bstnodev0 2>/dev/null) case "$_no" in *"unable to get interface information"*) bst_ok sethdlc/unknown-iface-stdout ;; *) bst_notok sethdlc/unknown-iface-stdout "not on stdout: '$(_bst_1line "$_no")'" ;; esac # --- argument validation, all of it before any ioctl ------------------------- # Every case in this block is decided by the parser alone: sethdlc exits from # error() without ever issuing SIOCWANDEV, so none of them depends on what this # kernel does or does not have. The interface name is only a string here -- lo # is used because it exists, but bstnodev0 gives the identical result. # # The two shapes are match()'s numeric arm -- sscanf("%u%c") must consume the # whole token, then the value must fall inside the arm's documented range -- and # checktab()'s keyword arm, which falls through to "Invalid parameter" naming the # token it could not place. `10x` is the case that separates them: it parses as a # number and is still rejected, because the %c catches the tail. _case sethdlc/bad-clock 1 "lo: Invalid clock type" sethdlc lo clock bogus _case sethdlc/range-clock-rate 1 "Parameter out of range [1 - 4294967295]: 0" sethdlc lo clock int rate 0 _case sethdlc/range-cisco-interval 1 "Parameter out of range [1 - 100]: 0" sethdlc lo cisco interval 0 _case sethdlc/range-cisco-timeout 1 "Parameter out of range [1 - 100]: 101" sethdlc lo cisco timeout 101 _case sethdlc/range-fr-t391 1 "Parameter out of range [1 - 1000]: 1001" sethdlc lo fr lmi ansi t391 1001 _case sethdlc/range-dlci 1 "Parameter out of range [0 - 1023]: 2000" sethdlc lo create 2000 _case sethdlc/nonnumeric 1 "Invalid parameter: abc" sethdlc lo cisco interval abc _case sethdlc/trailing-garbage 1 "Invalid parameter: 10x" sethdlc lo cisco interval 10x _case sethdlc/bad-encoding 1 "Invalid parameter: badenc" sethdlc lo hdlc badenc _case sethdlc/bad-cisco-keyword 1 "Invalid parameter: bogus" sethdlc lo cisco bogus _case sethdlc/bad-lmi 1 "Invalid LMI type: bogus" sethdlc lo fr lmi bogus # error() prefixes every diagnostic with the interface name and writes to # stderr. Both are asserted once, on one representative validation failure, # rather than folded into each case above -- a change to the prefix or the # stream is one finding, not eleven. _vo=$(sethdlc lo cisco interval 0 2>/dev/null) if [ -z "$_vo" ]; then bst_ok sethdlc/errors-on-stderr else bst_notok sethdlc/errors-on-stderr "validation error reached stdout: '$(_bst_1line "$_vo")'"; fi # A token that matches no arm at all is NOT an error: set_iface(), set_proto(), # set_pvc() and private() each decline it, main() falls off the end and calls # usage(), which exits 0. That is a real and easily-lost property -- a shell # script testing `sethdlc $if $mode` for success gets 0 for a mode that was # never applied -- so it is pinned rather than treated as a bug to assert away. _case sethdlc/unmatched-falls-to-usage 0 "Usage: sethdlc INTERFACE" sethdlc lo bogus # `private` takes no argument; one extra token makes set_pvc/private decline and # takes the same fall-through, which is what separates this from the real # SIOCDEVPRIVATE call asserted further down. _case sethdlc/private-extra-arg-usage 0 "Usage: sethdlc INTERFACE" sethdlc lo private extra # --- probe: is there an HDLC device on this machine? ------------------------- # The probe is `sethdlc IFACE` itself -- the IF_GET_IFACE read, byte for byte # what the port's own "show the port settings" mode issues -- run over every # interface in sysfs. It exits 0 when the device answered the read (or answered # EINVAL, which show_port() treats as "no settings yet" and steps past), and # non-zero when the interface cannot be read at all. That is exactly the # question "does an HDLC device exist here", asked with the port's own code. # # The cross-read is each interface's ARPHRD type. A generic HDLC device # registers as one of ARPHRD_X25 (271), ARPHRD_PPP (512), ARPHRD_HDLC/CISCO # (513), ARPHRD_LAPB (516) or ARPHRD_FRAD (770), depending on the protocol set # on it; loopback is ARPHRD_LOOPBACK (772). It is corroboration and not the # decision -- an hdlc-eth device registers as ARPHRD_ETHER (1) and is # indistinguishable from an ordinary NIC by type alone -- which is why the # ioctl reading is the one that decides and this one only has to agree. _hdlcif= _ifs= _hdlctype= for _d in /sys/class/net/*; do [ -d "$_d" ] || continue _n=${_d##*/} _t=unknown [ -r "$_d/type" ] && _t=$(cat "$_d/type" 2>/dev/null) _po=$(sethdlc "$_n" 2>&1); _pr=$? _ifs="$_ifs $_n(type=$_t,rc=$_pr)" [ "$_pr" -eq 0 ] && [ -z "$_hdlcif" ] && _hdlcif=$_n case "$_t" in 271|512|513|516|770) _hdlctype="$_hdlctype $_n" ;; esac done [ -n "$_ifs" ] || _ifs=" (no interfaces in /sys/class/net)" # The probe passes in both self-consistent worlds and reds only on a # contradiction. A kernel that grows a WAN driver is not a regression of this # port, so "a device answered" is a pass exactly as "nothing HDLC anywhere" is; # what is a genuine finding is sysfs advertising an HDLC-family interface that # sethdlc cannot read, because then something other than the absence of the # driver is refusing and the two skips below would blame the wrong cause. if [ -n "$_hdlcif" ]; then bst_ok sethdlc/probe-hdlc-device elif [ -z "$_hdlctype" ]; then bst_ok sethdlc/probe-hdlc-device else bst_notok sethdlc/probe-hdlc-device \ "sysfs advertises HDLC-family interface(s)$_hdlctype but none answered IF_GET_IFACE --$_ifs" fi # The interface the routing cases below are aimed at. Its identity does not # matter to them -- what they assert is which parser arm the ioctl came from -- # but a name that exists is what makes the refusal come from dev_siocwandev() # rather than from the earlier ENODEV lookup. _if=lo if [ ! -d /sys/class/net/lo ]; then for _d in /sys/class/net/*; do if [ -d "$_d" ]; then _if=${_d##*/}; break; fi done fi # --- reading and setting the modem port parameters --------------------------- # The two operations that genuinely need a device. Skipped only on the probe # above, and the skip carries every reading it took. if [ -z "$_hdlcif" ]; then _why="no HDLC device on this image -- CONFIG_NETDEVICES is not set in wasm32_defconfig, so there is no drivers/net/wan and no CONFIG_HDLC; no interface answered sethdlc's own IF_GET_IFACE read and none carries an HDLC-family ARPHRD type. Interfaces seen:$_ifs" bst_skip sethdlc/port-read "$_why" bst_skip sethdlc/port-set "$_why" else # Unreached on this image; kept correct for the day a WAN device appears. # The read prints "IFACE: ... clock ..." and a protocol line; the # set is a round trip -- put raw HDLC with a named encoding and parity on # the port, then read it back and require the same two words. _case sethdlc/port-read 0 "$_hdlcif: " sethdlc "$_hdlcif" if sethdlc "$_hdlcif" hdlc nrzi crc16-itu >/dev/null 2>&1; then _case sethdlc/port-set 0 "protocol hdlc nrzi crc16-itu" sethdlc "$_hdlcif" else bst_notok sethdlc/port-set "setting protocol hdlc nrzi crc16-itu on $_hdlcif failed" fi fi # --- the parser routes every documented mode to its own ioctl ---------------- # What each of these asserts is the ARM, not the outcome: the refusal string is # written by the parser's own error() call site, so "Unable to set Cisco HDLC # protocol information" can only be reached by set_proto_cisco(), and a `cisco` # that had fallen into set_proto_hdlc(), into set_pvc(), or through to usage() # reads differently and reds. On a machine with an HDLC device these same modes # would succeed instead, so the block is taken only in the probed-absent world # and skipped otherwise. if [ -n "$_hdlcif" ]; then _why="an HDLC device ($_hdlcif) answers on this image, so these modes are applied rather than refused; the refusal strings they assert are reachable only where no device accepts SIOCWANDEV" for _skid in proto-hdlc proto-hdlc-eth proto-cisco proto-fr proto-ppp proto-x25 \ physical-modes clock-modes hdlc-encodings hdlc-parities lmi-types \ loopback-aliases pvc-create pvc-delete pvc-ether private-ioctl \ refusal-errno; do bst_skip "sethdlc/$_skid" "$_why" done else # One case per PROTOCOL keyword the usage banner documents. Each names its # own arm; ppp and x25 share a call site and are separated by the string it # formats. _case sethdlc/proto-hdlc 1 "Unable to set HDLC protocol information" sethdlc "$_if" hdlc nrzi crc16-itu _case sethdlc/proto-hdlc-eth 1 "Unable to set HDLC-ETH protocol information" sethdlc "$_if" hdlc-eth manchester crc32-itu _case sethdlc/proto-cisco 1 "Unable to set Cisco HDLC protocol information" sethdlc "$_if" cisco interval 5 timeout 20 _case sethdlc/proto-fr 1 "Unable to set FR protocol information" sethdlc "$_if" fr lmi ansi dce t391 5 t392 6 n391 7 n392 8 n393 9 _case sethdlc/proto-ppp 1 "Unable to set PPP protocol information" sethdlc "$_if" ppp _case sethdlc/proto-x25 1 "Unable to set X.25 protocol information" sethdlc "$_if" x25 # The keyword TABLES, each swept whole. A table is asserted as a table # because that is how it fails: a checktab() entry that was dropped or # misspelled takes its keyword out of the parser and the token then falls # through to usage() with status 0, which no single-keyword case would # notice unless it happened to be the one that went. # # _sweep -- with the table's keywords on stdin # is what this would want, and it must not be: a stdin redirect at the top # level of a suite is what brintos/linux#73 turns into a silent truncation. # The keywords are positional instead and the leading arguments fixed per # call. _sweep_bad= _sweep() { _sid=$1; _sexp=$2; _sarg=$3; shift 3 _sweep_bad= for _k in "$@"; do # shellcheck disable=SC2086 -- $_sarg is a fixed literal per call site _so=$(sethdlc "$_if" $_sarg "$_k" 2>&1); _sr=$? case "$_so" in *"$_sexp"*) [ "$_sr" -eq 1 ] || _sweep_bad="$_sweep_bad $_k(rc=$_sr)" ;; *) _sweep_bad="$_sweep_bad $_k(rc=$_sr:$(_bst_1line "$_so"))" ;; esac done if [ -z "$_sweep_bad" ]; then bst_ok "$_sid" else bst_notok "$_sid" "not routed to '$_sexp':$_sweep_bad"; fi } # PHYSICAL and CLOCK both land in set_iface(), whose one refusal covers the # whole interface-settings ioctl. _sweep sethdlc/physical-modes "Unable to set interface information" "" \ v24 v35 x21 e1 t1 _sweep sethdlc/clock-modes "Unable to set interface information" "clock" \ int ext txint txfromrx # ENCODING and PARITY are set_proto_hdlc()'s two tables. Sweeping each with # the other left at its default is what checktab() sees in practice, since # the two are order-independent and either may be omitted. _sweep sethdlc/hdlc-encodings "Unable to set HDLC protocol information" "hdlc" \ nrz nrzi fm-mark fm-space manchester _sweep sethdlc/hdlc-parities "Unable to set HDLC protocol information" "hdlc" \ no-parity crc16 crc16-pr0 crc16-itu crc16-itu-pr0 crc32-itu # LMI is set_proto_fr()'s table. `none` takes a different branch inside the # parser (it suppresses the dce/t39x/n39x sub-grammar) and still reaches the # same ioctl, so sweeping it with the others is the assertion that both # branches converge. _sweep sethdlc/lmi-types "Unable to set FR protocol information" "fr lmi" \ none ansi ccitt cisco # `loopback` and `lb` are documented as aliases and are two separate # checkkey() calls in set_iface(); nothing else in the parser would notice # if one of them were lost. _sweep sethdlc/loopback-aliases "Unable to set interface information" "v35" \ loopback lb # The PVC arms. create/delete are distinguished in the message by the # operator's own token, which set_pvc() captures from argv BEFORE checktab() # consumes it -- so a message naming the wrong operation would mean the # table and the captured token had diverged. The `ether` arm rewrites the # request type to ADD/DEL_ETH_PVC and is asserted through the DLCI range # check it performs on a different code path (match("ether", ...) consumes # the keyword, so an `ether` that was not compiled in falls through to the # plain arm and rejects `ether` as a non-numeric DLCI). _case sethdlc/pvc-create 1 "Unable to create PVC" sethdlc "$_if" create 100 _case sethdlc/pvc-delete 1 "Unable to delete PVC" sethdlc "$_if" delete 100 _case sethdlc/pvc-ether 1 "Unable to create PVC" sethdlc "$_if" create ether 200 # The one arm that is not SIOCWANDEV. dev_siocdevprivate() refuses a netdev # with no ndo_siocdevprivate the same way, so this reaches the identical # errno by a different kernel entry point -- which is what makes it worth a # case rather than a variant of the ones above. _case sethdlc/private-ioctl 1 "SIOCDEVPRIVATE:" sethdlc "$_if" private # The errno itself, asserted ONCE. Every case above deliberately matches # only its arm's own prefix, so that a change in strerror(3) wording, or a # kernel that starts refusing with a different errno, reds here alone # instead of reddening the whole block and hiding which half moved. _case sethdlc/refusal-errno 1 "Operation not supported" sethdlc "$_if" ppp fi