Files

334 lines
12 KiB
Odin
Raw Permalink Normal View History

2026-09-25 23:32:29 +02:00
package pdf
import "core:mem/virtual"
import "core:testing"
// ---------------------------------------------------------------------------
// Coercions
// ---------------------------------------------------------------------------
@(test)
test_to_int_accepts_reals :: proc(t: ^testing.T) {
// Files write "1.0" where an integer is required often enough that
// refusing it would reject working documents.
v, ok := to_int(f64(1.0))
testing.expect(t, ok, "a real coerces to an integer")
testing.expect_value(t, v, i64(1))
v2, ok2 := to_int(i64(-7))
testing.expect(t, ok2, "")
testing.expect_value(t, v2, i64(-7))
// Truncation, not rounding -- an index of 2.9 is element 2.
v3, _ := to_int(f64(2.9))
testing.expect_value(t, v3, i64(2))
// A real too large for i64 must be refused, not wrapped.
_, ok4 := to_int(f64(1e300))
testing.expect(t, !ok4, "a real beyond i64 range is not an integer")
_, ok5 := to_int(f64(-1e300))
testing.expect(t, !ok5, "")
_, ok6 := to_int(nil)
testing.expect(t, !ok6, "null is not an integer")
_, ok7 := to_int(true)
testing.expect(t, !ok7, "a boolean is not an integer")
}
@(test)
test_to_number_and_real :: proc(t: ^testing.T) {
v, ok := to_number(i64(3))
testing.expect(t, ok, "an integer is a number")
testing.expect_value(t, v, 3.0)
v2, ok2 := to_number(f64(2.5))
testing.expect(t, ok2, "")
testing.expect_value(t, v2, 2.5)
_, ok3 := to_number(nil)
testing.expect(t, !ok3, "")
// to_real is strict: an integer object is not a real object.
_, ok4 := to_real(i64(3))
testing.expect(t, !ok4, "to_real does not coerce")
}
@(test)
test_to_name_bytes_and_ref :: proc(t: ^testing.T) {
n, ok := to_name(Name("Page"))
testing.expect(t, ok, "")
testing.expect_value(t, n, "Page")
b, ok2 := to_bytes(String(transmute([]byte)string("hi")))
testing.expect(t, ok2, "")
testing.expect_value(t, string(b), "hi")
r, ok3 := to_ref(Ref{12, 0})
testing.expect(t, ok3, "")
testing.expect_value(t, r, Ref{12, 0})
testing.expect(t, name_is(Name("Catalog"), "Catalog"), "")
testing.expect(t, !name_is(Name("Catalog"), "Page"), "")
testing.expect(t, !name_is(String(transmute([]byte)string("Catalog")), "Catalog"),
"a string is not a name, even with the same bytes")
testing.expect(t, !name_is(nil, ""), "null is not the empty name")
}
@(test)
test_object_kind :: proc(t: ^testing.T) {
testing.expect_value(t, object_kind(nil), "null")
testing.expect_value(t, object_kind(true), "boolean")
testing.expect_value(t, object_kind(i64(1)), "integer")
testing.expect_value(t, object_kind(f64(1)), "real")
testing.expect_value(t, object_kind(Name("x")), "name")
testing.expect_value(t, object_kind(Ref{1, 0}), "reference")
testing.expect(t, is_null(nil), "")
testing.expect(t, !is_null(i64(0)), "the integer zero is not null")
}
// A stream where a dictionary is wanted hands back the stream's dictionary:
// /Resources, /Group and annotation appearances are all reached through entries
// that are sometimes one and sometimes the other.
@(test)
test_to_dict_accepts_a_stream :: proc(t: ^testing.T) {
arena: virtual.Arena
_ = virtual.arena_init_growing(&arena)
defer virtual.arena_destroy(&arena)
a := virtual.arena_allocator(&arena)
d := new_dict(a)
dict_set(d, "Type", Name("XObject"))
s := new_stream(d, nil, a)
got, ok := to_dict(s)
testing.expect(t, ok, "a stream is usable as a dictionary")
testing.expect_value(t, got, d)
_, ok2 := to_stream(d)
testing.expect(t, !ok2, "but a dictionary is not a stream")
// A stream with no dictionary must not report success with a nil result.
bare := new_stream(nil, nil, a)
_, ok3 := to_dict(bare)
testing.expect(t, !ok3, "a stream without a dictionary is not a dictionary")
}
// ---------------------------------------------------------------------------
// Dictionary access
// ---------------------------------------------------------------------------
@(test)
test_dict_accessors_and_defaults :: proc(t: ^testing.T) {
arena: virtual.Arena
_ = virtual.arena_init_growing(&arena)
defer virtual.arena_destroy(&arena)
a := virtual.arena_allocator(&arena)
d := new_dict(a)
dict_set(d, "Count", i64(5))
dict_set(d, "Size", f64(2.5))
dict_set(d, "Type", Name("Pages"))
dict_set(d, "Flag", true)
dict_set(d, "Ref", Ref{9, 0})
testing.expect_value(t, dict_int(nil, d, "Count"), i64(5))
testing.expect_value(t, dict_num(nil, d, "Size"), 2.5)
testing.expect_value(t, dict_num(nil, d, "Count"), 5.0)
testing.expect_value(t, dict_name(nil, d, "Type"), "Pages")
testing.expect_value(t, dict_type(nil, d), "Pages")
testing.expect(t, dict_bool(nil, d, "Flag"), "")
// A missing key takes the default, and so does a key of the wrong type --
// a viewer substitutes rather than failing.
testing.expect_value(t, dict_int(nil, d, "Absent", 42), i64(42))
testing.expect_value(t, dict_int(nil, d, "Type", 42), i64(42))
testing.expect_value(t, dict_name(nil, d, "Count", "fallback"), "fallback")
testing.expect_value(t, dict_bool(nil, d, "Absent", true), true)
testing.expect(t, dict_has(d, "Count"), "")
testing.expect(t, !dict_has(d, "Nope"), "")
testing.expect(t, !dict_has(nil, "Count"), "a nil dictionary has nothing")
// dict_ref must not resolve: /Parent is followed for identity, and
// resolving it is exactly what makes a cyclic walk.
r, ok := dict_ref(d, "Ref")
testing.expect(t, ok, "")
testing.expect_value(t, r, Ref{9, 0})
_, ok2 := dict_ref(d, "Count")
testing.expect(t, !ok2, "a direct value is not a reference")
// Every accessor must survive a nil dictionary.
testing.expect_value(t, dict_int(nil, nil, "Count", 1), i64(1))
testing.expect(t, dict_get(nil, nil, "Count") == nil, "")
testing.expect(t, dict_dict(nil, nil, "x") == nil, "")
testing.expect(t, dict_array(nil, nil, "x") == nil, "")
testing.expect(t, dict_stream(nil, nil, "x") == nil, "")
dict_set(nil, "x", i64(1)) // must not crash
}
// Inline images abbreviate their keys, and both spellings are legal.
@(test)
test_dict_get_alt :: proc(t: ^testing.T) {
arena: virtual.Arena
_ = virtual.arena_init_growing(&arena)
defer virtual.arena_destroy(&arena)
a := virtual.arena_allocator(&arena)
d := new_dict(a)
dict_set(d, "W", i64(16))
v, ok := to_int(dict_get_alt(nil, d, "Width", "W"))
testing.expect(t, ok, "the abbreviation is found when the full name is absent")
testing.expect_value(t, v, i64(16))
dict_set(d, "Width", i64(32))
v2, _ := to_int(dict_get_alt(nil, d, "Width", "W"))
testing.expect_value(t, v2, i64(32), )
}
// ---------------------------------------------------------------------------
// Arrays
// ---------------------------------------------------------------------------
@(test)
test_array_accessors :: proc(t: ^testing.T) {
arena: virtual.Arena
_ = virtual.arena_init_growing(&arena)
defer virtual.arena_destroy(&arena)
a := virtual.arena_allocator(&arena)
arr := new_array(a)
array_push(arr, i64(10))
array_push(arr, f64(2.5))
array_push(arr, Name("Foo"))
testing.expect_value(t, array_len(arr), 3)
testing.expect_value(t, array_int(nil, arr, 0), i64(10))
testing.expect_value(t, array_num(nil, arr, 1), 2.5)
testing.expect_value(t, array_name(nil, arr, 2), "Foo")
// Out of range in either direction reads as the default, never a panic.
testing.expect_value(t, array_int(nil, arr, 99, -1), i64(-1))
testing.expect_value(t, array_int(nil, arr, -1, -1), i64(-1))
testing.expect(t, array_get_raw(arr, 99) == nil, "")
testing.expect_value(t, array_len(nil), 0)
testing.expect(t, array_get_raw(nil, 0) == nil, "")
array_push(nil, i64(1)) // must not crash
}
// A stray null in a /Widths or /Decode array reads as 0, which is what a
// viewer substitutes -- dropping the entry would shift every later index.
@(test)
test_array_numbers :: proc(t: ^testing.T) {
arena: virtual.Arena
_ = virtual.arena_init_growing(&arena)
defer virtual.arena_destroy(&arena)
a := virtual.arena_allocator(&arena)
arr := new_array(a)
array_push(arr, i64(1))
array_push(arr, nil)
array_push(arr, f64(3.5))
array_push(arr, Name("junk"))
nums := array_numbers(nil, arr, a)
testing.expect_value(t, len(nums), 4)
testing.expect_value(t, nums[0], 1.0)
testing.expect_value(t, nums[1], 0.0)
testing.expect_value(t, nums[2], 3.5)
testing.expect_value(t, nums[3], 0.0)
testing.expect_value(t, len(array_numbers(nil, nil, a)), 0)
}
// ---------------------------------------------------------------------------
// Names and numbers
// ---------------------------------------------------------------------------
@(test)
test_decode_name_escapes :: proc(t: ^testing.T) {
// The common case allocates nothing and returns the input unchanged.
plain := decode_name_escapes("Plain")
testing.expect_value(t, plain, "Plain")
check :: proc(t: ^testing.T, input, want: string) {
got := decode_name_escapes(input)
testing.expect_value(t, got, want)
if raw_data(got) != raw_data(input) {
delete(got)
}
}
check(t, "Adobe#20Green", "Adobe Green")
check(t, "#41#42", "AB")
check(t, "a#2Fb", "a/b")
check(t, "lower#7ecase", "lower~case")
// A '#' that is not followed by two hex digits is a broken name, not a
// reason to drop bytes.
check(t, "bad#zz", "bad#zz")
check(t, "trailing#", "trailing#")
check(t, "short#4", "short#4")
check(t, "", "")
}
@(test)
test_parse_number :: proc(t: ^testing.T) {
expect_int :: proc(t: ^testing.T, s: string, want: i64) {
o, ok := parse_number(s)
testing.expect(t, ok, "should parse")
v, is_int := o.(i64)
testing.expect(t, is_int, "should be an integer object")
testing.expect_value(t, v, want)
}
expect_real :: proc(t: ^testing.T, s: string, want: f64) {
o, ok := parse_number(s)
testing.expect(t, ok, "should parse")
v, is_real := o.(f64)
testing.expect(t, is_real, "should be a real object")
testing.expect_value(t, v, want)
}
expect_int(t, "0", 0)
expect_int(t, "42", 42)
expect_int(t, "-17", -17)
expect_int(t, "+6", 6)
expect_real(t, "3.5", 3.5)
expect_real(t, "-2.25", -2.25)
// ".5" and "4." are both legal PDF and both rejected by strict parsers.
expect_real(t, ".5", 0.5)
expect_real(t, "4.", 4.0)
expect_real(t, "-.5", -0.5)
// Garbage yields 0, the value a viewer substitutes, and reports failure so
// a caller that cares can tell.
_, ok := parse_number("")
testing.expect(t, !ok, "the empty string is not a number")
_, ok2 := parse_number("--5")
testing.expect(t, !ok2, "a doubled sign is not a number")
o, _ := parse_number("junk")
v, _ := to_number(o)
testing.expect_value(t, v, 0.0)
}
@(test)
test_new_object_constructors :: proc(t: ^testing.T) {
arena: virtual.Arena
_ = virtual.arena_init_growing(&arena)
defer virtual.arena_destroy(&arena)
a := virtual.arena_allocator(&arena)
d := new_dict(a)
testing.expect(t, d != nil, "")
testing.expect_value(t, len(d^), 0)
arr := new_array(a)
testing.expect(t, arr != nil, "")
testing.expect_value(t, array_len(arr), 0)
payload := []byte{1, 2, 3}
s := new_stream(d, payload, a)
testing.expect(t, s != nil, "")
testing.expect_value(t, s.state, Stream_Decode_State.Pending)
testing.expect_value(t, len(s.raw), 3)
testing.expect_value(t, s.dict, d)
}