package pdf import "core:fmt" import "core:os" import "core:testing" // --------------------------------------------------------------------------- // Recursion bounds // // The two tests below are regressions for a pair of crashes, not hypotheticals. // Both worked the same way: an object's definition cannot be finished until // another object is parsed, and a file can name a long chain of such objects in // which no object number ever repeats. The cycle guards look for repetition, so // they never fire, and the native stack runs out. `MAX_LOAD_DEPTH` bounds the // nesting itself, which is the only thing that catches an acyclic chain. // --------------------------------------------------------------------------- // Objects 3..N are streams whose /Length is an indirect reference to the next // one, so parsing object 3 cannot finish until object N has been parsed. // Before the depth bound this segfaulted at roughly six thousand links. @(test) test_indirect_length_chain_terminates :: proc(t: ^testing.T) { N :: 8000 pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Count 0 >>") for k in 3 ..< N { pb_mark(&pb, u32(k)) pb_num(&pb, k) pb_str(&pb, " 0 obj\n<< /Length ") pb_num(&pb, k + 1) pb_str(&pb, " 0 R >>\nstream\nX\nendstream\nendobj\n") } pb_mark(&pb, u32(N)) pb_num(&pb, N) pb_str(&pb, " 0 obj\n<< /Length 1 >>\nstream\nX\nendstream\nendobj\n") at := pb_classic_xref(&pb) pb_trailer(&pb, fmt.tprintf("<< /Size %d /Root 1 0 R >>", N + 1), at) doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) testing.expect_value(t, err, Error.None) if doc == nil { return } defer close(doc) // Walking the chain must return, and must give up rather than pretend. head := get_object(doc, Ref{3, 0}) testing.expect(t, head != nil, "the head of the chain should still parse") testing.expect(t, doc_has_repair(doc, .Recursion_Limit_Hit), "hitting the nesting bound must be recorded as a repair") // The bound is on nesting, so only a bounded prefix of the chain is ever // touched -- proof the walk stopped rather than merely surviving. testing.expect(t, len(doc.cache) <= MAX_LOAD_DEPTH + 8, "only a bounded prefix of the chain should have been parsed") } // Object k's xref entry says it lives inside object stream k+1, whose own entry // says the same of k+2, and so on. Resolving object 2 walks the whole chain. @(test) test_objstm_container_chain_terminates :: proc(t: ^testing.T) { N :: 3000 pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") // Rows 0..N+1: 0 free, 1 the catalog, 2..N-1 compressed pointing one // further along, N the xref stream itself. rows := make([]Xref_Row, N + 1) defer delete(rows) rows[0] = Xref_Row{0, 0, 65535} rows[1] = Xref_Row{1, u64(pb.offs[1]), 0} for k in 2 ..< N { rows[k] = Xref_Row{2, u64(k + 1), 0} } raw := xref_rows_encode(rows[:], 1, 4, 2) defer delete(raw) at := pb_here(&pb) rows[N] = Xref_Row{1, u64(at), 0} // The stream object's own row has to carry its real offset, so re-encode // now that it is known. raw2 := xref_rows_encode(rows[:], 1, 4, 2) defer delete(raw2) pb_stream_obj(&pb, u32(N), fmt.tprintf("/Type /XRef /Size %d /W [1 4 2] /Root 1 0 R", N + 1), raw2) pb_startxref(&pb, at) doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) testing.expect_value(t, err, Error.None) if doc == nil { return } defer close(doc) // The chain resolves to null instead of walking the stack off its end. obj := get_object(doc, Ref{2, 0}) testing.expect(t, obj == nil, "a container chain that goes nowhere resolves to null") testing.expect(t, doc_has_repair(doc, .Recursion_Limit_Hit), "hitting the nesting bound must be recorded as a repair") } // --------------------------------------------------------------------------- // Object access // --------------------------------------------------------------------------- @(test) test_get_object_rejects_bad_refs :: proc(t: ^testing.T) { pb := pb_sample_classic() defer pb_destroy(&pb) doc, err := pb_open(&pb) testing.expect_value(t, err, Error.None) if doc == nil { return } defer close(doc) // Object 0 is the head of the free list and is never a real object. testing.expect(t, get_object(doc, Ref{0, 0}) == nil, "object 0 is not an object") testing.expect(t, get_object(doc, Ref{9999, 0}) == nil, "past the table reads as null") testing.expect(t, get_object(doc, Ref{MAX_OBJECT_NUMBER + 1, 0}) == nil, "an object number above the format's cap reads as null") testing.expect(t, get_object(nil, Ref{1, 0}) == nil, "a nil document reads as null") // The catalog is really there, so the checks above are not passing by // accident on a document where nothing resolves. testing.expect(t, get_dict(doc, Ref{1, 0}) != nil, "the catalog should resolve") } // A reference pointing at a reference pointing at a reference ... is legal but // only ever written to make a reader loop. It must terminate at null. @(test) test_resolve_ref_chain_is_capped :: proc(t: ^testing.T) { LINKS :: MAX_REF_CHAIN + 20 pb := pb_make() defer pb_destroy(&pb) pb_obj(&pb, 1, "<< /Type /Catalog /Pages 2 0 R >>") pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Count 0 >>") for k in 3 ..< 3 + LINKS { pb_obj(&pb, u32(k), fmt.tprintf("%d 0 R", k + 1)) } pb_obj(&pb, u32(3 + LINKS), "42") at := pb_classic_xref(&pb) pb_trailer(&pb, fmt.tprintf("<< /Size %d /Root 1 0 R >>", 4 + LINKS), at) doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) testing.expect_value(t, err, Error.None) if doc == nil { return } defer close(doc) testing.expect(t, resolve(doc, Ref{3, 0}) == nil, "an over-long ref chain resolves to null") // A chain inside the cap still resolves to its value. short := resolve(doc, Ref{u32(2 + LINKS), 0}) v, ok := to_int(short) testing.expect(t, ok, "a short chain should reach the integer") testing.expect_value(t, v, 42) } // --------------------------------------------------------------------------- // open_file, ownership and teardown // // open_file is the entry point a viewer actually calls and the only place the // library frees memory it owns outside the arena, so it needs a test that does // not depend on a corpus directory being present. // --------------------------------------------------------------------------- @(test) test_open_file_owns_and_frees_its_bytes :: proc(t: ^testing.T) { pb := pb_sample_classic() defer pb_destroy(&pb) path := "pdf_odin_open_file_test.pdf" werr := os.write_entire_file(path, pb.buf[:]) if !testing.expect(t, werr == nil, "could not write the temporary file") { return } defer os.remove(path) doc, err := open_file(path) testing.expect_value(t, err, Error.None) if doc == nil { return } testing.expect(t, doc.owns_data, "open_file must take ownership of the bytes it read") testing.expect_value(t, page_count(doc), 1) // The test allocator tracks leaks, so this call is the assertion: if close // failed to free the file buffer it owns, the suite reports it. close(doc) } @(test) test_open_file_missing :: proc(t: ^testing.T) { doc, err := open_file("this_file_does_not_exist_pdf_odin.pdf") testing.expect_value(t, err, Error.Cant_Open_File) testing.expect(t, doc == nil, "a failed open must not hand back a document") } @(test) test_open_bytes_rejects_empty :: proc(t: ^testing.T) { doc, err := open_bytes(nil) testing.expect_value(t, err, Error.Not_A_PDF) testing.expect(t, doc == nil, "") } // --------------------------------------------------------------------------- // Version and header // --------------------------------------------------------------------------- @(test) test_document_version :: proc(t: ^testing.T) { check :: proc(t: ^testing.T, header, catalog_extra: string, want_major, want_minor: int) { pb := pb_make(header) defer pb_destroy(&pb) pb_obj(&pb, 1, fmt.tprintf("<< /Type /Catalog /Pages 2 0 R %s >>", catalog_extra)) pb_obj(&pb, 2, "<< /Type /Pages /Kids [] /Count 0 >>") at := pb_classic_xref(&pb) pb_trailer(&pb, "<< /Size 3 /Root 1 0 R >>", at) doc, err := open_bytes(pb.buf[:], Open_Options{skip_page_tree = true}) if !testing.expect(t, err == .None && doc != nil, "should open") { return } defer close(doc) major, minor := document_version(doc) testing.expect_value(t, major, want_major) testing.expect_value(t, minor, want_minor) } check(t, "%PDF-1.4\n", "", 1, 4) // An incremental update may raise the version without rewriting the header. check(t, "%PDF-1.4\n", "/Version /1.7", 1, 7) // It may not lower it. check(t, "%PDF-1.7\n", "/Version /1.4", 1, 7) check(t, "%PDF-2.0\n", "", 2, 0) // Garbage in /Version must be ignored, not read as arithmetic on bytes. check(t, "%PDF-1.4\n", "/Version /XX", 1, 4) check(t, "%PDF-1.4\n", "/Version /A.B", 1, 4) check(t, "%PDF-1.4\n", "/Version /1", 1, 4) } // Junk before %PDF- is common: a mail header, an HTTP response, a wrapper. // Every offset in such a file is relative to the header, not to zero. @(test) test_header_offset :: proc(t: ^testing.T) { pb := pb_sample_classic() defer pb_destroy(&pb) JUNK :: "GARBAGE BEFORE THE HEADER\r\n" shifted := make([dynamic]byte) defer delete(shifted) append(&shifted, JUNK) append(&shifted, ..pb.buf[:]) doc, err := open_bytes(shifted[:], Open_Options{skip_page_tree = true}) testing.expect_value(t, err, Error.None) if doc == nil { return } defer close(doc) testing.expect_value(t, doc.header_offset, i64(len(JUNK))) testing.expect(t, get_dict(doc, Ref{1, 0}) != nil, "objects must still resolve") } // --------------------------------------------------------------------------- // Memory accounting // --------------------------------------------------------------------------- @(test) test_document_memory_used :: proc(t: ^testing.T) { testing.expect_value(t, document_memory_used(nil), 0) testing.expect_value(t, document_memory_reserved(nil), 0) pb := pb_sample_classic() defer pb_destroy(&pb) doc, err := pb_open(&pb) if !testing.expect(t, err == .None && doc != nil, "should open") { return } defer close(doc) used := document_memory_used(doc) testing.expect(t, used > 0, "an opened document has parsed something") testing.expect(t, document_memory_reserved(doc) >= used, "reserved covers used") // Parsing more must be visible, since this is the only way a caller can // account for a document's footprint. _ = get_dict(doc, Ref{3, 0}) testing.expect(t, document_memory_used(doc) >= used, "usage does not go backwards") } // --------------------------------------------------------------------------- // Repairs // --------------------------------------------------------------------------- @(test) test_add_repair_is_bounded :: proc(t: ^testing.T) { pb := pb_sample_classic() defer pb_destroy(&pb) doc, err := pb_open(&pb) if !testing.expect(t, err == .None && doc != nil, "should open") { return } defer close(doc) for _ in 0 ..< MAX_REPAIRS + 100 { add_repair(doc, .Missing_Object, 0, Ref{1, 0}) } testing.expect(t, len(doc.repairs) <= MAX_REPAIRS, "the repair log must be capped") testing.expect(t, doc.is_damaged, "recording a repair marks the document damaged") // A nil document must be safe to report against. add_repair(nil, .Missing_Object, 0, {}) } @(test) test_repair_describe :: proc(t: ^testing.T) { with_ref := repair_describe(Repair{kind = .Bad_Stream_Length, offset = 42, ref = Ref{7, 0}}) defer delete(with_ref) testing.expect(t, len(with_ref) > 0, "") no_ref := repair_describe(Repair{kind = .Rebuilt_Xref, offset = 9}) defer delete(no_ref) testing.expect(t, len(no_ref) > 0, "") // Every kind must have a description, so a log line never says "unknown". for k in Repair_Kind { testing.expect(t, repair_string(k) != "unknown repair", "every repair kind needs a description") } for e in Error { testing.expect(t, error_string(e) != "unknown error", "every error needs a description") } }