# BST-TIMEOUT: 240 # 58 cases; measured elapsed=68s on hz1 2026-08-18, 3.5x headroom. # It is over the 60 s default because almost every assertion here reads the # OUTPUT FILE'S BYTES rather than tcpslice's report -- ~150 dd+od pairs, and an # exec is the expensive operation on this target. # # tcpslice 1.7 (meta-networking recipes-support/tcpdump, tcpslice_1.7.bb) -- the # tcpdump capture-file slicer. It copies to a new pcap every packet of one or # more input pcaps whose timestamp falls in a given range, merging several inputs # in timestamp order. # # WHAT THIS SUITE ESTABLISHES. tcpslice is file-to-file: it opens no interface, # captures nothing and needs no peer, so its whole reachable surface is reachable # on this target and nothing here is scoped away. # # THE FIXTURES ARE BUILT IN THE GUEST, by the awk below, not shipped in data/. # A 24-byte little-endian microsecond-magic global header (DLT_EN10MB, snaplen # 65535) followed by fixed-size 76-byte records: a 16-byte packet header and a # 60-byte Ethernet frame whose 46-byte payload opens with a little-endian packet # id. Generating them here is what lets the ranges below be chosen against known # timestamps instead of against whatever a shipped capture happened to hold. # # THE FIXED RECORD SIZE IS LOAD-BEARING. Every packet header and every id then # sits at a computable offset, so `dd`+`od` can read what tcpslice WROTE without # going back through libpcap -- which is what makes the merge-order case a direct # read of the output bytes rather than a restatement of what tcpslice says about # them. Packet count likewise comes from the file size, so a truncated output is # a wrong count rather than a short read nobody notices. # # Every pinned value comes from a NATIVE x86-64 build of tcpslice 1.7 against # libpcap 1.10.4 driven by this same shell, and it must agree: the savefile # format is fixed, the frames are IEEE 802.3 wire format and the arithmetic is # integer, so a difference between the two architectures is a real defect and not # an expected variation. # # ONE CASE IS RED BY DESIGN AND ATTRIBUTED. tcpslice calls posix_fadvise(3) on # every input it opens and warns on stderr when the call fails; on this target it # always fails, because syscall 221 is reserved in the wasm32 uapi and unwired in # the syscall table (brintos/linux#84, filed from this port). The case is left # FAILING rather than skipped or asserted-as-expected, so it flips green by # itself the day 221 is wired -- nothing here needs changing then. # # THAT GAP IS ALSO WHY EVERY REPORT IS PARSED THROUGH field(), WHICH SKIPS LINES # WITH NO TAB. The warnings land on stderr in the middle of the program's normal # output, so a `2>&1` capture puts them where the first report line should be. # Two assertions here read a warning as tcpslice's answer before that was fixed; # filtering in one place is what keeps the rest of the suite measuring tcpslice # instead of measuring the kernel gap. # # TZ IS PINNED because -r and -t render through localtime(3). UTC0 is a POSIX TZ # string, parsed by glibc without any tzdata file, so the two human-readable # cases assert a fixed string on an image that ships no zoneinfo. # # THE SINGLE-PACKET CASE IS AN UPSTREAM LIMITATION, NOT A PLATFORM GAP, and it is # pinned as one: sf_find_end() identifies the last packet by finding a header # whose SUCCESSOR also parses as a plausible header, so a capture holding exactly # one packet has no such pair and -R/-r/-t exit 1. Reproduced identically on the # native x86-64 build, so there is nothing here for brintOS to fix. . "$BST_LIB" # tcpslice_1.7.bb's do_install puts the program in ${sbindir}, not ${bindir}. # `require` resolves through PATH, so a PATH without /usr/sbin would report this # port as legitimately absent -- a skip that reads as coverage. PATH=$PATH:/usr/sbin:/sbin export PATH TZ=UTC0 export TZ require tcpslice tcpslice/present # The fixture generator and the byte reader are not the subject. Their absence is # an image regression rather than "tcpslice is not installed", so it is a red # here and not a skip -- a skip would retire the whole port's coverage silently. for _t in awk od dd wc cmp; do command -v "$_t" >/dev/null 2>&1 || { bst_notok tcpslice/harness-$_t "$_t is not on the image; the suite cannot build or read a fixture" exit 0 } done bst_ok tcpslice/harness-present FADVISE="posix_fadvise ENOSYS on this target (brintos/linux#84)" TMP=${TMPDIR:-/tmp} WORK=$TMP/bst-tcpslice.$$ mkdir -p "$WORK" 2>/dev/null if [ ! -d "$WORK" ]; then bst_notok tcpslice/workdir "cannot create $WORK -- the fixtures need somewhere writable" exit 0 fi bst_ok tcpslice/workdir # cd so tcpslice echoes bare filenames in its -R/-r/-t report, which is the field # the cases below compare against. cd "$WORK" || { bst_notok tcpslice/workdir-cd "cannot cd $WORK"; exit 0; } # --- fixture generator ------------------------------------------------------- # mkcap .. ... # No exponentiation and no printf %c: `d = d*256` and \0ddd octal escapes through # printf %b are the forms busybox awk and busybox printf both implement. mkcap() { _o=$1; shift _e=$(awk -v spec="$*" ' function b(x) { return sprintf("\\0%03o", x % 256) } function le32(v, i, s, d) { s=""; d=1; for (i=0;i<4;i++) { s = s b(int(v/d)); d = d*256 } return s } function le16(v, i, s, d) { s=""; d=1; for (i=0;i<2;i++) { s = s b(int(v/d)); d = d*256 } return s } function frame(id, s, i) { s = b(102) b(119) b(136) b(153) b(170) b(187) # dst 66:77:88:99:aa:bb s = s b(0) b(17) b(34) b(51) b(68) b(85) # src 00:11:22:33:44:55 s = s b(8) b(0) # ethertype 0x0800 s = s le32(id) # payload[0..3] = packet id for (i = 0; i < 42; i++) s = s b((id + i) % 256) return s } BEGIN { out = le32(2712847316) le16(2) le16(4) le32(0) le32(0) le32(65535) le32(1) n = split(spec, a, " ") for (k = 1; k <= n; k++) { split(a[k], p, ".") out = out le32(p[1]) le32(p[2]) le32(60) le32(60) frame(p[3]) } printf "%s", out }') || return 1 printf '%b' "$_e" > "$_o" } # --- byte readers ------------------------------------------------------------ # le32at : the little-endian uint32 at , in decimal. le32at() { _f=$1; _off=$2 set -- $(dd if="$_f" bs=1 skip="$_off" count=4 2>/dev/null | od -An -tx1) [ $# -eq 4 ] || { printf 'short-read'; return 1; } echo $(( 0x$4$3$2$1 )) } # npkt : packet count, derived from the file size and the fixed 76-byte # record. A truncated output therefore reads as the wrong count. npkt() { _s=$(wc -c < "$1" 2>/dev/null); [ -n "$_s" ] || _s=0; echo $(( ($_s - 24) / 76 )); } # ids : the packet ids in file order, space separated. ids() { _if=$1; _n=$(npkt "$_if"); _i=0; _r= while [ "$_i" -lt "$_n" ]; do _r="$_r $(le32at "$_if" $((24 + _i * 76 + 30)))" _i=$((_i + 1)) done printf '%s' "${_r# }" } # tstamps : "." per packet, in file order, space separated. tstamps() { _tf=$1; _n=$(npkt "$_tf"); _i=0; _r= while [ "$_i" -lt "$_n" ]; do _r="$_r $(le32at "$_tf" $((24 + _i * 76)))" _r="$_r.$(printf '%06d' "$(le32at "$_tf" $((24 + _i * 76 + 4)))")" _i=$((_i + 1)) done printf '%s' "${_r# }" } # BRINTOS-WORKAROUND: linux#84 revisit -- field() filters untabbed lines only # because posix_fadvise(3) returns ENOSYS here, so tcpslice prints two warnings # to stderr per input file and a report parsed as "the first line" reads one of # them instead. When 221 is wired the warnings stop and the filter stops # mattering; keeping it is still defensible on its own merits (any tool may write # to stderr), so this is a judgement call rather than an automatic removal -- # the same reading bst.sh's linux#44 guard carries. # # ghdr : the 24-byte pcap global header, as a hex string. busybox cmp has # no -n, so the header comparison is a value comparison -- which also prints the # bytes that differ instead of only that they do. ghdr() { dd if="$1" bs=1 count=24 2>/dev/null | od -An -tx1 | tr -d ' \n'; } # field : tab-separated field n of tcpslice's -R/-r/-t report. The # FIRST TAB-BEARING line, not the first line: a report shares stderr with the # posix_fadvise warnings (brintos/linux#84), which carry no tab. field() { printf '%s\n' "$2" | awk -F'\t' -v n="$1" 'NF>1{print $n; exit}'; } # --- the fixtures ------------------------------------------------------------ # capA: eight packets one second apart, usec 100..800, ids 1..8. # capB: five packets on the half-second, interleaving capA except for the last, # which lands past capA's end so the merge's last packet comes from B. SPEC_A="1700000000.100.1 1700000001.200.2 1700000002.300.3 1700000003.400.4 1700000004.500.5 1700000005.600.6 1700000006.700.7 1700000007.800.8" SPEC_B="1700000000.500000.101 1700000002.500000.102 1700000004.500000.103 1700000006.500000.104 1700000009.500000.105" mkcap capA.pcap $SPEC_A mkcap capB.pcap $SPEC_B # NULL CONTROLS ON THE FIXTURES THEMSELVES. Everything below reads tcpslice # against these bytes, so a generator that emitted the wrong thing would show up # as a tcpslice defect. These four cases are what separate the two. check_eq tcpslice/fixture-a-size 632 "$(wc -c < capA.pcap)" check_eq tcpslice/fixture-b-size 404 "$(wc -c < capB.pcap)" check_eq tcpslice/fixture-a-magic d4c3b2a1 "$(dd if=capA.pcap bs=1 count=4 2>/dev/null | od -An -tx1 | tr -d ' \n')" check_eq tcpslice/fixture-a-ids "1 2 3 4 5 6 7 8" "$(ids capA.pcap)" # ...and one on the byte reader: a reader that answered a constant would satisfy # every id case above and below. check_eq tcpslice/reader-control "1700000000 100 60 60" \ "$(le32at capA.pcap 24) $(le32at capA.pcap 28) $(le32at capA.pcap 32) $(le32at capA.pcap 36)" # --- the program ------------------------------------------------------------ check tcpslice/installed-in-sbindir test -x /usr/sbin/tcpslice h=$(tcpslice -h 2>&1); hrc=$? if bst_execfail "$h"; then bst_unrun tcpslice/version "$h" else check_eq tcpslice/version-rc 0 "$hrc" check_eq tcpslice/version "tcpslice version 1.7" "$(printf '%s\n' "$h" | awk 'NR==1')" # tcpslice prints the libpcap it was linked against; this is the port's # evidence that the library dependency resolved rather than being stubbed. case "$h" in *"libpcap version "*) bst_ok tcpslice/libpcap-linked ;; *) bst_notok tcpslice/libpcap-linked "no libpcap version line: $(_bst_1line "$h")" ;; esac fi # --- -R: raw first and last packet times ------------------------------------ rout=$(tcpslice -R capA.pcap 2>rerr.txt); rrc=$? if bst_execfail "$rout"; then bst_unrun tcpslice/raw-report "$rout" else check_eq tcpslice/raw-rc 0 "$rrc" check_eq tcpslice/raw-name capA.pcap "$(field 1 "$rout")" check_eq tcpslice/raw-first 1700000000.000100 "$(field 2 "$rout")" check_eq tcpslice/raw-last 1700000007.000800 "$(field 3 "$rout")" fi # tcpslice calls posix_fadvise(POSIX_FADV_RANDOM|NOREUSE) on every input it opens # and warns on stderr when the call fails. A clean stderr is therefore a # statement about this kernel's fadvise64, not only about the report -- and a red # here is a platform gap to attribute, with tcpslice itself still correct. rerr=$(cat rerr.txt) if [ -z "$rerr" ]; then bst_ok tcpslice/no-fadvise-warning else bst_notok tcpslice/no-fadvise-warning "$FADVISE -- $(_bst_1line "$rerr")" fi bout=$(tcpslice -R capB.pcap 2>/dev/null) check_eq tcpslice/raw-first-b 1700000000.500000 "$(field 2 "$bout")" check_eq tcpslice/raw-last-b 1700000009.500000 "$(field 3 "$bout")" # One report line per input file, in argument order. mout=$(tcpslice -R capA.pcap capB.pcap 2>/dev/null) check_eq tcpslice/raw-two-files-lines 2 "$(printf '%s\n' "$mout" | wc -l | tr -d ' ')" check_eq tcpslice/raw-two-files-second-name capB.pcap \ "$(printf '%s\n' "$mout" | awk -F'\t' 'NR==2{print $1}')" # --- -r / -t: the same two times, rendered ---------------------------------- hr=$(tcpslice -r capA.pcap 2>/dev/null) check_eq tcpslice/readable-first "Tue Nov 14 22:13:20 2023" "$(field 2 "$hr")" check_eq tcpslice/readable-last "Tue Nov 14 22:13:27 2023" "$(field 3 "$hr")" # -r renders whole seconds only; -t is the form that keeps the microseconds, so # the pair is what shows the sub-second part survived the round trip. pt=$(tcpslice -t capA.pcap 2>/dev/null) check_eq tcpslice/parseable-first "2023y11m14d22h13m20s000100u" "$(field 2 "$pt")" check_eq tcpslice/parseable-last "2023y11m14d22h13m27s000800u" "$(field 3 "$pt")" # --- extracting an interior time range -------------------------------------- # [1700000002.000000, 1700000005.000000] holds capA's packets 3, 4 and 5 and is # interior at both ends: packets 1-2 fall before it and 6-8 after it. xout=$(tcpslice -w slice.pcap 1700000002.000000 1700000005.000000 capA.pcap 2>&1); xrc=$? if bst_execfail "$xout"; then bst_unrun tcpslice/extract "$xout" else check_eq tcpslice/extract-rc 0 "$xrc" check_eq tcpslice/extract-count 3 "$(npkt slice.pcap)" check_eq tcpslice/extract-ids "3 4 5" "$(ids slice.pcap)" check_eq tcpslice/extract-timestamps "1700000002.000300 1700000003.000400 1700000004.000500" \ "$(tstamps slice.pcap)" # The output is re-read through libpcap, which is the validity check: a file # with a bad magic or a torn record does not get this far. sr=$(tcpslice -R slice.pcap 2>/dev/null) check_eq tcpslice/extract-reread-first 1700000002.000300 "$(field 2 "$sr")" check_eq tcpslice/extract-reread-last 1700000004.000500 "$(field 3 "$sr")" # ...and its global header is the input's, byte for byte, so the snaplen and # the link type survived the copy rather than being re-derived. check_eq tcpslice/extract-header-preserved "$(ghdr capA.pcap)" "$(ghdr slice.pcap)" fi # Both ends of a range are inclusive. Asked for exactly packet 3's and packet 4's # timestamps, the extract must hold both and nothing else -- an exclusive # implementation returns 0, 1 or 2 packets depending on which end it drops, and # only the id list tells those apart. tcpslice -w bound.pcap 1700000002.000300 1700000003.000400 capA.pcap 2>/dev/null check_eq tcpslice/range-inclusive-count 2 "$(npkt bound.pcap)" check_eq tcpslice/range-inclusive-ids "3 4" "$(ids bound.pcap)" # A range no packet falls in writes a valid but empty capture: the 24-byte global # header alone. Pinned because "wrote nothing at all" and "wrote a header" are # different answers and only one of them is a pcap. tcpslice -w empty.pcap 1700000100.000000 1700000200.000000 capA.pcap 2>/dev/null check_eq tcpslice/empty-range-rc 0 "$?" check_eq tcpslice/empty-range-bytes 24 "$(wc -c < empty.pcap)" check_eq tcpslice/empty-range-count 0 "$(npkt empty.pcap)" # --- merging two captures in timestamp order -------------------------------- gout=$(tcpslice -w merged.pcap capA.pcap capB.pcap 2>&1); grc=$? if bst_execfail "$gout"; then bst_unrun tcpslice/merge "$gout" else check_eq tcpslice/merge-rc 0 "$grc" check_eq tcpslice/merge-count 13 "$(npkt merged.pcap)" # THE TICKET'S NAMED ASSERTION, read out of the output file's own bytes: the # two inputs interleave by timestamp, they are not appended. A concatenating # implementation yields "1 2 3 4 5 6 7 8 101 102 103 104 105" and passes every # count and first/last case above. check_eq tcpslice/merge-order "1 101 2 3 102 4 5 103 6 7 104 8 105" "$(ids merged.pcap)" check_eq tcpslice/merge-timestamps \ "1700000000.000100 1700000000.500000 1700000001.000200 1700000002.000300 1700000002.500000 1700000003.000400 1700000004.000500 1700000004.500000 1700000005.000600 1700000006.000700 1700000006.500000 1700000007.000800 1700000009.500000" \ "$(tstamps merged.pcap)" mr=$(tcpslice -R merged.pcap 2>/dev/null) check_eq tcpslice/merge-reread-first 1700000000.000100 "$(field 2 "$mr")" check_eq tcpslice/merge-reread-last 1700000009.500000 "$(field 3 "$mr")" check_eq tcpslice/merge-header-preserved "$(ghdr capA.pcap)" "$(ghdr merged.pcap)" fi # Order of the inputs on the command line must not matter -- the merge is by # timestamp, so B-then-A has to produce the same file byte for byte. This is the # one case that cannot be satisfied by an implementation that merely sorts its # arguments. tcpslice -w merged-rev.pcap capB.pcap capA.pcap 2>/dev/null check tcpslice/merge-argument-order-irrelevant cmp -s merged.pcap merged-rev.pcap # --- the whole-file copy ---------------------------------------------------- # With no range, tcpslice copies the file. Byte-identical output is the # strongest single statement about the reader and the writer together. tcpslice -w copy.pcap capA.pcap 2>/dev/null check tcpslice/whole-file-copy-byte-exact cmp -s copy.pcap capA.pcap # NULL CONTROL for the comparator: it must be able to say no. Without this, # "identical" is equally consistent with a cmp that always agrees. cp capA.pcap flip.pcap printf '%b' '\0377' | dd of=flip.pcap bs=1 seek=54 count=1 conv=notrunc 2>/dev/null if cmp -s flip.pcap capA.pcap; then bst_notok tcpslice/cmp-control-finds-diff "cmp called a one-byte-flipped copy identical" else bst_ok tcpslice/cmp-control-finds-diff fi # --- inputs the reader must refuse ------------------------------------------ # A tool that answered "no packets, no error" to these would pass every case # above and be useless. e1=$(tcpslice -R nosuch.pcap 2>&1); check_eq tcpslice/absent-input-rc 1 "$?" case "$e1" in *"bad pcap file"*"No such file or directory"*) bst_ok tcpslice/absent-input-message ;; *) bst_notok tcpslice/absent-input-message "$(_bst_1line "$e1")" ;; esac printf 'this is not a capture file at all, not even close\n' > junk.bin e2=$(tcpslice -R junk.bin 2>&1); check_eq tcpslice/junk-input-rc 1 "$?" case "$e2" in *"unknown file format"*) bst_ok tcpslice/junk-input-message ;; *) bst_notok tcpslice/junk-input-message "$(_bst_1line "$e2")" ;; esac dd if=capA.pcap of=hdronly.pcap bs=1 count=24 2>/dev/null e3=$(tcpslice -R hdronly.pcap 2>&1); check_eq tcpslice/no-packets-input-rc 1 "$?" case "$e3" in *"error reading packet in"*) bst_ok tcpslice/no-packets-input-message ;; *) bst_notok tcpslice/no-packets-input-message "$(_bst_1line "$e3")" ;; esac # --- the single-packet upstream limitation ---------------------------------- # Pinned as observed, on both architectures, so it flips by itself if upstream # ever teaches sf_find_end() to accept a lone packet. It is NOT attributed to a # brintOS issue: the native x86-64 build of the same source does exactly this. mkcap one.pcap 1700000000.100.1 check_eq tcpslice/single-packet-fixture 1 "$(npkt one.pcap)" e4=$(tcpslice -R one.pcap 2>&1); check_eq tcpslice/single-packet-rc 1 "$?" case "$e4" in *"problems finding end packet"*) bst_ok tcpslice/single-packet-message ;; *) bst_notok tcpslice/single-packet-message "$(_bst_1line "$e4")" ;; esac # ...and the boundary of that limitation: two packets are enough. mkcap two.pcap 1700000000.100.1 1700000001.200.2 t2=$(tcpslice -R two.pcap 2>/dev/null); check_eq tcpslice/two-packet-rc 0 "$?" check_eq tcpslice/two-packet-first 1700000000.000100 "$(field 2 "$t2")" check_eq tcpslice/two-packet-last 1700000001.000200 "$(field 3 "$t2")" cd "$TMP" 2>/dev/null rm -rf "$WORK"