Files

1353 lines
51 KiB
Odin
Raw Permalink Normal View History

2026-09-25 23:32:29 +02:00
package pdf
import "core:mem/virtual"
import "core:slice"
import "core:testing"
// The predictor vectors below are hand-computed from RFC 2083 section 6 and
// TIFF 6.0 section 14 and are written out byte by byte in the test that uses
// them, so a change of behaviour has to be argued with rather than re-blessed.
// The compressed vectors come from zlib via CPython, and the LZW one is the
// published TIFF 6.0 example.
// ---------------------------------------------------------------------------
// Vectors
// ---------------------------------------------------------------------------
@(private = "file")
V_CHAIN_PLAIN :: "Hello, filter chain!"
// zlib.compress(V_CHAIN_PLAIN)
@(private = "file")
v_chain_flate := [?]byte{
0x78, 0xda, 0xf3, 0x48, 0xcd, 0xc9, 0xc9, 0xd7, 0x51, 0x48, 0xcb, 0xcc, 0x29, 0x49, 0x2d, 0x52,
0x48, 0xce, 0x48, 0xcc, 0xcc, 0x53, 0x04, 0x00, 0x4a, 0x94, 0x07, 0x0b,
}
// the same bytes in ASCII85: "GhVa[c,n)Z;+SmS.7l*h8BscMbYnWE8sBPg~>"
@(private = "file")
v_chain_a85 := [?]byte{
0x47, 0x68, 0x56, 0x61, 0x5b, 0x63, 0x2c, 0x6e, 0x29, 0x5a, 0x3b, 0x2b, 0x53, 0x6d, 0x53, 0x2e,
0x37, 0x6c, 0x2a, 0x68, 0x38, 0x42, 0x73, 0x63, 0x4d, 0x62, 0x59, 0x6e, 0x57, 0x45, 0x38, 0x73,
0x42, 0x50, 0x67, 0x7e, 0x3e,
}
// The shape an xref stream has: three four-byte rows, PNG Up-filtered, then
// deflated. The encoded rows are {2,1,0,0,0}, {2,0,0,0,1}, {2,0,0,0,1} and
// they reconstruct to V_PRED_PAYLOAD.
@(private = "file")
v_pred_flate := [?]byte{
0x78, 0xda, 0x63, 0x62, 0x64, 0x60, 0x60, 0x60, 0x02, 0x62, 0x46, 0x30, 0x01, 0x00, 0x00, 0x60,
0x00, 0x0a,
}
@(private = "file")
v_pred_payload := [?]byte{1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 2}
// TIFF 6.0 section 13: "-----A---B" packed at 9 bits.
@(private = "file")
v_lzw_tiff := [?]byte{0x80, 0x0b, 0x60, 0x50, 0x22, 0x0c, 0x0c, 0x85, 0x01}
@(private = "file")
V_LZW_PLAIN :: "-----A---B"
// ---------------------------------------------------------------------------
// Fixtures
//
// A Document built by hand: filters need nothing but the arena and the repair
// list, and going through open_bytes would drag in the whole file structure.
// ---------------------------------------------------------------------------
// Package-private rather than file-private: the codec test files measure how
// much of this arena a decode costs, and that rule belongs to apply_filter
// rather than to any one codec, so they should not each grow a Document of
// their own.
@(private)
filter_test_doc :: proc() -> ^Document {
doc := new(Document)
doc.backing = context.allocator
if err := virtual.arena_init_growing(&doc.arena); err != nil {
free(doc)
return nil
}
doc.allocator = virtual.arena_allocator(&doc.arena)
doc.repairs = make([dynamic]Repair, doc.allocator)
return doc
}
@(private)
filter_test_doc_free :: proc(doc: ^Document) {
if doc == nil {
return
}
virtual.arena_destroy(&doc.arena)
free(doc, doc.backing)
}
@(private = "file")
ft_names :: proc(doc: ^Document, names: []string) -> ^Array {
a := new_array(doc.allocator)
for n in names {
array_push(a, Name(n))
}
return a
}
// ft_parms builds a parameter dictionary from alternating key/value pairs,
// where every value is an integer -- which is all the predictor and
// /EarlyChange need.
@(private = "file")
ft_parms :: proc(doc: ^Document, keys: []string, values: []i64) -> ^Dict {
d := new_dict(doc.allocator)
for k, i in keys {
if i < len(values) {
dict_set(d, k, values[i])
}
}
return d
}
@(private = "file")
ft_stream :: proc(doc: ^Document, raw: []byte, filter: Object, parms: Object,
filter_key := "Filter", parms_key := "DecodeParms") -> ^Stream {
d := new_dict(doc.allocator)
if filter != nil {
dict_set(d, filter_key, filter)
}
if parms != nil {
dict_set(d, parms_key, parms)
}
return new_stream(d, raw, doc.allocator)
}
@(private = "file")
ft_repairs :: proc(doc: ^Document, kind: Repair_Kind) -> int {
n := 0
for r in doc.repairs {
if r.kind == kind {
n += 1
}
}
return n
}
// ---------------------------------------------------------------------------
// Filter names
// ---------------------------------------------------------------------------
@(test)
test_filter_kind_accepts_full_names_and_abbreviations :: proc(t: ^testing.T) {
Case :: struct {
name: string,
kind: Filter_Kind,
}
cases := [?]Case{
{"ASCIIHexDecode", .Ascii_Hex},
{"AHx", .Ascii_Hex},
{"ASCII85Decode", .Ascii85},
{"A85", .Ascii85},
{"LZWDecode", .Lzw},
{"LZW", .Lzw},
{"FlateDecode", .Flate},
{"Fl", .Flate},
{"RunLengthDecode", .Run_Length},
{"RL", .Run_Length},
{"CCITTFaxDecode", .Ccitt},
{"CCF", .Ccitt},
{"DCTDecode", .Dct},
{"DCT", .Dct},
{"JPXDecode", .Jpx},
{"JBIG2Decode", .Jbig2},
{"Crypt", .Crypt},
{"", .Unknown},
{"Flate", .Unknown},
{"flatedecode", .Unknown},
{"NotAFilter", .Unknown},
}
for c in cases {
testing.expectf(t, filter_kind(c.name) == c.kind,
"filter_kind(%q) = %v, want %v", c.name, filter_kind(c.name), c.kind)
}
// The abbreviation is always reported back in full, so the image layer has
// one spelling to compare against.
testing.expect_value(t, filter_name(filter_kind("DCT"), "DCT"), "DCTDecode")
testing.expect_value(t, filter_name(.Unknown, "Whatever"), "Whatever")
}
// ---------------------------------------------------------------------------
// PNG predictors -- RFC 2083, 6.2 to 6.6
// ---------------------------------------------------------------------------
// One row per filter type, each hand-reconstructed above:
//
// None 10 20 30
// Sub 1 1+2=3 3+3=6
// Up 5+1 5+3 5+6
// Average 1+(0+6)/2 1+(4+8)/2 1+(7+11)/2
// Paeth 1+b(4) 2+b(7) 3+b(10)
@(test)
test_predictor_png_every_filter :: proc(t: ^testing.T) {
data := [?]byte{
0, 10, 20, 30,
1, 1, 2, 3,
2, 5, 5, 5,
3, 1, 1, 1,
4, 1, 2, 3,
}
want := [?]byte{
10, 20, 30,
1, 3, 6,
6, 8, 11,
4, 7, 10,
5, 9, 13,
}
out, err := apply_predictor(data[:], 12, 1, 8, 3)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want)
}
// Every /Predictor from 10 to 15 decodes identically: the value records the
// widest filter the encoder was allowed to pick, and the row tag is what
// actually selects one.
@(test)
test_predictor_png_values_10_to_15_agree :: proc(t: ^testing.T) {
data := [?]byte{1, 1, 2, 3, 2, 5, 5, 5}
want := [?]byte{1, 3, 6, 6, 8, 11}
for p in 10 ..= 15 {
out, err := apply_predictor(data[:], p, 1, 8, 3)
defer delete(out)
testing.expectf(t, err == .None, "predictor %d: %s", p, error_string(err))
testing.expectf(t, slice.equal(out, want[:]), "predictor %d: got %v", p, out)
}
}
// On the first row the filters read zeroes for the line above, so Up is a
// no-op, Average halves the left neighbour and Paeth degenerates to Sub.
@(test)
test_predictor_png_first_row_reads_zeroes :: proc(t: ^testing.T) {
Case :: struct {
tag: byte,
want: [2]byte,
}
cases := [?]Case{
{0, {5, 3}},
{1, {5, 8}},
{2, {5, 3}},
{3, {5, 5}},
{4, {5, 8}},
}
for c in cases {
data := [?]byte{c.tag, 5, 3}
want := c.want
out, err := apply_predictor(data[:], 15, 1, 8, 2)
defer delete(out)
testing.expectf(t, err == .None, "tag %d: %s", c.tag, error_string(err))
testing.expectf(t, slice.equal(out, want[:]), "tag %d: got %v, want %v",
c.tag, out, want)
}
}
// bpp is the byte distance to the pixel on the left, so Sub over three
// 8-bit colours adds the value three bytes back, not one.
@(test)
test_predictor_png_multibyte_pixel :: proc(t: ^testing.T) {
data := [?]byte{1, 10, 20, 30, 1, 2, 3}
want := [?]byte{10, 20, 30, 11, 22, 33}
out, err := apply_predictor(data[:], 12, 3, 8, 2)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want)
}
// bpp has a floor of 1: four 1-bit colours are half a byte per pixel, and Sub
// still reaches one byte back.
@(test)
test_predictor_png_sub_byte_pixel :: proc(t: ^testing.T) {
data := [?]byte{1, 0x11, 0x22}
want := [?]byte{0x11, 0x33}
out, err := apply_predictor(data[:], 12, 4, 1, 4)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want)
}
// Arithmetic is modulo 256 in both directions (RFC 2083, 6.2).
@(test)
test_predictor_png_wraps_at_256 :: proc(t: ^testing.T) {
data := [?]byte{1, 200, 100, 2, 200}
// 200; 200+100 = 300 mod 256 = 44; then Up: 200+200 = 400 mod 256 = 144.
want := [?]byte{200, 44, 144, 44}
out, err := apply_predictor(data[:], 12, 1, 8, 2)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want)
}
// A tag no encoder emits: one byte lost from the front of the payload shifts
// every tag, and reading the row unfiltered keeps the rest of the stream in
// step instead of losing all of it. The row after it still decodes against
// the row the unknown tag produced.
@(test)
test_predictor_png_unknown_row_tag_is_none :: proc(t: ^testing.T) {
data := [?]byte{9, 7, 7, 2, 1, 2}
want := [?]byte{7, 7, 8, 9}
out, err := apply_predictor(data[:], 12, 1, 8, 2)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want)
}
@(test)
test_predictor_paeth_matches_the_reference :: proc(t: ^testing.T) {
reference :: proc(a, b, c: int) -> int {
p := a + b - c
pa, pb, pc := abs(p - a), abs(p - b), abs(p - c)
if pa <= pb && pa <= pc {
return a
} else if pb <= pc {
return b
}
return c
}
for a := 0; a < 256; a += 7 {
for b := 0; b < 256; b += 5 {
for c := 0; c < 256; c += 3 {
testing.expectf(t, predictor_paeth(a, b, c) == reference(a, b, c),
"paeth(%d,%d,%d) = %d, want %d", a, b, c,
predictor_paeth(a, b, c), reference(a, b, c))
}
}
}
}
// ---------------------------------------------------------------------------
// TIFF predictor -- TIFF 6.0, section 14
// ---------------------------------------------------------------------------
@(test)
test_predictor_tiff_8_bit :: proc(t: ^testing.T) {
data := [?]byte{10, 5, 5, 1, 1, 1}
want := [?]byte{10, 15, 20, 1, 2, 3}
out, err := apply_predictor(data[:], 2, 1, 8, 3)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want)
}
// Differencing is per colour, so with three colours each component is the
// difference from the same component of the pixel to its left.
@(test)
test_predictor_tiff_8_bit_three_colours :: proc(t: ^testing.T) {
data := [?]byte{10, 20, 30, 1, 2, 3}
want := [?]byte{10, 20, 30, 11, 22, 33}
out, err := apply_predictor(data[:], 2, 3, 8, 2)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want)
}
// Four 4-bit components in two bytes: 1 2 3 4 accumulates to 1 3 6 10.
@(test)
test_predictor_tiff_4_bit :: proc(t: ^testing.T) {
data := [?]byte{0x12, 0x34}
want := [?]byte{0x13, 0x6a}
out, err := apply_predictor(data[:], 2, 1, 4, 4)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %x, want %x", out, want)
}
// The same two bytes read as two pixels of two 4-bit colours: component 2 adds
// component 0 and component 3 adds component 1.
@(test)
test_predictor_tiff_4_bit_two_colours :: proc(t: ^testing.T) {
data := [?]byte{0x12, 0x34}
want := [?]byte{0x12, 0x46}
out, err := apply_predictor(data[:], 2, 2, 4, 2)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %x, want %x", out, want)
}
// Four 2-bit components in one byte: 1 2 3 1 accumulates to 1 3 2 3, each
// sum taken modulo 4.
@(test)
test_predictor_tiff_2_bit :: proc(t: ^testing.T) {
data := [?]byte{0b01_10_11_01}
want := [?]byte{0b01_11_10_11}
out, err := apply_predictor(data[:], 2, 1, 2, 4)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %b, want %b", out, want)
}
// 1-bit components add modulo 2, which is an exclusive or along the row.
@(test)
test_predictor_tiff_1_bit :: proc(t: ^testing.T) {
data := [?]byte{0xaa}
want := [?]byte{0xcc}
out, err := apply_predictor(data[:], 2, 1, 1, 8)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %x, want %x", out, want)
}
// 16-bit components are big-endian and wrap at 65536, not at 256.
@(test)
test_predictor_tiff_16_bit :: proc(t: ^testing.T) {
Case :: struct {
data: [4]byte,
want: [4]byte,
}
cases := [?]Case{
{{0x00, 0x10, 0x00, 0x20}, {0x00, 0x10, 0x00, 0x30}},
{{0xff, 0xff, 0x00, 0x02}, {0xff, 0xff, 0x00, 0x01}},
}
for c in cases {
input := c.data
want := c.want
out, err := apply_predictor(input[:], 2, 1, 16, 2)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %x, want %x", out, want)
}
}
// The padding bits at the end of a row are not components and must be left
// alone: 3 columns of 4 bits occupy a byte and a half.
@(test)
test_predictor_tiff_ignores_row_padding :: proc(t: ^testing.T) {
data := [?]byte{0x12, 0x3f}
// components 1, 2, 3 -> 1, 3, 6; the low nibble of byte 1 is padding.
want := [?]byte{0x13, 0x6f}
out, err := apply_predictor(data[:], 2, 1, 4, 3)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %x, want %x", out, want)
}
// ---------------------------------------------------------------------------
// Predictor edge cases
// ---------------------------------------------------------------------------
// A payload that stops mid-row is padded, not dropped. A truncated xref stream
// still describes the objects it got to.
@(test)
test_predictor_truncated_final_row_is_padded :: proc(t: ^testing.T) {
png := [?]byte{0, 10, 20, 30, 2, 1}
png_want := [?]byte{10, 20, 30, 11, 20, 30}
out, err := apply_predictor(png[:], 12, 1, 8, 3)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, png_want[:]), "png: got %v, want %v", out, png_want)
tiff := [?]byte{10, 5, 5, 1, 1}
tiff_want := [?]byte{10, 15, 20, 1, 2, 2}
out2, err2 := apply_predictor(tiff[:], 2, 1, 8, 3)
defer delete(out2)
testing.expect_value(t, err2, Error.None)
testing.expectf(t, slice.equal(out2, tiff_want[:]), "tiff: got %v, want %v", out2, tiff_want)
}
// A row consisting of nothing but its filter tag still produces a row.
@(test)
test_predictor_final_row_is_only_a_tag :: proc(t: ^testing.T) {
data := [?]byte{0, 10, 20, 1}
want := [?]byte{10, 20, 0, 0}
out, err := apply_predictor(data[:], 12, 1, 8, 2)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, want[:]), "got %v, want %v", out, want)
}
@(test)
test_predictor_none_copies :: proc(t: ^testing.T) {
data := [?]byte{1, 2, 3, 4}
for p in 0 ..= 1 {
out, err := apply_predictor(data[:], p, 1, 8, 4)
defer delete(out)
testing.expectf(t, err == .None, "predictor %d: %s", p, error_string(err))
testing.expectf(t, slice.equal(out, data[:]), "predictor %d changed the data", p)
testing.expectf(t, raw_data(out) != raw_data(data[:]),
"predictor %d aliased its input", p)
}
}
@(test)
test_predictor_empty_input :: proc(t: ^testing.T) {
for p in ([?]int{1, 2, 12}) {
out, err := apply_predictor(nil, p, 1, 8, 4)
defer delete(out)
testing.expectf(t, err == .None, "predictor %d: %s", p, error_string(err))
testing.expectf(t, len(out) == 0, "predictor %d invented %d bytes", p, len(out))
}
}
@(test)
test_predictor_rejects_absurd_parameters :: proc(t: ^testing.T) {
Case :: struct {
label: string,
predictor, colors, bpc, columns: int,
}
cases := [?]Case{
{"predictor 3 is not defined", 3, 1, 8, 4},
{"predictor 9 is not defined", 9, 1, 8, 4},
{"predictor 16 is past PNG", 16, 1, 8, 4},
{"negative predictor", -1, 1, 8, 4},
{"zero colours", 12, 0, 8, 4},
{"negative colours", 12, -3, 8, 4},
{"more colours than DeviceN", 12, 33, 8, 4},
{"3 bits per component", 12, 1, 3, 4},
{"zero bits per component", 12, 1, 0, 4},
{"32 bits per component", 12, 1, 32, 4},
{"negative bits per component", 12, 1, -8, 4},
{"zero columns", 12, 1, 8, 0},
{"negative columns", 12, 1, 8, -4},
{"absurd columns", 12, 1, 8, 1 << 30},
{"absurd row", 2, 32, 16, 1 << 20},
}
data := [?]byte{0, 1, 2, 3, 4, 5, 6, 7}
for c in cases {
out, err := apply_predictor(data[:], c.predictor, c.colors, c.bpc, c.columns)
defer delete(out)
testing.expectf(t, err == .Bad_Predictor, "%s: got %s", c.label, error_string(err))
testing.expectf(t, out == nil, "%s: returned %d bytes as well", c.label, len(out))
}
}
// Padding the final row is the only way the output can exceed the input, so
// the ceiling on an allocation the file controls is one row -- and a row is
// itself capped, which is what MAX_PREDICTOR_ROW_BYTES is for.
@(test)
test_predictor_output_is_bounded_by_one_row :: proc(t: ^testing.T) {
Case :: struct {
predictor, colors, bpc, columns: int,
row_bytes: int,
}
cases := [?]Case{
{15, 32, 16, 1 << 15, 32 * 16 * (1 << 15) / 8},
{12, 1, 8, 1 << 20, 1 << 20},
{2, 4, 8, 1000, 4000},
{12, 1, 1, 7, 1},
}
data := make([]byte, 4096)
defer delete(data)
for c in cases {
out, err := apply_predictor(data, c.predictor, c.colors, c.bpc, c.columns)
defer delete(out)
testing.expectf(t, err == .None, "%v: %s", c, error_string(err))
testing.expectf(t, len(out) <= len(data) + c.row_bytes,
"%v: %d bytes out of %d in, more than one row of slack",
c, len(out), len(data))
}
}
// ---------------------------------------------------------------------------
// apply_filter
// ---------------------------------------------------------------------------
@(test)
test_apply_filter_byte_filters :: proc(t: ^testing.T) {
Case :: struct {
name: string,
raw: string,
want: string,
}
cases := [?]Case{
{"ASCIIHexDecode", "48656C6C6F>", "Hello"},
{"AHx", "48656C6C6F>", "Hello"},
{"ASCII85Decode", "87cURD]j7BEbo7~>", "Hello world"},
{"A85", "87cURD]j7BEbo7~>", "Hello world"},
{"RunLengthDecode", "\x02abc\xfeZ\x80", "abcZZZ"},
{"RL", "\x02abc\xfeZ\x80", "abcZZZ"},
}
for c in cases {
out, err := apply_filter(nil, c.name, transmute([]byte)c.raw, nil)
defer delete(out)
testing.expectf(t, err == .None, "%s: %s", c.name, error_string(err))
testing.expectf(t, string(out) == c.want, "%s: got %q, want %q",
c.name, string(out), c.want)
}
}
@(test)
test_apply_filter_flate :: proc(t: ^testing.T) {
out, err := apply_filter(nil, "Fl", v_chain_flate[:], nil)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, string(out), V_CHAIN_PLAIN)
}
@(test)
test_apply_filter_lzw_honours_early_change :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
for value in ([?]i64{0, 1}) {
parms := ft_parms(doc, {"EarlyChange"}, {value})
out, err := apply_filter(doc, "LZW", v_lzw_tiff[:], parms)
defer delete(out)
testing.expectf(t, err == .None, "EarlyChange %d: %s", value, error_string(err))
testing.expectf(t, string(out) == V_LZW_PLAIN,
"EarlyChange %d: got %q", value, string(out))
}
// No parameters at all means /EarlyChange 1.
out, err := apply_filter(nil, "LZWDecode", v_lzw_tiff[:], nil)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, string(out), V_LZW_PLAIN)
}
@(test)
test_apply_filter_runs_the_predictor :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
parms := ft_parms(doc, {"Predictor", "Columns"}, {12, 4})
out, err := apply_filter(doc, "FlateDecode", v_pred_flate[:], parms)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, v_pred_payload[:]), "got %v, want %v", out, v_pred_payload)
}
// LZW with the TIFF predictor, end to end: "-----A---B" read as two rows of
// five 8-bit components, each accumulating along its row modulo 256.
@(test)
test_stream_data_lzw_with_the_tiff_predictor :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
parms := ft_parms(doc, {"Predictor", "Colors", "BitsPerComponent", "Columns"}, {2, 1, 8, 5})
s := ft_stream(doc, v_lzw_tiff[:], Name("LZWDecode"), parms)
got, err := stream_data(doc, s)
want := [?]byte{45, 90, 135, 180, 225, 65, 110, 155, 200, 10}
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(got, want[:]), "got %v, want %v", got, want)
}
// A predictor whose parameters cannot describe any image fails the filter
// rather than handing back bytes that only look like samples.
@(test)
test_apply_filter_bad_predictor_fails :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
parms := ft_parms(doc, {"Predictor", "Columns"}, {7, 4})
out, err := apply_filter(doc, "FlateDecode", v_pred_flate[:], parms)
defer delete(out)
testing.expect_value(t, err, Error.Bad_Predictor)
testing.expect(t, out == nil, "bytes returned alongside a predictor failure")
}
// The pass-through errors hand the input back untouched, which is what lets
// stream_image_data give a JPX codestream to the image layer.
@(test)
test_apply_filter_pass_through_errors :: proc(t: ^testing.T) {
Case :: struct {
name: string,
err: Error,
}
cases := [?]Case{
{"DCTDecode", .Unsupported_Image},
{"DCT", .Unsupported_Image},
{"JBIG2Decode", .Unsupported_Image},
{"JPXDecode", .Unsupported_Jpx},
{"NoSuchThing", .Unknown_Filter},
{"", .Unknown_Filter},
}
data := [?]byte{1, 2, 3, 4}
for c in cases {
out, err := apply_filter(nil, c.name, data[:], nil)
testing.expectf(t, err == c.err, "%s: got %s", c.name, error_string(err))
testing.expectf(t, raw_data(out) == raw_data(data[:]),
"%s: did not hand back the input", c.name)
}
}
// /Crypt is a marker: the bytes were decrypted at parse time.
@(test)
test_apply_filter_crypt_is_identity :: proc(t: ^testing.T) {
data := [?]byte{9, 8, 7}
out, err := apply_filter(nil, "Crypt", data[:], nil)
defer delete(out)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(out, data[:]), "got %v", out)
testing.expect(t, raw_data(out) != raw_data(data[:]), "Crypt aliased its input")
}
@(test)
test_apply_filter_ccitt_decodes_rows :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
// Group 4, one row of 8 columns, all white: horizontal mode (001), a white
// run of 8 (10011) and a black run of 0 (0000110111), zero padded.
encoded := [?]byte{0b0011_0011, 0b0000_1101, 0b1100_0000}
// With /BlackIs1 false -- the default -- a white pixel is a 1 bit.
parms := ft_parms(doc, {"K", "Columns", "Rows"}, {-1, 8, 1})
out, err := apply_filter(doc, "CCF", encoded[:], parms)
defer delete(out)
testing.expectf(t, err == .None, "%s", error_string(err))
testing.expectf(t, len(out) == 1 && out[0] == 0xff, "got %x", out)
// /BlackIs1 exchanges them.
inverted := ft_parms(doc, {"K", "Columns", "Rows"}, {-1, 8, 1})
dict_set(inverted, "BlackIs1", true)
out2, err2 := apply_filter(doc, "CCITTFaxDecode", encoded[:], inverted)
defer delete(out2)
testing.expectf(t, err2 == .None, "%s", error_string(err2))
testing.expectf(t, len(out2) == 1 && out2[0] == 0x00, "got %x", out2)
}
// ---------------------------------------------------------------------------
// stream_data: the chain
// ---------------------------------------------------------------------------
@(test)
test_stream_data_two_filter_chain :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"ASCII85Decode", "FlateDecode"}), nil)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, string(got), V_CHAIN_PLAIN)
testing.expect_value(t, s.state, Stream_Decode_State.Decoded)
testing.expect_value(t, len(doc.repairs), 0)
}
@(test)
test_stream_data_abbreviated_names :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"A85", "Fl"}), nil)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, string(got), V_CHAIN_PLAIN)
}
// Inline images abbreviate the keys as well as the filter names
// (ISO 32000-2, table 91).
@(test)
test_stream_data_inline_image_keys :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
parms := ft_parms(doc, {"Predictor", "Columns"}, {12, 4})
s := ft_stream(doc, v_pred_flate[:], Name("Fl"), parms, "F", "DP")
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(got, v_pred_payload[:]), "got %v", got)
}
// A single name is as legal as a one-element array.
@(test)
test_stream_data_single_name_filter :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
s := ft_stream(doc, v_chain_flate[:], Name("FlateDecode"), nil)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, string(got), V_CHAIN_PLAIN)
}
// /DecodeParms is positional, and a null stands for "this filter has none".
@(test)
test_stream_data_decode_parms_array_with_a_null :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
parms := new_array(doc.allocator)
array_push(parms, nil)
array_push(parms, ft_parms(doc, {"Predictor", "Columns"}, {12, 4}))
// ASCII85 armour over the flate stream that needs the predictor.
armoured := [?]byte{
0x47, 0x68, 0x51, 0x59, 0x3a, 0x41, 0x37, 0x2f, 0x65, 0x51, 0x21, 0x62, 0x2b, 0x56, 0x51,
0x21, 0x3c, 0x3c, 0x2b, 0x2d, 0x21, 0x22, 0x26, 0x7e, 0x3e,
}
s := ft_stream(doc, armoured[:], ft_names(doc, {"A85", "Fl"}), parms)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(got, v_pred_payload[:]), "got %v, want %v", got, v_pred_payload)
testing.expect_value(t, len(doc.repairs), 0)
}
// A short /DecodeParms array is padded with nulls rather than treated as a
// disagreement worth failing over.
@(test)
test_stream_data_short_decode_parms_array :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
parms := new_array(doc.allocator)
array_push(parms, nil)
s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"A85", "Fl"}), parms)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, string(got), V_CHAIN_PLAIN)
}
// A bare dictionary against a chain is malformed, but it can only have been
// meant for the filter that reads parameters -- here the flate, not the
// ASCII85 armour in front of it.
@(test)
test_stream_data_single_parms_dict_for_a_chain :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
parms := ft_parms(doc, {"Predictor", "Columns"}, {12, 4})
armoured := [?]byte{
0x47, 0x68, 0x51, 0x59, 0x3a, 0x41, 0x37, 0x2f, 0x65, 0x51, 0x21, 0x62, 0x2b, 0x56, 0x51,
0x21, 0x3c, 0x3c, 0x2b, 0x2d, 0x21, 0x22, 0x26, 0x7e, 0x3e,
}
s := ft_stream(doc, armoured[:], ft_names(doc, {"A85", "Fl"}), parms)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expectf(t, slice.equal(got, v_pred_payload[:]), "got %v, want %v", got, v_pred_payload)
}
// A stray null inside /Filter loses that entry, not the stream.
@(test)
test_stream_data_null_inside_the_filter_array :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
filters := new_array(doc.allocator)
array_push(filters, nil)
array_push(filters, Name("FlateDecode"))
s := ft_stream(doc, v_chain_flate[:], filters, nil)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, string(got), V_CHAIN_PLAIN)
}
// ---------------------------------------------------------------------------
// stream_data: degrading
// ---------------------------------------------------------------------------
@(test)
test_stream_data_unknown_filter_degrades :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
s := ft_stream(doc, v_chain_flate[:], Name("SuperZipDecode"), nil)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect(t, got == nil, "an unknown filter produced bytes")
testing.expect_value(t, s.state, Stream_Decode_State.Failed)
testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1)
testing.expect(t, doc.is_damaged, "an unknown filter did not mark the document damaged")
// And the failure is cached too: asking again is free and silent.
again, err2 := stream_data(doc, s)
testing.expect_value(t, err2, Error.None)
testing.expect(t, again == nil, "second call produced bytes")
testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1)
}
// An unknown filter anywhere in the chain empties the stream: the bytes that
// did decode are still encoded as far as the next stage is concerned, and
// handing a content parser half-encoded data is worse than handing it nothing.
@(test)
test_stream_data_unknown_filter_late_in_the_chain :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"A85", "SuperZipDecode"}), nil)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect(t, got == nil, "produced bytes past an unknown filter")
testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1)
}
// JPX and JBIG2 belong to the image layer. Reaching them through stream_data
// is a caller mistake, not a document defect worth an error.
@(test)
test_stream_data_image_filters_degrade :: proc(t: ^testing.T) {
for name in ([?]string{"DCTDecode", "JPXDecode", "JBIG2Decode"}) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
s := ft_stream(doc, v_chain_flate[:], Name(name), nil)
got, err := stream_data(doc, s)
testing.expectf(t, err == .None, "%s: %s", name, error_string(err))
testing.expectf(t, got == nil, "%s: produced bytes", name)
testing.expectf(t, ft_repairs(doc, .Bad_Filter_Data) == 1, "%s: no repair recorded", name)
}
}
@(test)
test_stream_data_broken_flate_degrades :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
junk := [?]byte{0xff, 0xff, 0xff, 0xff}
s := ft_stream(doc, junk[:], Name("FlateDecode"), nil)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect(t, got == nil, "produced bytes from junk")
testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1)
}
// A stream truncated part-way keeps what inflated. Half a content stream draws
// half a page, which beats a blank one.
@(test)
test_stream_data_truncated_flate_keeps_what_it_got :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
s := ft_stream(doc, v_chain_flate[:len(v_chain_flate) - 6], Name("FlateDecode"), nil)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect(t, len(got) > 0, "a truncated stream decoded to nothing")
plain := V_CHAIN_PLAIN
testing.expectf(t, len(got) <= len(plain) && plain[:len(got)] == string(got),
"recovered prefix disagrees: %q", string(got))
}
// A chain this long is not a document.
@(test)
test_stream_data_absurdly_long_chain :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
filters := new_array(doc.allocator)
for _ in 0 ..< MAX_FILTER_CHAIN + 1 {
array_push(filters, Name("FlateDecode"))
}
s := ft_stream(doc, v_chain_flate[:], filters, nil)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect(t, got == nil, "a 17-filter chain produced bytes")
testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1)
}
// ---------------------------------------------------------------------------
// stream_data: caching and borrowing
// ---------------------------------------------------------------------------
@(test)
test_stream_data_caches_the_decode :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"A85", "Fl"}), nil)
first, err1 := stream_data(doc, s)
second, err2 := stream_data(doc, s)
testing.expect_value(t, err1, Error.None)
testing.expect_value(t, err2, Error.None)
testing.expect_value(t, string(first), V_CHAIN_PLAIN)
// Same pointer, not merely the same bytes: a second decode would land
// somewhere else in the arena.
testing.expect(t, raw_data(first) == raw_data(second), "the decode was not cached")
testing.expect(t, raw_data(s.decoded) == raw_data(first), "the cache holds something else")
}
// A stream with no filters is its own decoded form; copying every content
// stream into the arena would double the memory a document costs for nothing.
@(test)
test_stream_data_without_filters_borrows_the_raw_bytes :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
raw := [?]byte{'B', 'T', ' ', 'E', 'T'}
s := ft_stream(doc, raw[:], nil, nil)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect(t, raw_data(got) == raw_data(raw[:]), "the raw bytes were copied")
testing.expect_value(t, s.state, Stream_Decode_State.Decoded)
}
// An empty payload is empty whatever the chain claims. Running flate over it
// would manufacture a repair for a stream that is legitimately empty.
@(test)
test_stream_data_empty_payload_is_not_a_repair :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
s := ft_stream(doc, nil, Name("FlateDecode"), nil)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, len(got), 0)
testing.expect_value(t, s.state, Stream_Decode_State.Decoded)
testing.expect_value(t, len(doc.repairs), 0)
}
@(test)
test_stream_data_nil_arguments :: proc(t: ^testing.T) {
got, err := stream_data(nil, nil)
testing.expect_value(t, err, Error.Invalid_Argument)
testing.expect(t, got == nil, "bytes from a nil stream")
// A nil Document is legal: the standalone object parser has none. The
// decoded bytes then come from the context allocator and are the caller's.
d := new_dict(context.allocator)
dict_set(d, "Filter", Name("FlateDecode"))
defer {
delete(d^)
free(d)
}
s := new_stream(d, v_chain_flate[:], context.allocator)
defer free(s)
got2, err2 := stream_data(nil, s)
defer delete(got2)
testing.expect_value(t, err2, Error.None)
testing.expect_value(t, string(got2), V_CHAIN_PLAIN)
testing.expect(t, stream_data_raw(nil, nil) == nil, "raw bytes from a nil stream")
testing.expect(t, raw_data(stream_data_raw(nil, s)) == raw_data(v_chain_flate[:]),
"stream_data_raw did not hand back the stored bytes")
}
// ---------------------------------------------------------------------------
// stream_image_data
// ---------------------------------------------------------------------------
@(test)
test_stream_image_data_stops_at_the_image_filter :: proc(t: ^testing.T) {
names := [?]string{"DCTDecode", "DCT", "JPXDecode", "JBIG2Decode", "CCITTFaxDecode", "CCF"}
for name in names {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
payload := [?]byte{0xff, 0xd8, 0xff, 0xe0}
s := ft_stream(doc, payload[:], Name(name), nil)
data, filter, parms, err := stream_image_data(doc, s)
defer delete(data)
testing.expectf(t, err == .None, "%s: %s", name, error_string(err))
testing.expectf(t, slice.equal(data, payload[:]), "%s: payload changed", name)
testing.expectf(t, filter == filter_name(filter_kind(name), name),
"%s: reported %q", name, filter)
testing.expectf(t, parms == nil, "%s: invented parameters", name)
// Caller-owned, so never the stream's own bytes.
testing.expectf(t, raw_data(data) != raw_data(payload[:]),
"%s: aliased the raw bytes", name)
}
}
// The armour comes off; the image codec's own bytes are handed over intact.
@(test)
test_stream_image_data_applies_the_byte_filters_first :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
// ASCII85 for the four bytes ff d8 ff e0.
armoured := [?]byte{'s', '4', 'I', 'A', '0', '~', '>'}
s := ft_stream(doc, armoured[:], ft_names(doc, {"A85", "DCTDecode"}), nil)
data, filter, _, err := stream_image_data(doc, s)
defer delete(data)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, filter, "DCTDecode")
want := [?]byte{0xff, 0xd8, 0xff, 0xe0}
testing.expectf(t, slice.equal(data, want[:]), "got %x, want %x", data, want)
}
// The parameters that go with the image filter are handed over, since the
// image layer is the one that has to act on them.
@(test)
test_stream_image_data_returns_the_image_parameters :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
parms := ft_parms(doc, {"K", "Columns", "Rows"}, {-1, 16, 4})
payload := [?]byte{0x00, 0x01}
s := ft_stream(doc, payload[:], Name("CCF"), parms)
data, filter, got_parms, err := stream_image_data(doc, s)
defer delete(data)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, filter, "CCITTFaxDecode")
testing.expect(t, got_parms == parms, "the parameter dictionary did not come back")
p := filter_ccitt_params(doc, got_parms)
testing.expect_value(t, p.k, -1)
testing.expect_value(t, p.columns, 16)
testing.expect_value(t, p.rows, 4)
testing.expect_value(t, p.end_of_block, true)
}
// No image filter at all: the samples are already bytes.
@(test)
test_stream_image_data_without_an_image_filter :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
s := ft_stream(doc, v_chain_flate[:], Name("FlateDecode"), nil)
data, filter, parms, err := stream_image_data(doc, s)
defer delete(data)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, filter, "")
testing.expect(t, parms == nil, "invented parameters")
testing.expect_value(t, string(data), V_CHAIN_PLAIN)
}
// An unfiltered image: still caller-owned memory, so the caller can free it
// without knowing how it got there.
@(test)
test_stream_image_data_unfiltered :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
payload := [?]byte{1, 2, 3, 4, 5}
s := ft_stream(doc, payload[:], nil, nil)
data, filter, _, err := stream_image_data(doc, s)
defer delete(data)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, filter, "")
testing.expectf(t, slice.equal(data, payload[:]), "got %v", data)
testing.expect(t, raw_data(data) != raw_data(payload[:]), "aliased the raw bytes")
}
@(test)
test_stream_image_data_broken_chain :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
s := ft_stream(doc, v_chain_flate[:], ft_names(doc, {"SuperZipDecode", "DCTDecode"}), nil)
data, filter, _, err := stream_image_data(doc, s)
defer delete(data)
testing.expect_value(t, err, Error.Unknown_Filter)
testing.expect(t, data == nil, "bytes past an unknown filter")
testing.expect_value(t, filter, "")
testing.expect_value(t, ft_repairs(doc, .Bad_Filter_Data), 1)
}
@(test)
test_stream_image_data_nil_stream :: proc(t: ^testing.T) {
data, filter, parms, err := stream_image_data(nil, nil)
testing.expect_value(t, err, Error.Invalid_Argument)
testing.expect(t, data == nil && parms == nil && filter == "", "something came back")
}
// ---------------------------------------------------------------------------
// Malformed input
// ---------------------------------------------------------------------------
// Every filter, over payloads of every shape, with parameters that make no
// sense: nothing here may panic, hang or leak.
@(test)
test_filters_never_crash_on_nonsense :: proc(t: ^testing.T) {
names := [?]string{
"FlateDecode", "LZWDecode", "ASCII85Decode", "ASCIIHexDecode", "RunLengthDecode",
"CCITTFaxDecode", "DCTDecode", "JPXDecode", "JBIG2Decode", "Crypt", "Nonsense", "",
}
payloads := [?][]byte{
nil,
{0x00},
{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
{'~', '>'},
{'>', '>', '>'},
{0x78, 0xda},
v_chain_flate[:],
v_chain_a85[:],
v_lzw_tiff[:],
}
parm_sets := [?][2]i64{{1, 1}, {2, 0}, {12, 4}, {15, 1 << 40}, {-1, -1}, {99, 7}}
doc := filter_test_doc()
defer filter_test_doc_free(doc)
for name in names {
for payload in payloads {
for set in parm_sets {
parms := ft_parms(doc,
{"Predictor", "Columns", "Colors", "BitsPerComponent", "K", "Rows"},
{set[0], set[1], set[0], set[1], set[0], set[1]})
s := ft_stream(doc, payload, Name(name), parms)
data, derr := stream_data(doc, s)
testing.expectf(t, derr == .None || derr == .Invalid_Argument,
"%s: stream_data returned %s", name, error_string(derr))
_ = data
s2 := ft_stream(doc, payload, Name(name), parms)
img, _, _, _ := stream_image_data(doc, s2)
delete(img)
}
}
}
}
// A /Filter that is not a name or an array of names is not a filter.
@(test)
test_stream_data_nonsense_filter_entry :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
raw := [?]byte{'x', 'y', 'z'}
entries := [?]Object{i64(42), f64(1.5), true, String(transmute([]byte)string("Fl"))}
for entry in entries {
s := ft_stream(doc, raw[:], entry, nil)
got, err := stream_data(doc, s)
testing.expectf(t, err == .None, "%v: %s", entry, error_string(err))
testing.expectf(t, slice.equal(got, raw[:]), "%v: data changed", entry)
}
}
// ---------------------------------------------------------------------------
// Allocator discipline
//
// Two rules, both invisible from the outside and both easy to break again.
// A filter chain must free its intermediates -- with no Document there is
// nobody else to. And a codec's speculation must not reach the Document arena,
// where `delete` is a silent no-op and nothing is reclaimed before `close`.
// The tests below are the only thing standing between either rule and a
// regression, so they measure rather than inspect: the tracking allocator the
// test runner installs catches the first, `document_memory_used` the second.
// ---------------------------------------------------------------------------
// "Hi" run through ASCIIHexDecode twice and three times over. Decoding applies
// the chain in the order /Filter lists it (ISO 32000-2, 7.4.1), so each pass
// peels one layer of hex off and every pass but the last leaves a buffer
// behind that only `stream_data` can free.
@(private = "file")
V_HEX_TWICE :: "34383639"
@(private = "file")
V_HEX_THRICE :: "3334333833363339"
@(test)
test_stream_data_nil_document_frees_the_intermediates :: proc(t: ^testing.T) {
cases := [?]string{V_HEX_TWICE, V_HEX_THRICE}
for raw, i in cases {
depth := i + 2
// A nil Document means no arena, so the chain runs on the context
// allocator and every buffer it produces is somebody's to free.
d := new_dict(context.allocator)
a := new_array(context.allocator)
for _ in 0 ..< depth {
array_push(a, Name("ASCIIHexDecode"))
}
dict_set(d, "Filter", a)
s := new_stream(d, transmute([]byte)raw, context.allocator)
got, err := stream_data(nil, s)
testing.expectf(t, err == .None, "%d filters: %s", depth, error_string(err))
testing.expectf(t, string(got) == "Hi", "%d filters: got %q", depth, string(got))
testing.expectf(t, s.state == .Decoded, "%d filters: state %v", depth, s.state)
// Only the last stage is reachable here; if the earlier ones were not
// freed as the chain advanced, nothing can free them now and the
// tracking allocator reports the leak.
delete(got)
delete(a.items)
free(a)
delete(d^)
free(d)
free(s)
}
}
// The same chain over a real Document: the intermediates must not merely be
// freed, they must never have been taken from the arena in the first place,
// because an arena cannot give them back.
@(test)
test_stream_data_leaves_only_the_last_stage_in_the_arena :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
// /ASCII85Decode over /FlateDecode: the armour comes off into a buffer
// that is 28 bytes of deflate, which inflates into the 20 bytes that are
// kept. Only the 20 may be charged to the arena.
s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"ASCII85Decode", "FlateDecode"}), nil)
before := document_memory_used(doc)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, string(got), V_CHAIN_PLAIN)
growth := document_memory_used(doc) - before
testing.expectf(t, growth <= len(got) + FT_ARENA_SLACK,
"the arena grew by %d bytes for a %d byte stream", growth, len(got))
// Decoding twice must not decode twice, let alone allocate twice.
settled := document_memory_used(doc)
again, err2 := stream_data(doc, s)
testing.expect_value(t, err2, Error.None)
testing.expect(t, raw_data(again) == raw_data(got), "the cache was not used")
testing.expect_value(t, document_memory_used(doc), settled)
}
// An arena rounds each allocation up to its alignment, so an exact byte count
// would be a test of core:mem/virtual rather than of this file. The point is
// that growth tracks the output and not the number of buffers it took to
// produce it, and one alignment quantum of slack does not blunt that.
@(private)
FT_ARENA_SLACK :: 64
// apply_filter is where a codec's guesswork stops. Every rejected attempt and
// every superseded growth buffer belongs to the scratch allocator; exactly one
// buffer, at its final length, is charged to the caller's.
@(test)
test_apply_filter_charges_the_arena_for_one_buffer :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
before := document_memory_used(doc)
out, err := apply_filter(doc, "FlateDecode", v_chain_flate[:], nil, doc.allocator)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, string(out), V_CHAIN_PLAIN)
growth := document_memory_used(doc) - before
testing.expectf(t, growth <= len(out) + FT_ARENA_SLACK,
"the arena grew by %d bytes for %d bytes of output", growth, len(out))
}
// A filter that fails hands back nil or the input untouched, and either way
// must not have spent a byte of the arena on the attempt.
@(test)
test_apply_filter_failure_costs_the_arena_nothing :: proc(t: ^testing.T) {
broken := [?]byte{0x78, 0x9c, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07}
cases := [?]struct{name: string, data: []byte, want: Error}{
{"FlateDecode", broken[:], .Bad_Flate_Data},
{"DCTDecode", broken[:], .Unsupported_Image},
{"JPXDecode", broken[:], .Unsupported_Jpx},
{"NoSuchThing", broken[:], .Unknown_Filter},
}
for c in cases {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
before := document_memory_used(doc)
out, err := apply_filter(doc, c.name, c.data, nil, doc.allocator)
testing.expectf(t, err == c.want, "%s: %s", c.name, error_string(err))
testing.expectf(t, document_memory_used(doc) == before,
"%s: %d bytes stranded in the arena by a filter that returned an error",
c.name, document_memory_used(doc) - before)
// The pass-through errors hand back the caller's own bytes, which
// means the result is never something to free.
if err != .Bad_Flate_Data {
testing.expectf(t, raw_data(out) == raw_data(c.data),
"%s: the input was copied on an error path", c.name)
}
}
}
// The chain runs on scratch even when it is one filter long, so a filter that
// fails half way through a chain leaves nothing behind either.
@(test)
test_stream_data_failed_chain_costs_the_arena_nothing :: proc(t: ^testing.T) {
doc := filter_test_doc()
defer filter_test_doc_free(doc)
// A85 decodes to eight bytes that JPXDecode will refuse, and refusing is
// what ends the chain.
s := ft_stream(doc, v_chain_a85[:], ft_names(doc, {"ASCII85Decode", "JPXDecode"}), nil)
before := document_memory_used(doc)
got, err := stream_data(doc, s)
testing.expect_value(t, err, Error.None)
testing.expect_value(t, len(got), 0)
testing.expect_value(t, s.state, Stream_Decode_State.Failed)
testing.expectf(t, ft_repairs(doc, .Bad_Filter_Data) == 1, "no repair was recorded")
growth := document_memory_used(doc) - before
// The repair itself is charged to the arena, which is the only thing that
// should be.
testing.expectf(t, growth <= size_of(Repair) * 8 + FT_ARENA_SLACK,
"the arena grew by %d bytes for a chain that decoded to nothing", growth)
}