package pdf import "core:mem/virtual" import "core:slice" import "core:testing" @(private = "file") Ascii_Case :: struct { name: string, in_: string, want: string, } @(private = "file") check_hex :: proc(t: ^testing.T, c: Ascii_Case) { out, err := ascii_hex_decode(transmute([]byte)c.in_) defer delete(out) testing.expectf(t, err == .None, "%s: err = %v", c.name, err) testing.expectf(t, slice.equal(out, transmute([]byte)c.want), "%s: got %v, want %v", c.name, out, transmute([]byte)c.want) } @(private = "file") check_a85 :: proc(t: ^testing.T, c: Ascii_Case) { out, err := ascii85_decode(transmute([]byte)c.in_) defer delete(out) testing.expectf(t, err == .None, "%s: err = %v", c.name, err) testing.expectf(t, slice.equal(out, transmute([]byte)c.want), "%s: got %v, want %v", c.name, out, transmute([]byte)c.want) } // --------------------------------------------------------------------------- // ASCIIHexDecode // --------------------------------------------------------------------------- @(test) test_ascii_hex_basic :: proc(t: ^testing.T) { cases := []Ascii_Case{ {"hello", "48656C6C6F>", "Hello"}, {"lowercase", "48656c6c6f>", "Hello"}, {"mixed case", "48656C6c6F>", "Hello"}, {"zero byte", "00>", "\x00"}, {"all nibbles", "0123456789ABCDEF>", "\x01\x23\x45\x67\x89\xAB\xCD\xEF"}, {"no terminator", "48656C6C6F", "Hello"}, {"data after '>'", "4142>43444546", "AB"}, } for c in cases { check_hex(t, c) } } @(test) test_ascii_hex_odd_digit :: proc(t: ^testing.T) { // ISO 32000-2, 7.4.2: an odd final digit is treated as if followed by '0'. cases := []Ascii_Case{ {"single digit", "4>", "\x40"}, {"odd tail", "414>", "A\x40"}, {"odd, unterminated", "41 4", "A\x40"}, {"odd 'f'", "f>", "\xF0"}, } for c in cases { check_hex(t, c) } } @(test) test_ascii_hex_whitespace_and_junk :: proc(t: ^testing.T) { cases := []Ascii_Case{ {"spaces", "48 65 6C 6C 6F >", "Hello"}, {"newlines", "48\n65\r\n6C\t6C\x0C6F>", "Hello"}, {"nul padding", "\x0048656C6C6F\x00>", "Hello"}, {"leading ws", " \n48>", "H"}, {"junk skipped", "4?8g6/5>", "H\x65"}, {"all junk", "hello world>", "\xed"}, } for c in cases { check_hex(t, c) } } @(test) test_ascii_hex_degenerate :: proc(t: ^testing.T) { cases := []Ascii_Case{ {"empty", "", ""}, {"eod only", ">", ""}, {"only ws", " \n\t ", ""}, {"eod first", "> 4142", ""}, {"only junk", "zz!!", ""}, } for c in cases { check_hex_nothing(t, c) } } // A decode that produces nothing must also allocate nothing, or every empty // stream in a document leaks a zero-length block. @(private = "file") check_hex_nothing :: proc(t: ^testing.T, c: Ascii_Case) { out, err := ascii_hex_decode(transmute([]byte)c.in_) defer delete(out) testing.expectf(t, err == .None, "%s: err = %v", c.name, err) testing.expectf(t, len(out) == 0, "%s: got %v, want empty", c.name, out) } @(test) test_ascii_hex_long_run :: proc(t: ^testing.T) { // 512 bytes through the filter, to exercise the measure/fill passes on // something longer than one cache line of hand-written input. src := make([]byte, 512) defer delete(src) enc := make([dynamic]byte, 0, 1100) defer delete(enc) digits := "0123456789ABCDEF" for i in 0 ..< len(src) { src[i] = byte(i * 7) append(&enc, digits[src[i] >> 4], digits[src[i] & 0xF]) if i % 40 == 39 { append(&enc, '\n') } } append(&enc, '>') out, err := ascii_hex_decode(enc[:]) defer delete(out) testing.expect(t, err == .None) testing.expect(t, slice.equal(out, src)) } // --------------------------------------------------------------------------- // ASCII85Decode // --------------------------------------------------------------------------- @(test) test_ascii85_basic :: proc(t: ^testing.T) { cases := []Ascii_Case{ {"full group", "9jqo^~>", "Man "}, {"three bytes", "9jqo~>", "Man"}, {"two bytes", "9jn~>", "Ma"}, {"one byte", "9`~>", "M"}, {"six bytes", "F*2M7/c~>", "sure."}, {"max u32", "s8W-!~>", "\xFF\xFF\xFF\xFF"}, {"no terminator", "9jqo^", "Man "}, {"partial, no terminator", "9jn", "Ma"}, } for c in cases { check_a85(t, c) } } @(test) test_ascii85_z_shorthand :: proc(t: ^testing.T) { cases := []Ascii_Case{ {"z alone", "z~>", "\x00\x00\x00\x00"}, {"z twice", "zz~>", "\x00\x00\x00\x00\x00\x00\x00\x00"}, {"z then data", "zBOu!rDZ~>", "\x00\x00\x00\x00hello"}, {"data then z", "9jqo^z~>", "Man \x00\x00\x00\x00"}, // 'z' inside a group has no meaning; dropping it lets the surrounding // group still decode, which is what a producer that emitted stray // bytes meant. {"z mid-group", "9jzqo^~>", "Man "}, } for c in cases { check_a85(t, c) } } @(test) test_ascii85_whitespace_and_prefix :: proc(t: ^testing.T) { cases := []Ascii_Case{ {"prefix", "<~9jqo^~>", "Man "}, {"prefix and ws", " \n<~9jqo^ ~>", "Man "}, {"ws everywhere", "9 j\nq\ro\t^\x0C~>", "Man "}, {"nul padding", "\x009jqo^\x00~>", "Man "}, {"ws in prefix-less", " 9jqo^~>", "Man "}, } for c in cases { check_a85(t, c) } } @(test) test_ascii85_degenerate :: proc(t: ^testing.T) { cases := []Ascii_Case{ {"empty", "", ""}, {"eod only", "~>", ""}, {"prefix and eod", "<~~>", ""}, {"only ws", " \n\t", ""}, // A final group of one character encodes nothing at all: it carries // fewer than 8 bits. {"single leftover", "9jqo^9~>", "Man "}, {"lone leftover", "9~>", ""}, // A lone '~' is not a legal terminator but is still the end of // anything decodable. {"bare tilde", "9jqo^~", "Man "}, {"data after eod", "9jqo^~>9jqo^", "Man "}, } for c in cases { check_a85(t, c) } } @(test) test_ascii85_out_of_range_group :: proc(t: ^testing.T) { // "uuuuu" is 85^5-1 = 4437053124, past 2^32-1. Clamping keeps the group; // rejecting it would lose the rest of the stream with it. out, err := ascii85_decode(transmute([]byte)string("uuuuu~>")) defer delete(out) testing.expect(t, err == .None) testing.expect(t, slice.equal(out, []byte{0xFF, 0xFF, 0xFF, 0xFF})) // A short group padded with 'u' can overflow the same way. out2, err2 := ascii85_decode(transmute([]byte)string("uuuu~>")) defer delete(out2) testing.expect(t, err2 == .None) testing.expect(t, slice.equal(out2, []byte{0xFF, 0xFF, 0xFF})) } @(test) test_ascii85_out_of_alphabet :: proc(t: ^testing.T) { cases := []Ascii_Case{ // 'v'..'y' and DEL are outside '!'..'u' and are dropped. {"high letters", "9jqvo^~>", "Man "}, {"del", "9jq\x7Fo^~>", "Man "}, {"high bit", "9jq\xFFo^~>", "Man "}, {"below bang", "9jq\x1Fo^~>", "Man "}, } for c in cases { check_a85(t, c) } } @(test) test_ascii85_spec_vector :: proc(t: ^testing.T) { // The canonical Ascii85 vector: Hamlet, II.ii. enc := `9jqo^BlbD-BleB1DJ+*+F(f,q/0JhKFCj@.4Gp$d7F!,L7@<6@)/0JDEF@3BB/F*&OCAfu2/AKYi(DIb9~>` want := "Man is distinguished, not only by his reason, but by this singular " + "passion from other animals, which is a lust of the mind," out, err := ascii85_decode(transmute([]byte)enc) defer delete(out) testing.expect(t, err == .None) testing.expectf(t, slice.equal(out, transmute([]byte)want), "got %q", string(out)) } @(test) test_ascii85_truncated_everywhere :: proc(t: ^testing.T) { // Truncating the vector at every offset must decode a prefix of the // plaintext and never crash: a stream cut short mid-group is the normal // shape of a damaged file. enc := `9jqo^BlbD-BleB1DJ+*+F(f,q/0JhKF~>` full := "Man is distinguished, not only by his reason, " for cut in 0 ..= len(enc) { out, err := ascii85_decode(transmute([]byte)enc[:cut]) testing.expectf(t, err == .None, "cut %d: err = %v", cut, err) testing.expectf(t, len(out) <= len(full), "cut %d: %d bytes", cut, len(out)) if len(out) > 0 { testing.expectf(t, slice.equal(out, transmute([]byte)full[:len(out)]), "cut %d: got %q", cut, string(out)) } delete(out) } } @(test) test_ascii_hex_truncated_everywhere :: proc(t: ^testing.T) { enc := "48656C6C6F2C20776F726C64>" full := "Hello, world" for cut in 0 ..= len(enc) { out, err := ascii_hex_decode(transmute([]byte)enc[:cut]) testing.expectf(t, err == .None, "cut %d: err = %v", cut, err) testing.expectf(t, len(out) <= len(full), "cut %d: %d bytes", cut, len(out)) // An odd cut lands mid-byte, whose value is the high nibble alone. n := cut / 2 if n > 0 && n <= len(full) { testing.expectf(t, slice.equal(out[:n], transmute([]byte)full[:n]), "cut %d: got %q", cut, string(out)) } delete(out) } } @(test) test_ascii85_cross_check :: proc(t: ^testing.T) { // Vectors produced by an independent encoder (Python's base64.a85encode) // over random bytes, at every length modulo 4, so the partial-group path // is checked against something other than this file's own arithmetic. cases := []Ascii_Case{ {"n=1", "i;~>", "\xE1"}, {"n=2", "3s,~>", ";\x03"}, {"n=3", "/eKQ~>", ".\x11*"}, {"n=4", "1:;B5~>", "2\xB5y\x08"}, {"n=5", "%g]H7m/~>", "\x0F\x08\xB1\xF7\xED"}, {"n=7", "9J0U3#Pii~>", "L.]:\x07\xF9\x7F"}, {"n=11", "+mpR,(Q'?(p9f\\~>", "!\xEE#-\x17\x8A \x9A\xF6\xB5\x88"}, {"n=16", "IqImN,Qau;o\"e", "\x7Ff\xE8\x09$\x02\xAAI\xF2\xC1U\x1B'\xFES&"}, {"n=23", "DFk;B3(TS''\\/0W>)Q1E:[Jn9ZMgp~>", "nI\x0D\xB18H\x9C\xE8\x14\xD5\x8D\x14Z\x8BO\x99O\xED\x15\xC5\xB2\xFD\xAE"}, } for c in cases { check_a85(t, c) } } @(test) test_ascii_filters_honour_allocator :: proc(t: ^testing.T) { // Decoded stream data usually goes straight into the Document arena, which // is never freed element by element, so the allocator parameter has to be // the one that is actually used. arena: virtual.Arena testing.expect(t, virtual.arena_init_growing(&arena) == nil) defer virtual.arena_destroy(&arena) alloc := virtual.arena_allocator(&arena) hex, herr := ascii_hex_decode(transmute([]byte)string("48656C6C6F>"), alloc) testing.expect(t, herr == .None) testing.expect(t, slice.equal(hex, transmute([]byte)string("Hello"))) a85, aerr := ascii85_decode(transmute([]byte)string("9jqo^~>"), alloc) testing.expect(t, aerr == .None) testing.expect(t, slice.equal(a85, transmute([]byte)string("Man "))) } // A tiny deterministic PRNG, so the fuzz tests are reproducible and do not // depend on the state of the context's random generator. @(private = "file") ascii_fuzz_next :: proc(state: ^u64) -> u32 { state^ = state^ * 6364136223846793005 + 1442695040888963407 return u32(state^ >> 33) } @(test) test_ascii_filters_fuzz :: proc(t: ^testing.T) { // Random bytes are not valid data for either filter, which is the point: // neither may crash, hang, or claim more output than the input can hold. state: u64 = 0xC0FFEE buf: [256]byte for round in 0 ..< 400 { n := int(ascii_fuzz_next(&state) % u32(len(buf) + 1)) for i in 0 ..< n { buf[i] = byte(ascii_fuzz_next(&state)) } data := buf[:n] hex, herr := ascii_hex_decode(data) testing.expectf(t, herr == .None, "round %d: hex err %v", round, herr) testing.expectf(t, len(hex) <= n / 2 + 1, "round %d: hex grew to %d from %d", round, len(hex), n) delete(hex) a85, aerr := ascii85_decode(data) testing.expectf(t, aerr == .None, "round %d: a85 err %v", round, aerr) testing.expectf(t, len(a85) <= 4 * n, "round %d: a85 grew to %d from %d", round, len(a85), n) delete(a85) } }