# pngcheck 2.3.0 -- the PNG/JNG/MNG integrity checker, plus the two GPL tools # the same tarball ships: pngsplit (decompose a PNG into its chunks) and # png-fix-IDAT-windowsize (re-encode the IDAT stream with a smaller window). # # EVERY INVOCATION IN THIS FILE REDIRECTS STDIN, WITHOUT EXCEPTION -- which is # why `run` below does it centrally rather than leaving it to each case. # pngcheck's second documented usage is "... | pngcheck", so a pngcheck given no # readable file argument READS STDIN, and under a runner that still holds a tty # it blocks there forever, taking the suite's ==BST-END== sentinel with it. That # is not hypothetical: a bare `pngcheck` was the first thing tried while writing # this file and it hung. # # The fixtures are written by src/pngcheck_mkfixtures.py and COMMITTED; the # committed bytes are the oracle, since their IDAT payloads come from whatever # zlib compressed them and the assertions below quote exact chunk lengths and # offsets. Nothing here decodes to pixels, because pngcheck does not decode to # pixels -- the exact-byte anchor is instead the pngsplit round trip, which # reassembles the input from its own output and compares it with cmp. . "$BST_LIB" require pngcheck pngcheck/present # A writable scratch dir with the fixtures under BARE NAMES: pngcheck echoes the # path it was given into "File: ..." and into the trailing summary, and pngsplit # writes its chunk files beside its input, so both want a directory we own. w=${TMPDIR:-/tmp}/bst-pngcheck.$$ rm -rf "$w"; mkdir -p "$w" || { bst_notok pngcheck/scratch "cannot mkdir $w"; exit 0; } for f in rgb pal gray16 interlaced badcrc badzlib trunc badsig; do cp "$BST_DATA/pngcheck-$f.png" "$w/pngcheck-$f.png" 2>/dev/null || { bst_notok pngcheck/fixtures "cannot stage $BST_DATA/pngcheck-$f.png"; rm -rf "$w"; exit 0; } done cd "$w" || { bst_notok pngcheck/scratch "cannot cd $w"; exit 0; } # run : execute ONCE, leaving the combined output in $o and the exit # status in $r, with stdin closed. # # Every error case below asserts the status and the message FROM THE SAME RUN, # and that is load-bearing rather than tidy. pngsplit and # png-fix-IDAT-windowsize both change their exit code once their output files # exist -- a second pngsplit of a damaged file reports "output exists" (18) # instead of the damage it found the first time (21 for a truncated file, 24 for # a bad CRC) -- so capturing the message in one run and the status in another # silently measures two different things. An earlier draft of this file did # exactly that and asserted 18 for the bad-CRC case: it passed, against the # wrong code, for the wrong reason. The host dry run is what caught it, and each # pngsplit case below gets its own pristine copy of the fixture so that every # run is a first run. run() { o=$("$@" 2>&1 : the half of check_out that check_out cannot express # on its own -- "the message is right" asserted SEPARATELY from "the status is # right", so a tool that reports the correct failure with the wrong exit code # (or the reverse) is diagnosed rather than lumped into a single red. has() { case "$3" in *"$2"*) bst_ok "$1" ;; *) if bst_execfail "$3"; then bst_unrun "$1" "$3" else bst_notok "$1" "want:'$2' got:'$(_bst_1line "$3")'"; fi ;; esac } # hasnt : the discrimination assertion. Used where two # error classes would otherwise be indistinguishable from "it failed". hasnt() { case "$3" in *"$2"*) bst_notok "$1" "unwanted:'$2' in:'$(_bst_1line "$3")'" ;; *) bst_ok "$1" ;; esac } # --------------------------------------------------------------------------- # 1. The ticket's first half: -v accepts a known-good PNG and reports its chunks # --------------------------------------------------------------------------- run pngcheck -v pngcheck-rgb.png check_eq pngcheck/verify-rc 0 "$r" has pngcheck/verify-clean "No errors detected in pngcheck-rgb.png" "$o" has pngcheck/geometry "32 x 24 image, 24-bit RGB, non-interlaced" "$o" # The chunk list, in full: every chunk of the file, each with the offset and the # length pngcheck computed for it. This is the assertion the ticket asks for, # written out chunk by chunk rather than as one blob so that a single wrong # offset names itself instead of failing a six-line comparison. has pngcheck/chunk-ihdr "chunk IHDR at offset 0x0000c, length 13" "$o" has pngcheck/chunk-text "chunk tEXt at offset 0x00025, length 33, keyword: Software" "$o" has pngcheck/chunk-idat "chunk IDAT at offset 0x00052, length 1656" "$o" has pngcheck/chunk-iend "chunk IEND at offset 0x006d6, length 0" "$o" has pngcheck/zlib-header "zlib: deflated, 32K window, maximum compression" "$o" has pngcheck/chunk-count "(4 chunks, 23.7% compression)" "$o" # --------------------------------------------------------------------------- # 2. The ticket's second half: one corrupted CRC -> non-zero exit, named chunk # --------------------------------------------------------------------------- # pngcheck-badcrc.png is pngcheck-rgb.png with ONE BIT of the tEXt chunk's # stored CRC flipped and nothing else touched, so "CRC error in chunk tEXt" is # the only correct diagnosis and any other message is a real finding. run pngcheck pngcheck-badcrc.png check_eq pngcheck/badcrc-rc 2 "$r" has pngcheck/badcrc-names-chunk "CRC error in chunk tEXt" "$o" has pngcheck/badcrc-values "computed 67a2fd90, expected 67a2fd91" "$o" has pngcheck/badcrc-verdict "ERROR: pngcheck-badcrc.png" "$o" # ...and -v reaches the same verdict, having printed the chunks up to the damage. run pngcheck -v pngcheck-badcrc.png check_eq pngcheck/badcrc-v-rc 2 "$r" has pngcheck/badcrc-v-chunks "chunk tEXt at offset 0x00025" "$o" has pngcheck/badcrc-v-verdict "ERRORS DETECTED in pngcheck-badcrc.png" "$o" # --------------------------------------------------------------------------- # 3. The three other damage classes, each distinguished from the CRC one # --------------------------------------------------------------------------- # Every one of these exits 2, exactly as the CRC case does. The exit status # therefore says nothing about WHY, and a suite that checked only the status # would pass against a pngcheck that had stopped telling them apart. The # `hasnt` line in each group is what actually holds the distinction. # Damaged deflate stream with a RECOMPUTED CRC: checksum-clean, so the failure # must come from zlib and must NOT be reported as a CRC error. run pngcheck pngcheck-badzlib.png check_eq pngcheck/badzlib-rc 2 "$r" has pngcheck/badzlib-msg "zlib: inflate error = -3 (data error)" "$o" hasnt pngcheck/badzlib-not-crc "CRC error" "$o" # Truncated mid-IDAT: the chunk header promises 1656 bytes and the file stops. run pngcheck pngcheck-trunc.png check_eq pngcheck/trunc-rc 2 "$r" has pngcheck/trunc-msg "EOF while reading IDAT data" "$o" hasnt pngcheck/trunc-not-crc "CRC error" "$o" # One bit of the 8-byte signature: rejected before any chunk is parsed, so the # chunk list must not appear at all -- asserted under -v, where it otherwise # would. run pngcheck -v pngcheck-badsig.png check_eq pngcheck/badsig-rc 2 "$r" has pngcheck/badsig-msg "this is neither a PNG or JNG image nor a MNG stream" "$o" hasnt pngcheck/badsig-no-chunks "chunk IHDR" "$o" # --------------------------------------------------------------------------- # 4. The other three colour types, and the option surface # --------------------------------------------------------------------------- run pngcheck -v pngcheck-pal.png check_eq pngcheck/pal-rc 0 "$r" has pngcheck/pal-geometry "16 x 16 image, 8-bit palette, non-interlaced" "$o" has pngcheck/pal-plte "chunk PLTE at offset 0x00025, length 24: 8 palette entries" "$o" has pngcheck/pal-trns "chunk tRNS at offset 0x00049, length 8: 8 transparency entries" "$o" # -p prints the palette itself. Exact rows of the table are quoted: it is # integer data with no tolerance anywhere, so a byte-order or width fault on the # target would have to show up here. run pngcheck -p pngcheck-pal.png check_eq pngcheck/pal-p-rc 0 "$r" has pngcheck/pal-entry0 "0: ( 0,255, 0) = (0x00,0xff,0x00)" "$o" has pngcheck/pal-entry7 "7: (224, 31,192) = (0xe0,0x1f,0xc0)" "$o" has pngcheck/pal-trns0 "0: 0 = 0x00" "$o" run pngcheck -v pngcheck-gray16.png check_eq pngcheck/gray16-rc 0 "$r" has pngcheck/gray16-depth "8 x 8 image, 16-bit grayscale, non-interlaced" "$o" # Adam7. The rows-per-pass breakdown is the only assertion in this file that # reads interlace STRUCTURE rather than a header field, and pngcheck derives it # from the geometry itself. run pngcheck -v pngcheck-interlaced.png check_eq pngcheck/interlaced-rc 0 "$r" has pngcheck/interlaced-flag "32 x 24 image, 24-bit RGB, interlaced" "$o" has pngcheck/interlaced-adam7 "rows per pass: 3, 3, 3, 6, 6, 12, 12" "$o" # -t prints tEXt content; the fixture carries a keyword and a value we wrote. run pngcheck -t pngcheck-rgb.png check_eq pngcheck/text-rc 0 "$r" has pngcheck/text-keyword "Software:" "$o" has pngcheck/text-value "brintOS pngcheck fixture" "$o" # -q is SILENCE on success -- its whole contract. An EMPTY capture is the # assertion; "contains nothing interesting" would pass against a broken -q. run pngcheck -q pngcheck-rgb.png check_eq pngcheck/quiet-rc 0 "$r" check_eq pngcheck/quiet-silent "" "$o" # ...and -q still speaks up for a bad file, which is the half that makes the # silence above meaningful rather than a tool that prints nothing ever. run pngcheck -q pngcheck-badcrc.png check_eq pngcheck/quiet-bad-rc 2 "$r" has pngcheck/quiet-reports "CRC error in chunk tEXt" "$o" # Several files in one run, and the per-run summary line. run pngcheck pngcheck-rgb.png pngcheck-pal.png pngcheck-gray16.png check_eq pngcheck/multi-rc 0 "$r" has pngcheck/multi-summary "No errors were detected in 3 of the 3 files tested." "$o" # A good file and a bad one together: the run fails, and the summary counts both # sides rather than collapsing to the failure. run pngcheck pngcheck-rgb.png pngcheck-badcrc.png check_eq pngcheck/mixed-rc 2 "$r" has pngcheck/mixed-ok-line "OK: pngcheck-rgb.png" "$o" has pngcheck/mixed-summary "Errors were detected in 1 of the 2 files tested." "$o" # --------------------------------------------------------------------------- # 5. pngsplit -- and the exact byte comparison this port is anchored on # --------------------------------------------------------------------------- # The three programs ship in one package, so an absent sibling is a PACKAGING # regression and must be red, not a skip. `require` is deliberately not used # here: it would end the file and take every remaining case with it. if command -v pngsplit >/dev/null 2>&1; then bst_ok pngcheck/have-pngsplit run pngsplit pngcheck-rgb.png check_eq pngcheck/split-rc 0 "$r" # One file per chunk, plus the 8-byte signature, each named by chunk type. check pngcheck/split-sig test -f pngcheck-rgb.png.0000.sig check pngcheck/split-ihdr test -f pngcheck-rgb.png.0001.IHDR check pngcheck/split-text test -f pngcheck-rgb.png.0002.tEXt check pngcheck/split-idat test -f pngcheck-rgb.png.0003.IDAT check pngcheck/split-iend test -f pngcheck-rgb.png.0004.IEND # THE EXACT BYTE COMPARISON. The parts are the whole file and nothing else: # concatenated back in order they must reproduce the input bit for bit. This # is integer bookkeeping over a byte stream with no tolerance anywhere, so it # is the most regression-sensitive case in the file -- and unlike a stored # digest it needs no reference payload, because the input IS the reference. cat pngcheck-rgb.png.0000.sig pngcheck-rgb.png.0001.IHDR \ pngcheck-rgb.png.0002.tEXt pngcheck-rgb.png.0003.IDAT \ pngcheck-rgb.png.0004.IEND > rejoined.png 2>/dev/null check pngcheck/split-roundtrip cmp pngcheck-rgb.png rejoined.png # The signature part is the PNG signature, all eight bytes of it. check_eq pngcheck/split-sig-size 8 "$(wc -c < pngcheck-rgb.png.0000.sig | tr -d ' ')" # Each damaged file gets its OWN pristine copy so the run is a first run -- # see the note on `run` above. pngsplit reports the same damage pngcheck does, # with its own status codes, and it prints the computed and the stored CRC as # two DIFFERENT values, which is exactly what its sibling # png-fix-IDAT-windowsize gets wrong (section 6). cp pngcheck-badcrc.png sp-badcrc.png run pngsplit sp-badcrc.png check_eq pngcheck/split-badcrc-rc 24 "$r" has pngcheck/split-badcrc-msg "has bad tEXt CRC (got 0x67a2fd90, expected 0x67a2fd91)" "$o" cp pngcheck-trunc.png sp-trunc.png run pngsplit sp-trunc.png check_eq pngcheck/split-trunc-rc 21 "$r" has pngcheck/split-trunc-msg "unexpected EOF while reading sp-trunc.png" "$o" cp pngcheck-badsig.png sp-badsig.png run pngsplit sp-badsig.png check_eq pngcheck/split-badsig-rc 17 "$r" has pngcheck/split-badsig-msg "does not appear to be a PNG, MNG, or JNG file" "$o" # The masking behaviour itself, pinned rather than left to be rediscovered: # split the SAME file twice and the second run reports "exists" instead of the # damage. Anyone who writes a pngsplit case without a fresh copy meets this. run pngsplit sp-badcrc.png check_eq pngcheck/split-rerun-masks-rc 18 "$r" else bst_notok pngcheck/have-pngsplit "pngsplit missing -- it ships in the same package as pngcheck" fi # --------------------------------------------------------------------------- # 6. png-fix-IDAT-windowsize # --------------------------------------------------------------------------- if command -v png-fix-IDAT-windowsize >/dev/null 2>&1; then bst_ok pngcheck/have-png-fix # It RE-ENCODES the IDAT with hardcoded settings, so its output is a property # of the target's zlib and MUST NOT be byte-compared against anything. What is # asserted instead is that the result is a valid PNG carrying the same image # and the same ancillary chunk -- all of which hold whatever zlib emits. run png-fix-IDAT-windowsize pngcheck-rgb.png check_eq pngcheck/fix-rc 0 "$r" check pngcheck/fix-wrote-output test -f pngcheck-rgb-fixed.png run pngcheck -v pngcheck-rgb-fixed.png check_eq pngcheck/fix-output-valid 0 "$r" has pngcheck/fix-keeps-geometry "32 x 24 image, 24-bit RGB, non-interlaced" "$o" has pngcheck/fix-keeps-text "chunk tEXt" "$o" has pngcheck/fix-keeps-iend "chunk IEND" "$o" # It refuses to clobber the output it just wrote unless told to, and says so # in the exit status rather than only on stderr. Order matters here and is # deliberate: this case depends on the run above having created the file. run png-fix-IDAT-windowsize pngcheck-rgb.png check_eq pngcheck/fix-no-clobber 15 "$r" run png-fix-IDAT-windowsize -force pngcheck-rgb.png check_eq pngcheck/fix-force 0 "$r" # Argument handling: no files at all, and an option it does not know. run png-fix-IDAT-windowsize check_eq pngcheck/fix-noargs 5 "$r" run png-fix-IDAT-windowsize -nosuchoption check_eq pngcheck/fix-badopt 1 "$r" # It rejects the damaged file, with its own status code... run png-fix-IDAT-windowsize pngcheck-badcrc.png check_eq pngcheck/fix-badcrc-rc 24 "$r" # ...and THESE TWO CASES EXIST TO HOLD A PATCH IN PLACE. Stock pngcheck 2.3.0 # passes `chunkcrc` twice to this message's two %08lx slots, so it prints the # file's STORED CRC as both the computed and the expected value and the # diagnostic reads "got 0x...91, expected 0x...91" -- a checksum mismatch in # which the two checksums are equal. Its sibling pngsplit formats the same # sentence from calc_crc and chunkcrc and gets it right, which is what makes # this a slip rather than a convention. If the bbappend's patch is ever # dropped, `fix-crc-computed` is the case that goes red; `fix-crc-expected` # passes either way and is here so the pair distinguishes "the patch is gone" # from "the message changed shape". has pngcheck/fix-crc-computed "got 0x67a2fd90" "$o" has pngcheck/fix-crc-expected "expected 0x67a2fd91" "$o" else bst_notok pngcheck/have-png-fix "png-fix-IDAT-windowsize missing -- it ships in the same package as pngcheck" fi # --------------------------------------------------------------------------- # 7. Packaging: the three programs land where the recipe says they do # --------------------------------------------------------------------------- check pngcheck/installed-pngcheck test -x /usr/bin/pngcheck check pngcheck/installed-pngsplit test -x /usr/bin/pngsplit check pngcheck/installed-png-fix test -x /usr/bin/png-fix-IDAT-windowsize cd / rm -rf "$w"