Skip to content

objects: the object graph as JSON, for a file that will not open anywhere else - #51

Merged
tannevaled merged 1 commit into
mainfrom
the-object-graph-as-json
Oct 12, 2026
Merged

tannevaled merged 1 commit into
mainfrom
the-object-graph-as-json

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

BentoPDF's pdf-to-json. It writes what the file is made of rather than
what it says.

pdfops objects broken.pdf | jq '.objects["12 0"]'

⛔ The graph reachable from the trailer, not every object in the file. That
is the honest set because it is the one a reader sees: an object no reference
points at is in the bytes and in nobody's document.

⛔ A stream's bytes are not in it, its length and dictionary are. They are
the bulk of most files and they are binary, so including them would mean
base64 — a 35 MB file becoming a 47 MB JSON nobody reads.

A name keeps its slash and a string is wrapped, {"text": …} or
{"hex": …}, because the two have to be told apart and a PDF string may be
exactly /Catalog. Through reader.TextString, since the bytes of a text
string are not text. A reference that could not be read is named in
unread rather than dropped.

⛔ Two guards that masked each other

Found by a surviving mutation. A PDF is a graph — the page tree's Kids
point at the pages and every page's Parent points back — so an object is
marked seen before it is read. Remove that and the walk should never end.

It ended anyway: the depth guard cut it off at 256 and produced a finite,
enormous, wrong answer. "It terminated" cannot tell the two apart. Counting
the reads can, and the test now asserts one read per object.

And a sort that did nothing

encoding/json sorts a map's keys itself, so sorting them here only looked
like the thing making the output deterministic — a mutation reversing it
changed no test. Removed, with two runs of a real file compared byte for byte
to show determinism survived it.

The seam

The walker holds a getter rather than the document, which is what makes
the unreadable-reference branch testable: reader.Get is forgiving by design
— a reference to an object never written resolves to Null, not an error — so
no file reaches it.

How it is checked

100% of statements with no uncovered blocks, 14 mutations caught. One dead
case nil removed: reader says in its own words that a missing entry
resolves to Null "so callers never have to nil-check".

🤖 Generated with Claude Code

…here else

BentoPDF's pdf-to-json. It writes what the file is MADE OF rather than what it
says: the trailer, and every object reachable from it.

The graph REACHABLE FROM THE TRAILER, not every object in the file. That is
the honest set because it is the one a reader sees: an object no reference
points at is in the bytes and in nobody's document.

A stream's bytes are not in it and its length is -- they are the bulk of most
files and they are binary, so including them would mean base64, and a 35 MB
file becoming a 47 MB JSON nobody reads.

A name keeps its slash and a string is WRAPPED, {"text":…} or {"hex":…},
because the two have to be told apart and a PDF string may be exactly
"/Catalog". Through reader.TextString, since the bytes of a text string are
not text. A reference that could not be read is NAMED in `unread` rather than
dropped.

TWO GUARDS THAT MASKED EACH OTHER, found by a surviving mutation. A PDF is a
graph -- the page tree's Kids point at the pages and every page's Parent
points back -- so an object is marked seen BEFORE it is read. Remove that and
the walk should never end; it ended anyway, because the depth guard cut it off
at 256 and produced a finite, enormous, wrong answer. "It terminated" cannot
tell the two apart. Counting the reads can, and the test now asserts ONE READ
PER OBJECT.

And a sort that did nothing: encoding/json sorts a map's keys itself, so
sorting them here only looked like the thing making the output deterministic.
A mutation reversing it changed no test. Removed, with two runs of the real
file compared byte for byte to show determinism survived it.

The walker holds a GETTER rather than the document, which is the seam that
makes the unreadable-reference branch testable: reader.Get is forgiving by
design -- a reference to an object never written resolves to Null, not an
error -- so no file reaches it.

100% of statements with no uncovered blocks, 14 mutations caught. One dead
`case nil` removed: reader says in its own words that a missing entry
resolves to Null "so callers never have to nil-check".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 06ab983 into main Oct 12, 2026
1 check passed
@tannevaled
tannevaled deleted the the-object-graph-as-json branch October 12, 2026 09:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant