. "$BST_LIB" # Zstandard is a dictionary-matching compressor with an entropy stage (Huff0 for # literals, FSE for everything else), reaching zlib-class ratios at LZ4-class # decode speeds. The shipped payload is three headers plus a pkg-config file # (-dev) and the archive libzstd.a (-staticdev); the distro is static-only, so # there is no libzstd.so and the recipe needed a bbappend to hold upstream to # that. # # Like xxhash and lz4, zstd brings its OWN consumer -- the zstd CLI links the # library statically and is installed beside it -- so this suite asserts in two # registers: the payload is real (below), and the codec actually RUNS (second # half). For a compressor the strongest available runtime property is a round # trip: data that survives compress->decompress byte-identical pins the encoder # and the decoder against each other, which no header-and-symbols check reaches. # # PERFORMANCE NOTE, because it shapes how this file is written. The runner caps # each suite at 60 s wall-clock (BRINTOS-172), and on wasm32 a fork+exec of a # multi-MB grep is expensive enough that ~80 of them blows the cap on their own # -- the first two drafts of this suite were cut off mid-run for exactly that, # with nothing wrong with the image. So the payload half does a SMALL number of # scans that each build a newline-delimited token set, and then tests membership # with the shell's `case` builtin, which forks nothing. Please keep that shape # when adding cases: a per-name `grep` here costs a process, not a string # compare. inc=/usr/include/zstd.h ince=/usr/include/zstd_errors.h incd=/usr/include/zdict.h pc=/usr/lib/pkgconfig/libzstd.pc lib=/usr/lib/libzstd.a NL=' ' # _have — true when is a whole line of $toks. Anchoring on the # newlines is what makes it exact: a plain substring test would let # huf_decompress_amd64.o satisfy huf_decompress.o, and ZSTD_compressBound # satisfy ZSTD_compress. _have() { case "$toks" in *"$NL$1$NL"*) return 0 ;; esac; return 1; } check zstd/header-present test -f "$inc" check zstd/header-nonempty test -s "$inc" check zstd/header-guard grep -q "define ZSTD_H_235446" "$inc" check zstd/errors-header-present test -f "$ince" check zstd/errors-header-guard grep -q "define ZSTD_ERRORS_H_398273423" "$ince" check zstd/dict-header-present test -f "$incd" check zstd/dict-header-guard grep -q "define ZSTD_ZDICT_H" "$incd" # 1.5.5, matching the pkg-config file. The header pads a macro name from its # value with runs of spaces, so these match whitespace-agnostically. check zstd/ver-major grep -qE "^#define[[:space:]]+ZSTD_VERSION_MAJOR[[:space:]]+1" "$inc" check zstd/ver-minor grep -qE "^#define[[:space:]]+ZSTD_VERSION_MINOR[[:space:]]+5" "$inc" check zstd/ver-release grep -qE "^#define[[:space:]]+ZSTD_VERSION_RELEASE[[:space:]]+5" "$inc" check zstd/ver-number grep -qE "^#define[[:space:]]+ZSTD_VERSION_NUMBER" "$inc" check zstd/ver-string grep -qE "^#define[[:space:]]+ZSTD_VERSION_STRING" "$inc" # ---- declared API, one scan per header ------------------------------------ # Collect every identifier that is followed by an open paren, whitespace # tolerated: upstream declares some entry points as `ZSTD_versionNumber (void)`, # and a literal "NAME(" needle silently misses exactly those. toks="$NL$(grep -oE '[A-Za-z_][A-Za-z0-9_]*[[:space:]]*\(' "$inc" | tr -d ' \011(' | sort -u)$NL" # The simple API: the one-shot pair, the bound a caller sizes its output buffer # with, the frame-header query that tells a decoder how much to allocate, and # the error pair every call reports through. # # The explicit-context API: reusing one context across many operations is the # difference between a toy and a library, so the create/free/compress triples # for both directions are pinned, plus the parameter-setting entry points the # advanced API routes everything through. # # The streaming API: the compress/decompress loops, and the in/out size hints a # caller needs to drive them. # # The dictionary API: zstd's headline feature for corpora of many small similar # payloads -- a dictionary trained over samples is prepended to the match window # so a few-hundred-byte record still finds matches. for f in ZSTD_compress ZSTD_decompress ZSTD_compressBound \ ZSTD_getFrameContentSize ZSTD_findFrameCompressedSize \ ZSTD_isError ZSTD_getErrorName \ ZSTD_minCLevel ZSTD_maxCLevel ZSTD_defaultCLevel \ ZSTD_versionNumber ZSTD_versionString \ ZSTD_createCCtx ZSTD_freeCCtx ZSTD_compressCCtx \ ZSTD_createDCtx ZSTD_freeDCtx ZSTD_decompressDCtx \ ZSTD_CCtx_setParameter ZSTD_DCtx_setParameter \ ZSTD_CCtx_reset ZSTD_DCtx_reset ZSTD_compress2 \ ZSTD_createCStream ZSTD_freeCStream ZSTD_compressStream2 \ ZSTD_createDStream ZSTD_freeDStream ZSTD_decompressStream \ ZSTD_CStreamInSize ZSTD_CStreamOutSize \ ZSTD_DStreamInSize ZSTD_DStreamOutSize \ ZSTD_createCDict ZSTD_freeCDict ZSTD_compress_usingCDict \ ZSTD_createDDict ZSTD_freeDDict ZSTD_decompress_usingDDict; do if _have "$f"; then bst_ok "zstd/api-$f"; else bst_notok "zstd/api-$f" "not declared in zstd.h"; fi done check zstd/type-inbuffer grep -q "ZSTD_inBuffer" "$inc" check zstd/type-outbuffer grep -q "ZSTD_outBuffer" "$inc" # The dictionary BUILDER lives in its own header. Note the trainer is not # exercised at runtime below: it wants a many-sample corpus and a large working # allocation, which is not a reasonable ask of this target yet. The API is # pinned here and the limit is stated rather than papered over. toks="$NL$(grep -oE '[A-Za-z_][A-Za-z0-9_]*[[:space:]]*\(' "$incd" | tr -d ' \011(' | sort -u)$NL" for f in ZDICT_trainFromBuffer ZDICT_getDictID ZDICT_isError ZDICT_getErrorName; do if _have "$f"; then bst_ok "zstd/api-$f"; else bst_notok "zstd/api-$f" "not declared in zdict.h"; fi done # The public error enum: callers switch on these rather than on strings. check zstd/err-type grep -q "ZSTD_ErrorCode" "$ince" check zstd/err-getcode grep -q "ZSTD_getErrorCode" "$ince" toks="$NL$(grep -oE 'ZSTD_error_[A-Za-z0-9_]*' "$ince" | sort -u)$NL" for e in ZSTD_error_no_error ZSTD_error_prefix_unknown \ ZSTD_error_corruption_detected ZSTD_error_checksum_wrong \ ZSTD_error_dstSize_tooSmall ZSTD_error_memory_allocation; do if _have "$e"; then bst_ok "zstd/errcode-$e"; else bst_notok "zstd/errcode-$e" "absent from zstd_errors.h"; fi done # ---- pkg-config ----------------------------------------------------------- # What a future recipe DEPENDS-ing on zstd actually consumes. check zstd/pc-present test -f "$pc" check_out zstd/pc-name "Name: zstd" cat "$pc" check_out zstd/pc-version "Version: 1.5.5" cat "$pc" check_out zstd/pc-libs "-lzstd" cat "$pc" check_out zstd/pc-cflags "Cflags: -I" cat "$pc" check_out zstd/pc-libdir "libdir=" cat "$pc" # Libs.private must NOT advertise -pthread. Upstream interpolates it from # LDFLAGS_DYNLIB, which defaults to -pthread because the SHARED library is the # multi-threaded one; this arm ships only the single-threaded static archive, so # the bbappend clears that variable across install-pc. A -pthread here would be # a false statement to the next recipe that links libzstd.a, and it is the one # assertion that proves the override actually took. if grep -q "^Libs.private:.*-pthread" "$pc"; then bst_notok zstd/pc-no-pthread "Libs.private advertises -pthread on a single-threaded static build" else bst_ok zstd/pc-no-pthread fi # ---- the archive ---------------------------------------------------------- check zstd/lib-present test -f "$lib" check zstd/lib-nonempty test -s "$lib" check_out zstd/lib-archive "!" head -c 8 "$lib" # One scan over the archive builds the whole token set for the three blocks # below. Flattening first because 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. flat=${TMPDIR:-/tmp}/bst-zstd-flat.$$ tr -c '\40-\176\n' ' ' < "$lib" > "$flat" 2>/dev/null toks="$NL$(grep -oE '[A-Za-z_][A-Za-z0-9_]*\.o|Z(STD|DICT)_[A-Za-z0-9_]*' "$flat" | sort -u)$NL" rm -f "$flat" # Members sampled across every module the recipe compiles in: common, compress, # decompress, the dictionary builder, and the legacy decoders. A partial build # would still archive and still link the CLI, because the CLI compiles its own # copies of these sources. # # zstd_v04.o is load-bearing beyond coverage: upstream's default is # ZSTD_LEGACY_SUPPORT=5, and the recipe asks for 4. Its presence is what proves # that setting reached the build rather than being silently dropped. for m in zstd_common.o error_private.o entropy_common.o xxhash.o \ zstd_compress.o zstd_fast.o zstd_lazy.o zstd_opt.o huf_compress.o \ zstd_decompress.o zstd_decompress_block.o huf_decompress.o \ zdict.o cover.o divsufsort.o \ zstd_v04.o zstd_v05.o zstd_v06.o zstd_v07.o; do if _have "$m"; then bst_ok "zstd/member-$m"; else bst_notok "zstd/member-$m" "absent from libzstd.a"; fi done # Exported symbols sampled across the simple, context, streaming and dictionary # APIs, so the archive's index is evidence the wasm32 compile really produced # each one rather than only that a header declares it. for s in ZSTD_compress ZSTD_decompress ZSTD_compressBound \ ZSTD_createCCtx ZSTD_compressCCtx ZSTD_createDCtx \ ZSTD_compressStream2 ZSTD_decompressStream \ ZSTD_createCDict ZSTD_decompress_usingDDict \ ZDICT_trainFromBuffer ZSTD_getErrorName ZSTD_versionString; do if _have "$s"; then bst_ok "zstd/sym-$s"; else bst_notok "zstd/sym-$s" "absent from libzstd.a"; fi done # zstd vendors its own copy of xxHash for the frame content checksum, and this # image ALSO ships the real libxxhash.a (BRINTOS-164) and liblz4.a, which # vendors a third copy. Upstream avoids the collision by compiling its copy # under -DXXH_NAMESPACE=ZSTD_ (lib/libzstd.mk), so the symbols come out as # ZSTD_XXH64. Assert both halves: the prefixed name is present, and no bare # XXH64 entry point escaped. The second is the one that would actually break a # link, and it is invisible to a presence-only check. if _have ZSTD_XXH64; then bst_ok zstd/xxh-namespaced else bst_notok zstd/xxh-namespaced "no ZSTD_XXH64 in libzstd.a"; fi if _have XXH64_createState; then bst_notok zstd/xxh-no-bare-symbol "un-namespaced XXH64_createState in libzstd.a" else bst_ok zstd/xxh-no-bare-symbol fi # No shared object and no libtool archive: static-only is distro policy, and # this recipe needed a bbappend to hold upstream to it -- upstream's install # rule BUILDS the shared library if it is missing, so a .so here would mean the # override was dropped. check zstd/no-shared sh -c '! ls /usr/lib/libzstd.so* >/dev/null 2>&1' check zstd/no-libtool sh -c '! ls /usr/lib/libzstd.la >/dev/null 2>&1' # --------------------------------------------------------------------------- # Runtime register: the codec executing on wasm32, through the shipped CLI. # --------------------------------------------------------------------------- require zstd zstd/cli-present check zstd/cli-alias-zstdcat test -e /usr/bin/zstdcat check zstd/cli-alias-unzstd test -e /usr/bin/unzstd check zstd/cli-alias-zstdmt test -e /usr/bin/zstdmt check zstd/cli-script-zstdgrep test -f /usr/bin/zstdgrep check zstd/cli-script-zstdless test -f /usr/bin/zstdless # The banner embeds the pointer width -- "(32-bit)" here, "(64-bit)" on a host # build -- so only the version is asserted; matching the whole banner would fail # for a reason that has nothing to do with zstd working. check_out zstd/version "v1.5.5" zstd --version d=${TMPDIR:-/tmp}/bst-zstd.$$ mkdir -p "$d" # A small fixed vector for the round-trip and framing assertions, and a larger # generated one for the level work. Both are reproducible from tools already on # the image, so the suite stays a single file with no payload to keep in sync. # # seq is deliberately 8000 lines, not more: zstd's high levels are far more # expensive on wasm32 than the compression itself suggests, and a 20000-line # input blew the runner's 60 s budget at level 22 alone. printf 'brintOS zstd round-trip vector 0123456789\n' > "$d/small.txt" seq 1 8000 > "$d/seq.txt" check zstd/compress-file zstd -q -f "$d/small.txt" -o "$d/small.zst" check zstd/compressed-exists test -s "$d/small.zst" # The Zstandard frame magic, 0xFD2FB528 little-endian. This is what makes the # output a self-describing frame rather than a bare block. check_out zstd/frame-magic "28 b5 2f fd" sh -c "od -An -tx1 -N4 '$d/small.zst' | tr -s ' '" check zstd/decompress-file zstd -d -q -f "$d/small.zst" -o "$d/small.out" check zstd/roundtrip-identical cmp -s "$d/small.txt" "$d/small.out" # -t decodes without writing and reports the decoded size. The report goes to # stderr, which check_out folds in. check_out zstd/test-mode "42 bytes" zstd -t "$d/small.zst" # The pipe route: compress to stdout, decompress from stdin, never touching an # intermediate file. This is the path a shell pipeline takes and it exercises # the streaming API rather than the whole-file one. zstd -c -q "$d/small.txt" 2>/dev/null | zstd -dc -q 2>/dev/null > "$d/pipe.out" check zstd/pipe-roundtrip cmp -s "$d/small.txt" "$d/pipe.out" # argv[0] dispatch: the installed symlinks select the mode by the name they are # invoked under, with no flag. zstdcat "$d/small.zst" > "$d/cat.out" 2>/dev/null check zstd/alias-zstdcat cmp -s "$d/small.txt" "$d/cat.out" unzstd -q -f "$d/small.zst" -o "$d/unzstd.out" 2>/dev/null check zstd/alias-unzstd cmp -s "$d/small.txt" "$d/unzstd.out" # Compression levels. Each must round-trip -- a smaller output that does not # decode is not compression -- and level 1 must actually shrink the input. # # NOTE for whoever extends this: zstd's size-vs-level curve is NOT monotonic, # so do not "fix" this block by asserting one. Measured on this exact input # (seq 1 8000, 38,893 B) with 1.5.5: L1 16,638 / L9 5,243 / L19 8,240 -- L19 is # HALF AGAIN as large as L9. The same shape held on a 108,894-byte seq # (L1 46,243 / L3 26,672 / L5 13,309 / L9 14,132 / L12 14,238 / L15 21,614 / # L19 24,973 -- best at L5, steadily worse above it) and on prose-like input # (L1 2,079 / L9 1,887 / L19 2,503), so it is a property of the codec on small # structured inputs, not of one vector: higher levels widen the search and # change the entropy-table and block-splitting decisions, which here costs more # than the better matches gain. So the only ordering asserted is L9 < L1, which # held on every input measured and is the one that actually says "the higher # level did more work". orig=$(wc -c < "$d/seq.txt") for lvl in 1 9 19; do zstd -q -f "-$lvl" "$d/seq.txt" -o "$d/seq$lvl.zst" 2>/dev/null check "zstd/level$lvl-compressed" test -s "$d/seq$lvl.zst" zstd -d -q -f "$d/seq$lvl.zst" -o "$d/seq$lvl.out" 2>/dev/null check "zstd/level$lvl-roundtrip" cmp -s "$d/seq.txt" "$d/seq$lvl.out" done s1=$(wc -c < "$d/seq1.zst") s9=$(wc -c < "$d/seq9.zst") check zstd/level1-smaller-than-input test "$s1" -lt "$orig" check zstd/level9-smaller-than-level1 test "$s9" -lt "$s1" # --ultra unlocks levels 20-22, the tier above the normal 1-19 range, and this # pins that the tier is really compiled in and round-trips. # # It runs on the SMALL vector, not on seq: level 22 is by far the most # expensive thing this suite can ask for, and on wasm32 it alone overran the # runner's 60 s budget on a 108 KB input. Nothing is lost -- what is being # asserted is that the level is accepted and decodes, not a ratio. zstd -q -f --ultra -22 "$d/small.txt" -o "$d/ultra.zst" 2>/dev/null check zstd/ultra-compressed test -s "$d/ultra.zst" zstd -d -q -f "$d/ultra.zst" -o "$d/ultra.out" 2>/dev/null check zstd/ultra-roundtrip cmp -s "$d/small.txt" "$d/ultra.out" # The frame carries an XXH64 content checksum by default, and --no-check turns # it off. --list reports which, so the pair proves the checksum is real AND # controllable rather than a constant string in the listing code. check_out zstd/list-check-xxh64 "XXH64" zstd --list "$d/small.zst" zstd -q -f --no-check "$d/small.txt" -o "$d/nock.zst" 2>/dev/null check_out zstd/list-check-none "None" zstd --list "$d/nock.zst" check_out zstd/list-frames "Frames" zstd --list "$d/small.zst" # Corruption must be DETECTED, not silently decoded: this is what separates # comparing from echoing, and it is exactly what the content checksum buys. # zstd reports every failure as exit 1 (it has no per-error status the way lz4 # does), so the status alone cannot distinguish corruption from a missing file # -- the message is asserted alongside it. # # The message is asserted as "match checksum" rather than a generic "error" # because it is the XXH64 content checksum -- the same one --list reports above # -- that actually catches this corruption, which ties the runtime half back to # the payload half. Measured verbatim on 1.5.5 for this exact input, level and # offset: "Decoding error (36) : Restored data doesn't match checksum". cp "$d/seq1.zst" "$d/corrupt.zst" printf 'ZZZZZZZZ' | dd of="$d/corrupt.zst" bs=1 seek=1000 conv=notrunc 2>/dev/null check_rc zstd/corrupt-rc 1 zstd -t "$d/corrupt.zst" check_out zstd/corrupt-detected "match checksum" sh -c "zstd -t '$d/corrupt.zst' 2>&1 || true" # Two further error paths -- a file that is not there, and a file that is there # but is not zstd data. Both are exit 1 too, so unlike lz4 (which has a distinct # status per class) the status is all there is to pin here; what they add is # that neither is mistaken for success. check_rc zstd/err-missing-rc 1 zstd -t "$d/nosuch.zst" check_rc zstd/err-not-zstd-rc 1 zstd -d -q -c "$d/small.txt" # -k keeps the source file, which is zstd's default; -k is what a caller writes # when it matters, so the flag is pinned. cp "$d/small.txt" "$d/keep.txt" zstd -q -f -k "$d/keep.txt" -o "$d/keep.zst" 2>/dev/null check zstd/keep-source test -f "$d/keep.txt" # Several files in one invocation, each to its own .zst. cp "$d/small.txt" "$d/m1.txt"; cp "$d/small.txt" "$d/m2.txt" zstd -q -f "$d/m1.txt" "$d/m2.txt" 2>/dev/null check zstd/multifile-first test -s "$d/m1.txt.zst" check zstd/multifile-second test -s "$d/m2.txt.zst" # zstdgrep pipes a decompressed frame into grep -- the shipped wrapper script # exercising the CLI through a real fork+exec+pipe rather than in-process. check_out zstd/zstdgrep-match "round-trip" zstdgrep "round-trip" "$d/small.zst" rm -rf "$d"