Files
pdf-odin-llm/xref.odin
T

1228 lines
44 KiB
Odin
Raw Normal View History

2026-09-25 23:32:29 +02:00
package pdf
// Cross-reference reading: classic tables (ISO 32000-2, 7.5.4), cross-reference
// streams (7.5.8), the trailer chain, and rebuild-by-scanning.
//
// The layout of this file follows the order a reader meets the file's
// structure in: find `startxref`, walk the /Prev chain from the newest section
// to the oldest, merge the trailers, and -- when any of that does not add up --
// throw the lot away and find the objects by scanning.
//
// Two rules run through everything here.
//
// The first is that a *newer* section always wins. The chain is walked newest
// to oldest and an entry that has already been established is never
// overwritten, because that is precisely what an incremental update means: the
// bytes at the end of the file supersede the ones before them.
//
// The second is that nothing in this file is allowed to fail the open while a
// rebuild is still possible. A cross-reference table is an index, not content;
// every object it describes is still in the file whether or not the index
// agrees, and a reader that refuses a document because its index is wrong is
// refusing a document that every other viewer opens.
// Bytes scanned backwards from the end of the file for `startxref` before
// falling back to searching the whole file. The format requires it to be in
// the last few dozen bytes; 2 KiB covers the files that append a signature,
// a trailing comment banner or a stray page of junk after it.
@(private)
STARTXREF_WINDOW :: 2048
// How many `startxref` keywords are tried, newest first. A file whose last
// startxref was written by a tool that crashed part way through still has the
// previous one, and following that is far better than a full rebuild.
@(private)
MAX_STARTXREF_CANDIDATES :: 4
// How far either side of a claimed section offset the section is looked for.
// Producers that miscount by a byte or two are common; a window this small
// cannot wander into an unrelated part of the file.
@(private)
XREF_LOCATE_WINDOW :: 64
// Smallest a classic cross-reference entry can be written in. The format says
// 20 bytes exactly, real files use 19, and the tolerant reader below will take
// "0 0 n" -- 5 -- so that is what allocations are bounded against.
@(private)
XREF_ENTRY_MIN_BYTES :: 5
// Bytes searched past the end of the subsections for the `trailer` keyword
// when it is not where it should be.
@(private)
XREF_TRAILER_WINDOW :: 4096
// Trailer dictionaries kept while rebuilding. Files with more `trailer`
// keywords than this are not going to be diagnosed by reading more of them.
@(private)
MAX_REBUILD_TRAILERS :: 64
// Bytes of an object's body looked at before deciding whether it is worth
// parsing during a rebuild. An /ObjStm, /XRef or /Catalog dictionary names its
// own type long before this.
@(private)
REBUILD_PEEK_BYTES :: 4096
@(private)
Xref_Section_Kind :: enum u8 {
None = 0,
Classic, // "xref" followed by subsections and a trailer
Stream, // an indirect object holding a /Type /XRef stream
}
// Xref_Loader is the state one call to `xref_load` (or `xref_rebuild`) needs
// beyond what the Document already holds.
//
// `defined` is parallel to `doc.xref` and is what makes "newer wins" work: a
// free entry written by a newer section has to shadow an in-use entry from an
// older one, and `Xref_Entry` on its own cannot tell "explicitly free" from
// "never mentioned".
@(private)
Xref_Loader :: struct {
doc: ^Document,
defined: [dynamic]bool,
visited: map[i64]bool,
// Newest first; merged oldest-to-newest so the newest entry wins.
trailers: [dynamic]^Dict,
max_objects: int,
// Entries established that are not free. A table that describes no live
// object at all is not a table.
n_live: int,
// Objects a rebuild's scan flagged as worth parsing: the ones whose bytes
// mention /ObjStm, /XRef or /Catalog.
candidates: [dynamic]u32,
}
// ---------------------------------------------------------------------------
// Entry points
// ---------------------------------------------------------------------------
// xref_load fills in `doc.xref` and `doc.trailer`.
//
// It returns an error only when the file has no recoverable structure at all:
// anything less than that -- a missing startxref, an offset pointing at
// nothing, a table that stops parsing half way, a /Root that is not a
// dictionary -- falls back to `xref_rebuild` and is recorded as a repair.
xref_load :: proc(doc: ^Document) -> Error {
if doc == nil { return .Invalid_Argument }
ld := xref_loader_make(doc)
ok := xref_load_chain(&ld) && xref_finish(&ld)
err := Error.None
if !ok {
err = xref_rebuild(doc)
}
// Everything resolved above was resolved against a table that was still
// being built, and -- in an encrypted document -- before the file key
// existed. Neither is a state to leave a cache in.
clear(&doc.cache)
if dict_has(doc.trailer, "Encrypt") {
clear(&doc.objstm_cache)
}
return err
}
// xref_rebuild discards the cross-reference table and finds the objects by
// scanning the whole file for "N G obj" headers.
//
// Later definitions of the same object number win, which is what an
// incremental update means when read this way. Objects that exist only inside
// an object stream are picked up afterwards, by reading the header of every
// /ObjStm the scan found.
xref_rebuild :: proc(doc: ^Document) -> Error {
if doc == nil { return .Invalid_Argument }
add_repair(doc, .Rebuilt_Xref, 0)
clear(&doc.xref)
clear(&doc.cache)
clear(&doc.objstm_cache)
doc.trailer = nil
ld := xref_loader_make(doc)
catalog: Ref
xref_scan_objects(&ld)
xref_scan_objstms(&ld, &catalog)
xref_rebuild_trailer(&ld, catalog)
if ld.n_live == 0 {
// No object header anywhere in the file. There is nothing to read.
return .No_Xref
}
return .None
}
@(private)
xref_loader_make :: proc(doc: ^Document) -> Xref_Loader {
ld := Xref_Loader{
doc = doc,
max_objects = xref_max_objects(doc),
}
ld.defined = make([dynamic]bool, 0, 64, doc.allocator)
ld.visited = make(map[i64]bool, 8, doc.allocator)
ld.trailers = make([dynamic]^Dict, 0, 8, doc.allocator)
ld.candidates = make([dynamic]u32, 0, 16, doc.allocator)
return ld
}
// xref_max_objects bounds the table by what the file could possibly describe.
// Every object costs bytes wherever it is written -- a header and an `endobj`
// at the top level, a "number offset" pair inside an object stream -- so the
// file's own length is a ceiling on how many there can be. The floor keeps a
// tiny hand-written file from being clamped below its own /Size.
@(private)
xref_max_objects :: proc(doc: ^Document) -> int {
n := max(len(doc.data), 1024)
return min(n, MAX_OBJECT_NUMBER + 1)
}
// ---------------------------------------------------------------------------
// Table storage
// ---------------------------------------------------------------------------
@(private)
xref_grow :: proc(ld: ^Xref_Loader, count: int) {
n := min(count, ld.max_objects)
old := len(ld.doc.xref)
if n <= old { return }
// `resize` reallocates to exactly the length asked for, and the table is
// grown one object at a time while a file is scanned. Reserving
// geometrically first is the difference between a linear rebuild and a
// quadratic one -- and, on an arena, between one copy of the table and
// thousands of abandoned ones.
if cap(ld.doc.xref) < n {
want := min(max(n, cap(ld.doc.xref) * 2, 64), ld.max_objects)
reserve(&ld.doc.xref, max(want, n))
reserve(&ld.defined, max(want, n))
}
resize(&ld.doc.xref, n)
resize(&ld.defined, n)
// Not every allocator hands back zeroed memory when a dynamic array is
// regrown into capacity it already held, and a stale entry here is an
// offset into the middle of the file.
for i in old ..< len(ld.doc.xref) {
ld.doc.xref[i] = {}
}
for i in old ..< len(ld.defined) {
ld.defined[i] = false
}
}
// xref_set records an entry unless a newer section already spoke for it.
//
// `over_free` is the one exception, and it exists for hybrid-reference files:
// there the classic table deliberately marks every compressed object free so
// that a reader which cannot follow /XRefStm ignores them, and the stream that
// follows carries their real entries (ISO 32000-2, 7.5.8.4).
@(private)
xref_set :: proc(ld: ^Xref_Loader, num: u32, entry: Xref_Entry, over_free := false) {
if num == 0 || int(num) >= ld.max_objects { return }
xref_grow(ld, int(num) + 1)
if int(num) >= len(ld.doc.xref) { return }
if ld.defined[num] {
replaces_free := over_free &&
ld.doc.xref[num].kind == .Free && entry.kind != .Free
// The entry being replaced was free, so it was never counted as live.
if !replaces_free { return }
}
ld.doc.xref[num] = entry
ld.defined[num] = true
if entry.kind != .Free {
ld.n_live += 1
}
}
// xref_put overwrites unconditionally. Only the rebuild uses it: there the
// scan runs from the front of the file to the back, so a later definition is a
// newer one and must replace what came before it.
@(private)
xref_put :: proc(ld: ^Xref_Loader, num: u32, entry: Xref_Entry) {
if num == 0 || int(num) >= ld.max_objects { return }
xref_grow(ld, int(num) + 1)
if int(num) >= len(ld.doc.xref) { return }
if ld.defined[num] && ld.doc.xref[num].kind != .Free {
ld.n_live -= 1
}
ld.doc.xref[num] = entry
ld.defined[num] = true
if entry.kind != .Free {
ld.n_live += 1
}
}
@(private)
xref_entry_at :: proc(ld: ^Xref_Loader, num: u32) -> Xref_Entry {
if int(num) >= len(ld.doc.xref) { return {} }
return ld.doc.xref[num]
}
// ---------------------------------------------------------------------------
// startxref
// ---------------------------------------------------------------------------
// xref_last_startxref finds the last `startxref` before `before` whose keyword
// is followed by a number, and reports the number and where the keyword was.
//
// The search runs backwards because that is where the newest one is, and it is
// only ever called again -- for the one before it -- when the newest turned out
// to point at nothing.
@(private)
xref_last_startxref :: proc(doc: ^Document, before: int) -> (value: i64, at: int, ok: bool) {
needle := transmute([]byte)string("startxref")
limit := min(before, len(doc.data))
for limit > 0 {
idx := last_index_bytes(doc.data[:limit], needle)
if idx < 0 { break }
limit = idx
if v, vok := xref_int_after(doc, idx + len(needle)); vok {
return v, idx, true
}
}
return 0, 0, false
}
// xref_first_startxref looks in the last couple of kilobytes, where the format
// puts the keyword, before paying for a scan of the whole file.
@(private)
xref_first_startxref :: proc(doc: ^Document) -> (value: i64, at: int, ok: bool) {
tail := max(0, len(doc.data) - STARTXREF_WINDOW)
if tail > 0 {
needle := transmute([]byte)string("startxref")
if idx := last_index_bytes(doc.data[tail:], needle); idx >= 0 {
if v, vok := xref_int_after(doc, tail + idx + len(needle)); vok {
return v, tail + idx, true
}
}
}
return xref_last_startxref(doc, len(doc.data))
}
@(private)
xref_int_after :: proc(doc: ^Document, pos: int) -> (value: i64, ok: bool) {
if pos < 0 || pos > len(doc.data) { return 0, false }
lex := Lexer{data = doc.data, pos = pos}
tok := lexer_next_structural(&lex)
if tok.kind != .Integer || tok.int_value < 0 { return 0, false }
return tok.int_value, true
}
// ---------------------------------------------------------------------------
// Locating a section
// ---------------------------------------------------------------------------
// xref_probe reports what kind of cross-reference section starts at `off`, if
// any. It is the only thing that decides an offset is worth following, so it
// insists the keyword or object header begins on a token boundary: "xref" also
// occurs as the tail of "startxref".
@(private)
xref_probe :: proc(doc: ^Document, off: i64) -> Xref_Section_Kind {
if off < 0 || off >= i64(len(doc.data)) { return .None }
lex := Lexer{data = doc.data, pos = int(off)}
t1 := lexer_next_structural(&lex)
if t1.start > 0 && is_pdf_regular(doc.data[t1.start - 1]) { return .None }
if t1.kind == .Keyword && t1.text == "xref" {
return .Classic
}
if t1.kind != .Integer || t1.int_value < 1 || t1.int_value > MAX_OBJECT_NUMBER {
return .None
}
t2 := lexer_next_structural(&lex)
// "12 obj" -- a header whose generation number was dropped.
if t2.kind == .Keyword && t2.text == "obj" { return .Stream }
if t2.kind != .Integer || t2.int_value < 0 || t2.int_value > 65535 { return .None }
t3 := lexer_next_structural(&lex)
if t3.kind == .Keyword && t3.text == "obj" { return .Stream }
return .None
}
// xref_locate turns an offset read out of the file into one that actually
// points at a section.
//
// Two things can be wrong with it. A file with junk before "%PDF-" measures
// its offsets from the header rather than from byte zero (and the ones written
// by a tool that did not notice the junk measure them from byte zero anyway,
// so both are tried). And a producer can simply be off by a few bytes, which
// is worth a small search rather than a whole-file rebuild.
@(private)
xref_locate :: proc(doc: ^Document, raw: i64) -> (off: i64, kind: Xref_Section_Kind) {
bases := [2]i64{raw + doc.header_offset, raw}
n_bases := doc.header_offset != 0 ? 2 : 1
for i in 0 ..< n_bases {
if k := xref_probe(doc, bases[i]); k != .None {
return bases[i], k
}
}
// Nearest first, so an exact-but-late match beats a spurious object header
// found behind us.
for d in 1 ..= i64(XREF_LOCATE_WINDOW) {
for i in 0 ..< n_bases {
for cand in ([2]i64{bases[i] + d, bases[i] - d}) {
if cand < 0 || cand >= i64(len(doc.data)) { continue }
if is_pdf_regular(doc.data[cand]) && cand > 0 && is_pdf_regular(doc.data[cand - 1]) {
continue
}
if k := xref_probe(doc, cand); k != .None {
return cand, k
}
}
}
}
return 0, .None
}
// ---------------------------------------------------------------------------
// The chain
// ---------------------------------------------------------------------------
@(private)
xref_load_chain :: proc(ld: ^Xref_Loader) -> bool {
doc := ld.doc
raw, at, found := xref_first_startxref(doc)
if !found {
add_repair(doc, .Bad_Startxref, 0)
return false
}
off, kind := xref_locate(doc, raw)
// A tool that crashed part way through an incremental update leaves a
// startxref pointing at nothing and the previous one still intact.
for _ in 1 ..< MAX_STARTXREF_CANDIDATES {
if kind != .None { break }
add_repair(doc, .Bad_Startxref, raw)
v, a, more := xref_last_startxref(doc, at)
if !more { break }
raw, at = v, a
off, kind = xref_locate(doc, raw)
}
if kind == .None {
add_repair(doc, .Bad_Startxref, raw)
return false
}
got_any := false
for _ in 0 ..< MAX_XREF_CHAIN {
if ld.visited[off] or_else false {
// /Prev loops are a real shape, not a hypothetical one: a file
// whose last two sections point at each other opens in every
// viewer because every viewer keeps this set.
add_repair(doc, .Bad_Startxref, off)
break
}
ld.visited[off] = true
trailer, ok := xref_read_section(ld, off, false)
if !ok { break }
got_any = true
append(&ld.trailers, trailer)
xref_read_hybrid(ld, trailer)
prev, has_prev := to_int(dict_get_raw(trailer, "Prev"))
if !has_prev { break }
poff, pkind := xref_locate(doc, prev)
if pkind == .None {
add_repair(doc, .Bad_Startxref, prev)
break
}
off = poff
}
return got_any
}
// xref_read_hybrid reads the cross-reference stream a classic trailer points
// at with /XRefStm (ISO 32000-2, 7.5.8.4).
//
// It is read after the classic section it belongs to, so the classic entries
// win -- except where the classic table marked an object free, which in a
// hybrid file is how it hides compressed objects from older readers.
@(private)
xref_read_hybrid :: proc(ld: ^Xref_Loader, trailer: ^Dict) {
if trailer == nil { return }
raw, ok := to_int(dict_get_raw(trailer, "XRefStm"))
if !ok { return }
off, kind := xref_locate(ld.doc, raw)
if kind != .Stream { return }
if ld.visited[off] or_else false { return }
ld.visited[off] = true
// Its own /Prev is deliberately not followed: the classic chain drives.
stm_dict, sok := xref_read_stream(ld, off, true)
if sok && stm_dict != nil {
append(&ld.trailers, stm_dict)
}
}
@(private)
xref_read_section :: proc(ld: ^Xref_Loader, off: i64, over_free: bool) -> (trailer: ^Dict, ok: bool) {
switch xref_probe(ld.doc, off) {
case .Classic:
return xref_read_table(ld, off)
case .Stream:
return xref_read_stream(ld, off, over_free)
case .None:
return nil, false
}
return nil, false
}
// ---------------------------------------------------------------------------
// Classic cross-reference tables (ISO 32000-2, 7.5.4)
// ---------------------------------------------------------------------------
// Xref_Raw_Entry is one line of a classic table, before it is decided which
// object number it belongs to.
@(private)
Xref_Raw_Entry :: struct {
offset: i64,
gen: i64,
used: bool, // 'n' rather than 'f'
ok: bool,
}
// xref_next_entry reads one entry by tokens rather than by fixed offsets.
//
// The spec fixes the entry at 20 bytes, but 19-byte entries (a single LF
// instead of CRLF) are written by a large fraction of producers, and entries
// with a single space before the EOL, with no leading zeros, or with the
// fields separated by a tab all occur. None of that changes the tokens.
@(private)
xref_next_entry :: proc(doc: ^Document, lex: ^Lexer) -> (e: Xref_Raw_Entry) {
save := lex.pos
t1 := lexer_next_structural(lex)
if t1.kind != .Integer || t1.int_value < 0 {
lex.pos = save
return
}
t2 := lexer_next_structural(lex)
if t2.kind != .Integer || t2.int_value < 0 {
lex.pos = save
return
}
t3 := lexer_next_structural(lex)
if t3.kind != .Keyword || len(t3.text) == 0 {
lex.pos = save
return
}
c := t3.text[0]
if c != 'n' && c != 'f' {
lex.pos = save
return
}
// "...00000 n0000000123 00000 n": no separator at all after the type
// letter, so the tokeniser swallowed the next entry's offset with it.
if len(t3.text) > 1 {
lex.pos = t3.start + 1
}
e.offset = t1.int_value
e.gen = t2.int_value
e.used = c == 'n'
e.ok = true
return
}
@(private)
xref_read_table :: proc(ld: ^Xref_Loader, off: i64) -> (trailer: ^Dict, ok: bool) {
doc := ld.doc
lex := Lexer{data = doc.data, pos = int(off)}
if !lexer_accept_keyword(&lex, "xref") { return nil, false }
// Each subsection header costs at least four bytes ("0 0"), and the loop
// only continues while it is consuming tokens, so this bound is never the
// thing that stops it -- it is here because a table is file data.
fuel := len(doc.data) / 4 + 16
first_sub := true
for fuel > 0 {
fuel -= 1
lexer_skip_whitespace(&lex)
if lex.pos >= len(doc.data) { break }
if lexer_at_keyword(&lex, "trailer") { break }
save := lex.pos
t1 := lexer_next_structural(&lex)
t2 := lexer_next_structural(&lex)
if t1.kind != .Integer || t2.kind != .Integer {
lex.pos = save
break
}
xref_read_subsection(ld, &lex, t1.int_value, t2.int_value, first_sub)
first_sub = false
}
return xref_read_trailer(ld, &lex), true
}
@(private)
xref_read_subsection :: proc(ld: ^Xref_Loader, lex: ^Lexer, start_in, count_in: i64, first_sub: bool) {
doc := ld.doc
if start_in < 0 || start_in > MAX_OBJECT_NUMBER { return }
count := count_in
if count <= 0 { return }
// A subsection cannot describe more objects than the rest of the file has
// room to spell out, however large a number the header claims.
room := i64(len(doc.data) - lex.pos) / XREF_ENTRY_MIN_BYTES + 1
if count > room {
add_repair(doc, .Xref_Entry_Mismatch, i64(lex.pos))
count = room
}
count = min(count, i64(ld.max_objects))
// Look at the first few entries without consuming them: whether the
// declared starting object number is right is decided from what the file
// actually has at the offsets they name.
peek: [8]Xref_Raw_Entry
n_peek := 0
{
probe := lex^
for n_peek < len(peek) {
e := xref_next_entry(doc, &probe)
if !e.ok { break }
peek[n_peek] = e
n_peek += 1
}
}
start := xref_correct_start(doc, peek[:n_peek], start_in, first_sub)
if start != start_in {
add_repair(doc, .Xref_Entry_Mismatch, i64(lex.pos), Ref{u32(min(start_in, MAX_OBJECT_NUMBER)), 0})
}
if start + count > MAX_OBJECT_NUMBER + 1 {
count = MAX_OBJECT_NUMBER + 1 - start
}
for i in 0 ..< count {
e := xref_next_entry(doc, lex)
if !e.ok {
// The table stops making sense here. Everything read so far is
// still good, and the entries after the damage are picked up from
// an older section or, failing that, by a rebuild.
add_repair(doc, .Xref_Entry_Mismatch, i64(lex.pos))
break
}
num := start + i
if num < 1 { continue }
gen := u16(clamp(e.gen, 0, 65535))
if !e.used {
// A free entry's first field is the next free object number, not
// an offset; nothing here needs the free list.
xref_set(ld, u32(num), Xref_Entry{kind = .Free, gen = gen})
continue
}
// Byte 0 is the header, so an in-use object can never be there. An
// offset past the end of the file is nonsense too. Leaving such an
// entry undefined lets an older section supply the object instead.
if e.offset <= 0 || e.offset >= i64(len(doc.data)) { continue }
at := xref_normalize_offset(doc, e.offset, num)
xref_set(ld, u32(num), Xref_Entry{kind = .Normal, offset = at, gen = gen})
}
}
// xref_object_at reports whether the header of object `num` is exactly at
// `off`.
@(private)
xref_object_at :: proc(doc: ^Document, off: i64, num: i64) -> bool {
if num < 1 || num > MAX_OBJECT_NUMBER { return false }
if off < 0 || off >= i64(len(doc.data)) { return false }
lex := Lexer{data = doc.data, pos = int(off)}
t1 := lexer_next_structural(&lex)
if t1.kind != .Integer || t1.int_value != num { return false }
t2 := lexer_next_structural(&lex)
// "12 obj": a header whose generation number was dropped.
if t2.kind == .Keyword && t2.text == "obj" { return true }
if t2.kind != .Integer { return false }
t3 := lexer_next_structural(&lex)
return t3.kind == .Keyword && t3.text == "obj"
}
// xref_offset_is_object reports whether "num G obj" really is at `off`, from
// either origin, for the same reason `xref_locate` tries both.
@(private)
xref_offset_is_object :: proc(doc: ^Document, off: i64, num: i64) -> bool {
return xref_object_at(doc, off, num) ||
(doc.header_offset != 0 && xref_object_at(doc, off + doc.header_offset, num))
}
// xref_normalize_offset resolves which origin an entry's offset was measured
// from, and returns one that indexes `doc.data`.
//
// A file with junk before "%PDF-" has two plausible readings of every offset in
// its table: from byte zero, and from the header. Producers do both -- the
// wrapper is usually added by a tool that never looked inside -- and the
// difference is every object in the file, so it is settled here, against the
// bytes, rather than left for the object reader to guess at one reference at a
// time.
@(private)
xref_normalize_offset :: proc(doc: ^Document, off: i64, num: i64) -> i64 {
if doc.header_offset == 0 { return off }
if xref_object_at(doc, off, num) { return off }
shifted := off + doc.header_offset
if xref_object_at(doc, shifted, num) { return shifted }
return off
}
@(private)
xref_score_start :: proc(doc: ^Document, ents: []Xref_Raw_Entry, start: i64) -> int {
score := 0
for e, i in ents {
if !e.used { continue }
if xref_offset_is_object(doc, e.offset, start + i64(i)) {
score += 1
}
}
return score
}
// xref_correct_start repairs the off-by-one in a subsection header.
//
// A table whose first subsection is written "1 N" when it really starts at 0
// (or "0 N" when it really starts at 1) is one of the most common
// malformations there is -- it shifts every object number in the file by one,
// so a reader that trusts the header reads the wrong object for every
// reference. The header is checked against the file: whichever starting number
// makes the most of the entries land on the object they claim wins.
@(private)
xref_correct_start :: proc(doc: ^Document, ents: []Xref_Raw_Entry, start: i64, first_sub: bool) -> i64 {
if len(ents) == 0 { return start }
base := xref_score_start(doc, ents, start)
best := start
best_score := base
for delta in ([2]i64{-1, 1}) {
cand := start + delta
if cand < 0 || cand > MAX_OBJECT_NUMBER { continue }
if s := xref_score_start(doc, ents, cand); s > best_score {
best, best_score = cand, s
}
}
if best != start {
return best
}
if base > 0 || !first_sub {
return start
}
// Nothing verified either way -- the entries may point into a part of the
// file this section does not describe. Fall back to what the format
// guarantees: object 0 is the head of the free list and can never be in
// use, and the head is always generation 65535 (7.5.4).
if start == 1 && ents[0].ok && !ents[0].used && ents[0].gen == 65535 {
return 0
}
if start == 0 && ents[0].ok && ents[0].used {
return 1
}
return start
}
@(private)
xref_read_trailer :: proc(ld: ^Xref_Loader, lex: ^Lexer) -> ^Dict {
doc := ld.doc
if !lexer_accept_keyword(lex, "trailer") {
// Some producers drop the keyword and write the dictionary straight
// after the table; that is unambiguous, so take it.
probe := lex^
tok := lexer_next_structural(&probe)
if tok.kind != .Dict_Open {
// Junk between the last entry and the keyword. Worth a bounded
// look: the alternative is losing /Root and rebuilding the file.
at := xref_find_token(doc.data, lex.pos, "trailer")
if at < 0 || at - lex.pos > XREF_TRAILER_WINDOW { return nil }
lex.pos = at + len("trailer")
}
}
p := parser_make(doc, doc.data, doc.allocator)
p.lex.pos = lex.pos
// The trailer is never encrypted (ISO 32000-2, 7.6.2), and the zero
// obj_ref is what says so to the parser.
obj, err := parse_object(&p)
if err != .None { return nil }
d, ok := to_dict(obj)
if !ok { return nil }
return d
}
// ---------------------------------------------------------------------------
// Cross-reference streams (ISO 32000-2, 7.5.8)
// ---------------------------------------------------------------------------
@(private)
xref_read_stream :: proc(ld: ^Xref_Loader, off: i64, over_free: bool) -> (trailer: ^Dict, ok: bool) {
doc := ld.doc
obj, _, err := parse_indirect_object_at(doc, off, Ref{})
if err != .None { return nil, false }
s, is_stream := to_stream(obj)
if !is_stream || s == nil || s.dict == nil { return nil, false }
d := s.dict
// /Type is required here, but a dictionary carrying /W and /Size is
// unambiguous enough that a missing one is not worth refusing.
if ty := dict_name(doc, d, "Type"); ty != "" && ty != "XRef" { return nil, false }
// Field widths. A width of 0 means the field is not stored and takes its
// default: type 1 for the first field, 0 for the others (7.5.8.2).
w := dict_array(doc, d, "W")
n_w := array_len(w)
if n_w < 1 || n_w > 32 { return nil, false }
widths: [32]int
row := 0
for i in 0 ..< n_w {
v := array_int(doc, w, i, 0)
if v < 0 || v > 8 { return nil, false }
widths[i] = int(v)
row += widths[i]
}
if row <= 0 { return nil, false }
data, derr := stream_data(doc, s)
if derr != .None { return nil, false }
n_rows := len(data) / row
if n_rows == 0 {
// A /Type /XRef stream that decoded to nothing still carries the
// trailer, and the entries may all live in an older section.
return d, true
}
w0 := widths[0]
w1 := n_w > 1 ? widths[1] : 0
w2 := n_w > 2 ? widths[2] : 0
off0 := 0
off1 := w0
off2 := w0 + w1
size := dict_int(doc, d, "Size", 0)
index := dict_array(doc, d, "Index")
n_pairs := array_len(index) / 2
if n_pairs == 0 {
n_pairs = 1 // the default /Index is [0 /Size]
}
consumed := 0
for p in 0 ..< n_pairs {
start := i64(0)
count := size
if array_len(index) >= 2 {
start = array_int(doc, index, p * 2, 0)
count = array_int(doc, index, p * 2 + 1, 0)
}
if start < 0 || start > MAX_OBJECT_NUMBER { continue }
if count <= 0 { continue }
if count > i64(n_rows - consumed) {
add_repair(doc, .Xref_Entry_Mismatch, off)
count = i64(n_rows - consumed)
}
if start + count > MAX_OBJECT_NUMBER + 1 {
count = MAX_OBJECT_NUMBER + 1 - start
}
for k in 0 ..< count {
base := (consumed + int(k)) * row
f0 := w0 == 0 ? u64(1) : xref_read_field(data, base + off0, w0)
f1 := xref_read_field(data, base + off1, w1)
f2 := xref_read_field(data, base + off2, w2)
xref_stream_entry(ld, u32(start + k), f0, f1, f2, over_free)
}
consumed += int(count)
if consumed >= n_rows { break }
}
return d, true
}
@(private)
xref_stream_entry :: proc(ld: ^Xref_Loader, num: u32, f0, f1, f2: u64, over_free: bool) {
doc := ld.doc
switch f0 {
case 0:
xref_set(ld, num, Xref_Entry{kind = .Free, gen = u16(min(f2, 65535))}, over_free)
case 1:
if f1 == 0 || f1 >= u64(len(doc.data)) { return }
xref_set(ld, num, Xref_Entry{
kind = .Normal,
offset = xref_normalize_offset(doc, i64(f1), i64(num)),
gen = u16(min(f2, 65535)),
}, over_free)
case 2:
if f1 == 0 || f1 > MAX_OBJECT_NUMBER { return }
if f2 > u64(max(u32)) { return }
xref_set(ld, num, Xref_Entry{
kind = .Compressed,
container = u32(f1),
index = u32(f2),
}, over_free)
case:
// "Any other value shall be interpreted as a reference to the null
// object" (7.5.8.3). It still shadows older sections.
xref_set(ld, num, Xref_Entry{kind = .Free}, over_free)
}
}
@(private)
xref_read_field :: proc(data: []byte, pos, width: int) -> u64 {
v := u64(0)
for i in 0 ..< width {
j := pos + i
if j < 0 || j >= len(data) { break }
v = v << 8 | u64(data[j])
}
return v
}
// ---------------------------------------------------------------------------
// Trailers
// ---------------------------------------------------------------------------
// xref_merge_trailers folds the chain into one dictionary.
//
// The list runs newest first and is walked backwards, so a key present in
// several trailers takes its newest value while a key that only an older
// trailer had -- an /Info or an /Encrypt that a later incremental update did
// not repeat -- survives.
@(private)
xref_merge_trailers :: proc(ld: ^Xref_Loader) -> (merged: ^Dict, size: i64) {
doc := ld.doc
merged = new_dict(doc.allocator, 16)
for i := len(ld.trailers) - 1; i >= 0; i -= 1 {
t := ld.trailers[i]
if t == nil { continue }
for k, v in t^ {
// Both are properties of one section, not of the document, and
// keeping them in the merged trailer would invite a second walk.
if k == "Prev" || k == "XRefStm" { continue }
merged[k] = v
}
if v, ok := to_int(dict_get_raw(t, "Size")); ok && v > size {
size = v
}
}
if size > 0 {
merged["Size"] = size
}
return
}
@(private)
xref_finish :: proc(ld: ^Xref_Loader) -> bool {
doc := ld.doc
if len(ld.trailers) == 0 { return false }
merged, size := xref_merge_trailers(ld)
doc.trailer = merged
if size > 0 {
xref_grow(ld, int(min(size, i64(ld.max_objects))))
}
if ld.n_live == 0 { return false }
if dict_has(merged, "Encrypt") {
// The catalog of an encrypted document may sit inside an object stream
// that cannot be decoded until crypt_init has run, so there is nothing
// to check yet beyond /Root being present at all.
return dict_get_raw(merged, "Root") != nil
}
root, ok := to_dict(dict_get(doc, merged, "Root"))
return ok && root != nil
}
// ---------------------------------------------------------------------------
// Rebuilding by scanning
// ---------------------------------------------------------------------------
// Kw_Cursor remembers where a keyword was last found so the scan below stays
// linear: every search resumes from the previous hit instead of from the
// caller's position.
@(private)
Kw_Cursor :: struct {
kw: string,
at: int,
done: bool,
}
@(private)
kw_cursor :: proc(kw: string) -> Kw_Cursor {
return Kw_Cursor{kw = kw, at = -1}
}
@(private)
kw_at_or_after :: proc(c: ^Kw_Cursor, data: []byte, from: int) -> int {
if c.done { return -1 }
if c.at >= from { return c.at }
c.at = xref_find_token(data, from, c.kw)
if c.at < 0 { c.done = true }
return c.at
}
// xref_find_token finds `kw` as a whole token: "obj" must not match inside
// "endobj", and "stream" must not match inside "endstream".
@(private)
xref_find_token :: proc(data: []byte, from: int, kw: string) -> int {
if from < 0 || len(kw) == 0 { return -1 }
needle := transmute([]byte)kw
i := max(from, 0)
for i < len(data) {
idx := index_bytes(data[i:], needle)
if idx < 0 { return -1 }
p := i + idx
after := p + len(kw)
before_ok := p == 0 || !is_pdf_regular(data[p - 1])
after_ok := after >= len(data) || !is_pdf_regular(data[after])
if before_ok && after_ok { return p }
i = p + 1
}
return -1
}
// xref_header_before reads the "N G" that precedes the `obj` keyword at `at`.
@(private)
xref_header_before :: proc(data: []byte, at: int) -> (num: u32, gen: u16, start: int, ok: bool) {
k := at - 1
for k >= 0 && is_pdf_whitespace(data[k]) { k -= 1 }
gen_end := k + 1
for k >= 0 && data[k] >= '0' && data[k] <= '9' { k -= 1 }
gen_start := k + 1
if gen_start == gen_end { return }
if k < 0 || !is_pdf_whitespace(data[k]) { return }
for k >= 0 && is_pdf_whitespace(data[k]) { k -= 1 }
num_end := k + 1
for k >= 0 && data[k] >= '0' && data[k] <= '9' { k -= 1 }
num_start := k + 1
if num_start == num_end { return }
if k >= 0 && is_pdf_regular(data[k]) { return }
n, nok := xref_digits(data[num_start:num_end])
g, gok := xref_digits(data[gen_start:gen_end])
if !nok || !gok { return }
if n < 1 || n > MAX_OBJECT_NUMBER || g > 65535 { return }
return u32(n), u16(g), num_start, true
}
@(private)
xref_digits :: proc(s: []byte) -> (value: i64, ok: bool) {
// A run this long is not an object number whatever it is.
if len(s) == 0 || len(s) > 12 { return 0, false }
for c in s {
if c < '0' || c > '9' { return 0, false }
value = value * 10 + i64(c - '0')
}
return value, true
}
// xref_skip_object_body returns where scanning should resume after an object
// header, so that "N G obj" sequences inside a stream's binary payload are not
// mistaken for object headers of their own.
@(private)
xref_skip_object_body :: proc(data: []byte, from: int, c_stream, c_endobj, c_endstream: ^Kw_Cursor) -> int {
s := kw_at_or_after(c_stream, data, from)
e := kw_at_or_after(c_endobj, data, from)
if s >= 0 && (e < 0 || s < e) {
if es := kw_at_or_after(c_endstream, data, s + len("stream")); es >= 0 {
return es + len("endstream")
}
// Truncated inside a stream: nothing after this is readable.
return len(data)
}
if e >= 0 { return e + len("endobj") }
return from
}
// xref_scan_objects walks the whole file recording every object header and
// every trailer dictionary it meets, in file order, so that a later definition
// of an object number replaces an earlier one.
@(private)
xref_scan_objects :: proc(ld: ^Xref_Loader) {
doc := ld.doc
data := doc.data
c_obj := kw_cursor("obj")
c_trailer := kw_cursor("trailer")
c_stream := kw_cursor("stream")
c_endobj := kw_cursor("endobj")
c_endstream := kw_cursor("endstream")
pos := 0
fuel := len(data) + 16
for fuel > 0 {
fuel -= 1
o := kw_at_or_after(&c_obj, data, pos)
t := kw_at_or_after(&c_trailer, data, pos)
if o < 0 && t < 0 { break }
if t >= 0 && (o < 0 || t < o) {
p := parser_make(doc, data, doc.allocator)
p.lex.pos = t + len("trailer")
if obj, err := parse_object(&p); err == .None {
if d, dok := to_dict(obj); dok && d != nil {
// At the cap the oldest goes, not the newest: the trailers
// worth keeping are the ones nearest the end of the file.
if len(ld.trailers) >= MAX_REBUILD_TRAILERS {
ordered_remove(&ld.trailers, 0)
}
append(&ld.trailers, d)
}
}
pos = t + len("trailer")
continue
}
num, gen, start, hok := xref_header_before(data, o)
if hok {
xref_put(ld, num, Xref_Entry{kind = .Normal, offset = i64(start), gen = gen})
}
pos = xref_skip_object_body(data, o + len("obj"), &c_stream, &c_endobj, &c_endstream)
if pos <= o {
pos = o + len("obj")
}
// The /Type test happens here rather than in a second pass because
// this is the only place the object's extent is known: bounding the
// window by it keeps the whole scan linear in the file's length
// instead of quadratic in its object count.
if hok {
hi := min(start + REBUILD_PEEK_BYTES, max(pos, start))
if xref_head_is_interesting(data[start:min(hi, len(data))]) {
append(&ld.candidates, num)
}
}
}
// The scan found them oldest first; everything downstream expects newest
// first.
for i, j := 0, len(ld.trailers) - 1; i < j; i, j = i + 1, j - 1 {
ld.trailers[i], ld.trailers[j] = ld.trailers[j], ld.trailers[i]
}
}
// xref_head_is_interesting reports whether the head of an object mentions one
// of the three /Type values a rebuild has to act on. One pass with a switch on
// the leading byte, because this runs over every object in a damaged file and
// three separate substring searches over the same window do not.
@(private)
xref_head_is_interesting :: proc(head: []byte) -> bool {
for i in 0 ..< len(head) {
switch head[i] {
case 'O': if xref_starts_with(head[i:], "ObjStm") { return true }
case 'C': if xref_starts_with(head[i:], "Catalog") { return true }
case 'X': if xref_starts_with(head[i:], "XRef") { return true }
}
}
return false
}
@(private)
xref_starts_with :: proc(data: []byte, prefix: string) -> bool {
if len(data) < len(prefix) { return false }
return string(data[:len(prefix)]) == prefix
}
// xref_scan_objstms registers the contents of every object stream the scan
// found, and picks up the /Type /XRef dictionaries and the catalog on the way.
//
// Without this pass a file whose cross-reference stream is damaged loses every
// object that lives in an object stream -- which, in a modern file, is most of
// them.
@(private)
xref_scan_objstms :: proc(ld: ^Xref_Loader, catalog: ^Ref) {
doc := ld.doc
// Parsing every object in the file to read its /Type would cost the arena
// a second copy of the document; the scan already narrowed it down to the
// objects whose bytes name one of the three types that matter here.
objstms := make([dynamic]u32, 0, 8, doc.allocator)
for num in ld.candidates {
e := xref_entry_at(ld, num)
if e.kind != .Normal { continue }
obj, got, err := parse_indirect_object_at(doc, e.offset, Ref{num, e.gen})
if err != .None || got.num != num { continue }
d, dok := to_dict(obj)
if !dok || d == nil { continue }
switch dict_name(doc, d, "Type") {
case "ObjStm":
append(&objstms, num)
case "XRef":
if len(ld.trailers) < MAX_REBUILD_TRAILERS {
// Lowest priority: a real `trailer` keyword, if the file has
// one, is more likely to be the newest word on the subject.
append(&ld.trailers, d)
}
case "Catalog":
catalog^ = Ref{num, e.gen}
}
}
// Object streams come last, because whether it is safe to read them at all
// depends on what the trailers just collected say. In an encrypted
// document the file key does not exist yet -- `crypt_init` runs after this
// -- so every object stream would decode to ciphertext, and a pair table
// read out of ciphertext invents object numbers that would then shadow the
// real, recoverable entries the scan just found. Losing the compressed
// objects is bad; overwriting the ones that survived is worse.
if xref_trailers_encrypted(ld) {
add_repair(doc, .Missing_Object, 0)
return
}
for num in objstms {
e := xref_entry_at(ld, num)
if e.kind != .Normal { continue }
xref_register_objstm(ld, num, e.offset)
}
}
@(private)
xref_trailers_encrypted :: proc(ld: ^Xref_Loader) -> bool {
for t in ld.trailers {
if dict_has(t, "Encrypt") { return true }
}
return false
}
@(private)
xref_register_objstm :: proc(ld: ^Xref_Loader, container: u32, container_offset: i64) {
doc := ld.doc
stm, err := objstm_load(doc, container)
if err != .None || stm == nil { return }
// The dictionary survives whatever went wrong with the payload, so /N
// disagreeing with the pair table means the bytes were not the pair table.
// In the normal path a short table is worth keeping; here, where the
// entries it produces overwrite ones a scan established, it is not.
if stm.declared_n > 0 && len(stm.nums) != stm.declared_n { return }
for n, i in stm.nums {
if n == 0 || n == container { continue }
cur := xref_entry_at(ld, n)
// A top-level definition that sits later in the file than the object
// stream is the newer of the two and keeps its place.
if cur.kind == .Normal && cur.offset > container_offset { continue }
xref_put(ld, n, Xref_Entry{kind = .Compressed, container = container, index = u32(i)})
}
}
@(private)
xref_rebuild_trailer :: proc(ld: ^Xref_Loader, catalog: Ref) {
doc := ld.doc
merged, _ := xref_merge_trailers(ld)
doc.trailer = merged
merged["Size"] = i64(len(doc.xref))
if root, ok := to_dict(dict_get(doc, merged, "Root")); ok && root != nil {
if dict_type(doc, root) == "Catalog" || dict_has(root, "Pages") {
return
}
}
if catalog.num != 0 {
merged["Root"] = catalog
add_repair(doc, .Missing_Root, 0, catalog)
}
}