Files
pdf-odin-llm/parser.odin
T

515 lines
18 KiB
Odin
Raw Normal View History

2026-09-25 23:32:29 +02:00
package pdf
import "core:mem"
// Token stream to Object (ISO 32000-2, 7.3).
//
// The parser is written so that no input can make it fail outright: an
// unterminated array yields the elements it did collect, a dictionary key that
// is not a name is dropped, a stream whose /Length lies is measured by finding
// its `endstream`. Each of those records a `Repair` on the Document and
// carries on, because a file with one broken object usually still renders. An
// `Error` comes back only when there is genuinely no object to hand over.
//
// A nil Document is supported throughout: inline image dictionaries in a
// content stream are parsed with no file behind them.
Parser :: struct {
lex: Lexer,
doc: ^Document,
// Everything the parser builds is allocated here and never freed
// individually, so this wants to be an arena -- `doc.allocator` for
// anything that belongs to the Document.
allocator: mem.Allocator,
depth: int,
// The indirect object being parsed, which is what its strings and stream
// are decrypted with. The zero Ref means "do not decrypt": that is the
// trailer, an inline image, and every object that came out of an object
// stream (those were decrypted with their container, ISO 32000-2, 7.5.7).
obj_ref: Ref,
// Token lookahead. Two are needed and two are enough: telling "12 0 R"
// from "12 0 obj" and from a plain "12" is the only place in the grammar
// that needs to see past the token in hand.
lookahead: [2]Token,
n_lookahead: int,
}
// parser_make prepares a parser over `data`.
//
// `data` is normally the whole file with `lex.pos` moved to the object, not a
// sub-slice: token offsets are then file offsets, which is what repairs
// record, and a stream payload can be a sub-slice of `doc.data` instead of a
// copy.
parser_make :: proc(doc: ^Document, data: []byte, allocator := context.allocator) -> Parser {
p := Parser{
lex = lexer_make(data),
doc = doc,
allocator = allocator,
}
if p.allocator.procedure == nil {
p.allocator = context.allocator
}
return p
}
// ---------------------------------------------------------------------------
// Token queue
// ---------------------------------------------------------------------------
@(private = "file")
parser_next :: proc(p: ^Parser) -> Token {
if p.n_lookahead > 0 {
tok := p.lookahead[0]
p.lookahead[0] = p.lookahead[1]
p.n_lookahead -= 1
return tok
}
return lexer_next(&p.lex, p.allocator)
}
// parser_peek looks `n` tokens ahead (0 or 1) without consuming anything.
@(private = "file")
parser_peek :: proc(p: ^Parser, n: int) -> Token {
idx := clamp(n, 0, len(p.lookahead) - 1)
for p.n_lookahead <= idx {
p.lookahead[p.n_lookahead] = lexer_next(&p.lex, p.allocator)
p.n_lookahead += 1
}
return p.lookahead[idx]
}
// token_ends_container names the keywords that can only appear outside of an
// array or dictionary. Meeting one means a closing bracket was lost, and
// swallowing it would take the rest of the file with it.
@(private = "file")
token_ends_container :: proc(t: Token) -> bool {
if t.kind != .Keyword { return false }
switch t.text {
case "endobj", "stream", "endstream", "obj", "xref", "trailer", "startxref":
return true
}
return false
}
// ---------------------------------------------------------------------------
// Objects
// ---------------------------------------------------------------------------
// parse_object reads one object. Indirect references are returned as `Ref`;
// nothing is resolved here.
parse_object :: proc(p: ^Parser) -> (Object, Error) {
tok := parser_next(p)
switch tok.kind {
case .EOF:
return nil, .Truncated
case .Integer:
// "12 0 R" is a reference; "12 0 obj" is a header that ran into us,
// and "12 0" is two integers. Only the full three-token shape counts.
t1 := parser_peek(p, 0)
if t1.kind == .Integer && t1.int_value >= 0 && t1.int_value <= 65535 {
t2 := parser_peek(p, 1)
if t2.kind == .Keyword && t2.text == "R" {
p.n_lookahead = 0
return make_ref(tok.int_value, t1.int_value), .None
}
}
return tok.int_value, .None
case .Real:
return tok.real_value, .None
case .String:
if tok.truncated {
add_repair(p.doc, .Unterminated_String, i64(tok.start), p.obj_ref)
}
return String(parser_decrypt_string(p, tok.bytes)), .None
case .Name:
return Name(tok.text), .None
case .Array_Open:
return parse_array(p)
case .Dict_Open:
return parse_dict_or_stream(p)
case .Keyword:
switch tok.text {
case "true": return true, .None
case "false": return false, .None
case "null": return nil, .None
}
return nil, .Unexpected_Token
case .Array_Close, .Dict_Close, .Brace_Open, .Brace_Close:
return nil, .Unexpected_Token
case .Error:
return nil, .Syntax_Error
}
return nil, .Syntax_Error
}
// make_ref validates an indirect reference. Object number 0 is the free-list
// head and can never be referenced, and a number past the format's limit
// cannot be in any xref table; both read as null, which is what a reference to
// a non-existent object means anyway (ISO 32000-2, 7.3.10).
@(private = "file")
make_ref :: proc(num, gen: i64) -> Object {
if num < 1 || num > MAX_OBJECT_NUMBER || gen < 0 || gen > 65535 {
return nil
}
return Ref{num = u32(num), gen = u16(gen)}
}
@(private = "file")
parser_decrypt_string :: proc(p: ^Parser, raw: []byte) -> []byte {
if p.doc == nil || p.doc.encrypt == nil || p.obj_ref.num == 0 {
return raw
}
return decrypt_string(p.doc, p.obj_ref, raw)
}
// ---------------------------------------------------------------------------
// Arrays
// ---------------------------------------------------------------------------
@(private = "file")
parse_array :: proc(p: ^Parser) -> (Object, Error) {
if p.depth >= MAX_DEPTH {
return nil, .Recursion_Limit
}
p.depth += 1
defer p.depth -= 1
a := new_array(p.allocator)
// One token consumes at least one byte, so the remaining input bounds the
// number of iterations. The counter is belt and braces against a token
// rule that stops advancing.
fuel := len(p.lex.data) - p.lex.pos + 8
for fuel > 0 {
fuel -= 1
tok := parser_peek(p, 0)
switch {
case tok.kind == .Array_Close:
parser_next(p)
return a, .None
case tok.kind == .EOF:
add_repair(p.doc, .Unterminated_Array, i64(tok.start), p.obj_ref)
return a, .None
case tok.kind == .Dict_Close, token_ends_container(tok):
// A ']' was lost. Leave the token for the enclosing dictionary or
// the object reader; taking it would lose that structure too.
add_repair(p.doc, .Unterminated_Array, i64(tok.start), p.obj_ref)
return a, .None
}
obj, err := parse_object(p)
if err == .Recursion_Limit {
return a, err
}
if err != .None {
// Junk between elements: the token was consumed, so step over it.
// A null *element* is kept -- array positions carry meaning.
continue
}
array_push(a, obj)
}
return a, .None
}
// ---------------------------------------------------------------------------
// Dictionaries and streams
// ---------------------------------------------------------------------------
@(private = "file")
parse_dict_or_stream :: proc(p: ^Parser) -> (Object, Error) {
d, err := parse_dict(p)
if err != .None {
// The nesting limit. Whatever was collected before it is not worth
// handing back as if it were the whole object, and an Object holding a
// nil ^Dict is not the null object.
return nil, err
}
// A stream is a dictionary followed by `stream`, and only ever as the
// whole body of an indirect object -- so only at the outermost level.
if p.depth == 0 {
tok := parser_peek(p, 0)
if tok.kind == .Keyword && tok.text == "stream" {
parser_next(p)
return parse_stream(p, d, tok.end), .None
}
}
return d, .None
}
@(private = "file")
parse_dict :: proc(p: ^Parser) -> (^Dict, Error) {
if p.depth >= MAX_DEPTH {
return nil, .Recursion_Limit
}
p.depth += 1
defer p.depth -= 1
d := new_dict(p.allocator)
fuel := len(p.lex.data) - p.lex.pos + 8
for fuel > 0 {
fuel -= 1
key_tok := parser_peek(p, 0)
switch {
case key_tok.kind == .Dict_Close:
parser_next(p)
return d, .None
case key_tok.kind == .EOF:
add_repair(p.doc, .Unterminated_Dict, i64(key_tok.start), p.obj_ref)
return d, .None
case token_ends_container(key_tok):
add_repair(p.doc, .Unterminated_Dict, i64(key_tok.start), p.obj_ref)
return d, .None
}
if key_tok.kind != .Name {
parser_next(p)
add_repair(p.doc, .Unterminated_Dict, i64(key_tok.start), p.obj_ref)
// The value belongs to a key that cannot be recorded, so it goes
// with it; left behind it would be read as the next key. A stray
// bracket is junk on its own and takes nothing with it.
#partial switch key_tok.kind {
case .Array_Close, .Brace_Open, .Brace_Close, .Error:
case:
skip := parser_peek(p, 0)
if skip.kind != .Dict_Close && skip.kind != .EOF && !token_ends_container(skip) {
_, _ = parse_object(p)
}
}
continue
}
parser_next(p)
// "/Key >>" -- a key whose value was lost. Closing the dictionary here
// keeps the '>>' meaningful instead of consuming it as the value.
vt := parser_peek(p, 0)
if vt.kind == .Dict_Close || vt.kind == .EOF || token_ends_container(vt) {
add_repair(p.doc, .Unterminated_Dict, i64(key_tok.start), p.obj_ref)
if vt.kind == .Dict_Close {
parser_next(p)
}
return d, .None
}
value, verr := parse_object(p)
if verr == .Recursion_Limit {
// The value is deeper than the library will follow, and its tokens
// are still on the stream, so nothing after it can be trusted to
// be a key. Fail the dictionary the way an array fails.
return d, verr
}
if verr != .None {
continue
}
// A key with a null value and an absent key mean the same thing
// everywhere in the format, so nulls are dropped at parse time.
if value != nil {
dict_set(d, key_tok.text, value)
}
}
return d, .None
}
// parse_stream reads the payload that follows a `stream` keyword ending at
// `kw_end`. The payload is a sub-slice of the input, never a copy -- except
// when it has to be decrypted.
@(private = "file")
parse_stream :: proc(p: ^Parser, d: ^Dict, kw_end: int) -> Object {
data := p.lex.data
// "stream" is followed by CRLF or LF, never by CR alone (ISO 32000-2,
// 7.3.8.1). Producers that emit a bare CR, or a space before the EOL, are
// common enough that both are tolerated.
i := kw_end
for i < len(data) && (data[i] == ' ' || data[i] == '\t') {
i += 1
}
if i < len(data) && data[i] == '\r' { i += 1 }
if i < len(data) && data[i] == '\n' { i += 1 }
body := i
length := i64(-1)
if lv, ok := to_int(dict_get(p.doc, d, "Length")); ok {
length = lv
}
end := -1
after := -1
// Trust /Length only if it fits in the file *and* `endstream` is where it
// says it is. A length that is merely plausible is not evidence.
if length >= 0 && length <= i64(len(data) - body) {
probe := Lexer{data = data, pos = body + int(length)}
if lexer_accept_keyword(&probe, "endstream") {
end = body + int(length)
after = probe.pos
}
}
if end < 0 {
needle := "endstream"
idx := index_bytes(data[body:], transmute([]byte)needle)
if idx < 0 {
// No endstream at all: the file is truncated inside the stream.
// Everything that is left is the best available payload.
add_repair(p.doc, .Missing_Endstream, i64(body), p.obj_ref)
end = len(data)
after = len(data)
} else {
e := body + idx
after = e + len(needle)
// One EOL before `endstream` is the separator, not data. CRLF is
// one marker, so both bytes come off.
if e > body && data[e - 1] == '\n' { e -= 1 }
if e > body && data[e - 1] == '\r' { e -= 1 }
end = e
add_repair(p.doc, .Bad_Stream_Length, i64(body), p.obj_ref)
}
}
raw := data[body:end]
// `raw` is a view of the file, so decrypt_stream has to return a new
// buffer rather than writing through this one. An object that came out of
// an object stream has the zero Ref and is already in the clear: its
// container was decrypted as a whole (ISO 32000-2, 7.5.7).
if p.doc != nil && p.doc.encrypt != nil && p.obj_ref.num != 0 {
raw = decrypt_stream(p.doc, p.obj_ref, raw, d)
}
p.n_lookahead = 0
p.lex.pos = clamp(after, 0, len(data))
s := new_stream(d, raw, p.allocator)
s.ref = p.obj_ref
return s
}
// ---------------------------------------------------------------------------
// Indirect objects
// ---------------------------------------------------------------------------
// parse_indirect_object_at reads "num gen obj <object> endobj" at `offset`,
// an absolute index into `doc.data`.
//
// `got` is the reference the file actually has there, which is not always the
// one the cross-reference table promised; the caller compares them to detect a
// stale table. A header that is not exactly at `offset` is searched for
// nearby: incremental updaters that miscount by a byte or two are common
// enough that giving up on the first mismatch loses readable objects.
parse_indirect_object_at :: proc(doc: ^Document, offset: i64, want: Ref) -> (obj: Object, got: Ref, err: Error) {
if doc == nil {
return nil, {}, .Invalid_Argument
}
if offset < 0 || offset >= i64(len(doc.data)) {
return nil, {}, .Object_Not_Found
}
start := int(offset)
num, gen, body, ok := object_header_at(doc.data, start)
if !ok {
num, gen, body, ok = object_header_near(doc.data, start, want)
// Offsets in a file with junk before "%PDF-" are sometimes measured
// from the header instead of from byte zero.
if !ok && doc.header_offset > 0 && offset + doc.header_offset < i64(len(doc.data)) {
num, gen, body, ok = object_header_near(doc.data, int(offset + doc.header_offset), want)
}
if ok {
add_repair(doc, .Xref_Entry_Mismatch, offset, want)
}
}
if !ok {
return nil, {}, .Object_Not_Found
}
got = Ref{num = num, gen = gen}
p := parser_make(doc, doc.data, doc.allocator)
p.lex.pos = body
p.obj_ref = got
// "12 0 obj endobj" is an empty object, which is null rather than an error.
if lexer_at_keyword(&p.lex, "endobj") {
return nil, got, .None
}
obj, err = parse_object(&p)
if err != .None {
return nil, got, err
}
return obj, got, .None
}
// object_header_at matches "num gen obj" exactly at `pos`.
@(private = "file")
object_header_at :: proc(data: []byte, pos: int) -> (num: u32, gen: u16, body: int, ok: bool) {
if pos < 0 || pos >= len(data) {
return 0, 0, 0, false
}
lex := Lexer{data = data, pos = pos}
// A header is three tokens, two integers and a keyword, and this runs at
// every byte of a 129-wide window for every object of a damaged file:
// `lexer_next_structural` is what keeps a rejected offset from copying a
// string body the probe is about to throw away.
t1 := lexer_next_structural(&lex)
if t1.kind != .Integer || t1.int_value < 1 || t1.int_value > MAX_OBJECT_NUMBER {
return 0, 0, 0, false
}
t2 := lexer_next_structural(&lex)
if t2.kind == .Keyword && t2.text == "obj" {
// "12 obj": the generation number was dropped. Generation 0 is right
// in every file this has been seen in.
return u32(t1.int_value), 0, lex.pos, true
}
if t2.kind != .Integer || t2.int_value < 0 || t2.int_value > 65535 {
return 0, 0, 0, false
}
t3 := lexer_next_structural(&lex)
if t3.kind != .Keyword || t3.text != "obj" {
return 0, 0, 0, false
}
return u32(t1.int_value), u16(t2.int_value), lex.pos, true
}
// OBJECT_HEADER_WINDOW is how far either side of a claimed offset a header is
// looked for. Wide enough for the off-by-a-few-bytes tables that broken
// incremental updates produce, narrow enough that it cannot wander into the
// next object.
@(private = "file")
OBJECT_HEADER_WINDOW :: 64
@(private = "file")
object_header_near :: proc(data: []byte, center: int, want: Ref) -> (num: u32, gen: u16, body: int, ok: bool) {
lo := max(center - OBJECT_HEADER_WINDOW, 0)
hi := min(center + OBJECT_HEADER_WINDOW, len(data) - 1)
for i := lo; i <= hi; i += 1 {
// Only start where a token can start, or "123 0 obj" also matches at
// its second digit and yields object 23.
if data[i] < '0' || data[i] > '9' { continue }
if i > 0 && is_pdf_regular(data[i - 1]) { continue }
n, g, b, hit := object_header_at(data, i)
if !hit { continue }
if want.num == 0 || n == want.num {
return n, g, b, true
}
if !ok {
// Remember the first header found, in case none matches: an object
// with the wrong number is still something the caller can judge.
num, gen, body, ok = n, g, b, true
}
}
return
}