# erofs-utils -- mkfs.erofs builds an EROFS image from a directory tree, # dump.erofs reads its layout back, fsck.erofs validates one and extracts it. # EROFS is the compressed read-only format Android and cloud images use, and # like dosfstools/mtools, e2fsprogs and squashfs-tools it operates on a plain # FILE rather than a block device -- which is what makes it usable here. # # THIS SUITE IS GREEN END TO END, WHICH ITS SQUASHFS SIBLING IS NOT. # erofs-utils 1.7.1 is single-threaded -- there is no pthread_create anywhere in # the tree (multi-threading arrives upstream in 1.8) -- so the thread trap that # leaves 95-squashfs-tools' write side red (linux-in-the-browser#85) has nothing # to attach to here. This is the first tool on the image that can CREATE a # compressed read-only filesystem, not just read one. # # THE WRITE SIDE IS GREEN ONLY BECAUSE OF THE RECIPE'S ONE PATCH, and the cases # below are built to say so. This kernel implements no xattr syscall at all -- # the whole {l,}{list,get,set}xattr family answers ENOSYS (38), measured on # target -- while mkfs.erofs calls llistxattr() on every source file and # tolerates only ENODATA and EOPNOTSUPP. Unpatched it therefore fails on the # FIRST file and builds nothing. The mkfs-plain and mkfs-xattr-tolerance-zero # cases go red if that patch is ever dropped; mkfs-xattr-disabled (-x-1) would # stay green even then, which is precisely why all three are asserted. # # THE lz4 CASES ARE THE POINT, NOT DECORATION. The recipe's PACKAGECONFIG asks # for lz4, and data/erofs-sample.erofs was built by UPSTREAM mkfs.erofs 1.7.1 on # the build host with -zlz4 over a 270 KiB compressible payload that lands in a # 8 KiB image (1.53% compress rate). Reading that file back on target therefore # requires our lz4 build to decompress extents produced by a DIFFERENT lz4 # build -- on-disk-format compatibility that an image we wrote ourselves cannot # demonstrate. The round-trip cases then close the loop the other way: we # compress with our own lz4 and read it back. # # Process count, not work, is what costs a suite its 60 s budget here (the # lesson 56-lz4.test learned the hard way), so the compressible payload is # reused from the fixture extraction rather than generated in a shell loop, and # every output match is a `case` against a captured string, not a forked grep. . "$BST_LIB" require mkfs.erofs erofs-utils/mkfs-present require dump.erofs erofs-utils/dump-present require fsck.erofs erofs-utils/fsck-present T=${TMPDIR:-/tmp}/bst-erofs.$$ rm -rf "$T"; mkdir -p "$T" FIX=$BST_DATA/erofs-sample.erofs UUID=3f5a1c22-9b7e-4d18-8a6f-2c0e7d4b9e01 # Assert on OUTPUT only, ignoring rc: mkfs.erofs --help and dump.erofs -V both # print correctly and then exit non-zero, so check_out (which also demands rc 0) # would report a working binary as broken. msg() { _i=$1; _n=$2; shift 2; _o=$("$@" 2>&1) case "$_o" in *"$_n"*) bst_ok "$_i" ;; *) bst_notok "$_i" "want '$_n' got: $(_bst_1line "$_o")" ;; esac; } has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) bst_notok "$1" "want '$2' in: $(_bst_1line "$3")" ;; esac; } hasnt(){ case "$3" in *"$2"*) bst_notok "$1" "'$2' is compiled in, but this image does not build it" ;; *) bst_ok "$1" ;; esac; } # ------------------------------------------------- all three binaries load and run msg erofs-utils/dump-version "dump.erofs 1.7.1" dump.erofs -V msg erofs-utils/fsck-version "fsck.erofs 1.7.1" fsck.erofs -V # ------------------------------------------------------- compiled-in compressors # mkfs.erofs --help ends with the compressor list it was built with. The negative # case is the point: lzma is an upstream option this PACKAGECONFIG does not ask # for, so without it a stock configure would pass the positives unnoticed. # `deflate` is erofs's own built-in kite_deflate and is present regardless -- it # is lz4/lz4hc that prove the recipe's lz4 PACKAGECONFIG took. HELP=$(mkfs.erofs --help 2>&1) has erofs-utils/comp-lz4 "lz4" "$HELP" has erofs-utils/comp-lz4hc "lz4hc" "$HELP" has erofs-utils/comp-deflate "deflate" "$HELP" hasnt erofs-utils/comp-no-lzma "lzma" "$HELP" # ------------------------------- reading an lz4 image THIS TARGET DID NOT WRITE # dump.erofs column-aligns with runs of spaces AND tabs, so every capture is # squeezed to single spaces once and matched against that -- a needle carrying # literal alignment whitespace is the kind that breaks on an upstream bump for # no functional reason. SB=$(dump.erofs -s "$FIX" 2>&1 | tr -s ' \t' ' ') has erofs-utils/fixture-magic "0xE0F5E1E2" "$SB" has erofs-utils/fixture-uuid "$UUID" "$SB" # mkfs.erofs -U parses the UUID with libuuid's uuid_parse and dump.erofs prints # it back with uuid_unparse, so an exact match end-to-end is this image's proof # that the util-linux-libuuid archive (BRINTOS-228) links and executes here. # # The fixture was built with -T 1700000000, so this asserts the fixed-timestamp # option round-trips. Match the DATE only, not the time: dump.erofs renders the # creation stamp in LOCAL time, so the full string is whatever TZ the reader is # in (this image is Universal, the build host was CEST -- and asserting the build # host's rendering is exactly how this case failed its first run). has erofs-utils/fixture-created "Nov 14" "$SB" ST=$(dump.erofs -S "$FIX" 2>&1 | tr -s ' \t' ' ') has erofs-utils/fixture-regular "regular file count: 3" "$ST" has erofs-utils/fixture-symlink "symlink file count: 1" "$ST" has erofs-utils/fixture-compressed "compressed files: 1" "$ST" # The directory table is metadata, so listing it walks structures the extent # decoder does not touch -- a separate signal from reading file contents below. LS=$(dump.erofs --ls --path=/ "$FIX" 2>&1) has erofs-utils/fixture-ls-file "compressible.txt" "$LS" has erofs-utils/fixture-ls-link "link" "$LS" check erofs-utils/fixture-fsck fsck.erofs "$FIX" # --extract decodes every extent. compressible.txt is 270336 bytes stored in a # 8 KiB image, so a correct byte count here means ~66x of lz4 extents produced by # UPSTREAM's lz4 were decompressed correctly by OURS. if fsck.erofs --extract="$T/ext" --no-preserve "$FIX" >/dev/null 2>&1; then bst_ok erofs-utils/fixture-extract else bst_notok erofs-utils/fixture-extract "fsck.erofs --extract failed on the shipped sample" fi check_eq erofs-utils/fixture-content "brintos erofs fixture" "$(cat "$T/ext/fixture.txt" 2>/dev/null)" check_eq erofs-utils/fixture-nested "nested payload" "$(cat "$T/ext/sub/nested.txt" 2>/dev/null)" check_eq erofs-utils/fixture-symlink-target "fixture.txt" "$(readlink "$T/ext/link" 2>/dev/null)" check_eq erofs-utils/fixture-lz4-size "270336" "$(wc -c < "$T/ext/compressible.txt" 2>/dev/null | tr -d ' ')" # Content, not just length: a decoder that emitted the right number of wrong # bytes would pass the size case above. check_eq erofs-utils/fixture-lz4-head "the quick brown fox jumps over the lazy dog 0123456789 ABCDEFGHIJ" \ "$(head -n 1 "$T/ext/compressible.txt" 2>/dev/null)" check_eq erofs-utils/fixture-lz4-tail "the quick brown fox jumps over the lazy dog 0123456789 ABCDEFGHIJ" \ "$(tail -n 1 "$T/ext/compressible.txt" 2>/dev/null)" # ------------------------------------------------ writing an image on the target # mkfs.erofs takes the IMAGE first and the SOURCE second -- the opposite of # mksquashfs, and the mistake a caller makes once. # # THIS CASE IS ALSO THE ON-TARGET PROOF OF THE RECIPE'S ONE PATCH. mkfs.erofs # calls llistxattr() on every source file it walks, and this kernel implements no # xattr syscall at all -- the whole family answers ENOSYS (38), measured on # target. Upstream tolerates only ENODATA and EOPNOTSUPP, so unpatched it fails # on the FIRST file with "failed to build shared xattrs" and can build nothing. check erofs-utils/mkfs-plain mkfs.erofs --quiet -T 1700000000 --all-root "$T/plain.erofs" "$T/ext" # -x0 is the sharp version of that proof, and -x-1 is the blunt one. -x-1 # disables xattr collection outright, so it never reaches llistxattr and works # with or without the patch; -x0 still queries and so fails on a stock build. # Asserting BOTH keeps the distinction visible: if only the -x-1 case is green, # the patch is missing and the tool is usable only by a caller who knows to pass # a flag. check erofs-utils/mkfs-xattr-tolerance-zero mkfs.erofs --quiet -x0 --all-root "$T/x0.erofs" "$T/ext" check erofs-utils/mkfs-xattr-disabled mkfs.erofs --quiet -x-1 --all-root "$T/xm.erofs" "$T/ext" check erofs-utils/plain-fsck fsck.erofs "$T/plain.erofs" if fsck.erofs --extract="$T/back" --no-preserve "$T/plain.erofs" >/dev/null 2>&1; then bst_ok erofs-utils/plain-extract else bst_notok erofs-utils/plain-extract "fsck.erofs --extract failed on our own uncompressed image" fi # cmp is exact and costs one fork, where a checksum would cost two. check erofs-utils/plain-roundtrip cmp -s "$T/ext/compressible.txt" "$T/back/compressible.txt" check_eq erofs-utils/plain-symlink "fixture.txt" "$(readlink "$T/back/link" 2>/dev/null)" # ---------------------------------------- our own lz4: compress AND decompress check erofs-utils/mkfs-lz4 mkfs.erofs --quiet -zlz4 -T 1700000000 -U "$UUID" --all-root "$T/lz4.erofs" "$T/ext" # The superblock records lz4_max_distance only when an lz4 algorithm was really # selected; an -zlz4 that silently fell back to storing plain would leave it 0. LSB=$(dump.erofs -s "$T/lz4.erofs" 2>&1 | tr -s ' \t' ' ') has erofs-utils/lz4-max-distance "lz4_max_distance: 65535" "$LSB" has erofs-utils/lz4-uuid-roundtrip "$UUID" "$LSB" LST=$(dump.erofs -S "$T/lz4.erofs" 2>&1 | tr -s ' \t' ' ') has erofs-utils/lz4-compressed "compressed files: 1" "$LST" if fsck.erofs --extract="$T/lz4out" --no-preserve "$T/lz4.erofs" >/dev/null 2>&1; then bst_ok erofs-utils/lz4-extract else bst_notok erofs-utils/lz4-extract "fsck.erofs --extract failed on our own lz4 image" fi check erofs-utils/lz4-roundtrip cmp -s "$T/ext/compressible.txt" "$T/lz4out/compressible.txt" # The image we wrote must be materially smaller than the data it holds, or # "compressed" above meant nothing. The LOWER bound is not decoration: a failed # mkfs.erofs leaves a ZERO-BYTE output file behind, and an upper-bound-only test # passes on it -- which is exactly how this case reported green on a run where # every other write-side case had failed. LZ4SZ=$(wc -c < "$T/lz4.erofs" 2>/dev/null | tr -d ' ') if [ -n "$LZ4SZ" ] && [ "$LZ4SZ" -gt 4096 ] && [ "$LZ4SZ" -lt 270336 ]; then bst_ok erofs-utils/lz4-smaller-than-source else bst_notok erofs-utils/lz4-smaller-than-source \ "image is ${LZ4SZ:-absent} bytes; a real one holding 270336 bytes is >4096 and <270336" fi # ------------------------------------------------------------------- controls # Without these, a reader that answered plausibly for any input would sweep # every positive case above. dd if=/dev/zero of="$T/zeroes" bs=1024 count=64 2>/dev/null case "$(dump.erofs -s "$T/zeroes" 2>&1)" in *0xE0F5E1E2*) bst_notok erofs-utils/reject-dump "64 KiB of zeroes reported a valid erofs superblock" ;; *) bst_ok erofs-utils/reject-dump ;; esac # fsck signals through rc, and 0 here would mean it called garbage a clean # filesystem -- the failure that matters is the silent one. if fsck.erofs "$T/zeroes" >/dev/null 2>&1; then bst_notok erofs-utils/reject-fsck "fsck.erofs returned 0 on 64 KiB of zeroes" else bst_ok erofs-utils/reject-fsck fi rm -rf "$T"