Skip to content
Back to Blog
2026-10-19Testing

Corrupt File Testing: Why Broken Files Belong in Your Test Suite

Every parser is tested against files that parse. Then production sends truncated uploads, files with bit rot, archives with flipped bytes, and executables renamed to .pdf — and the stack trace goes to your inbox. If your test suite only ever contains valid files, you haven't tested your parser. You've demoed it.

Why Clean Samples Aren't Enough

Valid files exercise the happy path: parse, store, render. Broken files exercise everything engineers actually get paged for:

  • 500s and stack traces from a null where a header object should be
  • Infinite loops or OOM when a malformed length field says "next chunk: 4 billion bytes"
  • Partial writes left on disk when parsing dies halfway through
  • Silent data corruption when a mangled file is accepted anyway and poisons downstream data

Users don't intentionally upload broken files — networks drop, laptops sleep mid-upload, disks fail. Your code has to fail cleanly anyway.

The Four Kinds of Broken

TypeHow it happens in real lifeWhat your code should do
TruncatedUpload interrupted, disk full, crashed writerLength/completeness check fails with a clear error
Damaged headerBit rot, buggy exporter, encoding manglingSignature check rejects before parsing
Corrupted middleNetwork error mid-transfer, bad sectorCRC or parse error surfaces — no crash, no hang
Extension lieUser renamed .exe to .pdfContent sniffing wins; never trust the filename

Each type hits a different code path, so you need all four in the suite — not just "some broken file."

Start From a Known-Good File

Don't hunt for corrupted files in the wild — manufacture them. Start from a file you know is valid, copy it, and break exactly one thing at a time. That gives you deterministic, reproducible failures instead of heisenbugs. Real samples make the best raw material: a PDF sample, a ZIP sample, an MP4 sample, or a PNG sample — copy first, corrupt the copy.

Truncate it

head -c 4096 sample.pdf > truncated.pdf     # keep only the first 4 KiB
truncate -s 1000 copy.zip                   # cut a copy down to 1000 bytes

Every surviving byte is still valid — the file just stops early. Truncation is the single best way to find missing length checks, because the header parses fine and the disappointment comes later.

Break the header

# Zero the first 4 bytes — destroys the file signature
dd if=/dev/zero of=copy.pdf bs=1 count=4 conv=notrunc

# Flip one byte at a fixed offset (2000) in a PNG
printf '\xff' | dd of=copy.png bs=1 seek=2000 conv=notrunc

conv=notrunc is the load-bearing part — without it, dd replaces the file instead of editing in place.

Scramble the middle

# Overwrite 8 KiB with random bytes at offset 100 KiB
dd if=/dev/urandom of=copy.mp4 bs=1024 count=8 seek=100 conv=notrunc

Headers survive, structure survives, payload doesn't. This is what a bad network day looks like.

Reproducible corruption in CI

Random corruption is great for fuzzing, but a regression test needs the same broken file every run. Seed it:

import random

random.seed(42)
with open("copy.zip", "r+b") as f:
    f.seek(2048)
    f.write(bytes(random.randrange(256) for _ in range(64)))

Seeded corruption means the byte that breaks your parser is identical on every CI run — a failing test now points at an exact, reproducible artifact.

What Breaks Per Format

FormatSoft spotWhat your app should do
PDFHeader is %PDF-, trailer is %%EOF; truncation kills the xrefReject on parse error; never trust a PDF missing its trailer
ZIPEvery entry carries a CRC-32; one flipped byte fails extract mid-streamReport "corrupt archive" and clean up temp files — zip -FF can sometimes repair, but don't build on that
MP4The moov index atom often sits at the end of the file — truncation removes it entirelyReject by size + signature early; don't buffer the file to discover it's unplayable
PNG8-byte signature (89 50 4E 47 0D 0A 1A 0A) plus a CRC per chunkMap strict-decoder CRC errors to a 4xx, not a 500

The pattern: each format fails in a different place, so a generic try/catch is not a strategy. Validate the signature, then parse defensively, then translate every failure into an HTTP status your API contract promises.

What to Assert When It Breaks

"Just throws" is not the goal. Assert the failure shape:

  • Right status: 4xx with a useful message — not a 500 with a stack trace leaking paths
  • No zombies: temp files and partial writes are cleaned up after the failure
  • No hangs: a request with a mangled length field must finish inside your timeout — cap your read loops
  • No memory bombs: check declared size vs actual bytes before allocating buffers

A Corrupt-File Test Matrix

ScenarioHow to build itAssert
Truncated to 10%head -cClean parse error, not EOF exception mid-loop
Zeroed headerdd if=/dev/zero bs=1 count=4 conv=notruncSignature check rejects
One flipped byteSeeded Python snippetCRC/parse error, no crash
8 KiB of noise mid-filedd if=/dev/urandom seek=... conv=notruncParser survives and reports
Empty filetruncate -s 0 copy.pdfExplicit "empty file" error, not treated as valid
Extension liecp sample.pdf fake.zipSniffing catches content/extension mismatch

Corruption you create yourself is free, targeted, and reproducible. Ten minutes of dd and a seeded Python script buys you the test cases your users will hand you eventually — on a Friday, in production.

#corrupt-files#testing#file-validation#error-handling#qa