779 lines
27 KiB
Odin
779 lines
27 KiB
Odin
package pdf
|
||||
|
|
|
|||
|
|
import "core:fmt"
|
|||
|
|
import "core:mem"
|
|||
|
|
import "core:mem/virtual"
|
|||
|
|
import "core:testing"
|
|||
|
|
import "core:unicode/utf8"
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Helpers
|
|||
|
|
//
|
|||
|
|
// Test objects are built in a local arena so that a failing assertion cannot
|
|||
|
|
// leak: the arena is destroyed on the way out whatever happened. Lookups are
|
|||
|
|
// driven with a nil Document, which `dict_get` and friends degrade to direct
|
|||
|
|
// access for -- the trees under test hold no indirect references.
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
@(private = "file")
|
|||
|
|
Fixture :: struct {
|
|||
|
|
arena: virtual.Arena,
|
|||
|
|
alloc: mem.Allocator,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(private = "file")
|
|||
|
|
Pair :: struct {
|
|||
|
|
key: string,
|
|||
|
|
value: Object,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(private = "file")
|
|||
|
|
fixture_init :: proc(f: ^Fixture) {
|
|||
|
|
_ = virtual.arena_init_growing(&f.arena)
|
|||
|
|
f.alloc = virtual.arena_allocator(&f.arena)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(private = "file")
|
|||
|
|
fixture_destroy :: proc(f: ^Fixture) {
|
|||
|
|
virtual.arena_destroy(&f.arena)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(private = "file")
|
|||
|
|
str :: proc(s: string) -> Object {
|
|||
|
|
return String(transmute([]byte)s)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(private = "file")
|
|||
|
|
arr :: proc(f: ^Fixture, items: ..Object) -> ^Array {
|
|||
|
|
a := new_array(f.alloc, max(len(items), 1))
|
|||
|
|
for it in items {
|
|||
|
|
array_push(a, it)
|
|||
|
|
}
|
|||
|
|
return a
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(private = "file")
|
|||
|
|
dict :: proc(f: ^Fixture, pairs: ..Pair) -> ^Dict {
|
|||
|
|
d := new_dict(f.alloc, max(len(pairs), 1))
|
|||
|
|
for p in pairs {
|
|||
|
|
dict_set(d, p.key, p.value)
|
|||
|
|
}
|
|||
|
|
return d
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(private = "file")
|
|||
|
|
kv :: proc(key: string, value: Object) -> Pair {
|
|||
|
|
return Pair{key, value}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// decode_text_string
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_text_string_empty :: proc(t: ^testing.T) {
|
|||
|
|
got := decode_text_string({})
|
|||
|
|
defer delete(got)
|
|||
|
|
testing.expect_value(t, got, "")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_text_string_ascii :: proc(t: ^testing.T) {
|
|||
|
|
got := decode_text_string(transmute([]byte)string("Hello, PDF"))
|
|||
|
|
defer delete(got)
|
|||
|
|
testing.expect_value(t, got, "Hello, PDF")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// The whole of PDFDocEncoding that is not Latin-1: ISO 32000-2, Annex D.2.
|
|||
|
|
@(test)
|
|||
|
|
test_pdfdoc_encoding_specials :: proc(t: ^testing.T) {
|
|||
|
|
Case :: struct {
|
|||
|
|
code: byte,
|
|||
|
|
want: rune,
|
|||
|
|
}
|
|||
|
|
cases := []Case{
|
|||
|
|
// 0x18-0x1F: the accents Latin-1 leaves out.
|
|||
|
|
{0x18, '˘'}, {0x19, 'ˇ'}, {0x1A, 'ˆ'}, {0x1B, '˙'},
|
|||
|
|
{0x1C, '˝'}, {0x1D, '˛'}, {0x1E, '˚'}, {0x1F, '˜'},
|
|||
|
|
// 0x80-0x9E: the typography Latin-1 has no room for.
|
|||
|
|
{0x80, '•'}, {0x81, '†'}, {0x82, '‡'}, {0x83, '…'},
|
|||
|
|
{0x84, '—'}, {0x85, '–'}, {0x86, 'ƒ'}, {0x87, '⁄'},
|
|||
|
|
{0x88, '‹'}, {0x89, '›'}, {0x8A, '−'}, {0x8B, '‰'},
|
|||
|
|
{0x8C, '„'}, {0x8D, '“'}, {0x8E, '”'}, {0x8F, '‘'},
|
|||
|
|
{0x90, '’'}, {0x91, '‚'}, {0x92, '™'}, {0x93, 'fi'},
|
|||
|
|
{0x94, 'fl'}, {0x95, 'Ł'}, {0x96, 'Œ'}, {0x97, 'Š'},
|
|||
|
|
{0x98, 'Ÿ'}, {0x99, 'Ž'}, {0x9A, 'ı'}, {0x9B, 'ł'},
|
|||
|
|
{0x9C, 'œ'}, {0x9D, 'š'}, {0x9E, 'ž'},
|
|||
|
|
// 0xA0 is the euro sign, not a no-break space.
|
|||
|
|
{0xA0, '€'},
|
|||
|
|
// The four codes PDFDocEncoding leaves undefined fall back to their
|
|||
|
|
// own value, which keeps a Latin-1 round trip lossless.
|
|||
|
|
{0x00, '\x00'}, {0x7F, '\x7f'}, {0x9F, '\u009f'}, {0xAD, '\u00ad'},
|
|||
|
|
// Everything else is Latin-1.
|
|||
|
|
{0x01, '\x01'}, {0x41, 'A'}, {0x20, ' '}, {0xE9, 'é'}, {0xFF, 'ÿ'},
|
|||
|
|
{0xA9, '©'}, {0xB5, 'µ'},
|
|||
|
|
}
|
|||
|
|
for c in cases {
|
|||
|
|
raw := []byte{c.code}
|
|||
|
|
got := decode_text_string(raw)
|
|||
|
|
defer delete(got)
|
|||
|
|
want := utf8.runes_to_string({c.want})
|
|||
|
|
defer delete(want)
|
|||
|
|
if got != want {
|
|||
|
|
testing.expectf(t, false, "PDFDocEncoding 0x%02X: got %q, want %q", c.code, got, want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_pdfdoc_encoding_round_trip :: proc(t: ^testing.T) {
|
|||
|
|
// Every byte decodes to exactly one rune, and no byte is dropped.
|
|||
|
|
raw := make([]byte, 256)
|
|||
|
|
defer delete(raw)
|
|||
|
|
for i in 0 ..< 256 {
|
|||
|
|
raw[i] = byte(i)
|
|||
|
|
}
|
|||
|
|
got := decode_text_string(raw)
|
|||
|
|
defer delete(got)
|
|||
|
|
testing.expect_value(t, utf8.rune_count_in_string(got), 256)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_text_string_utf16be :: proc(t: ^testing.T) {
|
|||
|
|
Case :: struct {
|
|||
|
|
name: string,
|
|||
|
|
raw: []byte,
|
|||
|
|
want: string,
|
|||
|
|
}
|
|||
|
|
cases := []Case{
|
|||
|
|
{"bom only", {0xFE, 0xFF}, ""},
|
|||
|
|
{"ascii", {0xFE, 0xFF, 0x00, 'H', 0x00, 'i'}, "Hi"},
|
|||
|
|
{"bmp", {0xFE, 0xFF, 0x00, 0xE9}, "é"},
|
|||
|
|
// U+1F600 GRINNING FACE, the canonical surrogate pair.
|
|||
|
|
{"surrogate pair", {0xFE, 0xFF, 0xD8, 0x3D, 0xDE, 0x00}, "\U0001F600"},
|
|||
|
|
{"pair then text", {0xFE, 0xFF, 0xD8, 0x3D, 0xDE, 0x00, 0x00, '!'}, "\U0001F600!"},
|
|||
|
|
// A high surrogate followed by an ordinary character: the character
|
|||
|
|
// survives and the surrogate becomes U+FFFD, never invalid UTF-8.
|
|||
|
|
{"unpaired high", {0xFE, 0xFF, 0xD8, 0x3D, 0x00, 'A'}, "�A"},
|
|||
|
|
{"unpaired high at end", {0xFE, 0xFF, 0xD8, 0x3D}, "�"},
|
|||
|
|
{"unpaired low", {0xFE, 0xFF, 0xDE, 0x00}, "�"},
|
|||
|
|
{"two highs", {0xFE, 0xFF, 0xD8, 0x3D, 0xD8, 0x3D, 0xDE, 0x00}, "�\U0001F600"},
|
|||
|
|
// Truncated in the middle of a code unit.
|
|||
|
|
{"odd length", {0xFE, 0xFF, 0x00, 'A', 0x00}, "A�"},
|
|||
|
|
{"odd length only", {0xFE, 0xFF, 0x41}, "�"},
|
|||
|
|
}
|
|||
|
|
for c in cases {
|
|||
|
|
got := decode_text_string(c.raw)
|
|||
|
|
defer delete(got)
|
|||
|
|
if got != c.want {
|
|||
|
|
testing.expectf(t, false, "utf16be %s: got %q, want %q", c.name, got, c.want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_text_string_utf16le :: proc(t: ^testing.T) {
|
|||
|
|
// Not in the format, but produced in the wild often enough to decode.
|
|||
|
|
got := decode_text_string({0xFF, 0xFE, 'H', 0x00, 'i', 0x00})
|
|||
|
|
defer delete(got)
|
|||
|
|
testing.expect_value(t, got, "Hi")
|
|||
|
|
|
|||
|
|
pair := decode_text_string({0xFF, 0xFE, 0x3D, 0xD8, 0x00, 0xDE})
|
|||
|
|
defer delete(pair)
|
|||
|
|
testing.expect_value(t, pair, "\U0001F600")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_text_string_utf8_bom :: proc(t: ^testing.T) {
|
|||
|
|
got := decode_text_string({0xEF, 0xBB, 0xBF, 0xC3, 0xA9, 'x'})
|
|||
|
|
defer delete(got)
|
|||
|
|
testing.expect_value(t, got, "éx")
|
|||
|
|
|
|||
|
|
// A truncated sequence after a mark that promised UTF-8.
|
|||
|
|
bad := decode_text_string({0xEF, 0xBB, 0xBF, 'a', 0xC3})
|
|||
|
|
defer delete(bad)
|
|||
|
|
testing.expect_value(t, bad, "a�")
|
|||
|
|
|
|||
|
|
// A byte that can never start a UTF-8 sequence.
|
|||
|
|
lone := decode_text_string({0xEF, 0xBB, 0xBF, 0xFF, 'z'})
|
|||
|
|
defer delete(lone)
|
|||
|
|
testing.expect_value(t, lone, "�z")
|
|||
|
|
|
|||
|
|
empty := decode_text_string({0xEF, 0xBB, 0xBF})
|
|||
|
|
defer delete(empty)
|
|||
|
|
testing.expect_value(t, empty, "")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_text_string_short_marks :: proc(t: ^testing.T) {
|
|||
|
|
// One byte of what would have been a mark is PDFDocEncoding, not a
|
|||
|
|
// truncated UTF-16 string.
|
|||
|
|
one := decode_text_string({0xFE})
|
|||
|
|
defer delete(one)
|
|||
|
|
testing.expect_value(t, one, "þ")
|
|||
|
|
|
|||
|
|
two := decode_text_string({0xEF, 0xBB})
|
|||
|
|
defer delete(two)
|
|||
|
|
testing.expect_value(t, two, "ï»")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_decode_text_object :: proc(t: ^testing.T) {
|
|||
|
|
got := decode_text_object(nil, str("Title"))
|
|||
|
|
defer delete(got)
|
|||
|
|
testing.expect_value(t, got, "Title")
|
|||
|
|
|
|||
|
|
// A missing or wrongly typed entry is the empty string, not a crash.
|
|||
|
|
none := decode_text_object(nil, nil)
|
|||
|
|
defer delete(none)
|
|||
|
|
testing.expect_value(t, none, "")
|
|||
|
|
|
|||
|
|
num := decode_text_object(nil, i64(7))
|
|||
|
|
defer delete(num)
|
|||
|
|
testing.expect_value(t, num, "")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// parse_date
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_parse_date_shapes :: proc(t: ^testing.T) {
|
|||
|
|
Case :: struct {
|
|||
|
|
raw: string,
|
|||
|
|
want: Date,
|
|||
|
|
}
|
|||
|
|
cases := []Case{
|
|||
|
|
// The form the spec gives (ISO 32000-2, 7.9.4).
|
|||
|
|
{"D:199812231952-08'00'", {1998, 12, 23, 19, 52, 0, -8, 0, true}},
|
|||
|
|
{"D:20050320174900+01'00'", {2005, 3, 20, 17, 49, 0, 1, 0, true}},
|
|||
|
|
// An offset whose minutes are not zero: the sign is on both fields.
|
|||
|
|
{"D:20050320174900-05'30'", {2005, 3, 20, 17, 49, 0, -5, -30, true}},
|
|||
|
|
// Apostrophes missing entirely -- several writers do this.
|
|||
|
|
{"D:200503201749+0100", {2005, 3, 20, 17, 49, 0, 1, 0, true}},
|
|||
|
|
// A doubled trailing apostrophe, which the 1.7 spec's own example had.
|
|||
|
|
{"D:20050320174900+01'00''", {2005, 3, 20, 17, 49, 0, 1, 0, true}},
|
|||
|
|
// A colon where the apostrophe belongs (XMP habits leaking in).
|
|||
|
|
{"D:20050320174900+01:00", {2005, 3, 20, 17, 49, 0, 1, 0, true}},
|
|||
|
|
// Only the hours of the offset.
|
|||
|
|
{"D:20050320174900+01", {2005, 3, 20, 17, 49, 0, 1, 0, true}},
|
|||
|
|
// Z, with and without the redundant zero offset after it.
|
|||
|
|
{"D:20050320174900Z", {2005, 3, 20, 17, 49, 0, 0, 0, true}},
|
|||
|
|
{"D:20050320174900Z00'00'", {2005, 3, 20, 17, 49, 0, 0, 0, true}},
|
|||
|
|
// No "D:" prefix, and a lower-case one.
|
|||
|
|
{"20050320174900", {2005, 3, 20, 17, 49, 0, 0, 0, false}},
|
|||
|
|
{"d:20050320", {2005, 3, 20, 0, 0, 0, 0, 0, false}},
|
|||
|
|
// Truncated at every field boundary.
|
|||
|
|
{"D:2005", {2005, 1, 1, 0, 0, 0, 0, 0, false}},
|
|||
|
|
{"D:200503", {2005, 3, 1, 0, 0, 0, 0, 0, false}},
|
|||
|
|
{"D:20050320", {2005, 3, 20, 0, 0, 0, 0, 0, false}},
|
|||
|
|
{"D:2005032017", {2005, 3, 20, 17, 0, 0, 0, 0, false}},
|
|||
|
|
{"D:200503201749", {2005, 3, 20, 17, 49, 0, 0, 0, false}},
|
|||
|
|
// An odd digit where a two-digit field belongs is dropped, not guessed.
|
|||
|
|
{"D:2005032", {2005, 3, 1, 0, 0, 0, 0, 0, false}},
|
|||
|
|
// Leading whitespace, and a space before the offset.
|
|||
|
|
{" D:20050320174900 -03'00'", {2005, 3, 20, 17, 49, 0, -3, 0, true}},
|
|||
|
|
// Every field out of range: clamped, not rejected.
|
|||
|
|
{"D:20051345996199+99'99'", {2005, 12, 31, 23, 59, 59, 23, 59, true}},
|
|||
|
|
// Month 0 and day 0 are what "unset" looks like in some writers.
|
|||
|
|
{"D:20050000000000", {2005, 1, 1, 0, 0, 0, 0, 0, false}},
|
|||
|
|
// 29 February against the Gregorian leap rule, century years included.
|
|||
|
|
{"D:20210229", {2021, 2, 28, 0, 0, 0, 0, 0, false}},
|
|||
|
|
{"D:20200229", {2020, 2, 29, 0, 0, 0, 0, 0, false}},
|
|||
|
|
{"D:19000229", {1900, 2, 28, 0, 0, 0, 0, 0, false}},
|
|||
|
|
{"D:20000229", {2000, 2, 29, 0, 0, 0, 0, 0, false}},
|
|||
|
|
// 31 April clamps to the 30th.
|
|||
|
|
{"D:20050431", {2005, 4, 30, 0, 0, 0, 0, 0, false}},
|
|||
|
|
// A sign with no digits after it is not an offset.
|
|||
|
|
{"D:20050320174900+", {2005, 3, 20, 17, 49, 0, 0, 0, false}},
|
|||
|
|
// Trailing junk after a complete date is ignored.
|
|||
|
|
{"D:20050320174900+01'00'garbage", {2005, 3, 20, 17, 49, 0, 1, 0, true}},
|
|||
|
|
// A 24th hour (midnight ending the day) clamps back rather than
|
|||
|
|
// rolling the date over.
|
|||
|
|
{"D:20050320240000", {2005, 3, 20, 23, 0, 0, 0, 0, false}},
|
|||
|
|
// A leap second clamps.
|
|||
|
|
{"D:20051231235960Z", {2005, 12, 31, 23, 59, 59, 0, 0, true}},
|
|||
|
|
// A lower-case z is accepted the way a lower-case d: is.
|
|||
|
|
{"D:20051231235900z", {2005, 12, 31, 23, 59, 0, 0, 0, true}},
|
|||
|
|
}
|
|||
|
|
for c in cases {
|
|||
|
|
got, ok := parse_date(transmute([]byte)c.raw)
|
|||
|
|
if !ok {
|
|||
|
|
testing.expectf(t, false, "parse_date(%q): not ok", c.raw)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if got != c.want {
|
|||
|
|
testing.expectf(t, false, "parse_date(%q):\n got %v\nwant %v", c.raw, got, c.want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_parse_date_rejects :: proc(t: ^testing.T) {
|
|||
|
|
bad := []string{
|
|||
|
|
"",
|
|||
|
|
"D:",
|
|||
|
|
"D",
|
|||
|
|
"not a date",
|
|||
|
|
"D:203", // three digits is not a year
|
|||
|
|
"\x00\x00\x00",
|
|||
|
|
"D: ",
|
|||
|
|
"199",
|
|||
|
|
// Junk long enough that a year-shaped run late in it is out of the
|
|||
|
|
// window: a date is a few dozen bytes, not a haystack.
|
|||
|
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1999",
|
|||
|
|
}
|
|||
|
|
for s in bad {
|
|||
|
|
if _, ok := parse_date(transmute([]byte)s); ok {
|
|||
|
|
testing.expectf(t, false, "parse_date(%q): expected failure", s)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_date_offset_minutes :: proc(t: ^testing.T) {
|
|||
|
|
d, ok := parse_date(transmute([]byte)string("D:20050320174900-05'30'"))
|
|||
|
|
testing.expect(t, ok)
|
|||
|
|
testing.expect_value(t, date_offset_minutes(d), -330)
|
|||
|
|
|
|||
|
|
d2, ok2 := parse_date(transmute([]byte)string("D:20050320174900+05'45'"))
|
|||
|
|
testing.expect(t, ok2)
|
|||
|
|
testing.expect_value(t, date_offset_minutes(d2), 345)
|
|||
|
|
|
|||
|
|
d3, ok3 := parse_date(transmute([]byte)string("D:20050320"))
|
|||
|
|
testing.expect(t, ok3)
|
|||
|
|
testing.expect_value(t, date_offset_minutes(d3), 0)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Name and number trees
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
// name_leaf builds a leaf holding the given keys, each mapped to the name
|
|||
|
|
// "v<key>", with the /Limits the format requires of a non-root node.
|
|||
|
|
@(private = "file")
|
|||
|
|
name_leaf :: proc(f: ^Fixture, keys: ..string) -> ^Dict {
|
|||
|
|
names := new_array(f.alloc, 2 * len(keys))
|
|||
|
|
for k in keys {
|
|||
|
|
array_push(names, str(k))
|
|||
|
|
array_push(names, Name(fmt.aprintf("v%s", k, allocator = f.alloc)))
|
|||
|
|
}
|
|||
|
|
return dict(f,
|
|||
|
|
kv("Names", names),
|
|||
|
|
kv("Limits", arr(f, str(keys[0]), str(keys[len(keys) - 1]))),
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(private = "file")
|
|||
|
|
inner :: proc(f: ^Fixture, lo, hi: string, kids: ..^Dict) -> ^Dict {
|
|||
|
|
ka := new_array(f.alloc, len(kids))
|
|||
|
|
for k in kids {
|
|||
|
|
array_push(ka, k)
|
|||
|
|
}
|
|||
|
|
return dict(f, kv("Kids", ka), kv("Limits", arr(f, str(lo), str(hi))))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// A three-level tree: root -> 2 branches -> 2 leaves each, 12 keys in all.
|
|||
|
|
@(private = "file")
|
|||
|
|
build_name_tree :: proc(f: ^Fixture) -> ^Dict {
|
|||
|
|
l1 := name_leaf(f, "alpha", "bravo", "charlie")
|
|||
|
|
l2 := name_leaf(f, "delta", "echo", "foxtrot")
|
|||
|
|
l3 := name_leaf(f, "golf", "hotel", "india")
|
|||
|
|
l4 := name_leaf(f, "juliet", "kilo", "lima")
|
|||
|
|
b1 := inner(f, "alpha", "foxtrot", l1, l2)
|
|||
|
|
b2 := inner(f, "golf", "lima", l3, l4)
|
|||
|
|
return dict(f, kv("Kids", arr(f, b1, b2)))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(private = "file")
|
|||
|
|
TREE_KEYS := []string{
|
|||
|
|
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot",
|
|||
|
|
"golf", "hotel", "india", "juliet", "kilo", "lima",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_name_tree_lookup :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
root := build_name_tree(&f)
|
|||
|
|
for k in TREE_KEYS {
|
|||
|
|
got := name_tree_lookup(nil, root, transmute([]byte)k)
|
|||
|
|
want := fmt.aprintf("v%s", k, allocator = f.alloc)
|
|||
|
|
if !name_is(got, want) {
|
|||
|
|
testing.expectf(t, false, "name_tree_lookup(%q): got %v, want /%s", k, got, want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_name_tree_misses :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
root := build_name_tree(&f)
|
|||
|
|
misses := []string{
|
|||
|
|
"", // before everything
|
|||
|
|
"aardvark", // below the first limit
|
|||
|
|
"bravado", // inside a leaf's range, not present
|
|||
|
|
"foxtrot2", // between two branches
|
|||
|
|
"zulu", // above the last limit
|
|||
|
|
"ALPHA", // byte order is case sensitive
|
|||
|
|
"alph", // a prefix of a key is not the key
|
|||
|
|
"alphabet", // a key with more after it is not the key
|
|||
|
|
}
|
|||
|
|
for k in misses {
|
|||
|
|
got := name_tree_lookup(nil, root, transmute([]byte)k)
|
|||
|
|
if got != nil {
|
|||
|
|
testing.expectf(t, false, "name_tree_lookup(%q): got %v, want null", k, got)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_name_tree_edge_nodes :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
// A nil root, an empty dictionary, and a root that is itself a leaf.
|
|||
|
|
testing.expect(t, name_tree_lookup(nil, nil, transmute([]byte)string("x")) == nil)
|
|||
|
|
|
|||
|
|
empty := dict(&f)
|
|||
|
|
testing.expect(t, name_tree_lookup(nil, empty, transmute([]byte)string("x")) == nil)
|
|||
|
|
|
|||
|
|
flat := dict(&f, kv("Names", arr(&f, str("only"), i64(42))))
|
|||
|
|
n, ok := to_int(name_tree_lookup(nil, flat, transmute([]byte)string("only")))
|
|||
|
|
testing.expect(t, ok)
|
|||
|
|
testing.expect_value(t, n, 42)
|
|||
|
|
|
|||
|
|
// An odd-length /Names array: the dangling key has no value, and reading
|
|||
|
|
// one must not run off the end of the array.
|
|||
|
|
ragged := dict(&f, kv("Names", arr(&f, str("a"), i64(1), str("b"))))
|
|||
|
|
testing.expect(t, name_tree_lookup(nil, ragged, transmute([]byte)string("b")) == nil)
|
|||
|
|
got, _ := to_int(name_tree_lookup(nil, ragged, transmute([]byte)string("a")))
|
|||
|
|
testing.expect_value(t, got, 1)
|
|||
|
|
|
|||
|
|
// /Names of the wrong type altogether.
|
|||
|
|
wrong := dict(&f, kv("Names", i64(3)), kv("Kids", str("nope")))
|
|||
|
|
testing.expect(t, name_tree_lookup(nil, wrong, transmute([]byte)string("a")) == nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_name_tree_unsorted_leaf :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
// A /Names array in the wrong order, and one whose keys are not all
|
|||
|
|
// strings. A binary search misses both; the linear fallback finds them.
|
|||
|
|
unsorted := dict(&f, kv("Names", arr(&f,
|
|||
|
|
str("zulu"), i64(1), str("alpha"), i64(2), str("mike"), i64(3))))
|
|||
|
|
Want :: struct {
|
|||
|
|
key: string,
|
|||
|
|
value: i64,
|
|||
|
|
}
|
|||
|
|
wants := []Want{{"zulu", 1}, {"alpha", 2}, {"mike", 3}}
|
|||
|
|
for w in wants {
|
|||
|
|
v, ok := to_int(name_tree_lookup(nil, unsorted, transmute([]byte)w.key))
|
|||
|
|
testing.expect(t, ok)
|
|||
|
|
testing.expect_value(t, v, w.value)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
typed := dict(&f, kv("Names", arr(&f,
|
|||
|
|
Name("notastring"), i64(1), str("real"), i64(2))))
|
|||
|
|
v, ok := to_int(name_tree_lookup(nil, typed, transmute([]byte)string("real")))
|
|||
|
|
testing.expect(t, ok)
|
|||
|
|
testing.expect_value(t, v, 2)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_name_tree_cycle_terminates :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
// A node whose only kid is itself.
|
|||
|
|
self := dict(&f)
|
|||
|
|
dict_set(self, "Kids", arr(&f, self))
|
|||
|
|
testing.expect(t, name_tree_lookup(nil, self, transmute([]byte)string("x")) == nil)
|
|||
|
|
|
|||
|
|
// A two-node cycle with a real leaf hanging off it: the lookup must still
|
|||
|
|
// find the key, still answer a miss, and still terminate.
|
|||
|
|
a := dict(&f)
|
|||
|
|
b := dict(&f)
|
|||
|
|
leaf := dict(&f, kv("Names", arr(&f, str("found"), i64(9))))
|
|||
|
|
dict_set(a, "Kids", arr(&f, b, leaf))
|
|||
|
|
dict_set(b, "Kids", arr(&f, a))
|
|||
|
|
v, ok := to_int(name_tree_lookup(nil, a, transmute([]byte)string("found")))
|
|||
|
|
testing.expect(t, ok)
|
|||
|
|
testing.expect_value(t, v, 9)
|
|||
|
|
testing.expect(t, name_tree_lookup(nil, a, transmute([]byte)string("missing")) == nil)
|
|||
|
|
|
|||
|
|
keys, values := name_tree_collect(nil, a)
|
|||
|
|
defer delete(keys)
|
|||
|
|
defer delete(values)
|
|||
|
|
testing.expect_value(t, len(keys), 1)
|
|||
|
|
testing.expect_value(t, len(values), 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_name_tree_deep_chain :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
// A chain far deeper than MAX_DEPTH with the payload at the bottom. The
|
|||
|
|
// walk must stop rather than recurse, and must not crash.
|
|||
|
|
deep := dict(&f, kv("Names", arr(&f, str("bottom"), i64(1))))
|
|||
|
|
for _ in 0 ..< 200 {
|
|||
|
|
deep = dict(&f, kv("Kids", arr(&f, deep)))
|
|||
|
|
}
|
|||
|
|
testing.expect(t, name_tree_lookup(nil, deep, transmute([]byte)string("bottom")) == nil)
|
|||
|
|
|
|||
|
|
keys, values := name_tree_collect(nil, deep)
|
|||
|
|
defer delete(keys)
|
|||
|
|
defer delete(values)
|
|||
|
|
testing.expect_value(t, len(keys), 0)
|
|||
|
|
testing.expect_value(t, len(values), 0)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_name_tree_collect :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
root := build_name_tree(&f)
|
|||
|
|
keys, values := name_tree_collect(nil, root)
|
|||
|
|
defer delete(keys)
|
|||
|
|
defer delete(values)
|
|||
|
|
|
|||
|
|
testing.expect_value(t, len(keys), len(TREE_KEYS))
|
|||
|
|
testing.expect_value(t, len(values), len(TREE_KEYS))
|
|||
|
|
for w, i in TREE_KEYS {
|
|||
|
|
if i >= len(keys) {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
if string(keys[i]) != w {
|
|||
|
|
testing.expectf(t, false, "collect[%d]: got %q, want %q", i, string(keys[i]), w)
|
|||
|
|
}
|
|||
|
|
if !name_is(values[i], fmt.aprintf("v%s", w, allocator = f.alloc)) {
|
|||
|
|
testing.expectf(t, false, "collect[%d]: value %v", i, values[i])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_name_tree_collect_empty :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
keys, values := name_tree_collect(nil, nil)
|
|||
|
|
testing.expect_value(t, len(keys), 0)
|
|||
|
|
testing.expect_value(t, len(values), 0)
|
|||
|
|
|
|||
|
|
keys2, values2 := name_tree_collect(nil, dict(&f))
|
|||
|
|
testing.expect_value(t, len(keys2), 0)
|
|||
|
|
testing.expect_value(t, len(values2), 0)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// The keys collect returns point into the objects, not at copies.
|
|||
|
|
@(test)
|
|||
|
|
test_name_tree_collect_borrows_keys :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
src := "borrowed"
|
|||
|
|
root := dict(&f, kv("Names", arr(&f, String(transmute([]byte)src), i64(1))))
|
|||
|
|
keys, values := name_tree_collect(nil, root)
|
|||
|
|
defer delete(keys)
|
|||
|
|
defer delete(values)
|
|||
|
|
|
|||
|
|
testing.expect_value(t, len(keys), 1)
|
|||
|
|
testing.expect_value(t, len(values), 1)
|
|||
|
|
testing.expect(t, raw_data(keys[0]) == raw_data(transmute([]byte)src))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_number_tree_lookup :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
// /PageLabels-shaped: three leaves, two of them under a branch.
|
|||
|
|
l1 := dict(&f,
|
|||
|
|
kv("Nums", arr(&f, i64(0), Name("a"), i64(4), Name("b"))),
|
|||
|
|
kv("Limits", arr(&f, i64(0), i64(4))))
|
|||
|
|
l2 := dict(&f,
|
|||
|
|
kv("Nums", arr(&f, i64(9), Name("c"), i64(16), Name("d"))),
|
|||
|
|
kv("Limits", arr(&f, i64(9), i64(16))))
|
|||
|
|
l3 := dict(&f,
|
|||
|
|
kv("Nums", arr(&f, i64(100), Name("e"))),
|
|||
|
|
kv("Limits", arr(&f, i64(100), i64(100))))
|
|||
|
|
b1 := dict(&f, kv("Kids", arr(&f, l1, l2)), kv("Limits", arr(&f, i64(0), i64(16))))
|
|||
|
|
root := dict(&f, kv("Kids", arr(&f, b1, l3)))
|
|||
|
|
|
|||
|
|
Case :: struct {
|
|||
|
|
key: i64,
|
|||
|
|
want: string, // "" for a miss
|
|||
|
|
}
|
|||
|
|
cases := []Case{
|
|||
|
|
{0, "a"}, {4, "b"}, {9, "c"}, {16, "d"}, {100, "e"},
|
|||
|
|
{-1, ""}, {1, ""}, {5, ""}, {17, ""}, {99, ""}, {101, ""},
|
|||
|
|
{max(i64), ""}, {min(i64), ""},
|
|||
|
|
}
|
|||
|
|
for c in cases {
|
|||
|
|
got := number_tree_lookup(nil, root, c.key)
|
|||
|
|
if c.want == "" {
|
|||
|
|
if got != nil {
|
|||
|
|
testing.expectf(t, false, "number_tree_lookup(%d): got %v, want null", c.key, got)
|
|||
|
|
}
|
|||
|
|
} else if !name_is(got, c.want) {
|
|||
|
|
testing.expectf(t, false, "number_tree_lookup(%d): got %v, want /%s", c.key, got, c.want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_number_tree_cycle_and_collect :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
a := dict(&f)
|
|||
|
|
leaf := dict(&f, kv("Nums", arr(&f, i64(3), Name("x"), i64(7), Name("y"))))
|
|||
|
|
dict_set(a, "Kids", arr(&f, a, leaf))
|
|||
|
|
testing.expect(t, name_is(number_tree_lookup(nil, a, 7), "y"))
|
|||
|
|
testing.expect(t, number_tree_lookup(nil, a, 8) == nil)
|
|||
|
|
|
|||
|
|
keys, values := number_tree_collect(nil, a)
|
|||
|
|
defer delete(keys)
|
|||
|
|
defer delete(values)
|
|||
|
|
testing.expect_value(t, len(keys), 2)
|
|||
|
|
testing.expect_value(t, keys[0], i64(3))
|
|||
|
|
testing.expect_value(t, keys[1], i64(7))
|
|||
|
|
testing.expect(t, name_is(values[1], "y"))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// A /Limits entry of the wrong type, or with too few elements, is unusable;
|
|||
|
|
// the node below it is searched anyway rather than silently losing its keys.
|
|||
|
|
@(test)
|
|||
|
|
test_tree_bad_limits :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
l1 := dict(&f,
|
|||
|
|
kv("Names", arr(&f, str("key"), i64(1))),
|
|||
|
|
kv("Limits", arr(&f, i64(0), i64(9))))
|
|||
|
|
l2 := dict(&f,
|
|||
|
|
kv("Names", arr(&f, str("other"), i64(2))),
|
|||
|
|
kv("Limits", arr(&f, str("other"))))
|
|||
|
|
root := dict(&f, kv("Kids", arr(&f, l1, l2)))
|
|||
|
|
|
|||
|
|
v1, ok1 := to_int(name_tree_lookup(nil, root, transmute([]byte)string("key")))
|
|||
|
|
testing.expect(t, ok1)
|
|||
|
|
testing.expect_value(t, v1, 1)
|
|||
|
|
v2, ok2 := to_int(name_tree_lookup(nil, root, transmute([]byte)string("other")))
|
|||
|
|
testing.expect(t, ok2)
|
|||
|
|
testing.expect_value(t, v2, 2)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// A /Kids array holding things that are not dictionaries.
|
|||
|
|
@(test)
|
|||
|
|
test_tree_junk_kids :: proc(t: ^testing.T) {
|
|||
|
|
f: Fixture
|
|||
|
|
fixture_init(&f)
|
|||
|
|
defer fixture_destroy(&f)
|
|||
|
|
|
|||
|
|
leaf := dict(&f, kv("Names", arr(&f, str("k"), i64(5))))
|
|||
|
|
root := dict(&f, kv("Kids", arr(&f, i64(1), Name("no"), nil, str("no"), leaf)))
|
|||
|
|
v, ok := to_int(name_tree_lookup(nil, root, transmute([]byte)string("k")))
|
|||
|
|
testing.expect(t, ok)
|
|||
|
|
testing.expect_value(t, v, 5)
|
|||
|
|
|
|||
|
|
keys, values := name_tree_collect(nil, root)
|
|||
|
|
defer delete(keys)
|
|||
|
|
defer delete(values)
|
|||
|
|
testing.expect_value(t, len(keys), 1)
|
|||
|
|
testing.expect_value(t, len(values), 1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
// Fuzz-shaped
|
|||
|
|
//
|
|||
|
|
// The bytes reaching these two procedures came out of a file, so the property
|
|||
|
|
// that matters is not "correct" but "never wrong in a way that hurts": the
|
|||
|
|
// text always UTF-8, the date always in range, neither ever hanging.
|
|||
|
|
// ---------------------------------------------------------------------------
|
|||
|
|
|
|||
|
|
@(private = "file")
|
|||
|
|
fuzz_next :: proc(state: ^u64) -> u64 {
|
|||
|
|
x := state^
|
|||
|
|
x ~= x << 13
|
|||
|
|
x ~= x >> 7
|
|||
|
|
x ~= x << 17
|
|||
|
|
state^ = x
|
|||
|
|
return x
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_text_string_output_is_always_utf8 :: proc(t: ^testing.T) {
|
|||
|
|
state: u64 = 0x2545F4914F6CDD1D
|
|||
|
|
buf: [40]byte
|
|||
|
|
marks := [][]byte{{}, {0xFE, 0xFF}, {0xFF, 0xFE}, {0xEF, 0xBB, 0xBF}}
|
|||
|
|
for _ in 0 ..< 6000 {
|
|||
|
|
n := int(fuzz_next(&state) % u64(len(buf) + 1))
|
|||
|
|
for i in 0 ..< n {
|
|||
|
|
buf[i] = byte(fuzz_next(&state))
|
|||
|
|
}
|
|||
|
|
// Steer a quarter of the cases onto each decoder rather than leaving
|
|||
|
|
// it to chance, which would almost always pick PDFDocEncoding.
|
|||
|
|
mark := marks[fuzz_next(&state) % u64(len(marks))]
|
|||
|
|
if n >= len(mark) {
|
|||
|
|
copy(buf[:], mark)
|
|||
|
|
}
|
|||
|
|
got := decode_text_string(buf[:n])
|
|||
|
|
defer delete(got)
|
|||
|
|
if !utf8.valid_string(got) {
|
|||
|
|
testing.expectf(t, false, "invalid UTF-8 out of %x: %x", buf[:n], transmute([]byte)got)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@(test)
|
|||
|
|
test_parse_date_output_is_always_in_range :: proc(t: ^testing.T) {
|
|||
|
|
state: u64 = 0x9E3779B97F4A7C15
|
|||
|
|
buf: [24]byte
|
|||
|
|
alphabet := "0123456789D:+-Z' \x00zx"
|
|||
|
|
for _ in 0 ..< 6000 {
|
|||
|
|
n := int(fuzz_next(&state) % u64(len(buf) + 1))
|
|||
|
|
for i in 0 ..< n {
|
|||
|
|
buf[i] = alphabet[fuzz_next(&state) % u64(len(alphabet))]
|
|||
|
|
}
|
|||
|
|
d, ok := parse_date(buf[:n])
|
|||
|
|
if !ok {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
in_range :=
|
|||
|
|
d.year >= 0 && d.year <= 9999 &&
|
|||
|
|
d.month >= 1 && d.month <= 12 &&
|
|||
|
|
d.day >= 1 && d.day <= date_days_in_month(d.year, d.month) &&
|
|||
|
|
d.hour >= 0 && d.hour <= 23 &&
|
|||
|
|
d.minute >= 0 && d.minute <= 59 &&
|
|||
|
|
d.second >= 0 && d.second <= 59 &&
|
|||
|
|
d.tz_hour >= -23 && d.tz_hour <= 23 &&
|
|||
|
|
d.tz_minute >= -59 && d.tz_minute <= 59
|
|||
|
|
if !in_range {
|
|||
|
|
testing.expectf(t, false, "out of range date %v from %q", d, string(buf[:n]))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|