Files
pdf-odin-llm/document.odin
T
2026-09-25 23:32:29 +02:00

553 lines
21 KiB
Odin

package pdf
import "core:mem"
import "core:mem/virtual"
import "core:os"
import "core:slice"
import "core:strings"
// ---------------------------------------------------------------------------
// Limits
//
// Every one of these bounds something an attacker (or a fuzzer, or a merely
// broken producer) controls. They are deliberately generous -- real documents
// stay far below them -- and exist only so that a malformed file fails instead
// of allocating the machine to death or spinning forever.
// ---------------------------------------------------------------------------
// Nested arrays and dictionaries, and form-XObject recursion.
MAX_DEPTH :: 32
// Object numbers above this are rejected outright (ISO 32000-2, 7.5.4 caps
// them at 8388607 for a reason: the xref table is indexed by them).
MAX_OBJECT_NUMBER :: 8388607
// /Prev hops through the cross-reference chain.
MAX_XREF_CHAIN :: 64
// Operands stacked before a content operator; the format never needs more
// than 8 apart from `d0`/`d1` and the shading operators.
MAX_CONTENT_OPERANDS :: 32
// 268M pixels, about 16k x 16k. Above this `render_page` refuses.
MAX_BITMAP_PIXELS :: 1 << 28
// Ref chains: "1 0 R" pointing at "2 0 R" pointing at ... Legal, but only
// ever seen in files trying to make a reader loop.
MAX_REF_CHAIN :: 32
// How deeply one object's definition may require another's before the library
// stops following. This is *nesting*, not chaining: it bounds only the cases
// where parsing object A cannot finish until object B is parsed -- an indirect
// /Length, an object stream's container, an indirect /Filter. Legitimate files
// nest two or three deep. Without this bound a file can name an acyclic chain
// of 100000 such objects and overflow the native stack, which the cycle
// guards cannot catch because no object number ever repeats.
MAX_LOAD_DEPTH :: 64
// Bytes an object stream is allowed to expand to.
MAX_OBJSTM_SIZE :: 1 << 30
// Repairs recorded before the library stops bothering; a file this broken is
// not going to be diagnosed by reading more of the list.
MAX_REPAIRS :: 4096
// Pages one document may contain. The largest real documents run to a few
// tens of thousands; a /Kids graph that yields more than this is a file
// trying to make the page-tree walk allocate without end.
MAX_PAGES :: 1 << 20
// ---------------------------------------------------------------------------
// Cross-reference table
//
// Declared here rather than in xref.odin because the table is the Document's
// own state; xref.odin holds the algorithms that fill it in.
// ---------------------------------------------------------------------------
Xref_Kind :: enum u8 {
// Never defined, or on the free list. Reads as null.
Free = 0,
// Lives at `offset` in the file as "num gen obj ... endobj".
Normal,
// Lives inside object stream `container` at index `index`.
Compressed,
}
Xref_Entry :: struct {
offset: i64, // Normal: byte offset in the file
container: u32, // Compressed: object number of the /ObjStm
index: u32, // Compressed: position within that stream
gen: u16,
kind: Xref_Kind,
// Set once the entry has been read and its object cached, so a second
// lookup skips straight to the cache.
cached: bool,
}
// ---------------------------------------------------------------------------
// Document
// ---------------------------------------------------------------------------
Open_Options :: struct {
// User or owner password for an encrypted document. Empty is correct for
// the common case of a file encrypted with an empty user password -- which
// is what "restricted permissions, opens without prompting" means.
password: string,
// When true the Document takes ownership of the byte slice handed to
// `open_bytes` and frees it in `close`. `open_file` sets this.
take_ownership: bool,
// Skip the page-tree walk. Useful for tools that only want to inspect
// objects, and for opening documents whose page tree is broken.
skip_page_tree: bool,
// Allocator for the Document handle itself, and for the file bytes when
// `open_file` reads them.
//
// It is deliberately NOT where parsed objects come from: those live in a
// growing virtual-memory arena that takes pages from the OS directly, so
// that a large document costs one address-space reservation rather than
// thousands of small allocations. A caller that needs to account for a
// document's real footprint should read `document_memory_used`, not
// instrument this allocator.
allocator: mem.Allocator,
}
Document :: struct {
// The whole file. Object offsets index into this directly, and most
// strings and stream payloads are sub-slices of it rather than copies.
data: []byte,
owns_data: bool,
backing: mem.Allocator,
// Everything parsed out of the file lives here and dies with `close`.
arena: virtual.Arena,
allocator: mem.Allocator,
version_major: int,
version_minor: int,
// Byte position of "%PDF-". Files with junk prepended (a mail header, an
// HTTP response, a self-extracting wrapper) are common, and every offset
// inside such a file is relative to this, not to zero.
header_offset: i64,
xref: [dynamic]Xref_Entry,
trailer: ^Dict,
catalog: ^Dict,
// Resolved objects, keyed by object number. Generation is not part of the
// key: a file never has two live generations of the same number, and the
// ones that try are broken in ways the generation check makes worse.
cache: map[u32]Object,
// Objects currently being parsed, to break reference cycles that would
// otherwise recurse until the stack ends.
loading: map[u32]bool,
// Decoded object streams, keyed by their object number.
objstm_cache: map[u32]^Objstm,
// Colour spaces and functions built from this file, keyed by the address
// of the object they were built from. A page that names the same space in
// four hundred XObjects, or a shading that names one tint transform from
// every pattern that uses it, must build each of them once. Both are
// filled in lazily and, like everything else the Document owns, die with
// its arena.
cs_cache: map[uintptr]^Colorspace,
fn_cache: map[uintptr]^Function,
// Fonts, keyed the same way, by the address of the font dictionary. A
// page that selects the same font two hundred times must build it once,
// and building one means parsing an embedded font program.
font_cache: map[uintptr]^Font,
// Images read from this file, and the decoded pixels of the ones most
// recently painted. Unlike the two caches above this one has a byte
// budget and evicts, because a decoded photograph is megabytes and a
// catalogue has hundreds of them; see image.odin.
img_cache: Image_Cache,
encrypt: ^Encryption,
// Page dictionaries in document order, flattened from the page tree.
pages: [dynamic]^Dict,
page_refs: [dynamic]Ref,
repairs: [dynamic]Repair,
// True once anything was worked around. Cheaper to test than len(repairs).
is_damaged: bool,
}
// ---------------------------------------------------------------------------
// Opening and closing
// ---------------------------------------------------------------------------
// open_file reads a PDF from disk. The Document owns the bytes.
open_file :: proc(path: string, options := Open_Options{}) -> (doc: ^Document, err: Error) {
opts := options
if opts.allocator.procedure == nil {
opts.allocator = context.allocator
}
data, read_err := os.read_entire_file(path, opts.allocator)
if read_err != nil {
return nil, .Cant_Open_File
}
opts.take_ownership = true
return open_bytes(data, opts)
}
// open_bytes reads a PDF already in memory.
//
// Unless `options.take_ownership` is set the slice is borrowed, not copied, and
// must outlive the Document -- object offsets, string values and stream
// payloads all point into it.
open_bytes :: proc(data: []byte, options := Open_Options{}) -> (doc: ^Document, err: Error) {
opts := options
if opts.allocator.procedure == nil {
opts.allocator = context.allocator
}
if len(data) == 0 {
return nil, .Not_A_PDF
}
doc = new(Document, opts.allocator)
doc.data = data
doc.owns_data = opts.take_ownership
doc.backing = opts.allocator
if arena_err := virtual.arena_init_growing(&doc.arena); arena_err != nil {
// `close` cannot run yet -- there is no arena for it to destroy -- so
// the bytes we took ownership of have to be released here.
if doc.owns_data {
delete(doc.data, opts.allocator)
}
free(doc, opts.allocator)
return nil, .Out_Of_Memory
}
doc.allocator = virtual.arena_allocator(&doc.arena)
doc.xref = make([dynamic]Xref_Entry, doc.allocator)
doc.cache = make(map[u32]Object, 64, doc.allocator)
doc.loading = make(map[u32]bool, 16, doc.allocator)
doc.objstm_cache = make(map[u32]^Objstm, 8, doc.allocator)
doc.pages = make([dynamic]^Dict, doc.allocator)
doc.page_refs = make([dynamic]Ref, doc.allocator)
doc.repairs = make([dynamic]Repair, doc.allocator)
// A missing header is not fatal on its own: enough files in the wild have
// one that a reader which insists on it is less useful than one that goes
// looking for objects.
read_header(doc)
if xerr := xref_load(doc); xerr != .None {
close(doc)
return nil, xerr
}
if cerr := crypt_init(doc, transmute([]byte)opts.password); cerr != .None {
close(doc)
return nil, cerr
}
if derr := find_catalog(doc); derr != .None {
close(doc)
return nil, derr
}
if !opts.skip_page_tree {
if perr := pagetree_build(doc); perr != .None {
close(doc)
return nil, perr
}
}
return doc, .None
}
// close releases everything the Document owns. The Document pointer, every
// object reachable from it, and (if it was taken) the file's byte slice are all
// invalid afterwards.
close :: proc(doc: ^Document) {
if doc == nil { return }
crypt_destroy(doc)
// Decoded image pixels are the one thing the arena does not own: they have
// to be individually freeable for the cache's budget to mean anything.
image_cache_release(doc)
virtual.arena_destroy(&doc.arena)
if doc.owns_data && doc.data != nil {
delete(doc.data, doc.backing)
}
backing := doc.backing
free(doc, backing)
}
// read_header locates "%PDF-M.N" and records the version and the offset
// everything else in the file is measured from.
@(private)
read_header :: proc(doc: ^Document) {
// The header is required to be at byte 0, but junk in front of it is
// ordinary enough that viewers all scan a window for it. 1 KiB matches
// what the format's own conformance guidance allows for.
limit := min(len(doc.data), 4096)
idx := strings.index(string(doc.data[:limit]), "%PDF-")
if idx < 0 {
// Last resort: some generators emit "%!PS-Adobe" wrappers, and some
// emit nothing at all. Leave the offset at zero and let the xref
// rebuild find objects by scanning.
doc.version_major = 1
doc.version_minor = 7
return
}
doc.header_offset = i64(idx)
v := doc.data[idx + 5:]
doc.version_major = 1
doc.version_minor = 7
if len(v) >= 3 && v[0] >= '0' && v[0] <= '9' && v[1] == '.' && v[2] >= '0' && v[2] <= '9' {
doc.version_major = int(v[0] - '0')
doc.version_minor = int(v[2] - '0')
}
}
// document_version returns the version from the header, upgraded by the
// catalog's /Version entry when that is newer (ISO 32000-2, 7.5.5: a file may
// be upgraded by an incremental update without rewriting the header).
document_version :: proc(doc: ^Document) -> (major, minor: int) {
major, minor = doc.version_major, doc.version_minor
if v := dict_name(doc, doc.catalog, "Version"); len(v) >= 3 && v[1] == '.' &&
v[0] >= '0' && v[0] <= '9' && v[2] >= '0' && v[2] <= '9' {
cm := int(v[0] - '0')
cn := int(v[2] - '0')
if cm > major || (cm == major && cn > minor) {
major, minor = cm, cn
}
}
return
}
// ---------------------------------------------------------------------------
// Object access
// ---------------------------------------------------------------------------
// resolve follows indirect references until it reaches a direct object.
//
// This is the single most-called procedure in the library: nearly every value
// read out of a dictionary is allowed to be a reference, and callers that skip
// it work on most files and mysteriously fail on the rest. A nil Document
// makes it a no-op, which is what the standalone object parser wants.
resolve :: proc(doc: ^Document, o: Object) -> Object {
if doc == nil { return o }
obj := o
for i in 0 ..< MAX_REF_CHAIN {
ref, is_ref := obj.(Ref)
if !is_ref { return obj }
obj = get_object(doc, ref)
_ = i
}
// A chain this long is a loop. Null is what a viewer shows.
return nil
}
// get_object loads one indirect object, from the cache if it is there.
//
// A reference to an object that does not exist is null, not an error: the
// format says so (ISO 32000-2, 7.3.10) and files rely on it.
get_object :: proc(doc: ^Document, ref: Ref) -> Object {
if doc == nil || ref.num == 0 || ref.num > MAX_OBJECT_NUMBER {
return nil
}
if cached, ok := doc.cache[ref.num]; ok {
return cached
}
// Already being parsed further up the stack: an object whose own
// definition refers back to it. Break the cycle with null.
if doc.loading[ref.num] or_else false {
add_repair(doc, .Missing_Object, 0, ref)
return nil
}
// `loading` holds exactly the objects whose parses are in flight, so its
// size is the current nesting depth. Bounding it stops an acyclic chain --
// object 2's /Length pointing at object 3's, and so on for thousands of
// distinct objects -- from walking the native stack off its end. The
// cycle guard above cannot see such a chain, because no object number
// ever repeats.
if len(doc.loading) >= MAX_LOAD_DEPTH {
add_repair(doc, .Recursion_Limit_Hit, 0, ref)
return nil
}
if int(ref.num) >= len(doc.xref) {
return nil
}
entry := doc.xref[ref.num]
doc.loading[ref.num] = true
defer delete_key(&doc.loading, ref.num)
obj: Object
switch entry.kind {
case .Free:
obj = nil
case .Normal:
parsed, got, err := parse_indirect_object_at(doc, entry.offset, Ref{ref.num, entry.gen})
if err == .None {
// An xref pointing at the wrong object is a classic symptom of a
// file that was edited without its table being updated. Take what
// is actually there, but only if the number matches -- taking a
// different object's body would be worse than nothing.
if got.num == ref.num {
obj = parsed
} else {
add_repair(doc, .Xref_Entry_Mismatch, entry.offset, ref)
obj = nil
}
} else {
add_repair(doc, .Missing_Object, entry.offset, ref)
}
case .Compressed:
parsed, err := objstm_get_object(doc, entry.container, entry.index, ref.num)
if err == .None {
obj = parsed
} else {
add_repair(doc, .Missing_Object, 0, ref)
}
}
doc.cache[ref.num] = obj
return obj
}
// get_dict, get_array, get_stream are the typed forms of get_object, for the
// many call sites that know what they are looking for.
get_dict :: proc(doc: ^Document, ref: Ref) -> ^Dict {
d, _ := to_dict(get_object(doc, ref))
return d
}
get_array :: proc(doc: ^Document, ref: Ref) -> ^Array {
a, _ := to_array(get_object(doc, ref))
return a
}
get_stream :: proc(doc: ^Document, ref: Ref) -> ^Stream {
s, _ := to_stream(get_object(doc, ref))
return s
}
// document_memory_used is how many bytes the Document's arena has handed out:
// every object, dictionary, stream payload and decoded buffer parsed so far.
// It grows as pages are visited, since streams decode lazily, and drops to
// zero only at `close`.
document_memory_used :: proc(doc: ^Document) -> int {
if doc == nil { return 0 }
return int(doc.arena.total_used)
}
// document_memory_reserved is the address space the arena holds. On a 64-bit
// system this is reserved, not committed, so it is much larger than the
// resident cost and is not the number to budget against.
document_memory_reserved :: proc(doc: ^Document) -> int {
if doc == nil { return 0 }
return int(doc.arena.total_reserved)
}
// object_count is the highest object number the file defines, plus one.
object_count :: proc(doc: ^Document) -> int {
return len(doc.xref)
}
// ---------------------------------------------------------------------------
// Catalog
// ---------------------------------------------------------------------------
@(private)
find_catalog :: proc(doc: ^Document) -> Error {
if root := dict_dict(doc, doc.trailer, "Root"); root != nil {
// Trust it only if it looks like a catalog. Some files have a /Root
// pointing at a page or at a free object after a bad incremental
// update, and scanning finds the real one.
if dict_type(doc, root) == "Catalog" || dict_has(root, "Pages") {
doc.catalog = root
return .None
}
}
// Scan every object for /Type /Catalog. Costly, but only reached on files
// whose trailer is unusable, and the alternative is refusing to open them.
for num in 1 ..< len(doc.xref) {
obj := get_object(doc, Ref{u32(num), doc.xref[num].gen})
d, ok := to_dict(obj)
if !ok { continue }
if dict_type(doc, d) == "Catalog" {
doc.catalog = d
add_repair(doc, .Missing_Root, 0, Ref{u32(num), 0})
return .None
}
}
// A file with no catalog can still have pages; pagetree_build falls back to
// scanning for /Type /Page. Report the damage and let it try.
add_repair(doc, .Missing_Root, 0, {})
return .None
}
// ---------------------------------------------------------------------------
// Repairs
// ---------------------------------------------------------------------------
add_repair :: proc(doc: ^Document, kind: Repair_Kind, offset: i64, ref := Ref{}) {
if doc == nil { return }
doc.is_damaged = true
if len(doc.repairs) >= MAX_REPAIRS { return }
append(&doc.repairs, Repair{kind = kind, offset = offset, ref = ref})
}
// ---------------------------------------------------------------------------
// Byte-slice helpers used across the reader
// ---------------------------------------------------------------------------
// slice_at returns doc.data[offset:] with the offset clamped, so a garbage
// offset out of a broken xref yields an empty slice instead of a panic.
@(private)
slice_at :: proc(doc: ^Document, offset: i64) -> []byte {
if offset < 0 || offset >= i64(len(doc.data)) {
return nil
}
return doc.data[offset:]
}
// arena_bytes copies `src` into the Document's arena. Used when a value cannot
// borrow from `doc.data` -- decrypted strings, decoded streams, anything
// synthesised.
@(private)
arena_bytes :: proc(doc: ^Document, src: []byte) -> []byte {
if len(src) == 0 { return nil }
dst := make([]byte, len(src), doc.allocator)
copy(dst, src)
return dst
}
@(private)
arena_string :: proc(doc: ^Document, src: string) -> string {
return string(arena_bytes(doc, transmute([]byte)src))
}
// index_bytes finds `needle` in `haystack`, or -1.
@(private)
index_bytes :: proc(haystack, needle: []byte) -> int {
return slice_indexof_bytes(haystack, needle)
}
@(private)
slice_indexof_bytes :: proc(haystack, needle: []byte) -> int {
if len(needle) == 0 { return 0 }
if len(needle) > len(haystack) { return -1 }
first := needle[0]
for i := 0; i + len(needle) <= len(haystack); i += 1 {
if haystack[i] != first { continue }
if slice.equal(haystack[i:i + len(needle)], needle) {
return i
}
}
return -1
}
// last_index_bytes finds the last occurrence of `needle`, or -1. Used to walk
// backwards to `startxref`.
@(private)
last_index_bytes :: proc(haystack, needle: []byte) -> int {
if len(needle) == 0 { return len(haystack) }
if len(needle) > len(haystack) { return -1 }
for i := len(haystack) - len(needle); i >= 0; i -= 1 {
if slice.equal(haystack[i:i + len(needle)], needle) {
return i
}
}
return -1
}