// verify

Do not take our word for it.

Pixel identity is the strongest claim this tool makes. It is also the easiest one to check without trusting anybody, so here is how.

The two commands

four-way verification
$ scrubpony photo.jpg
$ python3 tests/verify_scrub.py photo.jpg photo.scrubbed.jpg && echo identical
identical

What those four checks are

#CheckWhat it rules out
1 exiftool reports nothing from the drop list, and an EXIF block if present contains Orientation and nothing else. Metadata that survived, and a rewritten EXIF block that quietly carries more than it should.
2 The entropy-coded scan is byte-for-byte identical. Any re-encoding at all. This is the strong one.
3 Both files decode to identical pixels. A structural change before the scan that alters how it is decoded.
4 Both files look identical once EXIF orientation is applied. A photo that is byte-identical and still comes out sideways.

// the part that makes it worth anything

tests/verify_scrub.py locates the SOS offset with its own segment scanner rather than asking ScrubPony where it thinks the scan starts. Using the tool under test to find the bytes that prove the tool under test is correct would prove nothing at all.

Doing check 2 by hand

If you want the byte-identity claim without running anybody's Python, it is a hex viewer and two hashes. Find FF DA — the SOS marker — in each file, take everything from that offset to the end, and compare:

# find the SOS offset in each file with whatever you trust tail -c +$OFFSET_ORIGINAL photo.jpg | shasum -a 256 tail -c +$OFFSET_SCRUBBED photo.scrubbed.jpg | shasum -a 256

The two digests match. There is no decoder in ScrubPony and no encoder either, so there is nothing in the program that could have altered them — but a hash you computed yourself is a better argument than that sentence.

The rest of the evidence

Pixel identity is the claim you can check in a minute. These are the ones that took longer to establish, all reproducible from the repository with make check:

741

checks across 7 test binaries, plus 8 integration suites

7,575,421

libFuzzer executions with zero crashes and zero leaks

30

SIGKILLs mid-write on a 24 MB in-place scrub; zero partial files

13 / 13

fixtures agreeing with exiftool on segment structure and orientation

0

warnings under -Wall -Wextra -Wpedantic -Wconversion, gcc and clang, both platforms

2,251

lines of C, against a test suite with more lines in it than the program

Fuzzing

Every input this program reads was written by somebody else, so the read path gets fuzzed rather than merely tested. tests/fuzz_jpeg.c drives the segment parser, the policy, the prefix labelling and the EXIF reader over arbitrary bytes, in memory, with no filesystem in the loop.

make fuzz && ./build/fuzz -max_total_time=3600 corpus/ make fuzz-dumb && ./build/fuzz_dumb -n 5000000 tests/fixtures/synthetic/*.jpg

The second exists because a fuzzer nobody can run is a fuzzer nobody runs: it is a self-contained random mutator that needs nothing but a C compiler, so it works on a Mac without installing clang's fuzzer runtime. It finds less than libFuzzer and it finds it without ceremony. A 20,000-iteration run is part of make check so the harness cannot quietly rot.

The EXIF reader is handed the raw input directly as well as whatever the parser passes it. Most random bytes never survive as far as a valid APP1, so without that the most hostile parser in the project would barely be exercised.

Atomicity

tests/run_interrupt.sh SIGKILLs a 24 MB in-place scrub thirty times at varying moments and checks that nothing but the two legal states ever appears on disk: the file exactly as it was, or the file exactly as it should become. No partials, and no stray temporaries left behind either.

Fixtures

tests/fixtures/synthetic/ holds machine-drawn images carrying real metadata written by real tools. They are committed, because there is nothing personal in any of them, and they are what the 13/13 exiftool cross-check runs against.

tests/fixtures/real/ is gitignored and empty in the repository. Put actual phone photos there and the golden tests pick them up automatically. They are not committed for the obvious reason: they are precisely the GPS-tagged files this tool exists to keep out of public repositories.

Read the source

2,251 lines is small enough to read in an afternoon, and the parts that matter are smaller still. If you only read three files, read src/policy.c for the keep/drop decision, src/rewrite.c for the copy, and src/io.c for the write-then-rename.

Common questions.

Why not just trust the test suite?
Because a test suite is written by the same person as the tool. The four-way check exists so that the strongest claim in the project can be re-run by somebody who does not trust either. It deliberately finds the SOS offset with its own segment scanner rather than asking ScrubPony where the scan starts — using the tool under test to locate the bytes that prove the tool under test is correct would prove nothing.
What does "byte-identical scan" actually mean?
Everything from the SOS marker to the end of the file is entropy-coded image data: the compressed picture. ScrubPony copies that region verbatim in a single operation, so the compressed bytes are identical, which is a stronger statement than "the decoded pixels match" — no decoder is involved on either side.
Why is "looks identical" a separate check?
Because a file can have byte-identical stored pixels and still appear turned a quarter turn, if the orientation tag differs. Check 4 applies EXIF orientation to both files before comparing, so it catches a mistake that checks 2 and 3 would sail past.
Do I need exiftool?
Only for check 1, which asks an independent implementation whether anything from the drop list survived. The script skips cleanly if exiftool is not installed; checks 2, 3 and 4 do not need it. Checks 3 and 4 need Pillow.
Can I verify without Python at all?
Yes, for the byte-identity claim. Find the SOS offset in each file with any hex viewer you trust, take everything from there to end of file, and compare the hashes. That is check 2 done by hand, and it is the check that matters most.