201 lines
6.7 KiB
Odin
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")))
|
||
|
|
}
|