Skip to content

packPNG v1.8b - audit-driven bugfix: exit code on invalid magic

Choose a tag to compare

@YadeWira YadeWira released this 05 May 15:20
· 8 commits to master since this release

v1.8b - bugfix: exit non-zero when input has invalid magic (audit-driven)

Second bugfix on the v1.8 line, found by an 8-phase Windows 11 audit
harness run against the v1.8a binary on a real Windows 11 box
(DESKTOP-6R6L470, build 26200, PowerShell 5.1.26100). v1.8a passed 7
of 8 phases cleanly - the holdout was Phase 4 (malformed inputs).

The bug

Files with invalid magic - a 0-byte file named empty.png, or random
bytes named garbage.png - were silently skipped with a warning
but the program exited 0, lying to any script checking exit status.

packPNG a empty.png    -> "skipped 1 file(s) with invalid magic"  exit 0  (wrong)
packPNG a garbage.png  -> "skipped 1 file(s) with invalid magic"  exit 0  (wrong)
packPNG a missing.png  -> "ERROR missing file: missing.png"       exit 1  (correct)

Cause

The legacy filter near the end of main() accumulates unprocessable
inputs into three buckets:

  • skip_missing (file does not exist)
  • skip_bad_magic (file exists but is not a PNG / .ppg / .jng)
  • skip_compress / skip_decompress (wrong direction for the chosen
    subcommand)

skip_missing has always bumped g_errors per file - the program's
final return errs ? 1 : 0 then properly exits non-zero. skip_bad_magic
was missing the same line
, so when every input had bad magic the
program exited 0 even though nothing was processed.

The fix

One line. After the verbose-skip loop for skip_bad_magic, mirror the
existing skip_missing bookkeeping:

g_errors += (int)skip_bad_magic.size();

Mixed batches (some valid + some bad-magic) now also exit non-zero,
matching how Unix cp / tar / gzip behave when any source fails.
This is stricter than v1.8a behaviour. If you have a script that
intentionally feeds packPNG a directory containing non-PNG files
and was relying on exit 0, switch to per-file invocation or pre-filter
the input list.

Audit results (Windows 11, v1.8b)

Phase What it covers v1.8a v1.8b
1. PngSuite full corpus roundtrip 162 valid files (color_type x bit_depth x interlace) 162/162 162/162
2. Path edge cases space, paren, &, [], ;, %, #, +, ' 9/10 (audit-script bug) 10/10
3. Mode coverage mode 0 default + mode 2 (-ldf) 2/2 2/2
4. Malformed inputs 0-byte / garbage / truncated / sig-only / corrupt CRC 3/5 5/5
5. -r recurse + -fs 3-deep dir tree, structure preserve 2/2 2/2
6. Output collisions + -od dup basename rename, auto-mkdir 2/2 2/2
7. Big-file stress 3.42 MB random RGB pack+verify PASS (0.25s) PASS (0.25s)
8. Multi-thread determinism -th1 == -th4 == -th0 SHA256 PASS PASS

The Phase 2 long-path test (>260 char total) is consistently SKIPPED
because the host filesystem rejects creating such a path before
packPNG ever runs - that is a system / LongPathsEnabled group-policy
setting, not a packPNG bug. The manifest already declares
longPathAware=true.

What didn't change

  • Wire format unchanged: .ppg files written by v1.8 / v1.8a decode
    under v1.8b and vice versa. Both TCIP (PNG/APNG) and TCIJ (JNG)
    magics are intact.
  • All v1.8a behaviours preserved - the wait_and_return helper for
    the help/error console-close fix (encode.su feedback) still works.
  • Manifest assemblyIdentity@version bumped 1.8.0.1 -> 1.8.0.2.

Acknowledgements

Thanks to the encode.su community for the v1.8a console paper-cut
report and the YCoCg-R / STRATA pointer that prompted the v1.9
prototype work earlier in this cycle (the latter didn't pan out in
this pipeline, but useful negative evidence).

Binaries

  • packPNG-linux-x86_64-v1.8b - Linux x86-64, dynamically linked.
  • packPNG-windows-x86_64-full-v1.8b.exe - Windows x86-64, statically
    linked, mingw-w64 cross-compiled, UTF-8 active-code-page manifest.

🤖 Generated with Claude Code