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
nullwhere 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
| Type | How it happens in real life | What your code should do |
|---|---|---|
| Truncated | Upload interrupted, disk full, crashed writer | Length/completeness check fails with a clear error |
| Damaged header | Bit rot, buggy exporter, encoding mangling | Signature check rejects before parsing |
| Corrupted middle | Network error mid-transfer, bad sector | CRC or parse error surfaces — no crash, no hang |
| Extension lie | User renamed .exe to .pdf | Content 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
| Format | Soft spot | What your app should do |
|---|---|---|
Header is %PDF-, trailer is %%EOF; truncation kills the xref | Reject on parse error; never trust a PDF missing its trailer | |
| ZIP | Every entry carries a CRC-32; one flipped byte fails extract mid-stream | Report "corrupt archive" and clean up temp files — zip -FF can sometimes repair, but don't build on that |
| MP4 | The moov index atom often sits at the end of the file — truncation removes it entirely | Reject by size + signature early; don't buffer the file to discover it's unplayable |
| PNG | 8-byte signature (89 50 4E 47 0D 0A 1A 0A) plus a CRC per chunk | Map 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
| Scenario | How to build it | Assert |
|---|---|---|
| Truncated to 10% | head -c | Clean parse error, not EOF exception mid-loop |
| Zeroed header | dd if=/dev/zero bs=1 count=4 conv=notrunc | Signature check rejects |
| One flipped byte | Seeded Python snippet | CRC/parse error, no crash |
| 8 KiB of noise mid-file | dd if=/dev/urandom seek=... conv=notrunc | Parser survives and reports |
| Empty file | truncate -s 0 copy.pdf | Explicit "empty file" error, not treated as valid |
| Extension lie | cp sample.pdf fake.zip | Sniffing 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.
Related Posts
How to Test File Upload Features: A Developer's Guide
Learn the best practices for testing file upload functionality in web applications using sample files.
Read moreTestingSample PDF Files for Testing: Complete Guide
Everything you need to know about using sample PDF files for development and testing.
Read moreDevelopmentTesting with Dummy Data: Best Practices
Learn how to use dummy data effectively in your development and testing workflows.
Read more