674 lines
22 KiB
Odin
674 lines
22 KiB
Odin
package pdf
|
|||
|
|
|
||
|
|
import "core:bytes"
|
||
|
|
import "core:strings"
|
||
|
|
import "core:unicode/utf8"
|
||
|
|
|
||
|
|
// Text strings, dates, and the name/number trees that hold a document's
|
||
|
|
// named destinations, embedded files and page labels.
|
||
|
|
//
|
||
|
|
// Everything here works on bytes that came out of a file, so everything here
|
||
|
|
// is written to survive nonsense: a truncated date reads as far as it can, an
|
||
|
|
// unpaired UTF-16 surrogate becomes U+FFFD rather than invalid UTF-8, and a
|
||
|
|
// tree whose /Kids point back at their parent terminates.
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Text strings (ISO 32000-2, 7.9.2.2)
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// PDFDocEncoding as a byte -> Unicode table (ISO 32000-2, Annex D.2).
|
||
|
|
//
|
||
|
|
// It is Latin-1 apart from three regions: 0x18-0x1F, which hold the accents
|
||
|
|
// Latin-1 puts in the C1 area; 0x80-0x9F, which hold the typographic
|
||
|
|
// characters that Latin-1 has no room for; and 0xA0, which is the euro sign
|
||
|
|
// rather than a no-break space.
|
||
|
|
//
|
||
|
|
// A zero entry means "not defined by PDFDocEncoding" -- 0x00, 0x7F, 0x9F and
|
||
|
|
// 0xAD. Those decode to the byte's own value, which is what every viewer does
|
||
|
|
// and what keeps round-tripping a Latin-1 string through this table lossless.
|
||
|
|
@(private, rodata)
|
||
|
|
PDF_DOC_ENCODING := [256]rune{
|
||
|
|
0x18 = 0x02D8, // breve
|
||
|
|
0x19 = 0x02C7, // caron
|
||
|
|
0x1A = 0x02C6, // modifier letter circumflex accent
|
||
|
|
0x1B = 0x02D9, // dot above
|
||
|
|
0x1C = 0x02DD, // double acute accent
|
||
|
|
0x1D = 0x02DB, // ogonek
|
||
|
|
0x1E = 0x02DA, // ring above
|
||
|
|
0x1F = 0x02DC, // small tilde
|
||
|
|
|
||
|
|
0x80 = 0x2022, // bullet
|
||
|
|
0x81 = 0x2020, // dagger
|
||
|
|
0x82 = 0x2021, // double dagger
|
||
|
|
0x83 = 0x2026, // horizontal ellipsis
|
||
|
|
0x84 = 0x2014, // em dash
|
||
|
|
0x85 = 0x2013, // en dash
|
||
|
|
0x86 = 0x0192, // latin small letter f with hook
|
||
|
|
0x87 = 0x2044, // fraction slash
|
||
|
|
0x88 = 0x2039, // single left-pointing angle quotation mark
|
||
|
|
0x89 = 0x203A, // single right-pointing angle quotation mark
|
||
|
|
0x8A = 0x2212, // minus sign
|
||
|
|
0x8B = 0x2030, // per mille sign
|
||
|
|
0x8C = 0x201E, // double low-9 quotation mark
|
||
|
|
0x8D = 0x201C, // left double quotation mark
|
||
|
|
0x8E = 0x201D, // right double quotation mark
|
||
|
|
0x8F = 0x2018, // left single quotation mark
|
||
|
|
0x90 = 0x2019, // right single quotation mark
|
||
|
|
0x91 = 0x201A, // single low-9 quotation mark
|
||
|
|
0x92 = 0x2122, // trade mark sign
|
||
|
|
0x93 = 0xFB01, // latin small ligature fi
|
||
|
|
0x94 = 0xFB02, // latin small ligature fl
|
||
|
|
0x95 = 0x0141, // latin capital letter l with stroke
|
||
|
|
0x96 = 0x0152, // latin capital ligature oe
|
||
|
|
0x97 = 0x0160, // latin capital letter s with caron
|
||
|
|
0x98 = 0x0178, // latin capital letter y with diaeresis
|
||
|
|
0x99 = 0x017D, // latin capital letter z with caron
|
||
|
|
0x9A = 0x0131, // latin small letter dotless i
|
||
|
|
0x9B = 0x0142, // latin small letter l with stroke
|
||
|
|
0x9C = 0x0153, // latin small ligature oe
|
||
|
|
0x9D = 0x0161, // latin small letter s with caron
|
||
|
|
0x9E = 0x017E, // latin small letter z with caron
|
||
|
|
|
||
|
|
0xA0 = 0x20AC, // euro sign
|
||
|
|
}
|
||
|
|
|
||
|
|
// decode_text_string converts a PDF text string to UTF-8.
|
||
|
|
//
|
||
|
|
// A leading U+FEFF byte order mark (FE FF) means the rest is UTF-16BE; an
|
||
|
|
// EF BB BF mark means it is already UTF-8; anything else is PDFDocEncoding
|
||
|
|
// (ISO 32000-2, 7.9.2.2). An FF FE mark is not in the format, but enough
|
||
|
|
// producers emit little-endian UTF-16 with one that decoding it costs nothing
|
||
|
|
// and rescues those files -- no PDFDocEncoding text begins with "y-diaeresis,
|
||
|
|
// thorn".
|
||
|
|
//
|
||
|
|
// The result is always freshly allocated with `allocator` and owned by the
|
||
|
|
// caller. It is always valid UTF-8: a lone surrogate, a truncated UTF-16 code
|
||
|
|
// unit or a malformed UTF-8 sequence each become U+FFFD.
|
||
|
|
decode_text_string :: proc(raw: []byte, allocator := context.allocator) -> string {
|
||
|
|
// +1 so that the empty string is still a real allocation the caller can
|
||
|
|
// delete unconditionally.
|
||
|
|
b := strings.builder_make(0, len(raw) + 1, allocator)
|
||
|
|
switch {
|
||
|
|
case len(raw) >= 2 && raw[0] == 0xFE && raw[1] == 0xFF:
|
||
|
|
text_write_utf16(&b, raw[2:], true)
|
||
|
|
case len(raw) >= 2 && raw[0] == 0xFF && raw[1] == 0xFE:
|
||
|
|
text_write_utf16(&b, raw[2:], false)
|
||
|
|
case len(raw) >= 3 && raw[0] == 0xEF && raw[1] == 0xBB && raw[2] == 0xBF:
|
||
|
|
text_write_utf8(&b, raw[3:])
|
||
|
|
case:
|
||
|
|
text_write_pdfdoc(&b, raw)
|
||
|
|
}
|
||
|
|
return strings.to_string(b)
|
||
|
|
}
|
||
|
|
|
||
|
|
// decode_text_object is decode_text_string for a string object, and the form
|
||
|
|
// most callers want: /Title, /Author, /Subject and the rest are all strings
|
||
|
|
// that may be indirect.
|
||
|
|
decode_text_object :: proc(doc: ^Document, o: Object, allocator := context.allocator) -> string {
|
||
|
|
raw, _ := to_bytes(resolve(doc, o))
|
||
|
|
return decode_text_string(raw, allocator)
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
text_write_pdfdoc :: proc(b: ^strings.Builder, raw: []byte) {
|
||
|
|
for c in raw {
|
||
|
|
r := PDF_DOC_ENCODING[c]
|
||
|
|
if r == 0 {
|
||
|
|
r = rune(c)
|
||
|
|
}
|
||
|
|
strings.write_rune(b, r)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
text_write_utf8 :: proc(b: ^strings.Builder, raw: []byte) {
|
||
|
|
// Re-encoding rather than copying: the mark only claims the bytes are
|
||
|
|
// UTF-8, and a claim out of a file is not a guarantee.
|
||
|
|
for i := 0; i < len(raw); {
|
||
|
|
r, size := utf8.decode_rune(raw[i:])
|
||
|
|
if size <= 0 {
|
||
|
|
strings.write_rune(b, utf8.RUNE_ERROR)
|
||
|
|
break
|
||
|
|
}
|
||
|
|
strings.write_rune(b, r)
|
||
|
|
i += size
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
text_write_utf16 :: proc(b: ^strings.Builder, raw: []byte, big_endian: bool) {
|
||
|
|
unit :: proc(raw: []byte, i: int, big_endian: bool) -> u16 {
|
||
|
|
hi, lo := raw[i], raw[i + 1]
|
||
|
|
if !big_endian {
|
||
|
|
hi, lo = lo, hi
|
||
|
|
}
|
||
|
|
return u16(hi) << 8 | u16(lo)
|
||
|
|
}
|
||
|
|
|
||
|
|
i := 0
|
||
|
|
for i + 1 < len(raw) {
|
||
|
|
u := unit(raw, i, big_endian)
|
||
|
|
i += 2
|
||
|
|
switch {
|
||
|
|
case u >= 0xD800 && u <= 0xDBFF:
|
||
|
|
// High surrogate: only a following low surrogate completes it.
|
||
|
|
if i + 1 < len(raw) {
|
||
|
|
v := unit(raw, i, big_endian)
|
||
|
|
if v >= 0xDC00 && v <= 0xDFFF {
|
||
|
|
i += 2
|
||
|
|
strings.write_rune(b, rune(0x10000 + (u32(u - 0xD800) << 10 | u32(v - 0xDC00))))
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
strings.write_rune(b, utf8.RUNE_ERROR)
|
||
|
|
case u >= 0xDC00 && u <= 0xDFFF:
|
||
|
|
// Low surrogate with nothing in front of it.
|
||
|
|
strings.write_rune(b, utf8.RUNE_ERROR)
|
||
|
|
case:
|
||
|
|
strings.write_rune(b, rune(u))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// A trailing half code unit: the string was truncated somewhere.
|
||
|
|
if i < len(raw) {
|
||
|
|
strings.write_rune(b, utf8.RUNE_ERROR)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Dates (ISO 32000-2, 7.9.4)
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// Date is a decoded PDF date. Absent fields take their lowest legal value:
|
||
|
|
// month and day 1, everything else 0.
|
||
|
|
//
|
||
|
|
// `tz_hour` and `tz_minute` both carry the sign of the offset, so the offset
|
||
|
|
// from UT in minutes is always `tz_hour * 60 + tz_minute` and never needs a
|
||
|
|
// separate sign flag. They are meaningless unless `has_tz` is set -- a date
|
||
|
|
// with no offset is local time of an unknown locale, which is not the same
|
||
|
|
// thing as UT.
|
||
|
|
Date :: struct {
|
||
|
|
year, month, day: int,
|
||
|
|
hour, minute, second: int,
|
||
|
|
tz_hour, tz_minute: int,
|
||
|
|
has_tz: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
// parse_date decodes "D:YYYYMMDDHHmmSSOHH'mm'".
|
||
|
|
//
|
||
|
|
// Every field after the year is optional and truncated dates are the norm.
|
||
|
|
// So is malformation: the "D:" prefix is missing in plenty of files, the
|
||
|
|
// apostrophes around the offset minutes are variously absent, singular or
|
||
|
|
// doubled, and a colon or a space turns up where an apostrophe belongs. All of
|
||
|
|
// that is accepted. Out-of-range fields are clamped rather than rejected,
|
||
|
|
// because a viewer showing 31 December for a date that says month 13 is more
|
||
|
|
// useful than one showing nothing.
|
||
|
|
//
|
||
|
|
// ok is false only when no four-digit year could be found at all.
|
||
|
|
parse_date :: proc(raw: []byte) -> (date: Date, ok: bool) {
|
||
|
|
// A date is a few dozen bytes. Anything longer is not a date with junk
|
||
|
|
// after it, it is a different kind of object, and scanning all of it for
|
||
|
|
// something year-shaped would only invent a date out of arbitrary digits.
|
||
|
|
scan := raw
|
||
|
|
if len(scan) > 64 {
|
||
|
|
scan = scan[:64]
|
||
|
|
}
|
||
|
|
|
||
|
|
p := 0
|
||
|
|
for p < len(scan) && date_is_space(scan[p]) {
|
||
|
|
p += 1
|
||
|
|
}
|
||
|
|
if p + 1 < len(scan) && (scan[p] == 'D' || scan[p] == 'd') && scan[p + 1] == ':' {
|
||
|
|
p += 2
|
||
|
|
}
|
||
|
|
// Producers put spaces, a second colon, or nothing at all between the
|
||
|
|
// prefix and the year; skip forward to the first run of four digits.
|
||
|
|
start := -1
|
||
|
|
for j := p; j < len(scan); j += 1 {
|
||
|
|
if date_digit_run(scan, j) >= 4 {
|
||
|
|
start = j
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if start < 0 {
|
||
|
|
return {}, false
|
||
|
|
}
|
||
|
|
p = start
|
||
|
|
|
||
|
|
y, _ := date_take(scan, &p, 4)
|
||
|
|
date.year = clamp(y, 0, 9999)
|
||
|
|
date.month, date.day = 1, 1
|
||
|
|
|
||
|
|
if m, m_ok := date_take(scan, &p, 2); m_ok {
|
||
|
|
date.month = clamp(m, 1, 12)
|
||
|
|
if d, d_ok := date_take(scan, &p, 2); d_ok {
|
||
|
|
date.day = clamp(d, 1, date_days_in_month(date.year, date.month))
|
||
|
|
if h, h_ok := date_take(scan, &p, 2); h_ok {
|
||
|
|
// 24 as "midnight ending the day" turns up; clamp it back
|
||
|
|
// rather than rolling the date over.
|
||
|
|
date.hour = clamp(h, 0, 23)
|
||
|
|
if mi, mi_ok := date_take(scan, &p, 2); mi_ok {
|
||
|
|
date.minute = clamp(mi, 0, 59)
|
||
|
|
if s, s_ok := date_take(scan, &p, 2); s_ok {
|
||
|
|
// A leap second (60) is clamped: nothing downstream
|
||
|
|
// has a use for it.
|
||
|
|
date.second = clamp(s, 0, 59)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
date_parse_offset(scan, p, &date)
|
||
|
|
return date, true
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
date_parse_offset :: proc(raw: []byte, from: int, date: ^Date) {
|
||
|
|
p := from
|
||
|
|
for p < len(raw) && date_is_space(raw[p]) {
|
||
|
|
p += 1
|
||
|
|
}
|
||
|
|
if p >= len(raw) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
sign := 0
|
||
|
|
switch raw[p] {
|
||
|
|
case 'Z', 'z':
|
||
|
|
// Z is UT. Some producers write "Z00'00'"; a non-zero offset after a
|
||
|
|
// Z contradicts itself, so the Z wins and the digits are ignored.
|
||
|
|
date.has_tz = true
|
||
|
|
return
|
||
|
|
case '+':
|
||
|
|
sign = 1
|
||
|
|
case '-':
|
||
|
|
sign = -1
|
||
|
|
case:
|
||
|
|
return
|
||
|
|
}
|
||
|
|
p += 1
|
||
|
|
|
||
|
|
h, h_ok := date_take(raw, &p, 2)
|
||
|
|
if !h_ok {
|
||
|
|
// A sign with no hours is not an offset.
|
||
|
|
return
|
||
|
|
}
|
||
|
|
date.has_tz = true
|
||
|
|
date.tz_hour = sign * clamp(h, 0, 23)
|
||
|
|
|
||
|
|
for p < len(raw) && (raw[p] == '\'' || raw[p] == ':' || raw[p] == ' ') {
|
||
|
|
p += 1
|
||
|
|
}
|
||
|
|
if m, m_ok := date_take(raw, &p, 2); m_ok {
|
||
|
|
date.tz_minute = sign * clamp(m, 0, 59)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// date_offset_minutes is the offset from UT in minutes, 0 when the date
|
||
|
|
// carries none.
|
||
|
|
date_offset_minutes :: proc(d: Date) -> int {
|
||
|
|
if !d.has_tz {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
return d.tz_hour * 60 + d.tz_minute
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
date_is_space :: proc "contextless" (c: byte) -> bool {
|
||
|
|
return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == 0
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
date_is_digit :: proc "contextless" (c: byte) -> bool {
|
||
|
|
return c >= '0' && c <= '9'
|
||
|
|
}
|
||
|
|
|
||
|
|
// date_digit_run counts consecutive digits at `i`, stopping at 4: nothing
|
||
|
|
// here needs to tell a four-digit run from a fourteen-digit one.
|
||
|
|
@(private)
|
||
|
|
date_digit_run :: proc "contextless" (raw: []byte, i: int) -> int {
|
||
|
|
n := 0
|
||
|
|
for j := i; j < len(raw) && n < 4; j += 1 {
|
||
|
|
if !date_is_digit(raw[j]) {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
n += 1
|
||
|
|
}
|
||
|
|
return n
|
||
|
|
}
|
||
|
|
|
||
|
|
// date_take reads exactly `n` digits, advancing `p` only if it finds them.
|
||
|
|
// A field with one digit where two belong is treated as absent: guessing
|
||
|
|
// whether "D:2005032" means the 2nd or the 20th is worse than stopping.
|
||
|
|
@(private)
|
||
|
|
date_take :: proc(raw: []byte, p: ^int, n: int) -> (value: int, ok: bool) {
|
||
|
|
if p^ < 0 || p^ + n > len(raw) {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
v := 0
|
||
|
|
for i in 0 ..< n {
|
||
|
|
c := raw[p^ + i]
|
||
|
|
if !date_is_digit(c) {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
v = v * 10 + int(c - '0')
|
||
|
|
}
|
||
|
|
p^ += n
|
||
|
|
return v, true
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
date_days_in_month :: proc "contextless" (year, month: int) -> int {
|
||
|
|
switch month {
|
||
|
|
case 4, 6, 9, 11:
|
||
|
|
return 30
|
||
|
|
case 2:
|
||
|
|
leap := (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
|
||
|
|
return leap ? 29 : 28
|
||
|
|
}
|
||
|
|
return 31
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Name trees (ISO 32000-2, 7.9.6) and number trees (7.9.7)
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// Work budget for one traversal, counted in nodes visited plus entries
|
||
|
|
// linearly scanned. A well-formed tree of any realistic size stays far below
|
||
|
|
// it; a tree built to make a reader spin does not get to.
|
||
|
|
@(private)
|
||
|
|
MAX_TREE_STEPS :: 1 << 16
|
||
|
|
|
||
|
|
// Entries a single collect will return. A file claiming more than this has a
|
||
|
|
// tree that is not going to be useful to anyone.
|
||
|
|
@(private)
|
||
|
|
MAX_TREE_ENTRIES :: 1 << 20
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
Tree_Kind :: enum u8 {
|
||
|
|
Name,
|
||
|
|
Number,
|
||
|
|
}
|
||
|
|
|
||
|
|
// name_tree_lookup finds `key` in a name tree, or null if it is not there.
|
||
|
|
//
|
||
|
|
// `root` is the tree's root node -- the value of /Dests, /EmbeddedFiles and
|
||
|
|
// the rest of the catalog's /Names dictionary. Keys are compared by byte
|
||
|
|
// value, which is the order the format requires the /Names arrays to be in.
|
||
|
|
name_tree_lookup :: proc(doc: ^Document, root: ^Dict, key: []byte) -> Object {
|
||
|
|
path: [MAX_DEPTH]^Dict
|
||
|
|
steps := MAX_TREE_STEPS
|
||
|
|
value, _ := tree_find(doc, root, .Name, key, 0, 0, &path, &steps)
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
|
||
|
|
// number_tree_lookup finds `key` in a number tree (/PageLabels, /ParentTree),
|
||
|
|
// or null.
|
||
|
|
number_tree_lookup :: proc(doc: ^Document, root: ^Dict, key: i64) -> Object {
|
||
|
|
path: [MAX_DEPTH]^Dict
|
||
|
|
steps := MAX_TREE_STEPS
|
||
|
|
value, _ := tree_find(doc, root, .Number, nil, key, 0, &path, &steps)
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
|
||
|
|
// tree_find descends one node.
|
||
|
|
//
|
||
|
|
// `path` holds the ancestors of the current node so that a /Kids entry
|
||
|
|
// pointing back up the tree is detected; with the depth cap that is enough,
|
||
|
|
// because the walk only ever moves downwards. `steps` bounds the total work,
|
||
|
|
// which matters because a node with no /Limits has to be searched
|
||
|
|
// speculatively.
|
||
|
|
@(private)
|
||
|
|
tree_find :: proc(
|
||
|
|
doc: ^Document, node: ^Dict, kind: Tree_Kind, skey: []byte, ikey: i64,
|
||
|
|
depth: int, path: ^[MAX_DEPTH]^Dict, steps: ^int,
|
||
|
|
) -> (value: Object, found: bool) {
|
||
|
|
if node == nil || depth >= MAX_DEPTH || steps^ <= 0 {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
for i in 0 ..< depth {
|
||
|
|
if path[i] == node {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
path[depth] = node
|
||
|
|
steps^ -= 1
|
||
|
|
|
||
|
|
leaf := dict_array(doc, node, kind == .Name ? "Names" : "Nums")
|
||
|
|
if leaf != nil {
|
||
|
|
if v, hit := tree_search_leaf(doc, leaf, kind, skey, ikey, steps); hit {
|
||
|
|
return v, true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
kids := dict_array(doc, node, "Kids")
|
||
|
|
for i in 0 ..< array_len(kids) {
|
||
|
|
if steps^ <= 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
kid, is_dict := to_dict(array_get(doc, kids, i))
|
||
|
|
if !is_dict {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !tree_limits_contain(doc, kid, kind, skey, ikey) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if v, hit := tree_find(doc, kid, kind, skey, ikey, depth + 1, path, steps); hit {
|
||
|
|
return v, true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
|
||
|
|
// tree_limits_contain tests a kid's /Limits. A node with no usable /Limits is
|
||
|
|
// searched rather than skipped: the entry is required, but files that omit it
|
||
|
|
// are readable and the alternative is losing every key below that node.
|
||
|
|
@(private)
|
||
|
|
tree_limits_contain :: proc(doc: ^Document, node: ^Dict, kind: Tree_Kind, skey: []byte, ikey: i64) -> bool {
|
||
|
|
limits := dict_array(doc, node, "Limits")
|
||
|
|
if array_len(limits) < 2 {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
if kind == .Name {
|
||
|
|
if lo, lo_ok := to_bytes(array_get(doc, limits, 0)); lo_ok && bytes.compare(skey, lo) < 0 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if hi, hi_ok := to_bytes(array_get(doc, limits, 1)); hi_ok && bytes.compare(skey, hi) > 0 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
if lo, lo_ok := to_int(array_get(doc, limits, 0)); lo_ok && ikey < lo {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if hi, hi_ok := to_int(array_get(doc, limits, 1)); hi_ok && ikey > hi {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// tree_search_leaf binary-searches a leaf's key/value pairs.
|
||
|
|
//
|
||
|
|
// The format requires the pairs to be sorted, and a binary search over a few
|
||
|
|
// thousand named destinations is the whole point of the structure. But an
|
||
|
|
// unsorted (or ragged, or partly non-string) array does occur, and a miss
|
||
|
|
// there would silently lose a destination that is plainly present, so a miss
|
||
|
|
// falls back to a linear scan paid for out of the step budget.
|
||
|
|
@(private)
|
||
|
|
tree_search_leaf :: proc(
|
||
|
|
doc: ^Document, leaf: ^Array, kind: Tree_Kind, skey: []byte, ikey: i64, steps: ^int,
|
||
|
|
) -> (value: Object, found: bool) {
|
||
|
|
n := array_len(leaf) / 2
|
||
|
|
lo, hi := 0, n
|
||
|
|
for lo < hi {
|
||
|
|
if steps^ <= 0 {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
steps^ -= 1
|
||
|
|
mid := lo + (hi - lo) / 2
|
||
|
|
cmp, ok := tree_compare(kind, array_get(doc, leaf, mid * 2), skey, ikey)
|
||
|
|
if !ok {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
switch {
|
||
|
|
case cmp == 0:
|
||
|
|
return array_get(doc, leaf, mid * 2 + 1), true
|
||
|
|
case cmp < 0:
|
||
|
|
lo = mid + 1
|
||
|
|
case:
|
||
|
|
hi = mid
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for i in 0 ..< n {
|
||
|
|
if steps^ <= 0 {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
steps^ -= 1
|
||
|
|
if cmp, ok := tree_compare(kind, array_get(doc, leaf, i * 2), skey, ikey); ok && cmp == 0 {
|
||
|
|
return array_get(doc, leaf, i * 2 + 1), true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
|
||
|
|
// tree_compare orders a key found in the file against the key being looked
|
||
|
|
// for. ok is false when the entry is not of the tree's key type, which the
|
||
|
|
// caller reads as "this array cannot be binary-searched".
|
||
|
|
@(private)
|
||
|
|
tree_compare :: proc(kind: Tree_Kind, entry: Object, skey: []byte, ikey: i64) -> (cmp: int, ok: bool) {
|
||
|
|
if kind == .Name {
|
||
|
|
b, is_str := to_bytes(entry)
|
||
|
|
if !is_str {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
return bytes.compare(b, skey), true
|
||
|
|
}
|
||
|
|
v, is_int := to_int(entry)
|
||
|
|
if !is_int {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
switch {
|
||
|
|
case v < ikey: return -1, true
|
||
|
|
case v > ikey: return +1, true
|
||
|
|
}
|
||
|
|
return 0, true
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
Tree_Collector :: struct {
|
||
|
|
// All nil on the counting pass. A name tree fills `keys`, a number tree
|
||
|
|
// fills `nums`; only one of the two is ever non-nil.
|
||
|
|
keys: [][]byte,
|
||
|
|
nums: []i64,
|
||
|
|
values: []Object,
|
||
|
|
n: int,
|
||
|
|
steps: int,
|
||
|
|
}
|
||
|
|
|
||
|
|
// name_tree_collect returns every key and value in a name tree, in tree order.
|
||
|
|
//
|
||
|
|
// Both slices are allocated with `allocator` and are the caller's to free.
|
||
|
|
// The keys are *not* copies: each one points at the string bytes inside the
|
||
|
|
// Document and is only valid while that Document is open. Copy them if they
|
||
|
|
// have to outlive it.
|
||
|
|
name_tree_collect :: proc(
|
||
|
|
doc: ^Document, root: ^Dict, allocator := context.allocator,
|
||
|
|
) -> (keys: [][]byte, values: []Object) {
|
||
|
|
path: [MAX_DEPTH]^Dict
|
||
|
|
|
||
|
|
count := Tree_Collector{steps = MAX_TREE_STEPS}
|
||
|
|
tree_walk(doc, root, .Name, &count, 0, &path)
|
||
|
|
n := min(count.n, MAX_TREE_ENTRIES)
|
||
|
|
if n == 0 {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
keys = make([][]byte, n, allocator)
|
||
|
|
values = make([]Object, n, allocator)
|
||
|
|
fill := Tree_Collector{keys = keys, values = values, steps = MAX_TREE_STEPS}
|
||
|
|
tree_walk(doc, root, .Name, &fill, 0, &path)
|
||
|
|
return keys, values
|
||
|
|
}
|
||
|
|
|
||
|
|
// number_tree_collect is name_tree_collect for a number tree. Same ownership
|
||
|
|
// rules; the keys are values, not pointers, so they outlive the Document.
|
||
|
|
number_tree_collect :: proc(
|
||
|
|
doc: ^Document, root: ^Dict, allocator := context.allocator,
|
||
|
|
) -> (keys: []i64, values: []Object) {
|
||
|
|
path: [MAX_DEPTH]^Dict
|
||
|
|
|
||
|
|
count := Tree_Collector{steps = MAX_TREE_STEPS}
|
||
|
|
tree_walk(doc, root, .Number, &count, 0, &path)
|
||
|
|
n := min(count.n, MAX_TREE_ENTRIES)
|
||
|
|
if n == 0 {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// The walker fills a [][]byte; a number tree's keys are gathered here
|
||
|
|
// instead, out of the same values array, so only one walker exists.
|
||
|
|
nkeys := make([]i64, n, allocator)
|
||
|
|
values = make([]Object, n, allocator)
|
||
|
|
fill := Tree_Collector{nums = nkeys, values = values, steps = MAX_TREE_STEPS}
|
||
|
|
tree_walk(doc, root, .Number, &fill, 0, &path)
|
||
|
|
return nkeys, values
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private)
|
||
|
|
tree_walk :: proc(
|
||
|
|
doc: ^Document, node: ^Dict, kind: Tree_Kind, c: ^Tree_Collector, depth: int, path: ^[MAX_DEPTH]^Dict,
|
||
|
|
) {
|
||
|
|
if node == nil || depth >= MAX_DEPTH || c.steps <= 0 {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
for i in 0 ..< depth {
|
||
|
|
if path[i] == node {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
path[depth] = node
|
||
|
|
c.steps -= 1
|
||
|
|
|
||
|
|
leaf := dict_array(doc, node, kind == .Name ? "Names" : "Nums")
|
||
|
|
for i in 0 ..< array_len(leaf) / 2 {
|
||
|
|
if c.steps <= 0 || c.n >= MAX_TREE_ENTRIES {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
c.steps -= 1
|
||
|
|
entry := array_get(doc, leaf, i * 2)
|
||
|
|
if kind == .Name {
|
||
|
|
kb, is_str := to_bytes(entry)
|
||
|
|
if !is_str {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if c.keys != nil && c.n < len(c.keys) {
|
||
|
|
c.keys[c.n] = kb
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
kn, is_int := to_int(entry)
|
||
|
|
if !is_int {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if c.nums != nil && c.n < len(c.nums) {
|
||
|
|
c.nums[c.n] = kn
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if c.values != nil && c.n < len(c.values) {
|
||
|
|
c.values[c.n] = array_get(doc, leaf, i * 2 + 1)
|
||
|
|
}
|
||
|
|
c.n += 1
|
||
|
|
}
|
||
|
|
|
||
|
|
kids := dict_array(doc, node, "Kids")
|
||
|
|
for i in 0 ..< array_len(kids) {
|
||
|
|
if c.steps <= 0 {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
kid, is_dict := to_dict(array_get(doc, kids, i))
|
||
|
|
if !is_dict {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
tree_walk(doc, kid, kind, c, depth + 1, path)
|
||
|
|
}
|
||
|
|
}
|