initial commit

This commit is contained in:
2026-09-25 23:32:29 +02:00
commit fff8b677f8
114 changed files with 84233 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
package pdf
// Object streams (ISO 32000-2, 7.5.7).
//
// An /ObjStm is a stream whose decoded bytes hold /N complete objects, one
// after another, preceded by a header of /N "object-number offset" integer
// pairs. /First is where the objects start; each pair's offset is measured
// from there.
//
// Two properties of the format matter to everything below. The objects inside
// have no "N G obj" header of their own, so the pair table is the only thing
// that says which object is where -- and when it disagrees with the
// cross-reference table, one of the two is wrong and the pair table is the one
// that was written next to the bytes it describes. And the whole stream is
// decrypted as a unit (7.5.7, and 7.6.2 for the reason), so the objects taken
// out of it must not be decrypted a second time: they are parsed with the zero
// Ref, which is what tells the parser to leave their strings alone.
// Objstm is a decoded object stream and its pair table, cached on the Document
// for as long as the Document lives.
Objstm :: struct {
// The container object, so a repair can name it.
ref: Ref,
// Object numbers, in the order the pair table lists them.
nums: []u32,
// Where each object starts in `data`, or -1 for a pair whose offset does
// not land inside the stream.
offsets: []int,
// The decoded stream. Owned by the Document arena; never freed here.
data: []byte,
// Byte offset the objects start at, after any repair to /First.
first: int,
// What /N claimed, which is not always what the pair table holds. The
// dictionary is never encrypted even when the payload is, so the two
// disagreeing is evidence that the payload was not decoded correctly.
declared_n: int,
// Set while the header is being read, so an object stream that reaches
// itself -- directly, or through a second stream that points back -- is
// caught instead of recursed into.
loading: bool,
failed: bool,
}
// objstm_get_object returns object `num`, which the cross-reference table says
// is entry `index` of object stream `container`.
//
// `index` is treated as a hint rather than as the truth. A file that was
// incrementally updated without its cross-reference stream being rewritten has
// stale indices, and the pair table is right where the index is wrong, so a
// mismatch is resolved by searching the table for the number that was asked
// for.
objstm_get_object :: proc(doc: ^Document, container: u32, index: u32, num: u32) -> (Object, Error) {
if doc == nil || num == 0 { return nil, .Invalid_Argument }
stm, err := objstm_load(doc, container)
if err != .None { return nil, err }
if stm == nil || len(stm.nums) == 0 { return nil, .Object_Not_Found }
slot := -1
if int(index) < len(stm.nums) && stm.nums[index] == num {
slot = int(index)
} else {
for n, i in stm.nums {
if n == num {
slot = i
break
}
}
if slot >= 0 {
add_repair(doc, .Xref_Entry_Mismatch, 0, Ref{num, 0})
}
}
if slot < 0 { return nil, .Object_Not_Found }
off := stm.offsets[slot]
if off < 0 || off >= len(stm.data) {
add_repair(doc, .Missing_Object, 0, Ref{num, 0})
return nil, .Object_Not_Found
}
p := parser_make(doc, stm.data, doc.allocator)
p.lex.pos = off
// p.obj_ref stays {0,0}: this object's strings were decrypted with the
// container and must not be touched again (7.5.7).
obj, perr := parse_object(&p)
if perr != .None {
add_repair(doc, .Missing_Object, i64(off), Ref{num, 0})
return nil, perr
}
return obj, .None
}
// objstm_load decodes an object stream and reads its pair table, from the
// Document's cache if it has been read before.
@(private)
objstm_load :: proc(doc: ^Document, container: u32) -> (^Objstm, Error) {
if doc == nil || container == 0 || container > MAX_OBJECT_NUMBER {
return nil, .Invalid_Argument
}
if cached, ok := doc.objstm_cache[container]; ok && cached != nil {
if cached.loading {
add_repair(doc, .Missing_Object, 0, cached.ref)
return nil, .Cyclic_Reference
}
if cached.failed { return nil, .Bad_Stream }
return cached, .None
}
gen := u16(0)
if int(container) < len(doc.xref) {
gen = doc.xref[container].gen
}
stm := new(Objstm, doc.allocator)
stm.ref = Ref{container, gen}
stm.loading = true
// Cached before it is read, so a stream that contains itself meets its own
// half-built entry rather than recursing.
doc.objstm_cache[container] = stm
err := objstm_read(doc, stm)
stm.loading = false
if err != .None {
stm.failed = true
stm.nums = nil
stm.offsets = nil
stm.data = nil
return nil, err
}
return stm, .None
}
// objstm_pairs reads the "object-number offset" table at the front of the
// decoded stream. `hit_limit` says it stopped because it reached `header_limit`
// rather than because the tokens stopped being a pair.
@(private)
objstm_pairs :: proc(doc: ^Document, data: []byte, limit, header_limit: int) ->
(nums: [dynamic]u32, rels: [dynamic]i64, header_end: int, hit_limit: bool) {
nums = make([dynamic]u32, 0, min(limit, 64), doc.allocator)
rels = make([dynamic]i64, 0, min(limit, 64), doc.allocator)
lex := Lexer{data = data, pos = 0}
for len(nums) < limit {
save := lex.pos
t1 := lexer_next_structural(&lex)
t2 := lexer_next_structural(&lex)
if t1.kind != .Integer || t2.kind != .Integer {
lex.pos = save
break
}
if lex.pos > header_limit {
lex.pos = save
hit_limit = true
break
}
if t1.int_value < 1 || t1.int_value > MAX_OBJECT_NUMBER {
// A pair whose object number is impossible still occupies a slot:
// dropping it would shift every index after it.
append(&nums, u32(0))
} else {
append(&nums, u32(t1.int_value))
}
append(&rels, t2.int_value)
}
header_end = lex.pos
return
}
@(private)
objstm_read :: proc(doc: ^Document, stm: ^Objstm) -> Error {
s := get_stream(doc, stm.ref)
if s == nil { return .Object_Not_Found }
// A missing /Type is tolerated -- it is missing from a great many
// dictionaries that are otherwise perfectly formed -- but a /Type that
// says something else means the cross-reference table sent us to the
// wrong object, and taking its bytes apart as pairs would invent objects.
if ty := dict_name(doc, s.dict, "Type"); ty != "" && ty != "ObjStm" {
return .Type_Mismatch
}
data, derr := stream_data(doc, s)
if derr != .None { return derr }
if len(data) == 0 || len(data) > MAX_OBJSTM_SIZE { return .Bad_Stream }
stm.data = data
declared_n := dict_int(doc, s.dict, "N", 0)
declared_first := dict_int(doc, s.dict, "First", -1)
stm.declared_n = int(clamp(declared_n, 0, i64(max(i32))))
// A pair is two integers and a separator, so three bytes at the very
// least. Whatever /N claims, the header cannot hold more than that -- and
// a stream is allowed to decode to a gigabyte, so the file's own length
// bounds it too: every object in here also needs an entry in the
// cross-reference table, which is bounded the same way.
max_pairs := min(len(data) / 3 + 1, xref_max_objects(doc))
limit := max_pairs
if declared_n > 0 && declared_n <= i64(max_pairs) {
limit = int(declared_n)
} else {
// /N missing, zero, negative or larger than the stream can hold. Read
// pairs until they stop being pairs and take that as the count.
add_repair(doc, .Bad_Stream_Length, 0, stm.ref)
}
// The header ends where /First says it does, when /First is believable.
// Reading past it would take the first object's own bytes for pairs.
header_limit := len(data)
if declared_first > 0 && declared_first <= i64(len(data)) {
header_limit = int(declared_first)
}
nums, rels, header_end, hit_limit := objstm_pairs(doc, data, limit, header_limit)
// A /First that lands inside the pair table cuts it short. Reading the
// header again without that ceiling costs nothing -- the pairs stop at the
// first token that is not an integer either way -- and it is the
// difference between reading the stream and losing every object in it.
if hit_limit && (len(nums) == 0 || (declared_n > 0 && len(nums) < int(declared_n))) {
nums, rels, header_end, _ = objstm_pairs(doc, data, limit, len(data))
}
first := declared_first
if first < i64(header_end) || first > i64(len(data)) {
// /First is measured from the start of the decoded stream and must be
// at or after the end of the pair table. When it is not, the pair
// table itself says where the objects begin.
add_repair(doc, .Bad_Stream_Length, 0, stm.ref)
first = i64(header_end)
}
stm.first = int(first)
offsets := make([]int, len(rels), doc.allocator)
for r, i in rels {
abs := first + r
if r < 0 || abs < 0 || abs >= i64(len(data)) {
offsets[i] = -1
continue
}
offsets[i] = int(abs)
}
stm.nums = nums[:]
stm.offsets = offsets
if len(stm.nums) == 0 { return .Bad_Stream }
return .None
}