testdata — the golden PDFs
Fourteen committed files, one per structural case, asserted by
golden_test.odin. They are deliberately tiny (370 B to 1.4 KB) and, wherever
the format allows, plain text: open one in an editor and the whole of it is
visible at once.
Why these exist at all
The rest of the suite builds its PDFs in memory with the pb_* builders in
xref_test.odin, and should keep doing so — the builder records byte offsets as
it writes, so a test never asserts a hand-counted number. But a builder is part
of the same codebase as the reader, and there are three things it therefore
cannot show:
- that
open_fileworks on a real file on disk, not just on a byte slice; - that byte offsets written by some other tool are the ones we parse — a committed file's offsets cannot silently follow a change in how we emit them;
- that a refactor did not quietly change what the library accepts by changing the builder at the same time. These bytes are frozen; the builder is not.
They also give the render layer, when it arrives, fourteen pages it can draw without inventing its own fixtures.
What every file has in common
-
One page (two in
page-tree-deep.pdf) whose content stream draws a filled blue rectangle and a red stroked diagonal:% <file name> 0.20 0.40 0.90 rg 20 20 160 100 re f 0.90 0.10 0.10 RG 4 w 20 20 m 180 180 l SThe first line names the file, so a test that decodes a content stream can say which file's stream it got.
golden_test.odinasserts the marker line on page 0 and160 100 reon every page. -
/MediaBox [0 0 200 200], except where the file's whole point is a different box:incremental-update.pdf(300×300 after the update) andpage-tree-deep.pdf(300×400, inherited). -
No
/Filteron the content stream. Compression is exercised by the filter unit tests, which have the format's own vectors; a golden file that had to be inflated before it could be read would be a worse golden file.
The files
| File | Exercises | Opens with |
|---|---|---|
classic-xref.pdf |
the minimal well-formed document: four objects, one xref subsection, trailer, startxref |
no repairs |
xref-stream.pdf |
PDF 1.5 /Type /XRef stream, /W [1 4 2], FlateDecode + /Predictor 12 /Columns 7 |
no repairs |
object-stream.pdf |
catalog, page-tree root and page living inside a /Type /ObjStm; xref stream with type-2 entries |
no repairs |
incremental-update.pdf |
two generations; the second rewrites object 3 and adds object 5, with a three-subsection table and /Prev |
no repairs |
junk-before-header.pdf |
%PDF- at offset 70, every offset in the file measured from the header rather than from byte zero |
no repairs |
broken-xref.pdf |
startxref pointing into a run of garbage; recovery is by scanning for N G obj |
Bad_Startxref, Rebuilt_Xref |
bad-length.pdf |
a content stream whose /Length says 12 and whose body is 92 bytes; endstream has to be found by scanning |
Bad_Stream_Length |
no-xref.pdf |
no table, no trailer, no startxref at all — objects and %%EOF, nothing else |
Bad_Startxref, Rebuilt_Xref, Missing_Root |
page-tree-broken.pdf |
a /Kids cycle (2 → 3 → 2) with a real page hanging off the far side of it |
Cyclic_Page_Tree |
encrypted-rc4-40.pdf |
standard security handler, /V 1 /R 2, RC4 40-bit |
no repairs |
encrypted-rc4-128.pdf |
/V 2 /R 3 /Length 128, RC4 128-bit |
no repairs |
encrypted-aes128.pdf |
/V 4 /R 4, crypt filter /StdCF with /CFM /AESV2 |
no repairs |
encrypted-aes256.pdf |
/V 5 /R 6, /CFM /AESV3, with /UE, /OE and /Perms |
no repairs |
page-tree-deep.pdf |
three nested /Pages levels; /MediaBox and /Rotate declared only on the root, /Resources only on the middle node; page 2 overrides /Rotate alone |
no repairs |
"Opens with" is the exact set of Repair_Kinds the file must record once every
page's content stream has been read — golden_test.odin compares the set, not a
subset. A file that starts opening more cleanly than this is as much a
regression as one that starts opening less cleanly, and both fail here.
Two details worth knowing before editing one of these:
broken-xref.pdf's garbage run is bare keywords (garbagegarbage…), not a%comment. A comment is skipped by the lexer, which would carry the probe straight on to the realxreftable and make the bad offset work by accident. It is also longer than 2 ×XREF_LOCATE_WINDOWso that the ±64-byte search around the bad offset cannot reach an object header either.bad-length.pdf's repair is recorded when the stream is parsed, which is lazy. A test that readsdoc.repairsstraight afteropen_filesees nothing.
Encryption
All four encrypted files share one arrangement:
- user password empty — the file opens without prompting;
- owner password
owner— so the owner branch of the key derivation is exercised by a real file and not only by a unit vector; /P -44: bits 3 (modify) and 5 (annotate) clear, everything else set. R2 only ever defined four permission bits, soencrypted-rc4-40.pdfreports{Print, Copy}where the others report six;- a
/Infodictionary whose/Titleis the encrypted stringGolden File, so that string decryption is covered as well as stream decryption; - a fixed
/IDof9A1C2B3D4E5F60718293A4B5C6D7E8F9(both halves), which is an input to the R2–R4 key derivation and therefore may not be changed on its own; - a classic
xreftable rather than a cross-reference stream, because an xref stream is read before the file key exists and is never encrypted — which would make the file exercise less, not more.
encrypted-aes256.pdf additionally fixes the file encryption key itself (it is
wrapped in /UE and /OE rather than derived), the two 8-byte salts in /U and
the two in /O. /Perms is the 16 bytes P (little-endian) ‖ FF FF FF FF ‖
'T' ‖ "adb" ‖ "odin", AES-256-ECB encrypted with the file key.
AES payloads are IV ‖ CBC(PKCS#7(plaintext)). The IVs are fixed, not random:
iv[i] = obj_num XOR (i*17 + 3). Nothing checks them, and a golden file whose
bytes changed on every regeneration would not be golden.
Regenerating one
There is deliberately no generator in the repository. The point of these
files is that the bytes are fixed: a generator that ran at test time would be
one more piece of the same codebase, and would answer none of the three
questions at the top of this file. Editing a file by hand in a text editor is
the expected way to change one — every offset that matters is in the xref
table at the end, and golden_test.odin will tell you immediately if you got
one wrong.
If you do need to rebuild one from scratch, the recipe that produced these was a
throwaway @(test) procedure in the package (so that it could use the
@(private) pb_* builders and the crypt.odin primitives), which then wrote
the result with os.write_entire_file. The parts that are not obvious:
- Structure.
pb_make,pb_obj,pb_classic_xref,pb_trailerandpb_startxreffromxref_test.odindo the whole ofclassic-xref.pdf. The content-stream objects were written with a local variant ofpb_stream_objthat omits the double space an empty dictionary head leaves behind. - Cross-reference streams.
xref_rows_encode(rows, 1, 4, 2)lays out the rows;png_up_encode(raw, 7)applies the PNGUpfilter for/Predictor 12;zlib_storedwraps the result in stored DEFLATE blocks, which is a valid zlib stream and keeps the output byte-stable without needing a compressor. - Object streams.
objstm_payload(nums, bodies)returns the payload and the/Firstthe dictionary must declare. The one here is left unfiltered so that the pair table and the three objects stay legible. /Oand/Ufor R2–R4. The library only ever needs the inverse of Algorithm 3 (crypt_owner_password, which decrypts/Oback to the user password), so the encrypting half has to be written out: pad the owner password, MD5 it, iterate MD5 50 times over the full 16-byte digest when R ≥ 3, take the firstnbytes as an RC4 key, RC4 the padded user password, and for R ≥ 3 repeat 19 more times with the key XORed byifori= 1…19 — ascending, where the decrypting direction descends./Uis then justcrypt_key_r234followed bycrypt_user_value, both of which the library already has.- R6.
crypt_hash_r56is Algorithm 2.B and is used as-is for all four hashes./UEand/OEareaes_cbc_encryptof the 32-byte file key under the key-salt hash with a zero IV and no padding.
After regenerating, update the row in golden_test.odin's GOLDEN table if the
page size, page count, marker line or repair set changed. Nothing else in the
suite reads these files.