# mtd-utils, jffs2 half -- mkfs.jffs2 builds a JFFS2 image from a directory # tree, jffs2dump walks its raw nodes, jffs2reader lists and extracts one, and # sumtool adds the summary nodes a real flash mount wants for fast attach. # # THE DELIVERABLE IS IMAGE AUTHORING, NOT DEVICE ACCESS. There is no MTD # hardware behind a browser tab, so nothing here touches /dev/mtd*: every case # operates on a plain FILE. That is the half of mtd-utils that is useful on this # target -- a machine that can produce a flash image for a real embedded board -- # and it is the half the image ships. # # THE STRONGEST CASE HERE IS mkfs-byte-identical, AND IT IS WHY THE READER'S # LIMITS DO NOT UNDERMINE THE PORT. mkfs.jffs2 -f (faketime) -q (squash owners) # makes the output a pure function of the input tree, so the image this target # writes can be compared against one written by an x86 build of the SAME source. # They match byte for byte, which is a far stronger statement than any # read-back: the authoring path is not merely plausible here, it is bit-exact. # The comparison uses a ONE-FILE tree deliberately -- mkfs.jffs2 emits nodes in # readdir order, so a multi-entry tree could differ between two filesystems for # reasons that have nothing to do with this port, and pinning an md5 to that # would be a case that goes red for the wrong reason. # # THE lzo CASES ARE THE POINT, NOT DECORATION. lzo is a NEW recipe on this arm, # picked up by this ticket only because mkfs.jffs2 and mkfs.ubifs want it, so # nothing else on the image would notice if it silently failed to link. Note # upstream registers the LZO compressor with .disabled = 1 -- it is compiled in # but OFF unless -X selects it -- so `-x zlib -x rtime` alone does NOT exercise # lzo, it just leaves nothing enabled and stores plain. That cost a round of # diagnosis and is why compr-lzo asserts the disabled state explicitly and # lzo-enabled-* passes -X. # # Process count, not work, is what costs a suite its 60 s budget on this runtime # (the lesson 56-lz4.test learned the hard way), so the compressible payload is # built by doubling a shell string -- no forks, no multi-thousand-iteration # loop -- and every output match is a `case` against a captured string rather # than a forked grep. . "$BST_LIB" require mkfs.jffs2 mtd-utils-jffs2/mkfs-present require jffs2dump mtd-utils-jffs2/dump-present require jffs2reader mtd-utils-jffs2/reader-present require sumtool mtd-utils-jffs2/sumtool-present T=${TMPDIR:-/tmp}/bst-jffs2.$$ SRC=$T/src rm -rf "$T"; mkdir -p "$SRC/sub" EB=0x10000 # 64 KiB erase block -- mkfs.jffs2's own default # Assert on OUTPUT only, ignoring rc: mkfs.jffs2 -L prints the compressor table # through errmsg_die() and therefore exits 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 listed, but this build cannot have it" ;; *) bst_ok "$1" ;; esac; } # ok iff the command FAILS -- for the controls, where any non-zero rc is correct # and asserting a specific one would go red on an upstream errno change. fails() { _i=$1; shift if "$@" >/dev/null 2>&1; then bst_notok "$_i" "unexpectedly succeeded" else bst_ok "$_i"; fi; } size() { wc -c < "$1" 2>/dev/null | tr -d ' '; } printf 'jffs2-payload-marker\n' > "$SRC/hello.txt" printf 'nested payload\n' > "$SRC/sub/nested.txt" ln -s hello.txt "$SRC/link.txt" # ~67 KiB of highly compressible text, built by ten string doublings. A shell # loop of the same size costs thousands of builtin iterations on this runtime; # doubling costs ten. L='the quick brown fox jumps over the lazy dog 0123456789 ABCDEFGHIJ ' L="$L$L"; L="$L$L"; L="$L$L"; L="$L$L"; L="$L$L" L="$L$L"; L="$L$L"; L="$L$L"; L="$L$L"; L="$L$L" printf '%s' "$L" > "$SRC/fill.txt" FILLSZ=$(size "$SRC/fill.txt") check_eq mtd-utils-jffs2/payload-built 67584 "$FILLSZ" # ------------------------------------------------------ the binaries load and run msg mtd-utils-jffs2/mkfs-version "mkfs.jffs2 (mtd-utils) 2.1.6" mkfs.jffs2 -V msg mtd-utils-jffs2/dump-version "jffs2dump (mtd-utils) 2.1.6" jffs2dump -V # ------------------------------------------------------- compiled-in compressors # "%10s priority:%d enabled|disabled" per registered compressor. zlib and rtime # are built in unconditionally; lzo appears only under CONFIG_JFFS2_LZO, which # configure defines only when it found liblzo2 -- so this line is the direct # evidence that the new lzo recipe linked. It is listed DISABLED, which is # upstream's own default (compr_lzo.c sets .disabled = 1), not a build defect; # asserting the exact state keeps that distinction from being re-derived. lzma # is the control: JFFS2 has no such compressor in any build. LIST=$(mkfs.jffs2 -L 2>&1) has mtd-utils-jffs2/compr-zlib "zlib priority:60 enabled" "$LIST" has mtd-utils-jffs2/compr-rtime "rtime priority:50 enabled" "$LIST" has mtd-utils-jffs2/compr-lzo "lzo priority:80 disabled" "$LIST" hasnt mtd-utils-jffs2/compr-no-lzma "lzma" "$LIST" # ----------------------------------------------------------- authoring an image check mtd-utils-jffs2/mkfs mkfs.jffs2 -r "$SRC" -e $EB -q -f -o "$T/img.jffs2" # A failed mkfs.jffs2 leaves a ZERO-BYTE output behind, and an upper-bound-only # size test passes on it -- the trap 96-erofs-utils recorded. So the lower bound # is asserted first, and separately. IMGSZ=$(size "$T/img.jffs2") if [ -n "$IMGSZ" ] && [ "$IMGSZ" -gt 512 ]; then bst_ok mtd-utils-jffs2/image-nonempty else bst_notok mtd-utils-jffs2/image-nonempty "image is ${IMGSZ:-absent} bytes"; fi # The first node of a cleanmarked image is a CLEANMARKER whose common header # carries JFFS2_MAGIC_BITMAP 0x1985, stored little-endian -> bytes 85 19. check_eq mtd-utils-jffs2/image-magic "8519" \ "$(od -An -tx1 -N2 "$T/img.jffs2" 2>/dev/null | tr -d ' \n')" check mtd-utils-jffs2/mkfs-uncompressed mkfs.jffs2 -r "$SRC" -e $EB -q -f -m none -o "$T/none.jffs2" # THE BIT-EXACTNESS CASE. -f/-q make the output a pure function of the input, so # this md5 is the one an x86 mkfs.jffs2 2.1.6 produces from the identical # one-file tree (confirmed against both a from-source x86 build and the build # host's own distro mkfs.jffs2). It asserts the whole authoring path -- # directory walk, node layout, CRCs, cleanmarkers, padding -- in one value, and # it is what makes the reader gap below a convenience issue rather than a # correctness one. mkdir -p "$T/one"; cp "$SRC/fill.txt" "$T/one/fill.txt" check mtd-utils-jffs2/mkfs-one-file mkfs.jffs2 -r "$T/one" -e $EB -q -f -m none -o "$T/one.jffs2" check_eq mtd-utils-jffs2/mkfs-byte-identical "2826ed6a200bb11c2358a1887cb259d7" \ "$(md5sum < "$T/one.jffs2" 2>/dev/null | cut -c1-32)" # ------------------------------------------- the raw node stream, read on target # jffs2dump walks every node in the image. Unlike jffs2reader it has no # directory model, so this is the independent confirmation that BOTH files # really are in the image the tool wrote. DUMP=$(jffs2dump -c "$T/none.jffs2" 2>&1) has mtd-utils-jffs2/dump-hello "hello.txt" "$DUMP" has mtd-utils-jffs2/dump-fill "fill.txt" "$DUMP" # ------------------------------------------------------------------ compression # Same tree, same tool, compression on vs off. 67 KiB of one repeated line # cannot land in a still-67-KiB image unless every compressor fell through to # storing plain, so this is the assertion that zlib is really running. NONESZ=$(size "$T/none.jffs2") if [ -n "$NONESZ" ] && [ -n "$IMGSZ" ] && [ "$IMGSZ" -lt $((NONESZ / 4)) ]; then bst_ok mtd-utils-jffs2/compressed-smaller else bst_notok mtd-utils-jffs2/compressed-smaller "default ${IMGSZ:-absent} vs -m none ${NONESZ:-absent}" fi # ------------------------------------------------------------- lzo, switched on # -X ENABLES a compressor (upstream ships lzo disabled), and -x disables the # other two, so this image can only have been produced by OUR liblzo2. check mtd-utils-jffs2/lzo-enabled-mkfs \ mkfs.jffs2 -r "$SRC" -e $EB -q -f -X lzo -x zlib -x rtime -o "$T/lzo.jffs2" LZOSZ=$(size "$T/lzo.jffs2") if [ -n "$LZOSZ" ] && [ "$LZOSZ" -gt 512 ] && [ "$LZOSZ" -lt $((NONESZ / 4)) ]; then bst_ok mtd-utils-jffs2/lzo-compressed else bst_notok mtd-utils-jffs2/lzo-compressed "lzo image ${LZOSZ:-absent}, uncompressed $NONESZ" fi # -t decompresses every block inside mkfs.jffs2 and compares it against the # original, so this is upstream's own compressor round-trip running on target -- # and with only lzo enabled it exercises OUR lzo both ways. It is the read-back # proof that does not depend on jffs2reader. check mtd-utils-jffs2/lzo-roundtrip \ mkfs.jffs2 -r "$SRC" -e $EB -q -f -X lzo -x zlib -x rtime -t -o "$T/lzot.jffs2" check mtd-utils-jffs2/zlib-roundtrip \ mkfs.jffs2 -r "$SRC" -e $EB -q -f -x lzo -x rtime -t -o "$T/zlibt.jffs2" # ---------------------------------------------------------------- jffs2reader # What the reader CAN do here: list the root and extract a single-node file. LS=$(jffs2reader "$T/none.jffs2" -d / 2>&1) has mtd-utils-jffs2/reader-ls-file "hello.txt" "$LS" check_eq mtd-utils-jffs2/reader-cat "jffs2-payload-marker" \ "$(jffs2reader "$T/none.jffs2" -f /hello.txt 2>/dev/null)" # WHAT IT CANNOT, AND THIS CASE SHIPS RED ON PURPOSE. jffs2reader does not # enumerate fill.txt at all -- `-d /` omits it and `-f /fill.txt` answers "No # such file or directory" -- on an image jffs2dump above walks completely and # that an x86 build of the same source reads back in full. It is not the # writer: the image is byte-identical to the native one (asserted above), and # our reader fails on the NATIVE image too. It is not compression (it happens at # -m none), not read(2) (a single 69024-byte read is exact here) and not fstat # (it reports 69024). Filed as brintos/linux-in-the-browser#128. Left FAILING # rather than skipped so it flips green by itself when that closes -- which is # the regression signal this case exists to buy. jffs2reader "$T/none.jffs2" -f /fill.txt > "$T/fill.back" 2>/dev/null check mtd-utils-jffs2/reader-multinode cmp -s "$SRC/fill.txt" "$T/fill.back" # ------------------------------------------------------------------- sumtool # sumtool rewrites the image with a summary node per erase block, which is what # lets a real JFFS2 mount skip the full scan. It must grow the image, and the # result must still be a walkable node stream. check mtd-utils-jffs2/sumtool sumtool -i "$T/img.jffs2" -o "$T/sum.jffs2" -e $EB -p SUMSZ=$(size "$T/sum.jffs2") if [ -n "$SUMSZ" ] && [ "$SUMSZ" -ge "$IMGSZ" ]; then bst_ok mtd-utils-jffs2/sumtool-grew else bst_notok mtd-utils-jffs2/sumtool-grew "summed ${SUMSZ:-absent} < plain $IMGSZ"; fi has mtd-utils-jffs2/sumtool-readback "hello.txt" "$(jffs2dump -c "$T/sum.jffs2" 2>&1)" # ------------------------------------------------------------------- controls # Without these, a writer that produced nothing and a reader that answered # plausibly for any input would sweep every positive case above. fails mtd-utils-jffs2/reject-missing-root mkfs.jffs2 -r "$T/no-such-dir" -e $EB -o "$T/bad.jffs2" fails mtd-utils-jffs2/reject-bad-compr mkfs.jffs2 -r "$SRC" -e $EB -q -f -X nosuchcompressor -o "$T/bad2.jffs2" dd if=/dev/zero of="$T/zeroes" bs=1024 count=64 2>/dev/null hasnt mtd-utils-jffs2/reject-not-jffs2 "hello.txt" "$(jffs2dump -c "$T/zeroes" 2>&1)" rm -rf "$T"