Files
pdf-odin-llm/filter_runlength_test.odin
2026-09-25 23:32:29 +02:00

201 lines
6.7 KiB
Odin

package pdf
import "core:mem/virtual"
import "core:slice"
import "core:testing"
@(private = "file")
Rl_Case :: struct {
name: string,
in_: []byte,
want: []byte,
}
@(private = "file")
check_rl :: proc(t: ^testing.T, c: Rl_Case) {
out, err := run_length_decode(c.in_)
defer delete(out)
testing.expectf(t, err == .None, "%s: err = %v", c.name, err)
testing.expectf(t, slice.equal(out, c.want), "%s: got %v, want %v", c.name, out, c.want)
}
@(test)
test_run_length_literals :: proc(t: ^testing.T) {
cases := []Rl_Case{
{"one byte", {0, 'A', 128}, {'A'}},
{"three bytes", {2, 'a', 'b', 'c', 128}, {'a', 'b', 'c'}},
{"two runs", {1, 'a', 'b', 0, 'c', 128}, {'a', 'b', 'c'}},
{"nul literal", {1, 0x00, 0x80, 128}, {0x00, 0x80}},
// 128 only means EOD in a length position; as data it is just a byte.
{"128 as data", {0, 128, 128}, {128}},
}
for c in cases {
check_rl(t, c)
}
}
@(test)
test_run_length_repeats :: proc(t: ^testing.T) {
cases := []Rl_Case{
// 257-255 = 2 is the shortest repeat the encoding can express.
{"two", {255, 'x', 128}, {'x', 'x'}},
{"three", {254, 'x', 128}, {'x', 'x', 'x'}},
{"four", {253, 0xFF, 128}, {0xFF, 0xFF, 0xFF, 0xFF}},
{"mixed", {1, 'a', 'b', 254, 'c', 0, 'd', 128},
{'a', 'b', 'c', 'c', 'c', 'd'}},
}
for c in cases {
check_rl(t, c)
}
}
@(test)
test_run_length_boundaries :: proc(t: ^testing.T) {
// The longest literal run (127 -> 128 bytes) and the longest repeat
// (129 -> 128 copies), the two lengths an off-by-one lands on.
enc := make([dynamic]byte)
defer delete(enc)
want := make([dynamic]byte)
defer delete(want)
append(&enc, 127)
for i in 0 ..< 128 {
append(&enc, byte(i))
append(&want, byte(i))
}
append(&enc, 129, 'Z')
for _ in 0 ..< 128 {
append(&want, 'Z')
}
append(&enc, 128)
check_rl(t, {"max runs", enc[:], want[:]})
}
@(test)
test_run_length_degenerate :: proc(t: ^testing.T) {
cases := []Rl_Case{
{"empty", {}, {}},
{"eod immediately", {128}, {}},
{"eod then junk", {128, 5, 'a', 'b'}, {}},
// No EOD marker: the data simply ends.
{"missing eod", {1, 'a', 'b'}, {'a', 'b'}},
{"repeat, no eod", {254, 'q'}, {'q', 'q', 'q'}},
}
for c in cases {
check_rl(t, c)
}
}
@(test)
test_run_length_truncated :: proc(t: ^testing.T) {
cases := []Rl_Case{
// A literal run promising more bytes than remain keeps what is there.
{"short literal", {5, 'a', 'b'}, {'a', 'b'}},
{"literal, none", {5}, {}},
// A repeat with no byte to repeat produces nothing and ends.
{"repeat, no byte", {250}, {}},
{"tail after good", {1, 'a', 'b', 9, 'c'}, {'a', 'b', 'c'}},
}
for c in cases {
check_rl(t, c)
}
}
@(test)
test_run_length_truncated_everywhere :: proc(t: ^testing.T) {
// Every prefix of a well-formed stream must decode a prefix of the
// plaintext without crashing.
enc := []byte{2, 'a', 'b', 'c', 254, 'z', 0, 'Q', 129, '!', 128}
full := "abczzzQ"
for cut in 0 ..= len(enc) {
out, err := run_length_decode(enc[:cut])
testing.expectf(t, err == .None, "cut %d: err = %v", cut, err)
n := min(len(out), len(full))
testing.expectf(t, slice.equal(out[:n], transmute([]byte)full[:n]),
"cut %d: got %v", cut, out)
// Nothing past the '!' run can appear, so the length is bounded.
testing.expectf(t, len(out) <= len(full) + 128, "cut %d: %d bytes", cut, len(out))
delete(out)
}
}
@(test)
test_run_length_all_lengths :: proc(t: ^testing.T) {
// Sweep every length byte as the first byte of a stream, with one byte of
// payload behind it, and assert the decoded length is exactly what the
// encoding promises. This is the table the filter is.
for length in 0 ..= 255 {
buf := [3]byte{byte(length), 'p', 128}
out, err := run_length_decode(buf[:])
want := 0
switch {
case length == 128:
want = 0
case length < 128:
// "length+1 literal bytes", of which only 2 are present here
// ('p' and the 128 that would otherwise have been EOD).
want = min(length + 1, 2)
case:
want = 257 - length
}
testing.expectf(t, err == .None, "length %d: err = %v", length, err)
testing.expectf(t, len(out) == want, "length %d: got %d bytes, want %d",
length, len(out), want)
delete(out)
}
}
@(test)
test_run_length_cross_check :: proc(t: ^testing.T) {
// Vectors from an independent RunLength encoder over random bytes drawn
// from a small alphabet, so literal and repeat runs interleave.
cases := []struct{name, in_, want: string}{
{"n=1", "\x00\xFF\x80", "\xFF"},
{"n=3", "\xFF\x00\x00a\x80", "\x00\x00a"},
{"n=9", "\x05\x00\xFFb\x00\xFF\x00\xFFc\x00a\x80", "\x00\xFFb\x00\xFF\x00cca"},
{"n=40",
"\x00b\xFF\x00\x02a\xFFa\xFF\x00\x09cbcab\xFFcbc\x00\xFFa\x07\x00bacab\xFFa\xFFc\x09\x00a\xFFca\x00ba\x00b\x80",
"b\x00\x00a\xFFa\x00\x00cbcab\xFFcbc\x00aa\x00bacab\xFFacc\x00a\xFFca\x00ba\x00b"},
}
for c in cases {
check_rl(t, {c.name, transmute([]byte)c.in_, transmute([]byte)c.want})
}
}
@(private = "file")
rl_fuzz_next :: proc(state: ^u64) -> u32 {
state^ = state^ * 6364136223846793005 + 1442695040888963407
return u32(state^ >> 33)
}
@(test)
test_run_length_fuzz :: proc(t: ^testing.T) {
// Random bytes decode to at most 128 bytes per input byte and must never
// crash or hang, whatever the length bytes happen to say.
state: u64 = 0xBADF00D
buf: [256]byte
for round in 0 ..< 400 {
n := int(rl_fuzz_next(&state) % u32(len(buf) + 1))
for i in 0 ..< n {
buf[i] = byte(rl_fuzz_next(&state))
}
out, err := run_length_decode(buf[:n])
testing.expectf(t, err == .None, "round %d: err %v", round, err)
testing.expectf(t, len(out) <= 128 * n, "round %d: grew to %d from %d",
round, len(out), n)
delete(out)
}
}
@(test)
test_run_length_honours_allocator :: proc(t: ^testing.T) {
arena: virtual.Arena
testing.expect(t, virtual.arena_init_growing(&arena) == nil)
defer virtual.arena_destroy(&arena)
out, err := run_length_decode({2, 'a', 'b', 'c', 128}, virtual.arena_allocator(&arena))
testing.expect(t, err == .None)
testing.expect(t, slice.equal(out, transmute([]byte)string("abc")))
}