#include <qpdf/assert_test.h>

#include <qpdf/qpdf-c.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static char* whoami = 0;
static qpdf_data qpdf = 0;

static FILE*
safe_fopen(char const* filename, char const* mode)
{
    // This function is basically a "C" port of QUtil::safe_fopen.
    FILE* f = 0;
#ifdef _MSC_VER
    errno_t err = fopen_s(&f, filename, mode);
    if (err != 0) {
        char buf[94];
        strerror_s(buf, sizeof(buf), errno);
        fprintf(stderr, "%s: unable to open %s: %s\n", whoami, filename, buf);
        exit(2);
    }
#else
    f = fopen(filename, mode);
    if (f == NULL) {
        fprintf(stderr, "%s: unable to open %s: %s\n", whoami, filename, strerror(errno));
        exit(2);
    }
#endif
    return f;
}

static void
print_error(char const* label, qpdf_data q, qpdf_error e)
{
    printf("%s: %s\n", label, qpdf_get_error_full_text(q, e));
    printf("  code: %d\n", qpdf_get_error_code(q, e));
    printf("  file: %s\n", qpdf_get_error_filename(q, e));
    printf("  pos: %lld\n", qpdf_get_error_file_position(q, e));
    printf("  text: %s\n", qpdf_get_error_message_detail(q, e));
}

static void
report_errors()
{
    qpdf_error e = 0;
    while (qpdf_more_warnings(qpdf)) {
        e = qpdf_next_warning(qpdf);
        print_error("warning", qpdf, e);
    }
    if (qpdf_has_error(qpdf)) {
        e = qpdf_get_error(qpdf);
        assert(qpdf_has_error(qpdf) == QPDF_FALSE);
        print_error("error", qpdf, e);
    } else {
        e = qpdf_get_error(qpdf);
        assert(e == 0);
        assert(qpdf_get_error_code(qpdf, e) == qpdf_e_success);
        // Call these to ensure that they can be called on a null
        // error pointer.
        (void)qpdf_get_error_full_text(qpdf, e);
        (void)qpdf_get_error_filename(qpdf, e);
        (void)qpdf_get_error_file_position(qpdf, e);
        (void)qpdf_get_error_message_detail(qpdf, e);
    }
}

static void
handle_oh_error(qpdf_data q, char const* label)
{
    if (qpdf_has_error(q)) {
        print_error(label, q, qpdf_get_error(q));
    }
}

static void
read_file_into_memory(char const* filename, char** buf, unsigned long* size)
{
    char* buf_p = 0;
    FILE* f = NULL;
    size_t bytes_read = 0;
    size_t len = 0;

    f = safe_fopen(filename, "rb");
    fseek(f, 0, SEEK_END);
    *size = (unsigned long)ftell(f);
    fseek(f, 0, SEEK_SET);
    *buf = malloc(*size);
    if (*buf == NULL) {
        fprintf(stderr, "%s: unable to allocate %lu bytes\n", whoami, *size);
        exit(2);
    }
    buf_p = *buf;
    bytes_read = 0;
    len = 0;
    while ((len = fread(buf_p + bytes_read, 1, *size - bytes_read, f)) > 0) {
        bytes_read += len;
    }
    if (bytes_read != *size) {
        if (ferror(f)) {
            fprintf(stderr, "%s: failure reading file %s into memory:", whoami, filename);
        } else {
            fprintf(stderr, "%s: premature EOF reading file %s:", whoami, filename);
        }
        fprintf(stderr, " read %lu, wanted %lu\n", (unsigned long)bytes_read, (unsigned long)*size);
        exit(2);
    }
    fclose(f);
}

static void
count_progress(int percent, void* data)
{
    ++(*(int*)data);
}

static int
write_to_file(char const* data, size_t size, void* udata)
{
    FILE* f = (FILE*)udata;
    return fwrite(data, 1, size, f) != size;
}

static int
custom_log(char const* data, size_t size, void* udata)
{
    fprintf(stderr, "|custom|");
    fwrite(data, 1, size, stderr);
    fflush(stderr);
    return 0;
}

static void
test01(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    printf("version: %s\n", qpdf_get_pdf_version(qpdf));
    if (qpdf_get_pdf_extension_level(qpdf) > 0) {
        printf("extension level: %d\n", qpdf_get_pdf_extension_level(qpdf));
    }
    printf("linearized: %d\n", qpdf_is_linearized(qpdf));
    printf("encrypted: %d\n", qpdf_is_encrypted(qpdf));
    if (qpdf_is_encrypted(qpdf)) {
        printf("user password: %s\n", qpdf_get_user_password(qpdf));
        printf("extract for accessibility: %d\n", qpdf_allow_accessibility(qpdf));
        printf("extract for any purpose: %d\n", qpdf_allow_extract_all(qpdf));
        printf("print low resolution: %d\n", qpdf_allow_print_low_res(qpdf));
        printf("print high resolution: %d\n", qpdf_allow_print_high_res(qpdf));
        printf("modify document assembly: %d\n", qpdf_allow_modify_assembly(qpdf));
        printf("modify forms: %d\n", qpdf_allow_modify_form(qpdf));
        printf("modify annotations: %d\n", qpdf_allow_modify_annotation(qpdf));
        printf("modify other: %d\n", qpdf_allow_modify_other(qpdf));
        printf("modify anything: %d\n", qpdf_allow_modify_all(qpdf));
    }
    report_errors();
}

static void
test02(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_set_suppress_warnings(qpdf, QPDF_TRUE);
    if (((qpdf_read(qpdf, infile, password) & QPDF_ERRORS) == 0) &&
        ((qpdf_init_write(qpdf, outfile) & QPDF_ERRORS) == 0)) {
        qpdf_set_static_ID(qpdf, QPDF_TRUE);
        qpdf_write(qpdf);
    }
    report_errors();
}

static void
test03(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_content_normalization(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test04(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_set_ignore_xref_streams(qpdf, QPDF_TRUE);
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test05(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    int count = 0;
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_register_progress_reporter(qpdf, count_progress, (void*)&count);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_linearization(qpdf, QPDF_TRUE);
    qpdf_set_compress_streams(qpdf, QPDF_FALSE); // Don't depend on zlib
    qpdf_write(qpdf);
    /* make sure progress reporter was called */
    assert(count > 0);
    report_errors();
}

static void
test06(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    char* buf = NULL;
    unsigned long size = 0;
    read_file_into_memory(infile, &buf, &size);
    qpdf_read_memory(qpdf, infile, buf, size, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_object_stream_mode(qpdf, qpdf_o_generate);
    qpdf_write(qpdf);
    report_errors();
    free(buf);
}

static void
test07(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_qdf_mode(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test08(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_qdf_mode(qpdf, QPDF_TRUE);
    qpdf_set_suppress_original_object_IDs(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test09(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_stream_data_mode(qpdf, qpdf_s_uncompress);
    qpdf_write(qpdf);
    report_errors();
}

static void
test10(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_set_attempt_recovery(qpdf, QPDF_FALSE);
    qpdf_read(qpdf, infile, password);
    report_errors();
}

static void
test11(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_r2_encryption_parameters_insecure(
        qpdf, "user1", "owner1", QPDF_FALSE, QPDF_TRUE, QPDF_TRUE, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test12(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_r3_encryption_parameters_insecure(
        qpdf,
        "user2",
        "owner2",
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        qpdf_r3p_low);
    qpdf_write(qpdf);
    report_errors();
}

static void
test13(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    printf("user password: %s\n", qpdf_get_user_password(qpdf));
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_preserve_encryption(qpdf, QPDF_FALSE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test14(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_minimum_pdf_version_and_extension(qpdf, "1.7", 8);
    qpdf_write(qpdf);
    qpdf_init_write(qpdf, xarg);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_force_pdf_version(qpdf, "1.4");
    qpdf_write(qpdf);
    report_errors();
}

static void
test15(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_static_aes_IV(qpdf, QPDF_TRUE);
    qpdf_set_r4_encryption_parameters_insecure(
        qpdf,
        "user2",
        "owner2",
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        qpdf_r3p_low,
        QPDF_TRUE,
        QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
print_info(char const* key)
{
    char const* value = qpdf_get_info_key(qpdf, key);
    printf("Info key %s: %s\n", key, (value ? value : "(null)"));
}

static void
test16(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    unsigned long buflen = 0L;
    unsigned char const* buf = 0;
    FILE* f = 0;

    qpdf_read(qpdf, infile, password);
    print_info("/Author");
    print_info("/Producer");
    print_info("/Creator");
    qpdf_set_info_key(qpdf, "/Author", "Mr. Potato Head");
    qpdf_set_info_key(qpdf, "/Producer", "QPDF library");
    qpdf_set_info_key(qpdf, "/Creator", 0);
    print_info("/Author");
    print_info("/Producer");
    print_info("/Creator");
    qpdf_init_write_memory(qpdf);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_static_aes_IV(qpdf, QPDF_TRUE);
    qpdf_set_stream_data_mode(qpdf, qpdf_s_uncompress);
    qpdf_write(qpdf);
    f = safe_fopen(outfile, "wb");
    buflen = (unsigned long)(qpdf_get_buffer_length(qpdf));
    buf = qpdf_get_buffer(qpdf);
    fwrite(buf, 1, buflen, f);
    fclose(f);
    report_errors();
}

static void
test17(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_static_aes_IV(qpdf, QPDF_TRUE);
    qpdf_set_r5_encryption_parameters2(
        qpdf,
        "user3",
        "owner3",
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        qpdf_r3p_low,
        QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test18(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_static_aes_IV(qpdf, QPDF_TRUE);
    qpdf_set_r6_encryption_parameters2(
        qpdf,
        "user4",
        "owner4",
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        QPDF_TRUE,
        qpdf_r3p_low,
        QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test19(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_deterministic_ID(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test20(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_static_aes_IV(qpdf, QPDF_TRUE);
    qpdf_set_compress_streams(qpdf, QPDF_FALSE);
    qpdf_set_decode_level(qpdf, qpdf_dl_specialized);
    qpdf_write(qpdf);
    report_errors();
}

static void
test21(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_static_aes_IV(qpdf, QPDF_TRUE);
    qpdf_set_preserve_unreferenced_objects(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test22(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    qpdf_read(qpdf, infile, password);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_static_aes_IV(qpdf, QPDF_TRUE);
    qpdf_set_compress_streams(qpdf, QPDF_FALSE);
    qpdf_set_newline_before_endstream(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test23(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* Test check and also exercise custom logger */
    qpdflogger_handle l1 = qpdf_get_logger(qpdf);
    qpdflogger_handle l2 = qpdflogger_default_logger();
    assert(qpdflogger_equal(l1, l2));
    qpdflogger_cleanup(&l1);
    qpdflogger_cleanup(&l2);
    qpdflogger_handle l = qpdflogger_create();
    qpdflogger_set_warn(l, qpdf_log_dest_custom, custom_log, NULL);
    qpdf_set_logger(qpdf, l);
    qpdflogger_handle l3 = qpdf_get_logger(qpdf);
    assert(qpdflogger_equal(l, l3));
    qpdflogger_cleanup(&l);
    qpdflogger_cleanup(&l3);

    QPDF_ERROR_CODE status = 0;
    qpdf_read(qpdf, infile, password);
    status = qpdf_check_pdf(qpdf);
    printf("status: %d\n", status);
    report_errors();
}

static void
test24(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test case is designed for minimal.pdf. Pull objects out of
     * minimal.pdf to make sure all our accessors work as expected.
     */

    qpdf_read(qpdf, infile, password);
    qpdf_oh trailer = qpdf_get_trailer(qpdf);
    /* The library never returns 0 */
    assert(trailer == 1);

    /* Get root two different ways */
    qpdf_oh root = qpdf_get_root(qpdf);
    assert(qpdf_oh_get_generation(qpdf, root) == 0);
    qpdf_oh root_from_trailer = qpdf_oh_get_key(qpdf, trailer, "/Root");
    assert(qpdf_oh_get_object_id(qpdf, root) == qpdf_oh_get_object_id(qpdf, root_from_trailer));

    /* Go to the first page and look at all the keys */
    qpdf_oh pages = qpdf_oh_get_key(qpdf, root, "/Pages");
    assert(qpdf_oh_is_dictionary(qpdf, pages));
    assert(qpdf_oh_get_type_code(qpdf, pages) == ot_dictionary);
    assert(strcmp(qpdf_oh_get_type_name(qpdf, pages), "dictionary") == 0);
    assert(qpdf_oh_is_initialized(qpdf, pages));
    qpdf_oh kids = qpdf_oh_get_key(qpdf, pages, "/Kids");
    assert(qpdf_oh_is_array(qpdf, kids));
    assert(qpdf_oh_get_type_code(qpdf, kids) == ot_array);
    assert(strcmp(qpdf_oh_get_type_name(qpdf, kids), "array") == 0);
    assert(qpdf_oh_get_array_n_items(qpdf, kids) == 1);
    qpdf_oh page1 = qpdf_oh_get_array_item(qpdf, kids, 0);
    qpdf_oh_begin_dict_key_iter(qpdf, page1);
    while (qpdf_oh_dict_more_keys(qpdf)) {
        printf("page dictionary key: %s\n", qpdf_oh_dict_next_key(qpdf));
    }

    /* Inspect the first page */
    qpdf_oh type = qpdf_oh_get_key(qpdf, page1, "/Type");
    assert(qpdf_oh_is_name(qpdf, type));
    assert(qpdf_oh_get_type_code(qpdf, type) == ot_name);
    assert(strcmp(qpdf_oh_get_type_name(qpdf, type), "name") == 0);
    assert(strcmp(qpdf_oh_get_name(qpdf, type), "/Page") == 0);
    qpdf_oh parent = qpdf_oh_get_key(qpdf, page1, "/Parent");
    assert(qpdf_oh_is_indirect(qpdf, parent));
    qpdf_oh mediabox = qpdf_oh_get_key(qpdf, page1, "/MediaBox");
    assert(!qpdf_oh_is_scalar(qpdf, mediabox));
    assert(qpdf_oh_is_array(qpdf, mediabox));
    assert(qpdf_oh_get_array_n_items(qpdf, mediabox) == 4);
    for (int i = 0; i < 4; ++i) {
        qpdf_oh item = qpdf_oh_get_array_item(qpdf, mediabox, i);
        printf(
            "item %d: %d %.2f\n",
            i,
            qpdf_oh_get_int_value_as_int(qpdf, item),
            qpdf_oh_get_numeric_value(qpdf, item));
    }

    /* Exercise different ways of looking at integers */
    qpdf_oh i2 = qpdf_oh_get_array_item(qpdf, mediabox, 2);
    assert(qpdf_oh_get_int_value_as_int(qpdf, i2) == 612);
    assert(qpdf_oh_get_int_value(qpdf, i2) == 612ll);
    assert(qpdf_oh_get_uint_value_as_uint(qpdf, i2) == 612u);
    assert(qpdf_oh_get_uint_value(qpdf, i2) == 612ull);
    /* Exercise accessors of other object types */
    assert(!qpdf_oh_is_operator(qpdf, i2));
    assert(!qpdf_oh_is_inline_image(qpdf, i2));
    /* Chain calls. */
    qpdf_oh encoding = qpdf_oh_get_key(
        qpdf,
        qpdf_oh_get_key(
            qpdf,
            qpdf_oh_get_key(qpdf, qpdf_oh_get_key(qpdf, page1, "/Resources"), "/Font"),
            "/F1"),
        "/Encoding");
    assert(strcmp(qpdf_oh_get_name(qpdf, encoding), "/WinAnsiEncoding") == 0);

    qpdf_oh res = qpdf_oh_get_key_if_dict(qpdf, page1, "/Resources");
    assert(qpdf_oh_has_key(qpdf, res, "/Font"));
    /* check no warning when called with null */
    while (qpdf_more_warnings(qpdf)) {
        qpdf_next_warning(qpdf);
    }
    res = qpdf_oh_get_key_if_dict(qpdf, qpdf_oh_get_key_if_dict(qpdf, page1, "/Missing"), "/Font");
    assert(!qpdf_more_warnings(qpdf));

    /* Look at page contents to exercise stream functions */
    qpdf_oh contents = qpdf_oh_get_key(qpdf, page1, "/Contents");
    assert(qpdf_oh_is_stream(qpdf, contents));
    assert(qpdf_oh_get_type_code(qpdf, contents) == ot_stream);
    assert(strcmp(qpdf_oh_get_type_name(qpdf, contents), "stream") == 0);
    qpdf_oh contents_dict = qpdf_oh_get_dict(qpdf, contents);
    assert(!qpdf_oh_is_scalar(qpdf, contents));
    assert(!qpdf_oh_is_scalar(qpdf, contents_dict));
    qpdf_oh contents_length = qpdf_oh_get_key(qpdf, contents_dict, "/Length");
    assert(qpdf_oh_is_integer(qpdf, contents_length));
    assert(qpdf_oh_get_type_code(qpdf, contents_length) == ot_integer);
    assert(strcmp(qpdf_oh_get_type_name(qpdf, contents_length), "integer") == 0);
    assert(qpdf_oh_is_scalar(qpdf, contents_length));
    assert(qpdf_oh_is_number(qpdf, contents_length));
    qpdf_oh contents_array = qpdf_oh_wrap_in_array(qpdf, contents);
    assert(qpdf_oh_get_array_n_items(qpdf, contents_array) == 1);
    assert(
        qpdf_oh_get_object_id(qpdf, qpdf_oh_get_array_item(qpdf, contents_array, 0)) ==
        qpdf_oh_get_object_id(qpdf, contents));
    /* Wrap in array for a non-trivial case */
    qpdf_oh wrapped_contents_array = qpdf_oh_wrap_in_array(qpdf, contents_array);
    assert(qpdf_oh_get_array_n_items(qpdf, wrapped_contents_array) == 1);
    assert(
        qpdf_oh_get_object_id(qpdf, qpdf_oh_get_array_item(qpdf, wrapped_contents_array, 0)) ==
        qpdf_oh_get_object_id(qpdf, contents));

    /* Exercise functions that work with indirect objects */
    qpdf_oh resources = qpdf_oh_get_key(qpdf, page1, "/Resources");
    qpdf_oh procset = qpdf_oh_get_key(qpdf, resources, "/ProcSet");
    assert(strcmp(qpdf_oh_unparse(qpdf, procset), "5 0 R") == 0);
    assert(strcmp(qpdf_oh_unparse_resolved(qpdf, procset), "[ /PDF /Text ]") == 0);
    qpdf_oh_make_direct(qpdf, procset);
    assert(strcmp(qpdf_oh_unparse(qpdf, procset), "[ /PDF /Text ]") == 0);
    /* The replaced /ProcSet can be seen to be a direct object in the
     * expected output PDF.
     */
    qpdf_oh_replace_key(qpdf, resources, "/ProcSet", procset);

    /* Release and access to exercise handling of object handle errors
     * and to show that write still works after releasing. This test
     * uses the default oh error handler, so messages get written to
     * stderr. The warning about using the default error handler only
     * appears once.
     */
    qpdf_oh_release(qpdf, page1);
    contents = qpdf_oh_get_key(qpdf, page1, "/Contents");
    assert(qpdf_oh_is_null(qpdf, contents));
    assert(qpdf_oh_get_type_code(qpdf, contents) == ot_null);
    assert(strcmp(qpdf_oh_get_type_name(qpdf, contents), "null") == 0);
    assert(qpdf_oh_is_array(qpdf, mediabox));
    qpdf_oh_release_all(qpdf);
    assert(!qpdf_oh_is_null(qpdf, mediabox));
    assert(!qpdf_oh_is_array(qpdf, mediabox));
    /* Make sure something is assigned when we exit so we check that
     * it gets properly freed.
     */
    qpdf_get_root(qpdf);

    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_qdf_mode(qpdf, QPDF_TRUE);
    qpdf_set_suppress_original_object_IDs(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test25(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test case is designed for minimal.pdf. */
    qpdf_read(qpdf, infile, password);
    qpdf_oh root = qpdf_get_root(qpdf);

    /* Parse objects from a string */
    qpdf_oh parsed =
        qpdf_oh_parse(qpdf, "[ 1 2.0 (3\xf7) << /Four [/Five] >> null true false /Six]");
    qpdf_oh p_int = qpdf_oh_get_array_item(qpdf, parsed, 0);
    qpdf_oh p_real = qpdf_oh_get_array_item(qpdf, parsed, 1);
    qpdf_oh p_string = qpdf_oh_get_array_item(qpdf, parsed, 2);
    qpdf_oh p_dict = qpdf_oh_get_array_item(qpdf, parsed, 3);
    qpdf_oh p_null = qpdf_oh_get_array_item(qpdf, parsed, 4);
    qpdf_oh p_bool = qpdf_oh_get_array_item(qpdf, parsed, 5);
    qpdf_oh p_bool_f = qpdf_oh_get_array_item(qpdf, parsed, 6);
    qpdf_oh p_name = qpdf_oh_get_array_item(qpdf, parsed, 7);
    assert(qpdf_oh_is_integer(qpdf, p_int) && qpdf_oh_get_int_value_as_int(qpdf, p_int) == 1);
    long long l = 0;
    assert(qpdf_oh_get_value_as_longlong(qpdf, p_bool, &l) == QPDF_FALSE);
    assert((qpdf_oh_get_value_as_longlong(qpdf, p_int, &l) == QPDF_TRUE) && (l == 1));
    int i = 0;
    assert(qpdf_oh_get_value_as_int(qpdf, p_bool, &i) == QPDF_FALSE);
    assert((qpdf_oh_get_value_as_int(qpdf, p_int, &i) == QPDF_TRUE) && (i == 1));
    unsigned long long ul = 0u;
    assert(qpdf_oh_get_value_as_ulonglong(qpdf, p_bool, &ul) == QPDF_FALSE);
    assert((qpdf_oh_get_value_as_ulonglong(qpdf, p_int, &ul) == QPDF_TRUE) && (ul == 1u));
    unsigned int u = 0u;
    assert(qpdf_oh_get_value_as_uint(qpdf, p_bool, &u) == QPDF_FALSE);
    assert((qpdf_oh_get_value_as_uint(qpdf, p_int, &u) == QPDF_TRUE) && (u == 1u));
    double d = 0.0;
    assert(qpdf_oh_get_value_as_number(qpdf, p_bool, &d) == QPDF_FALSE);
    assert(
        (qpdf_oh_get_value_as_number(qpdf, p_int, &d) == QPDF_TRUE) && ((d - 1.0) < 1e-6) &&
        ((d - 1.0) > -1e-6));
    assert(qpdf_oh_get_type_code(qpdf, p_int) == ot_integer);
    assert(strcmp(qpdf_oh_get_type_name(qpdf, p_int), "integer") == 0);
    assert(
        qpdf_oh_is_real(qpdf, p_real) &&
        (strcmp(qpdf_oh_get_real_value(qpdf, p_real), "2.0") == 0) &&
        qpdf_oh_get_numeric_value(qpdf, p_real) == 2.0);
    const char* r = "";
    size_t length = 0;
    assert(
        (qpdf_oh_get_value_as_real(qpdf, p_name, &r, &length) == QPDF_FALSE) &&
        (strcmp(r, "") == 0) && (length == 0));
    assert(
        (qpdf_oh_get_value_as_real(qpdf, p_real, &r, &length) == QPDF_TRUE) &&
        (strcmp(r, "2.0") == 0) && (length == 3));
    assert(qpdf_oh_get_type_code(qpdf, p_real) == ot_real);
    assert(strcmp(qpdf_oh_get_type_name(qpdf, p_real), "real") == 0);
    assert(
        qpdf_oh_is_string(qpdf, p_string) &&
        (strcmp(qpdf_oh_get_string_value(qpdf, p_string), "3\xf7") == 0) &&
        (strcmp(qpdf_oh_get_utf8_value(qpdf, p_string), "3\xc3\xb7") == 0) &&
        (strcmp(qpdf_oh_unparse_binary(qpdf, p_string), "<33f7>") == 0));
    const char* str = "";
    length = 0;
    assert(
        (qpdf_oh_get_value_as_string(qpdf, p_name, &str, &length) == QPDF_FALSE) &&
        (strcmp(str, "") == 0) && (length == 0));
    assert(
        (qpdf_oh_get_value_as_string(qpdf, p_string, &str, &length) == QPDF_TRUE) &&
        (strcmp(str, "3\xf7") == 0) && (length == 2));
    const char* utf8 = "";
    length = 0;
    assert(
        (qpdf_oh_get_value_as_utf8(qpdf, p_name, &utf8, &length) == QPDF_FALSE) &&
        (strcmp(utf8, "") == 0) && (length == 0));
    assert(
        (qpdf_oh_get_value_as_utf8(qpdf, p_string, &utf8, &length) == QPDF_TRUE) &&
        (strcmp(utf8, "3\xc3\xb7") == 0) && (length == 3));
    assert(qpdf_oh_get_type_code(qpdf, p_string) == ot_string);
    assert(!qpdf_oh_is_name_and_equals(qpdf, p_string, "3\xf7"));
    assert(strcmp(qpdf_oh_get_type_name(qpdf, p_string), "string") == 0);
    assert(qpdf_oh_is_dictionary(qpdf, p_dict));
    qpdf_oh p_five = qpdf_oh_get_key(qpdf, p_dict, "/Four");
    assert(qpdf_oh_is_or_has_name(qpdf, p_five, "/Five"));
    assert(!qpdf_oh_is_name_and_equals(qpdf, p_five, "/Five"));
    assert(qpdf_oh_is_or_has_name(qpdf, qpdf_oh_get_array_item(qpdf, p_five, 0), "/Five"));
    assert(qpdf_oh_is_name_and_equals(qpdf, qpdf_oh_get_array_item(qpdf, p_five, 0), "/Five"));
    assert(qpdf_oh_is_null(qpdf, p_null));
    assert(qpdf_oh_get_type_code(qpdf, p_null) == ot_null);
    assert(strcmp(qpdf_oh_get_type_name(qpdf, p_null), "null") == 0);
    assert(qpdf_oh_is_bool(qpdf, p_bool) && (qpdf_oh_get_bool_value(qpdf, p_bool) == QPDF_TRUE));
    QPDF_BOOL b = QPDF_FALSE;
    assert((qpdf_oh_get_value_as_bool(qpdf, p_int, &b) == QPDF_FALSE) && b == QPDF_FALSE);
    assert((qpdf_oh_get_value_as_bool(qpdf, p_bool, &b) == QPDF_TRUE) && b == QPDF_TRUE);
    assert((qpdf_oh_get_value_as_bool(qpdf, p_bool_f, &b) == QPDF_TRUE) && b == QPDF_FALSE);
    assert(qpdf_oh_get_type_code(qpdf, p_bool) == ot_boolean);
    assert(strcmp(qpdf_oh_get_type_name(qpdf, p_bool), "boolean") == 0);
    const char* n = "";
    length = 0;
    assert(
        (qpdf_oh_get_value_as_name(qpdf, p_string, &n, &length) == QPDF_FALSE) &&
        (strcmp(n, "") == 0) && (length == 0));
    assert(
        (qpdf_oh_get_value_as_name(qpdf, p_name, &n, &length) == QPDF_TRUE) &&
        (strcmp(n, "/Six") == 0) && (length == 4));
    qpdf_oh_erase_item(qpdf, parsed, 4);
    qpdf_oh_insert_item(
        qpdf,
        parsed,
        2,
        qpdf_oh_parse(qpdf, "<</A 1 /B 2 /C 3 /D 4 /Type /Test /Subtype /Marvin>>"));
    qpdf_oh new_dict = qpdf_oh_get_array_item(qpdf, parsed, 2);
    assert(qpdf_oh_has_key(qpdf, new_dict, "/A"));
    assert(qpdf_oh_has_key(qpdf, new_dict, "/D"));
    assert(qpdf_oh_is_dictionary_of_type(qpdf, new_dict, "/Test", ""));
    assert(qpdf_oh_is_dictionary_of_type(qpdf, new_dict, "/Test", 0));
    assert(qpdf_oh_is_dictionary_of_type(qpdf, new_dict, "/Test", "/Marvin"));
    assert(!qpdf_oh_is_dictionary_of_type(qpdf, new_dict, "/Test2", ""));
    assert(!qpdf_oh_is_dictionary_of_type(qpdf, new_dict, "/Test", "/M"));
    assert(qpdf_oh_is_dictionary_of_type(qpdf, new_dict, "", ""));
    qpdf_oh new_array = qpdf_oh_new_array(qpdf);
    qpdf_oh_replace_or_remove_key(qpdf, new_dict, "/A", qpdf_oh_new_null(qpdf));
    qpdf_oh_replace_or_remove_key(qpdf, new_dict, "/B", new_array);
    qpdf_oh_replace_key(qpdf, new_dict, "/C", qpdf_oh_new_dictionary(qpdf));
    qpdf_oh_remove_key(qpdf, new_dict, "/D");
    assert(!qpdf_oh_has_key(qpdf, new_dict, "/A"));
    assert(!qpdf_oh_has_key(qpdf, new_dict, "/D"));
    qpdf_oh_append_item(qpdf, new_array, qpdf_oh_new_string(qpdf, "potato"));
    qpdf_oh_append_item(qpdf, new_array, qpdf_oh_new_unicode_string(qpdf, "qww\xc3\xb7\xcf\x80"));
    qpdf_oh_append_item(
        qpdf, new_array, qpdf_oh_new_binary_unicode_string(qpdf, "qw\x00w\xc3\xb7\xcf\x80", 8));
    qpdf_oh_append_item(qpdf, new_array, qpdf_oh_new_null(qpdf)); /* 2 */
    qpdf_oh_append_item(qpdf, new_array, qpdf_oh_new_null(qpdf)); /* 3 */
    qpdf_oh_set_array_item(qpdf, new_array, 3, qpdf_oh_new_name(qpdf, "/Quack"));
    qpdf_oh_append_item(qpdf, new_array, qpdf_oh_new_real_from_double(qpdf, 4.123, 2));
    qpdf_oh_append_item(qpdf, new_array, qpdf_oh_new_real_from_string(qpdf, "5.0"));
    qpdf_oh_append_item(qpdf, new_array, qpdf_oh_new_integer(qpdf, 6));
    qpdf_oh_append_item(qpdf, new_array, qpdf_oh_new_bool(qpdf, QPDF_TRUE));
    qpdf_oh_replace_key(qpdf, root, "/QTest", new_dict);

    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_qdf_mode(qpdf, QPDF_TRUE);
    qpdf_set_suppress_original_object_IDs(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}
static void
test26(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* Make sure we detect uninitialized objects */
    qpdf_data qpdf2 = qpdf_init();
    qpdf_oh trailer = qpdf_get_trailer(qpdf2);
    assert(!qpdf_oh_is_initialized(qpdf2, trailer));
    assert(qpdf_oh_get_type_code(qpdf, trailer) == ot_uninitialized);
    qpdf_cleanup(&qpdf2);
}

static void
test27(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* Exercise a string with a null. Since the regular methods return
     * char*, we can't see past the null character without looking
     * explicitly at the length.
     */
    qpdf_oh p_string_with_null = qpdf_oh_parse(qpdf, "<6f6e650074776f>");
    assert(qpdf_oh_is_string(qpdf, p_string_with_null));
    assert(strcmp(qpdf_oh_get_string_value(qpdf, p_string_with_null), "one") == 0);
    assert(qpdf_get_last_string_length(qpdf) == 7);
    /* memcmp adds a character to verify the trailing null */
    assert(memcmp(qpdf_oh_get_string_value(qpdf, p_string_with_null), "one\000two", 8) == 0);
    size_t length = 0;
    p_string_with_null = qpdf_oh_new_binary_string(qpdf, "potato\000salad", 12);
    /* memcmp adds a character to verify the trailing null */
    assert(
        memcmp(
            qpdf_oh_get_binary_string_value(qpdf, p_string_with_null, &length),
            "potato\000salad",
            13) == 0);
    assert(qpdf_get_last_string_length(qpdf) == 12);
    assert(length == 12);
    /* repeat for UTF8 string */
    qpdf_oh p_utf8_string_with_null = qpdf_oh_parse(qpdf, "<feff007100770000007700f703c0>");
    assert(qpdf_oh_is_string(qpdf, p_utf8_string_with_null));
    assert(
        strcmp(qpdf_oh_get_utf8_value(qpdf, p_utf8_string_with_null), "qw\x00w\xc3\xb7\xcf\x80") ==
        0);
    assert(qpdf_get_last_string_length(qpdf) == 8);
    /* memcmp adds a character to verify the trailing null */
    assert(
        memcmp(
            qpdf_oh_get_utf8_value(qpdf, p_utf8_string_with_null), "qw\x00w\xc3\xb7\xcf\x80", 8) ==
        0);
    p_utf8_string_with_null = qpdf_oh_new_binary_unicode_string(qpdf, "qw\x00w\xc3\xb7\xcf\x80", 8);
    /* memcmp adds a character to verify the trailing null */
    assert(
        memcmp(
            qpdf_oh_get_binary_utf8_value(qpdf, p_utf8_string_with_null, &length),
            "qw\x00w\xc3\xb7\xcf\x80",
            9) == 0);
    assert(qpdf_get_last_string_length(qpdf) == 8);
    assert(length == 8);
}

static void
test28(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test case is designed for minimal.pdf. */

    /* Look at the media box. The media box is in array. Trivially
     * wrap it and also clone it and make sure we get different
     * handles with the same contents.
     */
    qpdf_read(qpdf, infile, password);
    qpdf_oh root = qpdf_get_root(qpdf);
    qpdf_oh pages = qpdf_oh_get_key(qpdf, root, "/Pages");
    qpdf_oh kids = qpdf_oh_get_key(qpdf, pages, "/Kids");
    qpdf_oh page1 = qpdf_oh_get_array_item(qpdf, kids, 0);
    qpdf_oh mediabox = qpdf_oh_get_key(qpdf, page1, "/MediaBox");
    qpdf_oh wrapped_mediabox = qpdf_oh_wrap_in_array(qpdf, mediabox);
    qpdf_oh cloned_mediabox = qpdf_oh_new_object(qpdf, mediabox);
    assert(wrapped_mediabox != mediabox);
    assert(cloned_mediabox != mediabox);
    assert(qpdf_oh_get_array_n_items(qpdf, wrapped_mediabox) == 4);
    for (int i = 0; i < 4; ++i) {
        qpdf_oh item = qpdf_oh_get_array_item(qpdf, mediabox, i);
        qpdf_oh item2 = qpdf_oh_get_array_item(qpdf, wrapped_mediabox, i);
        qpdf_oh item3 = qpdf_oh_get_array_item(qpdf, cloned_mediabox, i);
        assert(
            qpdf_oh_get_int_value_as_int(qpdf, item) ==
            (i == 0       ? 0
                 : i == 1 ? 0
                 : i == 2 ? 612
                 : i == 3 ? 792
                          : -1));
        assert(
            qpdf_oh_get_int_value_as_int(qpdf, item) == qpdf_oh_get_int_value_as_int(qpdf, item2));
        assert(
            qpdf_oh_get_int_value_as_int(qpdf, item) == qpdf_oh_get_int_value_as_int(qpdf, item3));
        qpdf_oh_release(qpdf, item);
    }
}

static void
test29(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* Trap exceptions thrown by object accessors. Type mismatches are
     * errors rather than warnings when they don't have an owning QPDF
     * object.
     */
    qpdf_silence_errors(qpdf);

    /* get_root fails when we have no trailer */
    qpdf_oh root = qpdf_get_root(qpdf);
    handle_oh_error(qpdf, "get root");
    assert(root != 0);
    assert(!qpdf_oh_is_initialized(qpdf, root));

    assert(!qpdf_oh_is_initialized(qpdf, qpdf_oh_parse(qpdf, "[oops")));
    handle_oh_error(qpdf, "bad parse");
    report_errors();

    assert(qpdf_oh_get_int_value_as_int(qpdf, qpdf_oh_new_string(qpdf, "x")) == 0);
    handle_oh_error(qpdf, "type mismatch (int operation on string)");
    qpdf_oh int_oh = qpdf_oh_new_integer(qpdf, 12);
    assert(strlen(qpdf_oh_get_string_value(qpdf, int_oh)) == 0);
    handle_oh_error(qpdf, "type mismatch (string operation on int)");

    // This doesn't test every possible error flow, but it tests each
    // way of handling errors in the library code.
    assert(qpdf_oh_get_array_n_items(qpdf, int_oh) == 0);
    handle_oh_error(qpdf, "array type mismatch - n_items");
    assert(qpdf_oh_is_null(qpdf, qpdf_oh_get_array_item(qpdf, int_oh, 3)));
    handle_oh_error(qpdf, "array type mismatch - item");
    qpdf_oh_append_item(qpdf, int_oh, qpdf_oh_new_null(qpdf));
    handle_oh_error(qpdf, "append to non-array");
    qpdf_oh array = qpdf_oh_new_array(qpdf);
    assert(qpdf_oh_is_null(qpdf, qpdf_oh_get_array_item(qpdf, array, 3)));
    handle_oh_error(qpdf, "array bounds");

    qpdf_oh_begin_dict_key_iter(qpdf, int_oh);
    assert(qpdf_oh_dict_more_keys(qpdf) == QPDF_FALSE);
    handle_oh_error(qpdf, "dictionary iter type mismatch");
    assert(qpdf_oh_is_null(qpdf, qpdf_oh_get_key(qpdf, int_oh, "potato")));
    handle_oh_error(qpdf, "dictionary type mismatch");
    assert(qpdf_oh_has_key(qpdf, int_oh, "potato") == QPDF_FALSE);
    handle_oh_error(qpdf, "dictionary type mismatch");

    report_errors();
}

static void
test30(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    assert(qpdf_read(qpdf, infile, password) & QPDF_ERRORS);
    /* Fail to handle error */
}

static void
test31(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* Make sure type warnings have a specific error code. This test
     * case is designed for minimal.pdf.
     */
    qpdf_read(qpdf, infile, password);
    qpdf_oh trailer = qpdf_get_trailer(qpdf);
    assert(qpdf_oh_get_int_value(qpdf, trailer) == 0LL);
    assert(!qpdf_has_error(qpdf));
    assert(qpdf_more_warnings(qpdf));
    qpdf_error e = qpdf_next_warning(qpdf);
    assert(qpdf_get_error_code(qpdf, e) == qpdf_e_object);
    report_errors();
}

static void
test32(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test case is designed for minimal.pdf. */
    assert(qpdf_read(qpdf, infile, password) == 0);
    qpdf_oh page = qpdf_get_object_by_id(qpdf, 3, 0);
    assert(qpdf_oh_is_dictionary(qpdf, page));
    assert(qpdf_oh_has_key(qpdf, page, "/MediaBox"));
    report_errors();
}

static void
test33(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test case is designed for minimal.pdf. */

    /* Convert a direct object to an indirect object and replace it. */
    assert(qpdf_read(qpdf, infile, password) == 0);
    qpdf_oh root = qpdf_get_root(qpdf);
    qpdf_oh pages = qpdf_oh_get_key(qpdf, root, "/Pages");
    qpdf_oh kids = qpdf_oh_get_key(qpdf, pages, "/Kids");
    qpdf_oh page1 = qpdf_oh_get_array_item(qpdf, kids, 0);
    qpdf_oh mediabox = qpdf_oh_get_key(qpdf, page1, "/MediaBox");
    assert(!qpdf_oh_is_indirect(qpdf, mediabox));
    qpdf_oh i_mediabox = qpdf_make_indirect_object(qpdf, mediabox);
    assert(qpdf_oh_is_indirect(qpdf, i_mediabox));
    qpdf_oh_replace_key(qpdf, page1, "/MediaBox", i_mediabox);

    /* Replace a different indirect object */
    qpdf_oh resources = qpdf_oh_get_key(qpdf, page1, "/Resources");
    qpdf_oh procset = qpdf_oh_get_key(qpdf, resources, "/ProcSet");
    assert(qpdf_oh_is_indirect(qpdf, procset));
    qpdf_replace_object(
        qpdf,
        qpdf_oh_get_object_id(qpdf, procset),
        qpdf_oh_get_generation(qpdf, procset),
        qpdf_oh_parse(qpdf, "[/PDF]"));

    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_qdf_mode(qpdf, QPDF_TRUE);
    qpdf_set_suppress_original_object_IDs(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test34(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test expects 11-pages.pdf as file1 and minimal.pdf as xarg. */

    /* Non-error cases for page API */

    qpdf_data qpdf2 = qpdf_init();
    assert(qpdf_read(qpdf, infile, password) == 0);
    assert(qpdf_read(qpdf2, xarg, "") == 0);
    assert(qpdf_get_num_pages(qpdf) == 11);
    assert(qpdf_get_num_pages(qpdf2) == 1);

    /* At this time, there is no C API for accessing stream data, so
     * we hard-code object IDs from a known input file.
     */
    assert(qpdf_oh_get_object_id(qpdf, qpdf_get_page_n(qpdf, 0)) == 4);
    assert(qpdf_oh_get_object_id(qpdf, qpdf_get_page_n(qpdf, 10)) == 14);
    qpdf_oh page3 = qpdf_get_page_n(qpdf, 3);
    assert(qpdf_find_page_by_oh(qpdf, page3) == 3);
    assert(qpdf_find_page_by_id(qpdf, qpdf_oh_get_object_id(qpdf, page3), 0) == 3);

    /* Add other page to the end */
    qpdf_oh opage0 = qpdf_get_page_n(qpdf2, 0);
    assert(qpdf_add_page(qpdf, qpdf2, opage0, QPDF_FALSE) == 0);
    /* Add other page before page 3 */
    assert(qpdf_add_page_at(qpdf, qpdf2, opage0, QPDF_TRUE, page3) == 0);
    /* Remove page 3 */
    assert(qpdf_remove_page(qpdf, page3) == 0);
    /* At page 3 back at the beginning */
    assert(qpdf_add_page(qpdf, qpdf, page3, QPDF_TRUE) == 0);

    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_qdf_mode(qpdf, QPDF_TRUE);
    qpdf_set_suppress_original_object_IDs(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
    qpdf_cleanup(&qpdf2);
}

static void
test35(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test uses 11-pages.pdf */

    assert(qpdf_get_num_pages(qpdf) == -1);
    assert(qpdf_has_error(qpdf));
    qpdf_error e = qpdf_get_error(qpdf);
    assert(qpdf_get_error_code(qpdf, e) != QPDF_SUCCESS);
    assert(!qpdf_has_error(qpdf));

    assert(qpdf_read(qpdf, infile, password) == 0);

    qpdf_oh range = qpdf_get_page_n(qpdf, 11);
    assert(!qpdf_oh_is_initialized(qpdf, range));
    assert(qpdf_has_error(qpdf));
    e = qpdf_get_error(qpdf);
    assert(qpdf_get_error_code(qpdf, e) != QPDF_SUCCESS);
    assert(!qpdf_has_error(qpdf));

    assert(qpdf_find_page_by_id(qpdf, 100, 0) == -1);
    assert(qpdf_has_error(qpdf));
    e = qpdf_get_error(qpdf);
    assert(qpdf_get_error_code(qpdf, e) != QPDF_SUCCESS);
    assert(!qpdf_has_error(qpdf));

    assert(qpdf_find_page_by_oh(qpdf, qpdf_get_root(qpdf)) == -1);
    assert(qpdf_more_warnings(qpdf));
    e = qpdf_next_warning(qpdf);
    assert(qpdf_get_error_code(qpdf, e) != QPDF_SUCCESS);
    assert(qpdf_has_error(qpdf));
    e = qpdf_get_error(qpdf);
    assert(qpdf_get_error_code(qpdf, e) != QPDF_SUCCESS);
    assert(!qpdf_has_error(qpdf));

    assert(qpdf_find_page_by_id(qpdf, 100, 0) == -1);
    assert(qpdf_has_error(qpdf));
    e = qpdf_get_error(qpdf);
    assert(qpdf_get_error_code(qpdf, e) != QPDF_SUCCESS);
    assert(!qpdf_has_error(qpdf));

    assert(qpdf_add_page(qpdf, qpdf, 1000, QPDF_FALSE) != 0);

    report_errors();
}

static void
test36(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test uses inherited-rotate.pdf */

    assert(qpdf_read(qpdf, infile, password) == 0);

    /* Non-trivially push inherited attributes */
    qpdf_oh page0 = qpdf_get_object_by_id(qpdf, 3, 0);
    assert(qpdf_oh_is_dictionary(qpdf, page0));
    qpdf_oh r = qpdf_oh_get_key(qpdf, page0, "/Rotate");
    assert(qpdf_oh_get_int_value(qpdf, r) == 90);
    qpdf_oh_remove_key(qpdf, page0, "/Rotate");
    assert(!qpdf_oh_has_key(qpdf, page0, "/Rotate"));

    assert(qpdf_push_inherited_attributes_to_page(qpdf) == 0);
    r = qpdf_oh_get_key(qpdf, page0, "/Rotate");
    assert(qpdf_oh_get_int_value(qpdf, r) == 270);

    assert(qpdf_add_page(qpdf, qpdf, page0, QPDF_TRUE) == 0);
}

static void
test37(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test uses 11-pages.pdf */

    /* Manually manipulate pages tree */
    assert(qpdf_read(qpdf, infile, password) == 0);
    assert(qpdf_get_num_pages(qpdf) == 11);
    qpdf_oh pages = qpdf_get_object_by_id(qpdf, 3, 0);
    qpdf_oh kids = qpdf_oh_get_key(qpdf, pages, "/Kids");
    assert(qpdf_oh_get_array_n_items(qpdf, kids) == 11);
    qpdf_oh_erase_item(qpdf, kids, 0);
    assert(qpdf_get_num_pages(qpdf) == 11);
    assert(qpdf_update_all_pages_cache(qpdf) == 0);
    assert(qpdf_get_num_pages(qpdf) == 10);
}

static void
test38(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test expects 11-pages.pdf. */

    /* Read stream data */

    assert(qpdf_read(qpdf, infile, password) == 0);
    qpdf_oh stream = qpdf_get_object_by_id(qpdf, 17, 0);
    qpdf_oh dict = qpdf_oh_get_dict(qpdf, stream);
    assert(qpdf_oh_get_int_value_as_int(qpdf, qpdf_oh_get_key(qpdf, dict, "/Length")) == 53);
    /* Get raw data */
    unsigned char* buf = 0;
    size_t len = 0;
    assert(qpdf_oh_get_stream_data(qpdf, stream, qpdf_dl_none, 0, &buf, &len) == 0);
    assert(len == 53);
    assert(((int)buf[0] == 'x') && ((int)buf[1] == 0234));
    free(buf);

    /* Test whether filterable */
    QPDF_BOOL filtered = QPDF_FALSE;
    assert(qpdf_oh_get_stream_data(qpdf, stream, qpdf_dl_all, &filtered, 0, 0) == 0);
    assert(filtered == QPDF_TRUE);

    /* Get filtered data */
    assert(qpdf_oh_get_stream_data(qpdf, stream, qpdf_dl_all, 0, &buf, &len) == 0);
    assert(len == 47);
    assert(memcmp((char const*)buf, "BT /F1 15 Tf 72 720 Td (Original page 2) Tj ET\n", len) == 0);

    /* Get page data */
    qpdf_oh page2 = qpdf_get_page_n(qpdf, 1); /* 0-based index */
    unsigned char* buf2 = 0;
    assert(qpdf_oh_get_page_content_data(qpdf, page2, &buf2, &len) == 0);
    assert(len == 47);
    assert(memcmp(buf, buf2, len) == 0);
    free(buf);
    free(buf2);

    /* errors */
    printf("page content on broken page\n");
    qpdf_oh_replace_key(qpdf, page2, "/Contents", qpdf_oh_new_integer(qpdf, 3));
    buf = 0;
    qpdf_oh_get_page_content_data(qpdf, page2, &buf, &len);
    assert(buf == 0);
    report_errors();
    printf("stream data for non stream\n");
    qpdf_oh root = qpdf_get_root(qpdf);
    assert(qpdf_oh_get_stream_data(qpdf, root, qpdf_dl_all, 0, 0, 0) != 0);
    report_errors();
}

static void
test39(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test expects 11-pages.pdf as file1 and minimal.pdf as xarg. */

    /* Foreign object */

    qpdf_data qpdf2 = qpdf_init();
    assert(qpdf_read(qpdf, infile, password) == 0);
    assert(qpdf_read(qpdf2, xarg, "") == 0);

    qpdf_oh resources = qpdf_get_object_by_id(qpdf2, 3, 0);
    qpdf_oh copy = qpdf_oh_copy_foreign_object(qpdf, qpdf2, resources);
    qpdf_oh root = qpdf_get_root(qpdf);
    qpdf_oh_replace_key(qpdf, root, "/Copy", copy);

    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_qdf_mode(qpdf, QPDF_TRUE);
    qpdf_set_suppress_original_object_IDs(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
    qpdf_cleanup(&qpdf2);
}

static void
test40(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* This test expects minimal.pdf. */

    /* New stream */

    assert(qpdf_read(qpdf, infile, password) == 0);
    qpdf_oh stream = qpdf_oh_new_stream(qpdf);
    qpdf_oh_replace_stream_data(
        qpdf,
        stream,
        (unsigned char*)"12345\000abcde",
        11, /* embedded null */
        qpdf_oh_new_null(qpdf),
        qpdf_oh_new_null(qpdf));
    qpdf_oh root = qpdf_get_root(qpdf);
    qpdf_oh_replace_key(qpdf, root, "/Potato", stream);

    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_set_qdf_mode(qpdf, QPDF_TRUE);
    qpdf_set_suppress_original_object_IDs(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test41(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    /* Empty PDF -- infile is ignored */
    assert(qpdf_empty_pdf(qpdf) == 0);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test42(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    assert(qpdf_create_from_json_file(qpdf, infile) == QPDF_SUCCESS);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test43(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    char* buf = NULL;
    unsigned long size = 0;
    read_file_into_memory(infile, &buf, &size);
    assert(qpdf_create_from_json_data(qpdf, buf, size) == QPDF_SUCCESS);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
    free(buf);
}

static void
test44(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    assert(qpdf_read(qpdf, infile, password) == 0);
    assert(qpdf_update_from_json_file(qpdf, xarg) == QPDF_SUCCESS);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
}

static void
test45(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    char* buf = NULL;
    unsigned long size = 0;
    read_file_into_memory(xarg, &buf, &size);
    assert(qpdf_read(qpdf, infile, password) == 0);
    assert(qpdf_update_from_json_data(qpdf, buf, size) == QPDF_SUCCESS);
    qpdf_init_write(qpdf, outfile);
    qpdf_set_static_ID(qpdf, QPDF_TRUE);
    qpdf_write(qpdf);
    report_errors();
    free(buf);
}

static void
test46(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    FILE* f = safe_fopen(outfile, "wb");
    assert(qpdf_read(qpdf, infile, password) == 0);
    qpdf_write_json(qpdf, 2, write_to_file, f, qpdf_dl_none, qpdf_sj_inline, "", NULL);
    fclose(f);
    report_errors();
}

static void
test47(char const* infile, char const* password, char const* outfile, char const* xarg)
{
    FILE* f = safe_fopen(outfile, "wb");
    assert(qpdf_read(qpdf, infile, password) == 0);
    char const* wanted_objects[] = {"obj:4 0 R", "trailer", NULL};
    qpdf_write_json(
        qpdf, 2, write_to_file, f, qpdf_dl_specialized, qpdf_sj_file, xarg, wanted_objects);
    fclose(f);
    report_errors();
}

int
main(int argc, char* argv[])
{
    char* p = 0;
    int n = 0;
    char const* infile = 0;
    char const* password = 0;
    char const* outfile = 0;
    char const* xarg = 0;
    void (*fn)(char const*, char const*, char const*, char const*) = 0;

    if ((p = strrchr(argv[0], '/')) != NULL) {
        whoami = p + 1;
    } else if ((p = strrchr(argv[0], '\\')) != NULL) {
        whoami = p + 1;
    } else {
        whoami = argv[0];
    }
    if ((argc == 2) && (strcmp(argv[1], "--version") == 0)) {
        printf("qpdf-ctest version %s\n", qpdf_get_qpdf_version());
        return 0;
    }

    if (argc < 5) {
        fprintf(stderr, "usage: %s n infile password outfile\n", whoami);
        exit(2);
    }

    n = atoi(argv[1]);
    infile = argv[2];
    password = argv[3];
    outfile = argv[4];
    xarg = (argc > 5 ? argv[5] : 0);

    fn =
        ((n == 1)        ? test01
             : (n == 2)  ? test02
             : (n == 3)  ? test03
             : (n == 4)  ? test04
             : (n == 5)  ? test05
             : (n == 6)  ? test06
             : (n == 7)  ? test07
             : (n == 8)  ? test08
             : (n == 9)  ? test09
             : (n == 10) ? test10
             : (n == 11) ? test11
             : (n == 12) ? test12
             : (n == 13) ? test13
             : (n == 14) ? test14
             : (n == 15) ? test15
             : (n == 16) ? test16
             : (n == 17) ? test17
             : (n == 18) ? test18
             : (n == 19) ? test19
             : (n == 20) ? test20
             : (n == 21) ? test21
             : (n == 22) ? test22
             : (n == 23) ? test23
             : (n == 24) ? test24
             : (n == 25) ? test25
             : (n == 26) ? test26
             : (n == 27) ? test27
             : (n == 28) ? test28
             : (n == 29) ? test29
             : (n == 30) ? test30
             : (n == 31) ? test31
             : (n == 32) ? test32
             : (n == 33) ? test33
             : (n == 34) ? test34
             : (n == 35) ? test35
             : (n == 36) ? test36
             : (n == 37) ? test37
             : (n == 38) ? test38
             : (n == 39) ? test39
             : (n == 40) ? test40
             : (n == 41) ? test41
             : (n == 42) ? test42
             : (n == 43) ? test43
             : (n == 44) ? test44
             : (n == 45) ? test45
             : (n == 46) ? test46
             : (n == 47) ? test47
                         : 0);

    if (fn == 0) {
        fprintf(stderr, "%s: invalid test number %d\n", whoami, n);
        exit(2);
    }

    qpdf = qpdf_init();
    fn(infile, password, outfile, xarg);
    qpdf_cleanup(&qpdf);
    assert(qpdf == 0);
    printf("C test %d done\n", n);

    return 0;
}
