Program: CWE-125
Project: CVE-2017-18185
Buggy source:
#!/usr/bin/env perl
require 5.008;
BEGIN { $^W = 1; }
use strict;
use Cwd;
use Digest::MD5;
use File::Basename;
use File::Copy;
use File::Spec;

chdir("qpdf") or die "chdir testdir failed: $!\n";

require TestDriver;

cleanup();

my $devNull = File::Spec->devnull();
my $td = new TestDriver('qpdf');

my $compare_images = 1;
if ((exists $ENV{'QPDF_SKIP_TEST_COMPARE_IMAGES'}) &&
    ($ENV{'QPDF_SKIP_TEST_COMPARE_IMAGES'} eq '1'))
{
    $compare_images = 0;
}
my $large_file_test_path = $ENV{'QPDF_LARGE_FILE_TEST_PATH'} || undef;
if (defined($large_file_test_path))
{
    $large_file_test_path = File::Spec->rel2abs($large_file_test_path);
    $large_file_test_path =~ s!\\!/!g;
}

my $have_acroread = 0;

if ($compare_images)
{
    # check for working acroread
    if (system("acroread -toPostScript -pairs good1.pdf a.ps" .
               " >$devNull 2>&1") == 0)
    {
	$have_acroread = 1;
    }
}

# These variables are used to store the total number of tests in the
# test suite.  NOTE: qtest's requirement to indicate the number of
# tests serves as a check that the test suite is operating properly.
# Do not calculate these values as a side effect of running the tests.
# That defeats the purpose.  However, since this test suite consists
# of several separate series of tests, many of which iterate over
# static lists of things, we calculate the numbers as we go in terms
# of static values.

# This should be set to the number of times we called compare_pdfs.
# This has to be kept separate because the number of test cases
# compare_pdfs generates depends on the value of $compare_images.
my $n_compare_pdfs = 0;

# This should be set to the number of times we call acroread.
my $n_acroread = 0;

# Each section of tests should increment this number by the number of
# tests they generate excluding calls to acroread or compare_pdfs,
# which are tracked separately by $n_compare_pdfs and $n_acroread.
my $n_tests = 0;

# Call show_ntests after each block of test cases.  In show_ntests,
# you can turn on printing of the expected number of test cases.  This
# is useful for tracking down problems in the number of test cases.

show_ntests();
# ----------

$n_compare_pdfs += 5;

# Check compare_pdfs to make sure that it works properly.  Each call
# to compare_pdfs is worth three test cases.
compare_pdfs("p1-a-p2-b.pdf", "p1-a-p2-b.pdf");
compare_pdfs("p1-a.pdf", "p1-a.pdf");
compare_pdfs("p1-a.pdf", "p1-b.pdf", 1);
compare_pdfs("p1-a.pdf", "p1-a-p2-b.pdf", 1);
compare_pdfs("p1-a-p2-a.pdf", "p1-a-p2-b.pdf", 1);
flush_tiff_cache();

show_ntests();
# ----------
$td->notify("--- Stream Replacement Tests ---");
$n_tests += 8;

$td->runtest("replace stream data",
	     {$td->COMMAND => "test_driver 7 qstream.pdf"},
	     {$td->STRING => "test 7 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "replaced-stream-data.pdf"});
$td->runtest("replace stream data compressed",
	     {$td->COMMAND => "test_driver 8 qstream.pdf"},
	     {$td->FILE => "test8.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "replaced-stream-data-flate.pdf"});
$td->runtest("new streams",
	     {$td->COMMAND => "test_driver 9 minimal.pdf"},
	     {$td->FILE => "test9.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("new stream",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "new-streams.pdf"});
$td->runtest("add page contents",
	     {$td->COMMAND => "test_driver 10 minimal.pdf"},
	     {$td->STRING => "test 10 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("new stream",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "add-contents.pdf"});

show_ntests();
# ----------
$td->notify("--- Extensions Dictionary Tests ---");
my @ext_inputs = ('minimal.pdf', 'extensions-adbe.pdf',
                  'extensions-other.pdf', 'extensions-adbe-other.pdf');
my @new_versions = ('1.3', '1.6', '1.7.1', '1.7.2', '1.7.3',
                    '1.8', '1.8.0', '1.8.2', '1.8.5');
$n_tests += (4 * @new_versions + 3) * @ext_inputs;
foreach my $input (@ext_inputs)
{
    my $base = $input;
    $base =~ s/\.pdf$//;
    if ($base eq 'minimal')
    {
        $base = 'extensions-none';
    }
    foreach my $version (@new_versions)
    {
        foreach my $op (qw(min force))
        {
            $td->runtest("$input: $op version to $version",
                         {$td->COMMAND =>
                              "qpdf --static-id" .
                              " --$op-version=$version $input a.pdf"},
                         {$td->STRING => "", $td->EXIT_STATUS => 0});
            $td->runtest("check version information ($op $version)",
                         {$td->COMMAND => "test_driver 34 a.pdf"},
                         {$td->FILE => "$base-$op-$version.out",
                          $td->EXIT_STATUS => 0},
                         $td->NORMALIZE_NEWLINES);
            if (($op eq 'force') && ($version eq '1.8.5'))
            {
                # Look at the actual file for a few cases to make sure
                # qdf and non-qdf output are okay
                $td->runtest("check file",
                             {$td->FILE => "a.pdf"},
                             {$td->FILE => "$base-$op-$version.pdf"});
                $td->runtest("$input: $op version to $version",
                             {$td->COMMAND =>
                                  "qpdf --qdf --static-id" .
                                  " --$op-version=$version $input a.qdf"},
                             {$td->STRING => "", $td->EXIT_STATUS => 0});
                $td->runtest("check file",
                             {$td->FILE => "a.qdf"},
                             {$td->FILE => "$base-$op-$version.qdf"});
            }
        }
    }
}
show_ntests();
# ----------
$td->notify("--- Page API Tests ---");
$n_tests += 9;

$td->runtest("basic page API",
	     {$td->COMMAND => "test_driver 15 page_api_1.pdf"},
	     {$td->STRING => "test 15 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "page_api_1-out.pdf"});
$td->runtest("manual page manipulation",
	     {$td->COMMAND => "test_driver 16 page_api_1.pdf"},
	     {$td->STRING => "test 16 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "page_api_1-out2.pdf"});
$td->runtest("duplicate page",
	     {$td->COMMAND => "test_driver 17 page_api_2.pdf"},
	     {$td->FILE => "page_api_2.out", $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("delete and re-add a page",
	     {$td->COMMAND => "test_driver 18 page_api_1.pdf"},
	     {$td->STRING => "test 18 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "page_api_1-out3.pdf"});
$td->runtest("duplicate page",
	     {$td->COMMAND => "test_driver 19 page_api_1.pdf"},
	     {$td->FILE => "page_api_1.out", $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("remove page we don't have",
	     {$td->COMMAND => "test_driver 22 page_api_1.pdf"},
	     {$td->FILE => "page_api_1.out2", $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);
show_ntests();
# ----------
$td->notify("--- Files for specific bugs ---");
# The number is the github issue number in which the bug was reported.
my @bug_tests = (
    ["51", "resolve loop", 3],
    ["99", "object 0", 2],
    ["99b", "object 0", 2],
    ["100", "xref reconstruction loop", 2],
    ["101", "resolve for exception text", 2],
    ["117", "other infinite loop", 2],
    ["118", "other infinite loop", 2],
    ["119", "other infinite loop", 3],
    ["120", "other infinite loop", 3],
    ["106", "zlib data error", 3],
    ["141a", "/W entry size 0", 2],
    ["141b", "/W entry size 0", 2],
    ["143", "self-referential ostream", 3],
    ["146", "very deeply nested array", 2],
    ["147", "previously caused memory error", 2],
    ["148", "free memory on bad flate", 2],
    ["149", "xref prev pointer loop", 3],
    );
$n_tests += scalar(@bug_tests);
foreach my $d (@bug_tests)
{
    my ($n, $description, $exit_status) = @$d;
    $td->runtest($description,
                 {$td->COMMAND => "qpdf issue-$n.pdf a.pdf"},
                 {$td->FILE => "issue-$n.out",
                  $td->EXIT_STATUS => $exit_status},
                 $td->NORMALIZE_NEWLINES);
}
show_ntests();
# ----------
$td->notify("--- Miscellaneous Tests ---");
$n_tests += 86;

$td->runtest("qpdf version",
	     {$td->COMMAND => "qpdf --version"},
	     {$td->REGEXP => "qpdf version \\S+\n.*", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C API: qpdf version",
	     {$td->COMMAND => "qpdf-ctest --version"},
	     {$td->REGEXP => "qpdf-ctest version \\S+\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

foreach (my $i = 1; $i <= 3; ++$i)
{
    $td->runtest("misc tests",
		 {$td->COMMAND => "test_driver 5 misc-$i.pdf"},
		 {$td->FILE => "misc-$i.out", $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
}

$td->runtest("get stream data",
	     {$td->COMMAND => "test_driver 11 stream-data.pdf"},
	     {$td->FILE => "test11.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Make sure we ignore decode parameters that we don't understand
$td->runtest("unknown decode parameters",
	     {$td->COMMAND => "qpdf --check fax-decode-parms.pdf"},
	     {$td->FILE => "fax-decode-parms.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Handle xref stream with more entries than reported (bug 2872265)
$td->runtest("xref with short size",
	     {$td->COMMAND => "qpdf --show-xref xref-with-short-size.pdf"},
	     {$td->FILE => "xref-with-short-size.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("recover xref with short size",
	     {$td->COMMAND => "qpdf xref-with-short-size.pdf a.pdf"},
	     {$td->FILE => "xref-with-short-size-recover.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("show new xref stream",
	     {$td->COMMAND => "qpdf --show-xref a.pdf"},
	     {$td->FILE => "xref-with-short-size-new.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Handle file with object stream containing an unreferenced object
# that in turn contains an indirect scalar (bug 2974522).
$td->runtest("unreferenced indirect scalar",
	     {$td->COMMAND =>
		  "qpdf --qdf --static-id --object-streams=preserve" .
		  " unreferenced-indirect-scalar.pdf a.qdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.qdf"},
	     {$td->FILE => "unreferenced-indirect-scalar.out"});

# Encrypt files whose /ID strings are other than 32 bytes long (bug
# 2991412).
foreach my $file (qw(short-id long-id))
{
    $td->runtest("encrypt $file.pdf",
		 {$td->COMMAND =>
		      "qpdf --encrypt '' pass 40 -- $file.pdf a.pdf"},
		 {$td->STRING => "",
		  $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);

    $td->runtest("check $file.pdf",
		 {$td->COMMAND => "qpdf --check a.pdf"},
		 {$td->FILE => "$file-check.out",
		  $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
}

# Handle file with invalid xref table and object 0 as a regular object
# (bug 3159950).
$td->runtest("check obj0.pdf",
	     {$td->COMMAND => "qpdf --check obj0.pdf"},
	     {$td->FILE => "obj0-check.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);

# Min/Force version
$td->runtest("set min version",
	     {$td->COMMAND => "qpdf --verbose --min-version=1.6 good1.pdf a.pdf"},
	     {$td->STRING => "qpdf: wrote file a.pdf\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check version",
	     {$td->COMMAND => "qpdf --check a.pdf"},
	     {$td->FILE => "min-version.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("force version",
	     {$td->COMMAND => "qpdf --force-version=1.4 a.pdf b.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check version",
	     {$td->COMMAND => "qpdf --check b.pdf"},
	     {$td->FILE => "forced-version.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
unlink "a.pdf", "b.pdf" or die;
$td->runtest("C API: min/force versions",
	     {$td->COMMAND => "qpdf-ctest 14 object-stream.pdf '' a.pdf b.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C check version 1",
	     {$td->COMMAND => "qpdf-ctest 1 a.pdf '' ''"},
	     {$td->FILE => "c-min-version.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C check version 2",
	     {$td->COMMAND => "qpdf --check b.pdf"},
	     {$td->FILE => "forced-version.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Stream filter abbreviations from table H.1
$td->runtest("stream filter abbreviations",
	     {$td->COMMAND => "qpdf --static-id filter-abbreviation.pdf a.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "filter-abbreviation.out"});

$td->runtest("empty object",
	     {$td->COMMAND => "qpdf -show-object=7,0 empty-object.pdf"},
	     {$td->FILE => "empty-object.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("error/output redirection to null",
	     {$td->COMMAND => "test_driver 12 linearized-and-warnings.pdf"},
	     {$td->FILE => "linearized-and-warnings-1.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("error/output redirection to strings",
	     {$td->COMMAND => "test_driver 13 linearized-and-warnings.pdf"},
	     {$td->FILE => "linearized-and-warnings-2.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("odd terminators for stream keyword",
	     {$td->COMMAND =>
		  "qpdf --qdf --static-id" .
		  " stream-line-enders.pdf a.qdf"},
	     {$td->FILE => "stream-line-enders.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.qdf"},
	     {$td->FILE => "stream-line-enders.qdf"});

$td->runtest("swap and replace",
	     {$td->COMMAND => "test_driver 14 test14-in.pdf"},
	     {$td->FILE => "test14.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "test14-out.pdf"});
# Test 14 also exercises writing to memory without static ID.
$td->runtest("check non-static ID version",
             {$td->COMMAND => "sh ./diff-ignore-ID-version a.pdf b.pdf"},
             {$td->STRING => "okay\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

$td->runtest("C API info key functions",
	     {$td->COMMAND => "qpdf-ctest 16 minimal.pdf '' a.pdf"},
	     {$td->FILE => "c-info1.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "c-info-out.pdf"});
unlink "a.pdf" or die;

$td->runtest("C API info key functions",
	     {$td->COMMAND => "qpdf-ctest 16 c-info2-in.pdf '' a.pdf"},
	     {$td->FILE => "c-info2.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "c-info-out.pdf"});
unlink "a.pdf" or die;

$td->runtest("shallow copy an array",
	     {$td->COMMAND => "test_driver 20 shallow_array.pdf"},
	     {$td->STRING => "test 20 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "shallow_array-out.pdf"});
$td->runtest("shallow copy a stream",
	     {$td->COMMAND => "test_driver 21 shallow_array.pdf"},
	     {$td->FILE => "shallow_stream.out", $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("warn for unknown key in Pages",
             {$td->COMMAND => "test_driver 23 lin-special.pdf"},
             {$td->FILE => "pages-warning.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("reserved objects",
             {$td->COMMAND => "test_driver 24 minimal.pdf"},
             {$td->FILE => "reserved-objects.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "reserved-objects.pdf"});
$td->runtest("detect foreign object in write",
             {$td->COMMAND => "test_driver 29" .
                  " copy-foreign-objects-in.pdf minimal.pdf"},
             {$td->FILE => "foreign-in-write.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("parse objects from string",
             {$td->COMMAND => "test_driver 31 minimal.pdf"}, # file not used
             {$td->FILE => "parse-object.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("EOF terminating literal tokens",
             {$td->COMMAND => "qpdf --check eof-terminates-literal.pdf"},
             {$td->FILE => "eof-terminates-literal.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("EOF reading token",
             {$td->COMMAND => "qpdf --check eof-reading-token.pdf"},
             {$td->FILE => "eof-reading-token.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
$td->runtest("extra header text",
             {$td->COMMAND => "test_driver 32 minimal.pdf"},
             {$td->FILE => "test-32.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "extra-header-no-newline.pdf"});
$td->runtest("check output",
             {$td->FILE => "b.pdf"},
             {$td->FILE => "extra-header-lin-no-newline.pdf"});
$td->runtest("check output",
             {$td->FILE => "c.pdf"},
             {$td->FILE => "extra-header-newline.pdf"});
$td->runtest("check output",
             {$td->FILE => "d.pdf"},
             {$td->FILE => "extra-header-lin-newline.pdf"});
$td->runtest("output to custom pipeline",
             {$td->COMMAND => "test_driver 33 minimal.pdf"},
             {$td->STRING => "test 33 done\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "custom-pipeline.pdf"});
$td->runtest("object with zero offset",
             {$td->COMMAND => "qpdf --check zero-offset.pdf"},
             {$td->FILE => "zero-offset.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
# leading-junk also has a space instead of a newline after xref
$td->runtest("check file with leading junk",
             {$td->COMMAND => "qpdf --check leading-junk.pdf"},
             {$td->FILE => "leading-junk.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("EOF inside inline image",
             {$td->COMMAND => "test_driver 37 eof-in-inline-image.pdf"},
             {$td->FILE => "eof-in-inline-image.out",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);
$td->runtest("tokenize content streams",
             {$td->COMMAND => "test_driver 37 tokenize-content-streams.pdf"},
             {$td->FILE => "tokenize-content-streams.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("terminate parsing",
             {$td->COMMAND => "test_driver 37 terminate-parsing.pdf"},
             {$td->FILE => "terminate-parsing.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("content stream errors",
             {$td->COMMAND => "qpdf --check content-stream-errors.pdf"},
             {$td->FILE => "content-stream-errors.out",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);

# The file override-compressed-object.pdf contains an object stream
# with four strings in it.  The file is then appended.  The appended
# section overrides one of the four strings with a string in another
# object stream and another one in an uncompressed object.  The other
# two strings are left alone.  The test case exercises that all four
# objects have the correct value.
$td->runtest("overridden compressed objects",
             {$td->COMMAND => "test_driver 38 override-compressed-object.pdf"},
             {$td->FILE => "override-compressed-object.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

$td->runtest("generate object streams for gen > 0",
             {$td->COMMAND => "qpdf --qdf --static-id" .
		  " --object-streams=generate gen1.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check file",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "gen1.qdf"});

# A user provided a file that was missing /ID in its trailer even
# though it is encrypted and also has a space instead of a newline
# after its xref keyword. This file has those same properties.
$td->runtest("check broken file",
             {$td->COMMAND => "qpdf --check invalid-id-xref.pdf"},
             {$td->FILE => "invalid-id-xref.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);

$td->runtest("show number of pages",
             {$td->COMMAND =>
                  "qpdf --show-npages 20-pages.pdf --password=user"},
             {$td->STRING => "20\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("ignore broken decode parms with no filters",
             {$td->COMMAND => "qpdf --check broken-decode-parms-no-filter.pdf"},
             {$td->FILE => "broken-decode-parms-no-filter.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("bounds check linearization data 1",
             {$td->COMMAND => "qpdf --check linearization-bounds-1.pdf"},
             {$td->FILE => "linearization-bounds-1.out",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);
$td->runtest("bounds check linearization data 2",
             {$td->COMMAND => "qpdf --check linearization-bounds-2.pdf"},
             {$td->FILE => "linearization-bounds-2.out",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);
# Throws logic error, not bad_alloc
$td->runtest("sanity check array size",
             {$td->COMMAND =>
                  "qpdf --check linearization-large-vector-alloc.pdf"},
             {$td->FILE => "linearization-large-vector-alloc.out",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);

$td->runtest("stream with indirect decode parms",
             {$td->COMMAND =>
                  "qpdf --static-id indirect-decode-parms.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check file",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "indirect-decode-parms-out.pdf"});

$td->runtest("handle page no with contents",
             {$td->COMMAND => "qpdf --show-pages page-no-content.pdf"},
             {$td->FILE => "page-no-content.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("no type key for page nodes",
             {$td->COMMAND => "qpdf --check no-pages-types.pdf"},
             {$td->FILE => "no-pages-types.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("ensure arguments to R are direct",
             {$td->COMMAND => "qpdf --check indirect-r-arg.pdf"},
             {$td->FILE => "indirect-r-arg.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
$td->runtest("detect loops in pages structure",
             {$td->COMMAND => "qpdf --check pages-loop.pdf"},
             {$td->FILE => "pages-loop.out", $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);
$td->runtest("no trailing space in xref table",
             {$td->COMMAND => "qpdf --check no-space-in-xref.pdf"},
             {$td->FILE => "no-space-in-xref.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

# An array is split across multiple content streams starting object
# 42. This was reported in github issue 73. The file is modified from
# that example.
$td->runtest("parse split content stream",
             {$td->COMMAND => "qpdf --check split-content-stream.pdf"},
             {$td->FILE => "split-content-stream.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("split content stream errors",
             {$td->COMMAND => "qpdf --check split-content-stream-errors.pdf"},
             {$td->FILE => "split-content-stream-errors.out",
              $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);

# Demonstrate show-xref after check and not after check to illustrate
# that it can dump the real xref or the recovered xref.
$td->runtest("dump bad xref",
             {$td->COMMAND => "qpdf --show-xref bad-xref-entry.pdf"},
             {$td->FILE => "bad-xref-entry.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
# Test @file here too.
open(F, ">args") or die;
print F "--check\n";
print F "--show-xref\n";
close(F);
$td->runtest("dump corrected bad xref",
             {$td->COMMAND => "qpdf \@args bad-xref-entry.pdf"},
             {$td->FILE => "bad-xref-entry-corrected.out",
              $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
unlink "args";

$td->runtest("don't overwrite self",
             {$td->COMMAND => "(echo a.pdf; echo a.pdf) | qpdf \@-"},
             {$td->REGEXP => "input file and output file are the same.*",
              $td->EXIT_STATUS => 2});

$td->runtest("combine show and --pages",
             {$td->COMMAND =>
                  "qpdf --empty --pages minimal.pdf -- --show-pages"},
             {$td->FILE => "show-pages-pages.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

$td->runtest("ignore bad token",
             {$td->COMMAND =>
                  "qpdf --show-xref bad-token-startxref.pdf"},
             {$td->FILE => "bad-token-startxref.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

$td->runtest("recoverable xref errors",
             {$td->COMMAND =>
                  "qpdf --check --show-xref xref-errors.pdf"},
             {$td->FILE => "xref-errors.out",
              $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);

# A file was emailed privately with issue 96. short-O-U.pdf was
# created by copying encryption parameters from that file. It exhibits
# the same behavior as the original file.
$td->runtest("short /O or /U",
             {$td->COMMAND =>
                  "qpdf --password=19723102477 --check short-O-U.pdf"},
             {$td->FILE => "short-O-U.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Newline before endstream ---");
$n_tests += 8;

# From issue 133, http://verapdf.org/software/ is an open source
# package that can verify PDF/A compliance. This could potentially be
# useful for manual or automated verification that qpdf doesn't break
# PDF/A compliance should that ever be desired.

foreach my $d (
    ['--qdf', 'qdf', 'qdf'],
    ['--newline-before-endstream', 'newline', 'nl'],
    ['--qdf --newline-before-endstream', 'newline and qdf', 'nl-qdf'],
    )
{
    my ($flags, $description, $suffix) = @$d;
    $td->runtest("newline before endstream: $description",
                 {$td->COMMAND => "qpdf --static-id --stream-data=preserve" .
                      " $flags streams-with-newlines.pdf a.pdf"},
                 {$td->STRING => "", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("check output",
                 {$td->FILE => "a.pdf"},
                 {$td->FILE => "newline-before-endstream-$suffix.pdf"});
    if ($flags =~ /qdf/)
    {
        $td->runtest("fix-qdf",
                     {$td->COMMAND => "fix-qdf a.pdf"},
                     {$td->FILE => "a.pdf", $td->EXIT_STATUS => 0});
    }
}
show_ntests();
# ----------
$td->notify("--- Split Pages ---");
# sp = split-pages
my @sp_cases = (
    [11, '%d at beginning', '', '%d_split-out.zdf'],
    [11, '%d at end', '--qdf', 'split-out.zdf_%d'],
    [11, '%d in middle', '--encrypt u o 128 --', 'a-%d-split-out.zdf'],
    [11, 'pdf extension', '', 'split-out.Pdf'],
    [4, 'fallback', '--pages 11-pages.pdf 1-3 minimal.pdf --', 'split-out'],
    );
$n_tests += 5;
for (@sp_cases)
{
    $n_tests += 1 + $_->[0];
}

$td->runtest("split page group > 1",
             {$td->COMMAND => "qpdf --static-id --split-pages=5 11-pages.pdf" .
                  " --verbose split-out-group.pdf"},
             {$td->FILE => "split-pages-group.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
foreach my $f ('01-05', '06-10', '11-11')
{
    $td->runtest("checkout group $f",
                 {$td->FILE => "split-out-group-$f.pdf"},
                 {$td->FILE => "split-exp-group-$f.pdf"});
}

$td->runtest("no split-pages to stdout",
             {$td->COMMAND => "qpdf --split-pages 11-pages.pdf -"},
             {$td->FILE => "split-pages-stdout.out", $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);

foreach my $d (@sp_cases)
{
    my ($n, $description, $xargs, $out) = @$d;
    $td->runtest("split pages " . $description,
                 {$td->COMMAND =>
                      "qpdf --static-id --split-pages 11-pages.pdf" .
                      " $xargs $out"},
                 {$td->STRING => "", $td->EXIT_STATUS => 0});
    my $pattern = $out;
    my $nlen = length($n);
    if ($pattern =~ m/\%d/)
    {
        $pattern =~ s/\%d/\%0${nlen}d/;
    }
    elsif ($pattern =~ m/\.pdf$/i)
    {
        $pattern =~ s/(\.pdf$)/-%0${nlen}d$1/i;
    }
    else
    {
        $pattern .= "-%0${nlen}d";
    }
    for (my $i = 1; $i <= $n; ++$i)
    {
        my $actual = sprintf($pattern, $i);
        my $expected = $actual;
        $expected =~ s/split-out/split-exp/;
        $td->runtest("checkout output page $i",
                     {$td->FILE => $actual},
                     {$td->FILE => $expected});
    }
}

show_ntests();
# ----------
$td->notify("--- Rotate Pages ---");
$n_tests += 2;
# XXX do absolute, positive, and negative on ranges that include
# inherited and non-inherited.
# Pages 11-15 inherit /Rotate 90
# Pages 1 and 2 have explicit /Rotate 270
# Pages 16 and 17 have explicit /Rotate 180

$td->runtest("page rotation",
             {$td->COMMAND => "qpdf --static-id to-rotate.pdf a.pdf" .
                  " --rotate=+90:1,4,11,16" .
                  " --rotate=180:2,5,12-13" .
                  " --rotate=-90:3,15,17,18"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "rotated.pdf"});

show_ntests();
# ----------
$td->notify("--- Numeric range parsing tests ---");
my @nrange_tests = (
    [",5",
     "qpdf: error at * in numeric range *,5: unexpected separator",
     2],
    ["4,,5",
     "qpdf: error at * in numeric range 4,*,5: unexpected separator",
     2],
    ["4,5,",
     "qpdf: error at * in numeric range 4,5,*: number expected",
     2],
    ["z1,",
     "qpdf: error at * in numeric range z*1,: digit not expected",
     2],
    ["1z,",
     "qpdf: error at * in numeric range 1*z,: z not expected",
     2],
    ["1-5?",
     "qpdf: error at * in numeric range 1-5*?: unexpected character",
     2],
    ["1-30",
     "qpdf: error in numeric range 1-30: number 30 out of range",
     2],
    ["1-10,0,5",
     "qpdf: error in numeric range 1-10,0,5: number 0 out of range",
     2],
    ["1-10,1234,5",
     "qpdf: error in numeric range 1-10,1234,5: number 1234 out of range",
     2],
    ["1,3,5-10,z-13,13,9,z,2",
     "numeric range 1,3,5-10,z-13,13,9,z,2" .
     " -> 1 3 5 6 7 8 9 10 15 14 13 13 9 15 2",
     0],
    );
$n_tests += scalar(@nrange_tests);
foreach my $d (@nrange_tests)
{
    my ($range, $output, $status) = @$d;
    $td->runtest("numeric range $range",
                 {$td->COMMAND => ['qpdf', '-- test-numrange=' . $range],
                  $td->FILTER => "grep 'numeric range'"},
                 {$td->STRING => $output . "\n", $td->EXIT_STATUS => $status},
                 $td->NORMALIZE_NEWLINES);
}

show_ntests();
# ----------
$td->notify("--- Merging and Splitting ---");
$n_tests += 8;

# Select pages from the same file multiple times including selecting
# twice from an encrypted file and specifying the password only the
# first time.  The file 20-pages.pdf is specified with two different
# paths to duplicate a page.
my $pages_options = "--pages page-labels-and-outlines.pdf 1,3,5-7,z" .
    " 20-pages.pdf --password=user z-15" .
    " page-labels-and-outlines.pdf 12" .
    " 20-pages.pdf 10" .
    " ./20-pages.pdf --password=owner 10" .
    " minimal.pdf 1 --";

$td->runtest("merge three files",
             {$td->COMMAND => "qpdf page-labels-and-outlines.pdf a.pdf" .
                  " $pages_options --static-id"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
# Manually verified about this file: make sure that outline entries
# that pointed to pages that were preserved still work in the copy,
# and verify that all pages are as expected.  page-labels-and-outlines
# as well as 20-pages have text on page n (from 1) that shows its page
# position from 0, so page 1 says it's page 0.
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "merge-three-files-1.pdf"});
# Select the same pages but add them to an empty file
$td->runtest("merge three files",
             {$td->COMMAND => "qpdf --empty a.pdf" .
                  " $pages_options --static-id"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
# Manually verified about this file: it has the same pages but does
# not contain outlines, page labels, or other things from the original
# file.
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "merge-three-files-2.pdf"});
$td->runtest("avoid respecification of password",
             {$td->COMMAND =>
                  "qpdf --empty a.pdf --copy-encryption=20-pages.pdf" .
                  " --encryption-file-password=user" .
                  " --pages 20-pages.pdf 1,z -- --static-id"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "pages-copy-encryption.pdf"});
$td->runtest("merge with implicit ranges",
             {$td->COMMAND =>
                  "qpdf --empty a.pdf" .
                  " --pages minimal.pdf 20-pages.pdf --password=user" .
                  " page-labels-and-outlines.pdf --" .
                  " --static-id"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "merge-implicit-ranges.pdf"});
show_ntests();
# ----------
$td->notify("--- PDF From Scratch ---");
$n_tests += 2;

$td->runtest("basic qpdf from scratch",
	     {$td->COMMAND => "pdf_from_scratch 0"},
	     {$td->STRING => "test 0 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "from-scratch-0.pdf"});
show_ntests();
# ----------
$td->notify("--- PCLm ---");
$n_tests += 2;

$td->runtest("write as PCLm",
	     {$td->COMMAND => "test_driver 40 pclm-in.pdf a.pdf"},
	     {$td->STRING => "test 40 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "pclm-out.pdf"});

show_ntests();
# ----------
$td->notify("--- Precheck streams ---");
$n_tests += 2;

$td->runtest("bad stream",
	     {$td->COMMAND => "qpdf --static-id bad-data.pdf a.pdf"},
	     {$td->FILE => "bad-data.out", $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "bad-data-out.pdf"});

show_ntests();
# ----------
$td->notify("--- Decode levels ---");
$n_tests += 12;

# image-streams.pdf is the output of examples/pdf-create.
# examples/pdf-create validates the actual image data.
foreach my $l (qw(none generalized specialized all))
{
    $td->runtest("image-streams: $l",
                 {$td->COMMAND =>
                      "qpdf image-streams.pdf --compress-streams=n" .
                      " --decode-level=$l a.pdf"},
                 {$td->STRING => "", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("check image-streams: $l",
                 {$td->COMMAND => "test_driver 39 a.pdf"},
                 {$td->FILE => "image-streams-$l.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
}

# Bad JPEG data
$td->runtest("check finds bad jpeg data",
             {$td->COMMAND => "qpdf --check bad-jpeg.pdf"},
             {$td->FILE => "bad-jpeg-check.out",
              $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
$td->runtest("precheck detects bad jpeg data",
             {$td->COMMAND => "qpdf --static-id --decode-level=all" .
                  " bad-jpeg.pdf a.pdf"},
             {$td->FILE => "bad-jpeg.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check file",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "bad-jpeg-out.pdf"});
$td->runtest("get data",
             {$td->COMMAND => "qpdf --show-object=6" .
                  " --filtered-stream-data bad-jpeg.pdf"},
             {$td->FILE => "bad-jpeg-show.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Preserve unreferenced objects ---");
$n_tests += 4;

$td->runtest("drop unused objects",
	     {$td->COMMAND => "qpdf --static-id unreferenced-objects.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "unreferenced-dropped.pdf"});
$td->runtest("keep unused objects",
	     {$td->COMMAND => "qpdf --static-id --preserve-unreferenced" .
                  " unreferenced-objects.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "unreferenced-preserved.pdf"});
show_ntests();
# ----------
$td->notify("--- Copy Foreign Objects ---");
$n_tests += 7;

foreach my $d ([25, 1], [26, 2], [27, 3])
{
    my ($testn, $outn) = @$d;
    $td->runtest("copy objects $outn",
                 {$td->COMMAND => "test_driver $testn" .
                      " copy-foreign-objects-in.pdf minimal.pdf"},
                 {$td->STRING => "test $testn done\n", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("check output",
                 {$td->FILE => "a.pdf"},
                 {$td->FILE => "copy-foreign-objects-out$outn.pdf"});
}
$td->runtest("copy objects error",
             {$td->COMMAND => "test_driver 28" .
                  " copy-foreign-objects-in.pdf minimal.pdf"},
             {$td->FILE => "copy-foreign-objects-errors.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
show_ntests();
# ----------
$td->notify("--- Error Condition Tests ---");
# $n_tests incremented after initialization of badfiles below.

my @badfiles = ("not a PDF file", 			# 1
		"no startxref",				# 2
		"bad primary xref offset",		# 3
		"invalid xref syntax",			# 4
		"invalid xref entry",			# 5
		"free table inconsistency",		# 6
		"no trailer dictionary",		# 7
		"bad secondary xref",			# 8
		"no /Size in trailer",			# 9
		"/Size not integer",			# 10
		"/Prev not integer",			# 11
		"/Size inconsistency",			# 12
		"bad {",				# 13
		"bad }",				# 14
		"bad ]",				# 15
		"bad >>",				# 16
		"odd number of dictionary items",	# 17
		"bad )",				# 18
		"bad >",				# 19
		"invalid hexstring character",		# 20
		"invalid name token",			# 21
		"no /Length for stream dictionary",	# 22
		"/Length not integer",			# 23
		"expected endstream",			# 24
		"bad obj declaration (objid)",		# 25
		"bad obj declaration (generation)",	# 26
		"bad obj declaration (obj)",		# 27
		"expected endobj",			# 28
		"null in name",				# 29
		"invalid stream /Filter",		# 30
		"unknown stream /Filter",		# 31
		"obj/gen mismatch",			# 32
		"invalid stream /Filter and xref",	# 33
		"obj/gen in wrong place",		# 34
		"object stream of wrong type",		# 35
                "bad dictionary key",                   # 36
		);

$n_tests += @badfiles + 3;

# Test 6 contains errors in the free table consistency, but we no
# longer have any consistency check for this since it is not important
# neither Acrobat nor other PDF viewers really care.  Tests 12 and 28
# have error conditions that used to be fatal but are now considered
# non-fatal.
my %badtest_overrides = ();
for(6, 12..15, 17, 22..28, 30..32, 34, 36)
{
    $badtest_overrides{$_} = 0;
}

for (my $i = 1; $i <= scalar(@badfiles); ++$i)
{
    my $status = $badtest_overrides{$i};
    $status = 2 unless defined $status;
    $td->runtest($badfiles[$i-1],
		 {$td->COMMAND => "test_driver 0 bad$i.pdf"},
		 {$td->FILE => "bad$i.out",
		  $td->EXIT_STATUS => $status},
		 $td->NORMALIZE_NEWLINES);
}

$td->runtest("C API: errors",
	     {$td->COMMAND => "qpdf-ctest 2 bad1.pdf '' a.pdf"},
	     {$td->FILE => "c-read-errors.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C API: warnings writing",
	     {$td->COMMAND => "qpdf-ctest 2 bad33.pdf '' a.pdf"},
	     {$td->FILE => "c-write-warnings.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C API: no recovery",
	     {$td->COMMAND => "qpdf-ctest 10 bad33.pdf '' a.pdf"},
	     {$td->FILE => "c-no-recovery.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Recovery Tests ---");
$n_tests += @badfiles + 6;

# Recovery tests.  These are mostly after-the-fact -- when recovery
# was implemented, some degree of recovery was possible on many of the
# files.  Mostly the recovery does not actually repair the error,
# though in some cases it may.  Acrobat Reader would not be able to
# recover any of these files any better.
my %recover_failures = ();
for (1, 7, 16, 18..21, 29, 35)
{
    $recover_failures{$_} = 1;
}
for (my $i = 1; $i <= scalar(@badfiles); ++$i)
{
    my $status = 0;
    if (exists $recover_failures{$i})
    {
	$status = 2;
    }
    $td->runtest("recover " . $badfiles[$i-1],
		 {$td->COMMAND => "test_driver 1 bad$i.pdf"},
		 {$td->FILE => "bad$i-recover.out",
		  $td->EXIT_STATUS => $status},
		 $td->NORMALIZE_NEWLINES);
}

# See if we can recover the cross reference table on a file that has
# been appended to even when it deletes and reuses objects.  We can't
# completely do it in the case of deleted objects, but we can get
# mostly there.
$td->runtest("good replaced page contents",
	     {$td->COMMAND =>
		  "qpdf --static-id -qdf --no-original-object-ids" .
		  " append-page-content.pdf a.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "append-page-content-good.qdf"});
$td->runtest("damaged replaced page contents",
	     {$td->COMMAND =>
		  "qpdf --static-id -qdf --no-original-object-ids" .
		  " append-page-content-damaged.pdf a.pdf"},
	     {$td->FILE => "append-page-content-damaged.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "append-page-content-damaged.qdf"});
$td->runtest("run check on damaged file",
	     {$td->COMMAND => "qpdf --check append-page-content-damaged.pdf"},
	     {$td->FILE => "append-page-content-damaged-check.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check with C API",
	     {$td->COMMAND =>
		  "qpdf-ctest 1 append-page-content-damaged.pdf '' ''"},
	     {$td->FILE => "append-page-content-damaged-c-check.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Basic Parsing Tests ---");
# $n_tests incremented below after initialization of @goodfiles.

my @goodfiles = ("implicit null",			# 1
		 "direct null",				# 2
		 "unresolved null",			# 3
		 "indirect null",			# 4
		 "indirect bool, real",			# 5
		 "direct bool",				# 6
		 "integer",				# 7
		 "real, ASCIIHexDecode",		# 8
		 "string",				# 9
		 "array",				# 10
		 "dictionary",				# 11
		 "stream",				# 12
		 "nesting, strings, names",		# 13
		 "tokenizing pipeline",			# 14
		 "name",				# 15
		 "object-stream",			# 16
		 "hybrid xref",				# 17
		 "hybrid xref old mode",		# 18
		 "xref with prev",			# 19
		 "lots of compressible objects",	# 20
		 "pound in name",                       # 21
		 );

$n_tests += (3 * @goodfiles) + 6;

my %goodtest_overrides = ('14' => 3);
my %goodtest_flags =
    ('18' => '-ignore-xref-streams',
     '20' => '-object-streams=generate',
    );
for (my $i = 1; $i <= scalar(@goodfiles); ++$i)
{
    my $n = $goodtest_overrides{$i} || 1;
    $td->runtest("$goodfiles[$i-1]",
		 {$td->COMMAND => "test_driver $n good$i.pdf"},
		 {$td->FILE => "good$i.out",
		  $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
    my $xflags = $goodtest_flags{$i} || '';
    check_pdf("create qdf",
	      "qpdf --static-id -qdf $xflags good$i.pdf",
	      "good$i.qdf", 0);
}

check_pdf("no normalization",
	  "qpdf -qdf --static-id --normalize-content=n good7.pdf",
	  "good7-not-normalized.qdf",
	  0);

check_pdf("no qdf",
	  "qpdf --static-id good17.pdf",
	  "good17-not-qdf.pdf",
	  0);

check_pdf("no recompression",
	  "qpdf --static-id --stream-data=preserve good17.pdf",
	  "good17-not-recompressed.pdf",
	  0);

show_ntests();
# ----------
$td->notify("--- C API Tests ---");

my @capi = (
    [2, 'no options'],
    [3, 'normalized content'],
    [4, 'ignore xref streams'],
    [5, 'linearized'],
    [6, 'object streams'],
    [7, 'qdf'],
    [8, 'no original object ids'],
    [9, 'uncompressed streams'],
    );
$n_tests += (2 * @capi) + 3;
foreach my $d (@capi)
{
    my ($n, $description) = @$d;
    my $outfile = $description;
    $outfile =~ s/ /-/g;
    $outfile = "c-$outfile.pdf";
    $td->runtest($description,
		 {$td->COMMAND => "qpdf-ctest $n hybrid-xref.pdf '' a.pdf"},
		 {$td->STRING => "", $td->EXIT_STATUS => 0});
    $td->runtest("check $description",
		 {$td->FILE => "a.pdf"},
		 {$td->FILE => $outfile});
}
$td->runtest("write to bad file name",
	     {$td->COMMAND => "qpdf-ctest 2 hybrid-xref.pdf '' /:a:/:b:"},
	     {$td->REGEXP => "error: open /:a:/:b:: .*",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("write damaged to bad file name",
	     {$td->COMMAND => "qpdf-ctest 2 append-page-content-damaged.pdf" .
		  " '' /:a:/:b:"},
	     {$td->REGEXP =>
		  "warning:(?s:.*)\n" .
		  "error: open /:a:/:b:: .*",
		  $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("write damaged",
	     {$td->COMMAND => "qpdf-ctest 2 append-page-content-damaged.pdf" .
		  " '' a.pdf"},
	     {$td->FILE => "c-write-damaged.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Deterministic ID Tests ---");
$n_tests += 11;
foreach my $d ('nn', 'ny', 'yn', 'yy')
{
    my $linearize = ($d =~ m/^y/);
    my $ostream = ($d =~ m/y$/);
    $td->runtest("deterministic ID: linearize/ostream=$d",
                 {$td->COMMAND =>
                      "qpdf -deterministic-id" .
                      ($linearize ? " -linearize" : "") .
                      " -object-streams=" . ($ostream ? "generate" : "disable") .
                      " deterministic-id-in.pdf a.pdf"},
                 {$td->STRING => "",
                  $td->EXIT_STATUS => 0});
    $td->runtest("compare files",
                 {$td->FILE => "a.pdf"},
                 {$td->FILE => "deterministic-id-$d.pdf"});
}

$td->runtest("deterministic ID with encryption",
             {$td->COMMAND => "qpdf -deterministic-id encrypted-with-images.pdf a.pdf"},
             {$td->STRING => "INTERNAL ERROR: QPDFWriter::generateID" .
                  " has no data for deterministic ID." .
                  "  This may happen if deterministic ID and" .
                  " file encryption are requested together.\n",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);
$td->runtest("deterministic ID (C API)",
             {$td->COMMAND =>
                  "qpdf-ctest 19 deterministic-id-in.pdf '' a.pdf"},
             {$td->STRING => "",
              $td->EXIT_STATUS => 0});
$td->runtest("compare files",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "deterministic-id-nn.pdf"});

# ----------
$td->notify("--- Object Stream Tests ---");
$n_tests += (36 * 4) + (12 * 2);
$n_compare_pdfs += 36;

for (my $n = 16; $n <= 19; ++$n)
{
    my $in = "good$n.pdf";
    foreach my $flags ('-object-streams=disable',
		       '-object-streams=preserve',
		       '-object-streams=generate')
    {
	foreach my $qdf ('-qdf', '', '-encrypt "" x 128 --')
	{
	    # 4 tests + 1 compare_pdfs * 36 cases
	    # 2 additional tests * 12 cases
	    $td->runtest("object stream mode",
			 {$td->COMMAND =>
			      "qpdf --static-id $flags $qdf $in a.pdf"},
			 {$td->STRING => "",
			  $td->EXIT_STATUS => 0});
	    compare_pdfs("good$n.pdf", "a.pdf");
	    if ($qdf eq '-qdf')
	    {
		$td->runtest("fix-qdf identity check",
			     {$td->COMMAND => "fix-qdf a.pdf >| b.pdf"},
			     {$td->STRING => "", $td->EXIT_STATUS => 0});
		$td->runtest("compare files",
			     {$td->FILE => "a.pdf"},
			     {$td->FILE => "b.pdf"});
	    }
	    $td->runtest("convert to qdf",
			 {$td->COMMAND =>
			      "qpdf --static-id --no-original-object-ids" .
			      " -qdf -decrypt" .
			      " -object-streams=disable $in a.qdf"},
			 {$td->STRING => "",
			  $td->EXIT_STATUS => 0});
	    $td->runtest("convert output to qdf",
			 {$td->COMMAND =>
			      "qpdf --static-id --no-original-object-ids" .
			      " -qdf -object-streams=disable a.pdf b.qdf"},
			 {$td->STRING => "",
			  $td->EXIT_STATUS => 0});
	    $td->runtest("compare files",
			 {$td->FILE => "a.qdf"},
			 {$td->FILE => "b.qdf"});
	}
    }
    flush_tiff_cache();
}

show_ntests();
# ----------
$td->notify("--- Specific File Tests ---");
$n_tests += 2;

# Special PDF files that caused problems at some point

$td->runtest("damaged stream",
	     {$td->COMMAND => "qpdf --check damaged-stream.pdf"},
	     {$td->FILE => "damaged-stream.out", $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("damaged stream (C)",
	     {$td->COMMAND => "qpdf-ctest 2 damaged-stream.pdf '' a.pdf"},
	     {$td->FILE => "damaged-stream-c-check.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Mutability Tests ---");
$n_tests += 4;

$td->runtest("no normalization",
	     {$td->COMMAND => "test_driver 4 test4-1.pdf"},
	     {$td->FILE => "test4-1.qdf",
	      $td->EXIT_STATUS => 0});

$td->runtest("object ordering",
	     {$td->COMMAND => "test_driver 4 test4-4.pdf"},
	     {$td->FILE => "test4-4.qdf",
	      $td->EXIT_STATUS => 0});

$td->runtest("loop detected",
	     {$td->COMMAND => "test_driver 4 test4-2.pdf"},
	     {$td->FILE => "test4-2.out",
	      $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("stream detected",
	     {$td->COMMAND => "test_driver 4 test4-3.pdf"},
	     {$td->FILE => "test4-3.out",
	      $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Extraction Tests ---");
$n_tests += 11;

$td->runtest("show xref",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-xref"},
	     {$td->FILE => "show-xref.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show pages",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-pages"},
	     {$td->FILE => "show-pages.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show-pages-images",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-pages --with-images"},
	     {$td->FILE => "show-pages-images.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show-page-1",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=5,0"},
	     {$td->FILE => "show-page-1.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show-page-1-content-raw",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=7 --raw-stream-data"},
	     {$td->FILE => "show-page-1-content-raw.out",
	      $td->EXIT_STATUS => 0});

$td->runtest("show-page-1-content-filtered",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=7 --filtered-stream-data"},
	     {$td->FILE => "show-page-1-content-filtered.out",
	      $td->EXIT_STATUS => 0});

$td->runtest("show-page-1-content-normalized",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=7,0 --filtered-stream-data --normalize-content=y"},
	     {$td->FILE => "show-page-1-content-normalized.out",
	      $td->EXIT_STATUS => 0});

$td->runtest("show-page-1-image",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=8 --raw-stream-data"},
	     {$td->FILE => "show-page-1-image.out",
	      $td->EXIT_STATUS => 0});

$td->runtest("unfilterable stream data",
	     {$td->COMMAND => "qpdf unfilterable.pdf" .
		  " --show-object=4 --filtered-stream-data"},
	     {$td->FILE => "show-unfilterable.out",
	      $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show-xref-by-id",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=12"},
	     {$td->FILE => "show-xref-by-id.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show-xref-by-id-filtered",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=12 --filtered-stream-data"},
	     {$td->FILE => "show-xref-by-id-filtered.out",
	      $td->EXIT_STATUS => 0});

show_ntests();
# ----------
$td->notify("--- Clear-text Metadata Tests ---");
$n_tests += 58;

# args: file, exp_encrypted, exp_cleartext
check_metadata("compressed-metadata.pdf", 0, 0);
check_metadata("enc-base.pdf", 0, 1);

foreach my $f (qw(compressed-metadata.pdf enc-base.pdf))
{
    foreach my $w (qw(compress preserve))
    {
	$td->runtest("$w streams ($f)",
		     {$td->COMMAND => "qpdf --stream-data=$w $f a.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("a.pdf", 0, 1);
	$td->runtest("encrypt normally",
		     {$td->COMMAND =>
			  "qpdf --encrypt '' '' 128 -- a.pdf b.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("b.pdf", 1, 0);
	unlink "b.pdf";
	$td->runtest("encrypt V4",
		     {$td->COMMAND =>
			  "qpdf --encrypt '' '' 128 --force-V4 -- a.pdf b.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("b.pdf", 1, 0);
	unlink "b.pdf";
	$td->runtest("encrypt with cleartext metadata",
		     {$td->COMMAND =>
			  "qpdf --encrypt '' '' 128 --cleartext-metadata --" .
			  " a.pdf b.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("b.pdf", 1, 1);
	$td->runtest("preserve encryption",
		     {$td->COMMAND => "qpdf b.pdf c.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("c.pdf", 1, 1);
	unlink "b.pdf", "c.pdf";
	$td->runtest("encrypt with aes and cleartext metadata",
		     {$td->COMMAND =>
			  "qpdf --encrypt '' '' 128" .
			  " --cleartext-metadata --use-aes=y -- a.pdf b.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("b.pdf", 1, 1);
	$td->runtest("preserve encryption",
		     {$td->COMMAND => "qpdf b.pdf c.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("c.pdf", 1, 1);
	unlink "b.pdf", "c.pdf";
    }
}

show_ntests();
# ----------
$td->notify("--- Linearization Tests ---");
# $n_tests incremented after initialization of @linearized_files and
# @to_linearize.

# *'ed files were linearized with Pdlin.
my @linearized_files =
    ('lin0',			#   not linearized
     'lin1',			# * outlines, page labels, pdlin
     'lin2',			# * lin1 with null and newline
     'lin3',			#   same file saved with acrobat
     'lin4',			# * lin1 with no /PageMode
     'lin5',			#   lin3 with embedded thumbnails
     'lin6',			# * lin5 with pdlin
     'lin7',			#   lin5 with /PageMode /UseThumbs
     'lin8',			# * lin7 with pdlin
     'lin9',			# * shared objects, indirect null
     'badlin1',			#   parameter dictionary errors
     );

my @to_linearize =
    ('lin-special',		# lots of weird cases -- see file comments
     'delete-and-reuse',	# deleted, reused objects
     'lin-delete-and-reuse',	# linearized, then delete and reuse
     'object-stream',		# contains object streams
     'hybrid-xref',	        # contains both xref tables and streams
     'gen1',                    # has objects with generation > 0
     'direct-outlines',         # /Outlines is a direct object
     @linearized_files,		# we should be able to relinearize
     );

$n_tests += @linearized_files + 6;
$n_tests += (3 * @to_linearize * 5) + 6;

foreach my $base (@linearized_files)
{
    $td->runtest("dump linearization: $base",
		 {$td->COMMAND => "qpdf --show-linearization $base.pdf"},
		 {$td->FILE => "$base.out",
		  $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
}

# Check normal modified and linearized modified files, making sure
# that their qdf files are identical.  The next two tests have the
# same expected output files and different input files.
check_pdf("modified",
	  "qpdf --static-id --qdf --no-original-object-ids" .
	  " delete-and-reuse.pdf", "delete-and-reuse.qdf",
	  0);
check_pdf("linearized and modified",
	  "qpdf --static-id --qdf --no-original-object-ids" .
	  " lin-delete-and-reuse.pdf", "delete-and-reuse.qdf", # same output
	  0);

$td->runtest("check linearized and modified",
	     {$td->COMMAND => "qpdf --check lin-delete-and-reuse.pdf"},
	     {$td->FILE => "lin-delete-and-reuse-check.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check multiple modifications",
	     {$td->COMMAND => "qpdf --check delete-and-reuse.pdf"},
	     {$td->FILE => "delete-and-reuse-check.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

foreach my $base (@to_linearize)
{
    foreach my $omode (qw(disable preserve generate))
    {
	my $oarg = "-object-streams=$omode";
        my $sdarg = "";
        if (($base eq 'lin-special') || ($base eq 'object-stream'))
        {
            $sdarg = "--stream-data=uncompress";
        }
	$td->runtest("linearize $base ($omode)",
		     {$td->COMMAND =>
			  "qpdf -linearize $oarg $sdarg" .
                          " --static-id $base.pdf a.pdf"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	$td->runtest("check linearization",
		     {$td->COMMAND => "qpdf --check-linearization a.pdf"},
		     {$td->STRING => "a.pdf: no linearization errors\n",
		      $td->EXIT_STATUS => 0},
		     $td->NORMALIZE_NEWLINES);
	# Relinearizing twice should produce identical results.  We
	# have to do it twice because, if objects changed ordering
	# during the original linearization, the hint tables won't
	# exactly match.  This is because object identifiers are
	# inserted into the hint table in their original order since
	# we don't yet have renumbering information when we compute
	# the table values.
	$td->runtest("relinearize $base 1",
		     {$td->COMMAND =>
			  "qpdf -linearize $sdarg --static-id a.pdf b.pdf"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	$td->runtest("relinearize $base 2",
		     {$td->COMMAND =>
			  "qpdf -linearize $sdarg --static-id b.pdf c.pdf"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	$td->runtest("compare files ($omode)",
		     {$td->FILE => "b.pdf"},
		     {$td->FILE => "c.pdf"});
	if (($base eq 'lin-special') || ($base eq 'object-stream'))
	{
	    $td->runtest("check $base ($omode)",
			 {$td->FILE => "a.pdf"},
			 {$td->FILE => "$base.$omode.exp"});
	}
    }
}

show_ntests();
# ----------
$td->notify("--- Encryption Tests ---");
# $n_tests incremented below

# The enc-file.pdf files were encrypted using Acrobat 5.0, not the
# qpdf library.  The files are decrypted using qpdf, then re-encrypted
# using qpdf with specific flags.  The /P value is checked.  The
# resulting files were saved and manually checked with Acrobat 5.0 to
# ensure that the security settings were as intended.

# The enc-XI-file.pdf files were treated the same way but with Acrobat
# XI instead of Acrobat 5.0.  They were used to create test files with
# newer encryption formats.

# Values: basename, password, encryption flags, /P Encrypt key,
# extract-for-accessibility, extract-for-any-purpose,
# print-low-res, print-high-res, modify-assembly, modify-forms,
# modify-annotate, modify-other, modify-all
my @encrypted_files =
    (['base', ''],
     ['R3,V2', '',
      '-accessibility=n -extract=n -print=full -modify=all', -532,
      0, 0, 1, 1, 1, 1, 1, 1, 1],
     ['R3,V2,U=view', 'view',
      '-accessibility=y -extract=n -print=none -modify=none', -3392,
      1, 0, 0, 0, 0, 0, 0, 0, 0],
     ['R3,V2,O=master', 'master',
      '-accessibility=n -extract=y -print=none -modify=annotate', -2576,
      0, 1, 0, 0, 1, 1, 1, 0, 0],
     ['R3,V2,O=master', '',
      '-accessibility=n -extract=n -print=none -modify=form', -2624,
      0, 0, 0, 0, 1, 1, 0, 0, 0],
     ['R3,V2,U=view,O=master', 'view',
      '-accessibility=n -extract=n -print=none -modify=assembly', -2880,
      0, 0, 0, 0, 1, 0, 0, 0, 0],
     ['R3,V2,U=view,O=master', 'master',
      '-accessibility=n -print=low', -2564,
      0, 1, 1, 0, 1, 1, 1, 1, 1],
     ['R2,V1', '',
      '-print=n -modify=n -extract=n -annotate=n', -64,
      0, 0, 0, 0, 0, 0, 0, 0, 0],
     ['R2,V1,U=view', 'view',
      '-print=y -modify=n -extract=n -annotate=n', -60,
      0, 0, 1, 1, 0, 0, 0, 0, 0],
     ['R2,V1,O=master', 'master',
      '-print=n -modify=y -extract=n -annotate=n', -56,
      0, 0, 0, 0, 1, 0, 0, 1, 0],
     ['R2,V1,O=master', '',
      '-print=n -modify=n -extract=y -annotate=n', -48,
      1, 1, 0, 0, 0, 0, 0, 0, 0],
     ['R2,V1,U=view,O=master', 'view',
      '-print=n -modify=n -extract=n -annotate=y', -32,
      0, 0, 0, 0, 0, 1, 1, 0, 0],
     ['R2,V1,U=view,O=master', 'master',
      '', -4,
      1, 1, 1, 1, 1, 1, 1, 1, 1],
     ['long-password', 'asdf asdf asdf asdf asdf asdf qwer'],
     ['long-password', 'asdf asdf asdf asdf asdf asdf qw'],
     ['XI-base', ''],
     ['XI-R6,V5,O=master', '',
      '-extract=n -print=none -modify=assembly', -2368,
      1, 0, 0, 0, 1, 0, 0, 0, 0],
     ['XI-R6,V5,O=master', 'master',
      '-extract=n -print=none -modify=assembly', -2368,
      1, 0, 0, 0, 1, 0, 0, 0, 0],
     ['XI-R6,V5,U=view,O=master', 'view',
      '-print=low', -2052,
      1, 1, 1, 0, 1, 1, 1, 1, 1],
     ['XI-R6,V5,U=view,O=master', 'master',
      '-print=low', -2052,
      1, 1, 1, 0, 1, 1, 1, 1, 1],
     ['XI-R6,V5,U=view,O=master', 'master',
      '-accessibility=n', -4, # -accessibility=n has no effect
      1, 1, 1, 1, 1, 1, 1, 1, 1],
     ['XI-long-password', 'qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'],
     ['XI-long-password', 'qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcv'],
    );

$n_tests += 5 + (2 * (@encrypted_files)) + (6 * (@encrypted_files - 6)) + 9;

$td->runtest("encrypted file",
	     {$td->COMMAND => "test_driver 2 encrypted-with-images.pdf"},
	     {$td->FILE => "encrypted1.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("preserve encryption",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf encrypted-with-images.enc"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0});
$td->runtest("recheck encrypted file",
	     {$td->COMMAND => "test_driver 2 encrypted-with-images.enc"},
	     {$td->FILE => "encrypted1.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Test that long passwords that are one character too short fail.  We
# test the truncation cases in the loop below by using passwords
# longer than the supported length.
$td->runtest("significant password characters (V < 5)",
             {$td->COMMAND => "qpdf --check enc-long-password.pdf" .
                  " --password='asdf asdf asdf asdf asdf asdf q'"},
             {$td->REGEXP => ".*invalid password.*", $td->EXIT_STATUS => 2});
$td->runtest("significant password characters (V = 5)",
             {$td->COMMAND => "qpdf --check enc-XI-long-password.pdf" .
                  " --password=qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxc"},
             {$td->REGEXP => ".*invalid password.*", $td->EXIT_STATUS => 2});

my $enc_base = undef;
foreach my $d (@encrypted_files)
{
    my ($file, $pass, $xeflags, $P,
	$accessible, $extract, $printlow, $printhigh,
	$modifyassembly, $modifyform, $modifyannot,
	$modifyother, $modifyall) = @$d;

    my $f = sub { $_[0] ? "allowed" : "not allowed" };
    my $enc_details =
	"extract for accessibility: " . &$f($accessible) . "\n" .
	"extract for any purpose: " . &$f($extract) . "\n" .
	"print low resolution: " . &$f($printlow) . "\n" .
	"print high resolution: " . &$f($printhigh) . "\n" .
	"modify document assembly: " . &$f($modifyassembly) . "\n" .
	"modify forms: " . &$f($modifyform) . "\n" .
	"modify annotations: " . &$f($modifyannot) . "\n" .
	"modify other: " . &$f($modifyother) . "\n" .
	"modify anything: " . &$f($modifyall) . "\n";
    if ($file =~ m/XI-/)
    {
        $enc_details .=
            "stream encryption method: AESv3\n" .
            "string encryption method: AESv3\n" .
            "file encryption method: AESv3\n";
    }

    # Test writing to stdout
    $td->runtest("decrypt $file",
		 {$td->COMMAND =>
		      "qpdf --static-id -qdf --object-streams=disable" .
                      " --no-original-object-ids" .
		      " --password=\"$pass\" enc-$file.pdf -" .
		      " > $file.enc"},
		 {$td->STRING => "",
		  $td->EXIT_STATUS => 0});
    if ($file =~ m/base$/)
    {
        $enc_base = $file;
	$td->runtest("check ID",
		     {$td->COMMAND => "perl check-ID.pl $file.enc"},
		     {$td->STRING => "ID okay\n",
		      $td->EXIT_STATUS => 0},
		     $td->NORMALIZE_NEWLINES);
    }
    else
    {
	$td->runtest("check against base",
		     {$td->COMMAND =>
                          "sh ./diff-encrypted $enc_base.enc $file.enc"},
		     {$td->STRING => "okay\n",
		      $td->EXIT_STATUS => 0},
		     $td->NORMALIZE_NEWLINES);
    }
    if ($file =~ m/^(?:XI-)?R(\d),V(\d)(?:,U=(\w+))?(?:,O=(\w+))?$/)
    {
	my $R = $1;
	my $V = $2;
	my $upass = $3 || "";
	my $opass = $4 || "";
	my $bits = (($V == 5) ? 256 : ($V == 2) ? 128 : 40);

	my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --";
        if (($pass ne $upass) && ($V >= 5))
        {
            # V >= 5 can no longer recover user password with owner
            # password.
            $upass = "";
        }
        my $accessibility_warning = "";
        if (($R > 3) && ($eflags =~ /accessibility=n/))
        {
            $accessibility_warning =
                "qpdf: -accessibility=n is ignored" .
                " for modern encryption formats\n";
        }
	$td->runtest("encrypt $file",
		     {$td->COMMAND =>
			  "qpdf --static-id --no-original-object-ids -qdf" .
			  " $eflags $file.enc $file.enc2"},
		     {$td->STRING => $accessibility_warning,
		      $td->EXIT_STATUS => 0},
                     $td->NORMALIZE_NEWLINES);
	$td->runtest("check /P",
		     {$td->COMMAND =>
			  "qpdf --show-encryption --password=\"$pass\"" .
			  " $file.enc2"},
		     {$td->STRING => "R = $R\nP = $P\n" .
			  "User password = $upass\n$enc_details",
			  $td->EXIT_STATUS => 0},
		     $td->NORMALIZE_NEWLINES);
	$td->runtest("decrypt again",
		     {$td->COMMAND =>
			  "qpdf --static-id --no-original-object-ids -qdf" .
			  " --password=\"$pass\"" .
			  " $file.enc2 $file.enc3"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	$td->runtest("compare",
		     {$td->FILE => "$file.enc"},
		     {$td->FILE => "$file.enc3"});
	$td->runtest("preserve encryption",
		     {$td->COMMAND =>
			  "qpdf --static-id --password=\"$pass\"" .
			  " $file.enc2 $file.enc4"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	$td->runtest("check /P",
		     {$td->COMMAND =>
			  "qpdf --show-encryption --password=\"$pass\"" .
			  " $file.enc4"},
		     {$td->STRING => "R = $R\nP = $P\n" .
			  "User password = $upass\n$enc_details",
			  $td->EXIT_STATUS => 0},
		     $td->NORMALIZE_NEWLINES);
    }
}

$td->runtest("non-encrypted",
	     {$td->COMMAND => "qpdf --show-encryption enc-base.pdf"},
	     {$td->STRING => "File is not encrypted\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("invalid password",
	     {$td->COMMAND => "qpdf -qdf --password=quack" .
		  " enc-R2,V1,U=view.pdf a.qdf"},
	     {$td->STRING => "enc-R2,V1,U=view.pdf: invalid password\n",
	      $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C API: invalid password",
	     {$td->COMMAND => "qpdf-ctest 2 enc-R2,V1,U=view.pdf '' a.qdf"},
	     {$td->FILE => "c-invalid-password.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

my @cenc = (
    [11, 'hybrid-xref.pdf', "''", 'r2', "", ""],
    [12, 'hybrid-xref.pdf', "''", 'r3', "", ""],
    [15, 'hybrid-xref.pdf', "''", 'r4', "", ""],
    [17, 'hybrid-xref.pdf', "''", 'r5', "", "owner3"],
    [18, 'hybrid-xref.pdf', "''", 'r6', "", "user4"],
    [13, 'c-r2.pdf', 'user1', 'decrypt with user',
     "user password: user1\n", ""],
    [13, 'c-r3.pdf', 'owner2', 'decrypt with owner',
     "user password: user2\n", ""],
    [13, 'c-r5-in.pdf', 'user3', 'decrypt R5 with user',
     "user password: user3\n", ""],
    [13, 'c-r6-in.pdf', 'owner4', 'decrypt R6 with owner',
     "user password: \n", ""],
    );
$n_tests += 2 * @cenc;

foreach my $d (@cenc)
{
    my ($n, $infile, $pass, $description, $output, $checkpass) = @$d;
    my $outfile = $description;
    $outfile =~ s/ /-/g;
    my $pdf_outfile = "c-$outfile.pdf";
    my $check_outfile = "c-$outfile.out";
    $td->runtest("C API encryption: $description",
		 {$td->COMMAND => "qpdf-ctest $n $infile $pass a.pdf"},
		 {$td->STRING => $output, $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
    if (-f $pdf_outfile)
    {
        $td->runtest("check $description content",
                     {$td->FILE => "a.pdf"},
                     {$td->FILE => $pdf_outfile});
    }
    else
    {
        # QPDF doesn't provide any way to make the random bits in
        # /Perms static, so we have no way to predictably create a
        # /V=5 encrypted file.  It's not worth adding this...the test
        # suite is adequate without having a statically predictable
        # file.
        $td->runtest("check $description",
                     {$td->COMMAND =>
                          "qpdf --check a.pdf --password=$checkpass"},
                     {$td->FILE => $check_outfile, $td->EXIT_STATUS => 0},
                     $td->NORMALIZE_NEWLINES);
    }
}

# Test combinations of linearization and encryption.  Note that we do
# content checks on encrypted and linearized files in various
# combinations below.  Here we are just making sure that they are
# linearized and/or encrypted as desired.

$td->runtest("linearize encrypted file",
	     {$td->COMMAND => "qpdf --linearize encrypted-with-images.pdf a.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --show-encryption a.pdf",
	      $td->FILTER => "grep -v allowed"},
	     {$td->STRING => "R = 3\nP = -4\nUser password = \n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check linearization",
	     {$td->COMMAND => "qpdf --check-linearization a.pdf"},
	     {$td->STRING => "a.pdf: no linearization errors\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("linearize and encrypt file",
	     {$td->COMMAND =>
		  "qpdf --linearize --encrypt user owner 128 --use-aes=y --" .
		  " lin-special.pdf a.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --show-encryption --password=owner a.pdf",
	      $td->FILTER => "grep -v allowed | grep -v method"},
	     {$td->STRING => "R = 4\nP = -4\nUser password = user\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check linearization",
	     {$td->COMMAND => "qpdf --check-linearization" .
		  " --password=user a.pdf"},
	     {$td->STRING => "a.pdf: no linearization errors\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Test AES encryption in various ways.
$n_tests += 18;
$td->runtest("encrypt with AES",
	     {$td->COMMAND => "qpdf --encrypt '' '' 128 --use-aes=y --" .
		  " enc-base.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --show-encryption a.pdf",
	      $td->FILTER => "grep -v allowed | grep -v method"},
	     {$td->STRING => "R = 4\nP = -4\nUser password = \n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("convert original to qdf",
	     {$td->COMMAND => "qpdf --static-id --no-original-object-ids" .
		  " --qdf --min-version=1.6 enc-base.pdf a.qdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("convert encrypted to qdf",
	     {$td->COMMAND => "qpdf --static-id --no-original-object-ids" .
		  " --qdf a.pdf b.qdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("compare files",
	     {$td->FILE => 'a.qdf'},
	     {$td->FILE => 'b.qdf'});
$td->runtest("linearize with AES and object streams",
	     {$td->COMMAND => "qpdf --encrypt '' '' 128 --use-aes=y --" .
		  " --linearize --object-streams=generate enc-base.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --show-encryption a.pdf",
	      $td->FILTER => "grep -v allowed | grep -v method"},
	     {$td->STRING => "R = 4\nP = -4\nUser password = \n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("linearize original",
	     {$td->COMMAND => "qpdf --linearize --object-streams=generate" .
		  " enc-base.pdf b.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("convert linearized original to qdf",
	     {$td->COMMAND => "qpdf --static-id --no-original-object-ids" .
		  " --qdf --object-streams=generate --min-version=1.6" .
		  " b.pdf a.qdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("convert encrypted to qdf",
	     {$td->COMMAND => "qpdf --static-id --no-original-object-ids" .
		  " --qdf --object-streams=generate a.pdf b.qdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("compare files",
	     {$td->FILE => 'a.qdf'},
	     {$td->FILE => 'b.qdf'});
$td->runtest("force version on aes encrypted",
	     {$td->COMMAND => "qpdf --force-version=1.4 a.pdf b.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check",
	     {$td->COMMAND => "qpdf --check b.pdf"},
	     {$td->FILE => "aes-forced-check.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("make sure there is no xref stream",
	     {$td->COMMAND => "grep /ObjStm b.pdf | wc -l"},
	     {$td->REGEXP => "\\s*0\\s*", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("encrypt with V=5,R=5",
             {$td->COMMAND =>
                  "qpdf --encrypt user owner 256 --force-R5 -- " .
                  "minimal.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --check a.pdf --password=owner"},
	     {$td->FILE => "V5R5.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("encrypt with V=5,R=6",
             {$td->COMMAND =>
                  "qpdf --encrypt user owner 256 -- " .
                  "minimal.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --check a.pdf --password=user"},
	     {$td->FILE => "V5R6.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Look at some actual V4 files
$n_tests += 14;
foreach my $d (['--force-V4', 'V4'],
	       ['--cleartext-metadata', 'V4-clearmeta'],
	       ['--use-aes=y', 'V4-aes'],
	       ['--cleartext-metadata --use-aes=y', 'V4-aes-clearmeta'])
{
    my ($args, $out) = @$d;
    $td->runtest("encrypt $args",
		 {$td->COMMAND => "qpdf --static-aes-iv --static-id" .
		      " --encrypt '' '' 128 $args -- enc-base.pdf a.pdf"},
		 {$td->STRING => "", $td->EXIT_STATUS => 0});
    $td->runtest("check output",
		 {$td->FILE => "a.pdf"},
		 {$td->FILE => "$out.pdf"});
    $td->runtest("show encryption",
		 {$td->COMMAND => "qpdf --show-encryption a.pdf"},
		 {$td->FILE => "$out-encryption.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
}
# Crypt Filter
$td->runtest("decrypt with crypt filter",
	     {$td->COMMAND => "qpdf --decrypt --static-id" .
		  " metadata-crypt-filter.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
	     {$td->FILE => 'a.pdf'},
	     {$td->FILE => 'decrypted-crypt-filter.pdf'});

# Copy encryption parameters
$n_tests += 10;
$td->runtest("create reference qdf",
             {$td->COMMAND =>
                  "qpdf --qdf --no-original-object-ids minimal.pdf a.qdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("create encrypted file",
	     {$td->COMMAND =>
		  "qpdf --encrypt user owner 128 --use-aes=y --extract=n --" .
                  " minimal.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("copy encryption parameters",
             {$td->COMMAND => "test_driver 30 minimal.pdf a.pdf"},
             {$td->STRING => "test 30 done\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check output encryption",
	     {$td->COMMAND => "qpdf --show-encryption b.pdf --password=owner"},
	     {$td->FILE => "copied-encryption.out",
	      $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("convert to qdf",
             {$td->COMMAND =>
                  "qpdf --qdf b.pdf b.qdf" .
                  " --password=owner --no-original-object-ids"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("compare qdf",
             {$td->COMMAND => "sh ./diff-ignore-ID-version a.qdf b.qdf"},
             {$td->STRING => "okay\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("copy encryption with qpdf",
             {$td->COMMAND =>
                  "qpdf --copy-encryption=a.pdf".
                  " --encryption-file-password=user" .
                  " minimal.pdf c.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check output encryption",
	     {$td->COMMAND => "qpdf --show-encryption c.pdf --password=owner"},
	     {$td->FILE => "copied-encryption.out",
	      $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("convert to qdf",
             {$td->COMMAND =>
                  "qpdf --qdf c.pdf c.qdf" .
                  " --password=owner --no-original-object-ids"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("compare qdf",
             {$td->COMMAND => "sh ./diff-ignore-ID-version a.qdf c.qdf"},
             {$td->STRING => "okay\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

# Files with attachments
my @attachments = (
    'enc-XI-attachments-base.pdf',
    'enc-XI-R6,V5,U=attachment,encrypted-attachments.pdf',
    'enc-XI-R6,V5,U=view,attachments,cleartext-metadata.pdf');
$n_tests += 4 * @attachments + 3;
foreach my $f (@attachments)
{
    my $pass = '';
    my $tpass = '';
    if ($f =~ m/U=([^,\.]+)/)
    {
        $pass = "--password=$1";
        $tpass = $1;
    }
    $td->runtest("decrypt $f",
                 {$td->COMMAND => "qpdf --decrypt $pass $f a.pdf"},
                 {$td->STRING => "", $td->EXIT_STATUS => 0});
    $td->runtest("extract attachments",
                 {$td->COMMAND => "test_driver 35 a.pdf"},
                 {$td->FILE => "attachments.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("copy $f",
                 {$td->COMMAND => "qpdf $pass $f a.pdf"},
                 {$td->STRING => "", $td->EXIT_STATUS => 0});
    $td->runtest("extract attachments",
                 {$td->COMMAND => "test_driver 35 a.pdf $tpass"},
                 {$td->FILE => "attachments.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
}
$td->runtest("unfilterable with crypt",
             {$td->COMMAND =>
                  "test_driver 36 unfilterable-with-crypt.pdf attachment"},
             {$td->FILE => "unfilterable-with-crypt-before.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
unlink "a.pdf";
$td->runtest("decrypt file",
             {$td->COMMAND => "qpdf -decrypt --password=attachment" .
                  " unfilterable-with-crypt.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("copy of unfilterable with crypt",
             {$td->COMMAND =>
                  "test_driver 36 a.pdf attachment"},
             {$td->FILE => "unfilterable-with-crypt-after.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Content Preservation Tests ---");
# $n_tests incremented below

my @files = ("encrypted-with-images.pdf", # encrypted
	     "inline-images.pdf",
	     "lin-special.pdf",
	     "object-stream.pdf",
	     "hybrid-xref.pdf");
my @flags = (["-qdf",						# 1
	      "qdf"],
	     ["-qdf --normalize-content=n",			# 2
	      "qdf not normalized"],
	     ["-qdf --stream-data=preserve",			# 3
	      "qdf not uncompressed"],
	     ["-qdf --stream-data=preserve --normalize-content=n", # 4
	      "qdf not normalized or uncompressed"],
	     ["--stream-data=uncompress",			# 5
	      "uncompresed"],
	     ["--normalize-content=y",				# 6
	      "normalized"],
	     ["--stream-data=uncompress --normalize-content=y",	# 7
	      "uncompressed and normalized"],
	     ["-decrypt",					# 8
	      "decrypted"],
	     ["-linearize",					# 9
	      "linearized"],
	     ["-encrypt \"\" owner 128 --",			# 10
	      "encrypted"],
	     ["-linearize -encrypt \"\" o 128 --",		# 11
	      "linearized and encrypted"],
	     ["",						# 12
	      "no arguments"],
	     );

$n_tests += 1 + (@files * @flags  * 2 * 3);
$n_compare_pdfs += 1 + (@files * @flags * 2);
$n_acroread += (@files * @flags * 2);

foreach my $file (@files)
{
    my $base = basename($file, '.pdf');

    foreach my $o (qw(disable generate))
    {
	my $n = 0;
	my $oflags = "--object-streams=$o";
	my $odescrip = "os:" . substr($o, 0, 1);
	my $osuf = ($o eq 'generate' ? "-ogen" : "");
	foreach my $d (@flags)
	{
	    my ($flags, $fdescrip) = @$d;
	    ++$n;
	    system("rm -f *.pnm");

	    $td->runtest("$file ($odescrip $fdescrip)",
			 {$td->COMMAND => "qpdf $flags $oflags $file a.pdf"},
			 {$td->STRING => "",
			  $td->EXIT_STATUS => 0});

	    $td->runtest("check status",
			 {$td->COMMAND => "qpdf --check a.pdf"},
			 {$td->FILE => "$base.$n$osuf.check",
			  $td->EXIT_STATUS => 0},
			 $td->NORMALIZE_NEWLINES);

	    $td->runtest("check with C API",
			 {$td->COMMAND => [qw(qpdf-ctest 1 a.pdf), "", ""]},
			 {$td->FILE => "$base.$n$osuf.c-check",
			  $td->EXIT_STATUS => 0},
			 $td->NORMALIZE_NEWLINES);

	    compare_pdfs($file, "a.pdf");

	    if ($have_acroread)
	    {
		# These tests require Adobe Reader > 7.x to work with
		# encrypted files.
		$td->runtest("check with Adobe Reader",
			     {$td->COMMAND =>
				  "acroread -toPostScript -pairs a.pdf 1.ps"},
			     {$td->STRING => "",
			      $td->EXIT_STATUS => 0});
	    }
	}
	flush_tiff_cache();
    }
}

$td->runtest("convert inline-images to qdf",
	     {$td->COMMAND => "qpdf --static-id --no-original-object-ids" .
		  " --qdf inline-images.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});

compare_pdfs("inline-images.pdf", "a.pdf");

show_ntests();
# ----------
$td->notify("--- fix-qdf Tests ---");
$n_tests += 5;

for (my $n = 1; $n <= 2; ++$n)
{
    $td->runtest("fix-qdf $n",
		 {$td->COMMAND => "fix-qdf fix$n.qdf"},
		 {$td->FILE => "fix$n.qdf.out",
		  $td->EXIT_STATUS => 0});

    $td->runtest("identity fix-qdf $n",
		 {$td->COMMAND => "fix-qdf fix$n.qdf.out"},
		 {$td->FILE => "fix$n.qdf.out",
		  $td->EXIT_STATUS => 0});
}
$td->runtest("fix-qdf with big object stream", # > 255 objects in a stream
             {$td->COMMAND => "fix-qdf big-ostream.pdf"},
             {$td->FILE => "big-ostream.pdf",
              $td->EXIT_STATUS => 0});

show_ntests();
# ----------
$td->notify("--- Large File Tests ---");
my $nlarge = 1;
if (defined $large_file_test_path)
{
    $nlarge = 2;
}
else
{
    $td->notify("--- Skipping tests on actual large files ---");
}
$n_tests += $nlarge * 13;
for (my $large = 0; $large < $nlarge; ++$large)
{
    if ($large)
    {
        $td->notify("--- Running tests on actual large files ---");
    }
    else
    {
        $td->notify("--- Running large file tests on small files ---");
    }
    my $size = ($large ? "large" : "small");
    my $file = $large ? "$large_file_test_path/a.pdf" : "a.pdf";
    $td->runtest("write test file",
                 {$td->COMMAND => "test_large_file write $size '$file'"},
                 {$td->FILE => "large_file.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("read test file",
                 {$td->COMMAND => "test_large_file read $size '$file'"},
                 {$td->FILE => "large_file.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("check",
                 {$td->COMMAND => "qpdf --suppress-recovery --check '$file'",
                  $td->FILTER => "grep -v checking"},
                 {$td->FILE => "large_file-check-normal.out",
                  $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);

    for my $ostream (0, 1)
    {
        for my $linearize (0, 1)
        {
            if (($ostream == 0) && ($linearize == 0))
            {
                # Original file has no object streams and is not linearized.
                next;
            }
            my $args = "";
            my $omode = $ostream ? "generate" : "disable";
            my $lin = $linearize ? "--linearize" : "";
            my $newfile = "$file-new";

            $td->runtest("transform: ostream=$ostream, linearize=$linearize",
                         {$td->COMMAND =>
                              "qpdf --stream-data=preserve" .
                              " --object-streams=$omode" .
                              " $lin '$file' '$newfile'"},
                         {$td->STRING => "", $td->EXIT_STATUS => 0});
            $td->runtest("read test file",
                         {$td->COMMAND =>
                              "test_large_file read $size '$newfile'"},
                         {$td->FILE => "large_file.out", $td->EXIT_STATUS => 0},
                         $td->NORMALIZE_NEWLINES);
            my $check_out =
                ($linearize
                 ? ($ostream
                    ? "large_file-check-ostream-linearized.out"
                    : "large_file-check-linearized.out")
                 : ($ostream
                    ? "large_file-check-ostream.out"
                    : "large_file-check-normal.out"));
            $td->runtest("check: ostream=$ostream, linearize=$linearize",
                         {$td->COMMAND =>
                              "qpdf --suppress-recovery --check '$newfile'",
                          $td->FILTER => "grep -v checking"},
                         {$td->FILE => $check_out, $td->EXIT_STATUS => 0},
                         $td->NORMALIZE_NEWLINES);
            unlink $newfile;
        }
    }

    # Clobber xref
    open(F, "+<$file") or die;
    seek(F, -50, 2);
    my $pos = tell F;
    my $buf;
    read(F, $buf, 50);
    die unless $buf =~ m/^(.*startxref\n)\d+/s;
    $pos += length($1);
    seek(F, $pos, 0) or die;
    print F "oops" or die;
    close(F);

    my $cmd = +{$td->COMMAND => "test_large_file read $size '$file'"};
    if ($large)
    {
        $cmd->{$td->FILTER} = "sed -e 's,$large_file_test_path/,,'";
    }
    $td->runtest("reconstruct xref table",
                 $cmd,
                 {$td->FILE => "large_file_xref_reconstruct.out",
                  $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    unlink $file;
}
# ----------

cleanup();

# See comments at beginning about calculation of number of tests.  We
# do it strictly based on static values, not as a by-product of
# running the test suite.
$td->report(calc_ntests());

sub calc_ntests
{
    my $result = $n_tests;
    if ($have_acroread)
    {
	$result += $n_acroread;
    }
    if ($compare_images)
    {
	$result += 3 * ($n_compare_pdfs);
    }
    $result;
}

sub show_ntests
{
    if (0)
    {
	$td->emphasize("tests so far: ". calc_ntests());
    }
}

sub check_pdf
{
    my ($description, $command, $output, $status) = @_;
    unlink "a.pdf";
    $td->runtest($description,
		 {$td->COMMAND => "$command a.pdf"},
		 {$td->STRING => "",
		  $td->EXIT_STATUS => $status});
    $td->runtest("check output",
		 {$td->FILE => "a.pdf"},
		 {$td->FILE => $output});
}

sub flush_tiff_cache
{
    system("rm -rf tiff-cache");
}

sub compare_pdfs
{
    return unless $compare_images;

    my ($f1, $f2, $exp) = @_;

    $exp = 0 unless defined $exp;

    system("rm -rf tif1 tif2");

    mkdir "tiff-cache", 0777 unless -d "tiff-cache";

    my $md5_1 = get_md5_checksum($f1);
    my $md5_2 = get_md5_checksum($f2);

    mkdir "tif1", 0777 or die;
    mkdir "tif2", 0777 or die;

    if (-f "tiff-cache/$md5_1.tif")
    {
	$td->runtest("get cached original file image",
		     {$td->COMMAND => "cp tiff-cache/$md5_1.tif tif1/a.tif"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
    }
    else
    {
        # We discard gs's stderr since it has sometimes been known to
        # complain about files that are not bad.  In particular, gs
        # 9.04 can't handle empty xref sections such as those found in
        # the hybrid xref cases.  We don't really care whether gs
        # complains or not as long as it creates correct images.  If
        # it doesn't create correct images, the test will fail, and we
        # can run manually to see the error message.  If it does, then
        # we don't care about the warning.
	$td->runtest("convert original file to image",
		     {$td->COMMAND =>
			  "(cd tif1;" .
			  " gs 2>$devNull -q -dNOPAUSE -sDEVICE=tiff24nc" .
			  " -sOutputFile=a.tif - < ../$f1)"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	copy("tif1/a.tif", "tiff-cache/$md5_1.tif");
    }

    if (-f "tiff-cache/$md5_2.tif")
    {
	$td->runtest("get cached new file image",
		     {$td->COMMAND => "cp tiff-cache/$md5_2.tif tif2/a.tif"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
    }
    else
    {
	$td->runtest("convert new file to image",
		     {$td->COMMAND =>
			  "(cd tif2;" .
			  " gs 2>$devNull -q -dNOPAUSE -sDEVICE=tiff24nc" .
			  " -sOutputFile=a.tif - < ../$f2)"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	copy("tif2/a.tif", "tiff-cache/$md5_2.tif");
    }

    $td->runtest("compare images",
		 {$td->COMMAND => "tiffcmp -t tif1/a.tif tif2/a.tif"},
		 {$td->REGEXP => ".*",
		  $td->EXIT_STATUS => $exp});

    system("rm -rf tif1 tif2");
}

sub check_metadata
{
    my ($file, $exp_encrypted, $exp_cleartext) = @_;
    my $out = "encrypted=$exp_encrypted; cleartext=$exp_cleartext\n" .
	"test 6 done\n";
    $td->runtest("check metadata: $file",
		 {$td->COMMAND => "test_driver 6 $file"},
		 {$td->STRING => $out, $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
}

sub get_md5_checksum
{
    my $file = shift;
    open(F, "<$file") or fatal("can't open $file: $!");
    binmode F;
    my $digest = Digest::MD5->new->addfile(*F)->hexdigest;
    close(F);
    $digest;
}

sub cleanup
{
    system("rm -rf *.ps *.pnm ?.pdf ?.qdf *.enc* tif1 tif2 tiff-cache");
    system("rm -rf *split-out*");
}


Fixed version (ground truth):
#!/usr/bin/env perl
require 5.008;
BEGIN { $^W = 1; }
use strict;
use Cwd;
use Digest::MD5;
use File::Basename;
use File::Copy;
use File::Spec;

chdir("qpdf") or die "chdir testdir failed: $!\n";

require TestDriver;

cleanup();

my $devNull = File::Spec->devnull();
my $td = new TestDriver('qpdf');

my $compare_images = 1;
if ((exists $ENV{'QPDF_SKIP_TEST_COMPARE_IMAGES'}) &&
    ($ENV{'QPDF_SKIP_TEST_COMPARE_IMAGES'} eq '1'))
{
    $compare_images = 0;
}
my $large_file_test_path = $ENV{'QPDF_LARGE_FILE_TEST_PATH'} || undef;
if (defined($large_file_test_path))
{
    $large_file_test_path = File::Spec->rel2abs($large_file_test_path);
    $large_file_test_path =~ s!\\!/!g;
}

my $have_acroread = 0;

if ($compare_images)
{
    # check for working acroread
    if (system("acroread -toPostScript -pairs good1.pdf a.ps" .
               " >$devNull 2>&1") == 0)
    {
	$have_acroread = 1;
    }
}

# These variables are used to store the total number of tests in the
# test suite.  NOTE: qtest's requirement to indicate the number of
# tests serves as a check that the test suite is operating properly.
# Do not calculate these values as a side effect of running the tests.
# That defeats the purpose.  However, since this test suite consists
# of several separate series of tests, many of which iterate over
# static lists of things, we calculate the numbers as we go in terms
# of static values.

# This should be set to the number of times we called compare_pdfs.
# This has to be kept separate because the number of test cases
# compare_pdfs generates depends on the value of $compare_images.
my $n_compare_pdfs = 0;

# This should be set to the number of times we call acroread.
my $n_acroread = 0;

# Each section of tests should increment this number by the number of
# tests they generate excluding calls to acroread or compare_pdfs,
# which are tracked separately by $n_compare_pdfs and $n_acroread.
my $n_tests = 0;

# Call show_ntests after each block of test cases.  In show_ntests,
# you can turn on printing of the expected number of test cases.  This
# is useful for tracking down problems in the number of test cases.

show_ntests();
# ----------

$n_compare_pdfs += 5;

# Check compare_pdfs to make sure that it works properly.  Each call
# to compare_pdfs is worth three test cases.
compare_pdfs("p1-a-p2-b.pdf", "p1-a-p2-b.pdf");
compare_pdfs("p1-a.pdf", "p1-a.pdf");
compare_pdfs("p1-a.pdf", "p1-b.pdf", 1);
compare_pdfs("p1-a.pdf", "p1-a-p2-b.pdf", 1);
compare_pdfs("p1-a-p2-a.pdf", "p1-a-p2-b.pdf", 1);
flush_tiff_cache();

show_ntests();
# ----------
$td->notify("--- Stream Replacement Tests ---");
$n_tests += 8;

$td->runtest("replace stream data",
	     {$td->COMMAND => "test_driver 7 qstream.pdf"},
	     {$td->STRING => "test 7 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "replaced-stream-data.pdf"});
$td->runtest("replace stream data compressed",
	     {$td->COMMAND => "test_driver 8 qstream.pdf"},
	     {$td->FILE => "test8.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "replaced-stream-data-flate.pdf"});
$td->runtest("new streams",
	     {$td->COMMAND => "test_driver 9 minimal.pdf"},
	     {$td->FILE => "test9.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("new stream",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "new-streams.pdf"});
$td->runtest("add page contents",
	     {$td->COMMAND => "test_driver 10 minimal.pdf"},
	     {$td->STRING => "test 10 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("new stream",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "add-contents.pdf"});

show_ntests();
# ----------
$td->notify("--- Extensions Dictionary Tests ---");
my @ext_inputs = ('minimal.pdf', 'extensions-adbe.pdf',
                  'extensions-other.pdf', 'extensions-adbe-other.pdf');
my @new_versions = ('1.3', '1.6', '1.7.1', '1.7.2', '1.7.3',
                    '1.8', '1.8.0', '1.8.2', '1.8.5');
$n_tests += (4 * @new_versions + 3) * @ext_inputs;
foreach my $input (@ext_inputs)
{
    my $base = $input;
    $base =~ s/\.pdf$//;
    if ($base eq 'minimal')
    {
        $base = 'extensions-none';
    }
    foreach my $version (@new_versions)
    {
        foreach my $op (qw(min force))
        {
            $td->runtest("$input: $op version to $version",
                         {$td->COMMAND =>
                              "qpdf --static-id" .
                              " --$op-version=$version $input a.pdf"},
                         {$td->STRING => "", $td->EXIT_STATUS => 0});
            $td->runtest("check version information ($op $version)",
                         {$td->COMMAND => "test_driver 34 a.pdf"},
                         {$td->FILE => "$base-$op-$version.out",
                          $td->EXIT_STATUS => 0},
                         $td->NORMALIZE_NEWLINES);
            if (($op eq 'force') && ($version eq '1.8.5'))
            {
                # Look at the actual file for a few cases to make sure
                # qdf and non-qdf output are okay
                $td->runtest("check file",
                             {$td->FILE => "a.pdf"},
                             {$td->FILE => "$base-$op-$version.pdf"});
                $td->runtest("$input: $op version to $version",
                             {$td->COMMAND =>
                                  "qpdf --qdf --static-id" .
                                  " --$op-version=$version $input a.qdf"},
                             {$td->STRING => "", $td->EXIT_STATUS => 0});
                $td->runtest("check file",
                             {$td->FILE => "a.qdf"},
                             {$td->FILE => "$base-$op-$version.qdf"});
            }
        }
    }
}
show_ntests();
# ----------
$td->notify("--- Page API Tests ---");
$n_tests += 9;

$td->runtest("basic page API",
	     {$td->COMMAND => "test_driver 15 page_api_1.pdf"},
	     {$td->STRING => "test 15 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "page_api_1-out.pdf"});
$td->runtest("manual page manipulation",
	     {$td->COMMAND => "test_driver 16 page_api_1.pdf"},
	     {$td->STRING => "test 16 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "page_api_1-out2.pdf"});
$td->runtest("duplicate page",
	     {$td->COMMAND => "test_driver 17 page_api_2.pdf"},
	     {$td->FILE => "page_api_2.out", $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("delete and re-add a page",
	     {$td->COMMAND => "test_driver 18 page_api_1.pdf"},
	     {$td->STRING => "test 18 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "page_api_1-out3.pdf"});
$td->runtest("duplicate page",
	     {$td->COMMAND => "test_driver 19 page_api_1.pdf"},
	     {$td->FILE => "page_api_1.out", $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("remove page we don't have",
	     {$td->COMMAND => "test_driver 22 page_api_1.pdf"},
	     {$td->FILE => "page_api_1.out2", $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);
show_ntests();
# ----------
$td->notify("--- Files for specific bugs ---");
# The number is the github issue number in which the bug was reported.
my @bug_tests = (
    ["51", "resolve loop", 3],
    ["99", "object 0", 2],
    ["99b", "object 0", 2],
    ["100", "xref reconstruction loop", 2],
    ["101", "resolve for exception text", 2],
    ["117", "other infinite loop", 2],
    ["118", "other infinite loop", 2],
    ["119", "other infinite loop", 3],
    ["120", "other infinite loop", 3],
    ["106", "zlib data error", 3],
    ["141a", "/W entry size 0", 2],
    ["141b", "/W entry size 0", 2],
    ["143", "self-referential ostream", 3],
    ["146", "very deeply nested array", 2],
    ["147", "previously caused memory error", 2],
    ["148", "free memory on bad flate", 2],
    ["149", "xref prev pointer loop", 3],
    ["150", "integer overflow", 2],
    );
$n_tests += scalar(@bug_tests);
foreach my $d (@bug_tests)
{
    my ($n, $description, $exit_status) = @$d;
    $td->runtest($description,
                 {$td->COMMAND => "qpdf issue-$n.pdf a.pdf"},
                 {$td->FILE => "issue-$n.out",
                  $td->EXIT_STATUS => $exit_status},
                 $td->NORMALIZE_NEWLINES);
}
show_ntests();
# ----------
$td->notify("--- Miscellaneous Tests ---");
$n_tests += 86;

$td->runtest("qpdf version",
	     {$td->COMMAND => "qpdf --version"},
	     {$td->REGEXP => "qpdf version \\S+\n.*", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C API: qpdf version",
	     {$td->COMMAND => "qpdf-ctest --version"},
	     {$td->REGEXP => "qpdf-ctest version \\S+\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

foreach (my $i = 1; $i <= 3; ++$i)
{
    $td->runtest("misc tests",
		 {$td->COMMAND => "test_driver 5 misc-$i.pdf"},
		 {$td->FILE => "misc-$i.out", $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
}

$td->runtest("get stream data",
	     {$td->COMMAND => "test_driver 11 stream-data.pdf"},
	     {$td->FILE => "test11.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Make sure we ignore decode parameters that we don't understand
$td->runtest("unknown decode parameters",
	     {$td->COMMAND => "qpdf --check fax-decode-parms.pdf"},
	     {$td->FILE => "fax-decode-parms.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Handle xref stream with more entries than reported (bug 2872265)
$td->runtest("xref with short size",
	     {$td->COMMAND => "qpdf --show-xref xref-with-short-size.pdf"},
	     {$td->FILE => "xref-with-short-size.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("recover xref with short size",
	     {$td->COMMAND => "qpdf xref-with-short-size.pdf a.pdf"},
	     {$td->FILE => "xref-with-short-size-recover.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("show new xref stream",
	     {$td->COMMAND => "qpdf --show-xref a.pdf"},
	     {$td->FILE => "xref-with-short-size-new.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Handle file with object stream containing an unreferenced object
# that in turn contains an indirect scalar (bug 2974522).
$td->runtest("unreferenced indirect scalar",
	     {$td->COMMAND =>
		  "qpdf --qdf --static-id --object-streams=preserve" .
		  " unreferenced-indirect-scalar.pdf a.qdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.qdf"},
	     {$td->FILE => "unreferenced-indirect-scalar.out"});

# Encrypt files whose /ID strings are other than 32 bytes long (bug
# 2991412).
foreach my $file (qw(short-id long-id))
{
    $td->runtest("encrypt $file.pdf",
		 {$td->COMMAND =>
		      "qpdf --encrypt '' pass 40 -- $file.pdf a.pdf"},
		 {$td->STRING => "",
		  $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);

    $td->runtest("check $file.pdf",
		 {$td->COMMAND => "qpdf --check a.pdf"},
		 {$td->FILE => "$file-check.out",
		  $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
}

# Handle file with invalid xref table and object 0 as a regular object
# (bug 3159950).
$td->runtest("check obj0.pdf",
	     {$td->COMMAND => "qpdf --check obj0.pdf"},
	     {$td->FILE => "obj0-check.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);

# Min/Force version
$td->runtest("set min version",
	     {$td->COMMAND => "qpdf --verbose --min-version=1.6 good1.pdf a.pdf"},
	     {$td->STRING => "qpdf: wrote file a.pdf\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check version",
	     {$td->COMMAND => "qpdf --check a.pdf"},
	     {$td->FILE => "min-version.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("force version",
	     {$td->COMMAND => "qpdf --force-version=1.4 a.pdf b.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check version",
	     {$td->COMMAND => "qpdf --check b.pdf"},
	     {$td->FILE => "forced-version.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
unlink "a.pdf", "b.pdf" or die;
$td->runtest("C API: min/force versions",
	     {$td->COMMAND => "qpdf-ctest 14 object-stream.pdf '' a.pdf b.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C check version 1",
	     {$td->COMMAND => "qpdf-ctest 1 a.pdf '' ''"},
	     {$td->FILE => "c-min-version.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C check version 2",
	     {$td->COMMAND => "qpdf --check b.pdf"},
	     {$td->FILE => "forced-version.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Stream filter abbreviations from table H.1
$td->runtest("stream filter abbreviations",
	     {$td->COMMAND => "qpdf --static-id filter-abbreviation.pdf a.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "filter-abbreviation.out"});

$td->runtest("empty object",
	     {$td->COMMAND => "qpdf -show-object=7,0 empty-object.pdf"},
	     {$td->FILE => "empty-object.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("error/output redirection to null",
	     {$td->COMMAND => "test_driver 12 linearized-and-warnings.pdf"},
	     {$td->FILE => "linearized-and-warnings-1.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("error/output redirection to strings",
	     {$td->COMMAND => "test_driver 13 linearized-and-warnings.pdf"},
	     {$td->FILE => "linearized-and-warnings-2.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("odd terminators for stream keyword",
	     {$td->COMMAND =>
		  "qpdf --qdf --static-id" .
		  " stream-line-enders.pdf a.qdf"},
	     {$td->FILE => "stream-line-enders.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.qdf"},
	     {$td->FILE => "stream-line-enders.qdf"});

$td->runtest("swap and replace",
	     {$td->COMMAND => "test_driver 14 test14-in.pdf"},
	     {$td->FILE => "test14.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "test14-out.pdf"});
# Test 14 also exercises writing to memory without static ID.
$td->runtest("check non-static ID version",
             {$td->COMMAND => "sh ./diff-ignore-ID-version a.pdf b.pdf"},
             {$td->STRING => "okay\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

$td->runtest("C API info key functions",
	     {$td->COMMAND => "qpdf-ctest 16 minimal.pdf '' a.pdf"},
	     {$td->FILE => "c-info1.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "c-info-out.pdf"});
unlink "a.pdf" or die;

$td->runtest("C API info key functions",
	     {$td->COMMAND => "qpdf-ctest 16 c-info2-in.pdf '' a.pdf"},
	     {$td->FILE => "c-info2.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "c-info-out.pdf"});
unlink "a.pdf" or die;

$td->runtest("shallow copy an array",
	     {$td->COMMAND => "test_driver 20 shallow_array.pdf"},
	     {$td->STRING => "test 20 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "shallow_array-out.pdf"});
$td->runtest("shallow copy a stream",
	     {$td->COMMAND => "test_driver 21 shallow_array.pdf"},
	     {$td->FILE => "shallow_stream.out", $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("warn for unknown key in Pages",
             {$td->COMMAND => "test_driver 23 lin-special.pdf"},
             {$td->FILE => "pages-warning.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("reserved objects",
             {$td->COMMAND => "test_driver 24 minimal.pdf"},
             {$td->FILE => "reserved-objects.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "reserved-objects.pdf"});
$td->runtest("detect foreign object in write",
             {$td->COMMAND => "test_driver 29" .
                  " copy-foreign-objects-in.pdf minimal.pdf"},
             {$td->FILE => "foreign-in-write.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("parse objects from string",
             {$td->COMMAND => "test_driver 31 minimal.pdf"}, # file not used
             {$td->FILE => "parse-object.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("EOF terminating literal tokens",
             {$td->COMMAND => "qpdf --check eof-terminates-literal.pdf"},
             {$td->FILE => "eof-terminates-literal.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("EOF reading token",
             {$td->COMMAND => "qpdf --check eof-reading-token.pdf"},
             {$td->FILE => "eof-reading-token.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
$td->runtest("extra header text",
             {$td->COMMAND => "test_driver 32 minimal.pdf"},
             {$td->FILE => "test-32.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "extra-header-no-newline.pdf"});
$td->runtest("check output",
             {$td->FILE => "b.pdf"},
             {$td->FILE => "extra-header-lin-no-newline.pdf"});
$td->runtest("check output",
             {$td->FILE => "c.pdf"},
             {$td->FILE => "extra-header-newline.pdf"});
$td->runtest("check output",
             {$td->FILE => "d.pdf"},
             {$td->FILE => "extra-header-lin-newline.pdf"});
$td->runtest("output to custom pipeline",
             {$td->COMMAND => "test_driver 33 minimal.pdf"},
             {$td->STRING => "test 33 done\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "custom-pipeline.pdf"});
$td->runtest("object with zero offset",
             {$td->COMMAND => "qpdf --check zero-offset.pdf"},
             {$td->FILE => "zero-offset.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
# leading-junk also has a space instead of a newline after xref
$td->runtest("check file with leading junk",
             {$td->COMMAND => "qpdf --check leading-junk.pdf"},
             {$td->FILE => "leading-junk.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("EOF inside inline image",
             {$td->COMMAND => "test_driver 37 eof-in-inline-image.pdf"},
             {$td->FILE => "eof-in-inline-image.out",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);
$td->runtest("tokenize content streams",
             {$td->COMMAND => "test_driver 37 tokenize-content-streams.pdf"},
             {$td->FILE => "tokenize-content-streams.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("terminate parsing",
             {$td->COMMAND => "test_driver 37 terminate-parsing.pdf"},
             {$td->FILE => "terminate-parsing.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("content stream errors",
             {$td->COMMAND => "qpdf --check content-stream-errors.pdf"},
             {$td->FILE => "content-stream-errors.out",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);

# The file override-compressed-object.pdf contains an object stream
# with four strings in it.  The file is then appended.  The appended
# section overrides one of the four strings with a string in another
# object stream and another one in an uncompressed object.  The other
# two strings are left alone.  The test case exercises that all four
# objects have the correct value.
$td->runtest("overridden compressed objects",
             {$td->COMMAND => "test_driver 38 override-compressed-object.pdf"},
             {$td->FILE => "override-compressed-object.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

$td->runtest("generate object streams for gen > 0",
             {$td->COMMAND => "qpdf --qdf --static-id" .
		  " --object-streams=generate gen1.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check file",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "gen1.qdf"});

# A user provided a file that was missing /ID in its trailer even
# though it is encrypted and also has a space instead of a newline
# after its xref keyword. This file has those same properties.
$td->runtest("check broken file",
             {$td->COMMAND => "qpdf --check invalid-id-xref.pdf"},
             {$td->FILE => "invalid-id-xref.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);

$td->runtest("show number of pages",
             {$td->COMMAND =>
                  "qpdf --show-npages 20-pages.pdf --password=user"},
             {$td->STRING => "20\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("ignore broken decode parms with no filters",
             {$td->COMMAND => "qpdf --check broken-decode-parms-no-filter.pdf"},
             {$td->FILE => "broken-decode-parms-no-filter.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("bounds check linearization data 1",
             {$td->COMMAND => "qpdf --check linearization-bounds-1.pdf"},
             {$td->FILE => "linearization-bounds-1.out",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);
$td->runtest("bounds check linearization data 2",
             {$td->COMMAND => "qpdf --check linearization-bounds-2.pdf"},
             {$td->FILE => "linearization-bounds-2.out",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);
# Throws logic error, not bad_alloc
$td->runtest("sanity check array size",
             {$td->COMMAND =>
                  "qpdf --check linearization-large-vector-alloc.pdf"},
             {$td->FILE => "linearization-large-vector-alloc.out",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);

$td->runtest("stream with indirect decode parms",
             {$td->COMMAND =>
                  "qpdf --static-id indirect-decode-parms.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check file",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "indirect-decode-parms-out.pdf"});

$td->runtest("handle page no with contents",
             {$td->COMMAND => "qpdf --show-pages page-no-content.pdf"},
             {$td->FILE => "page-no-content.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("no type key for page nodes",
             {$td->COMMAND => "qpdf --check no-pages-types.pdf"},
             {$td->FILE => "no-pages-types.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("ensure arguments to R are direct",
             {$td->COMMAND => "qpdf --check indirect-r-arg.pdf"},
             {$td->FILE => "indirect-r-arg.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
$td->runtest("detect loops in pages structure",
             {$td->COMMAND => "qpdf --check pages-loop.pdf"},
             {$td->FILE => "pages-loop.out", $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);
$td->runtest("no trailing space in xref table",
             {$td->COMMAND => "qpdf --check no-space-in-xref.pdf"},
             {$td->FILE => "no-space-in-xref.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

# An array is split across multiple content streams starting object
# 42. This was reported in github issue 73. The file is modified from
# that example.
$td->runtest("parse split content stream",
             {$td->COMMAND => "qpdf --check split-content-stream.pdf"},
             {$td->FILE => "split-content-stream.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("split content stream errors",
             {$td->COMMAND => "qpdf --check split-content-stream-errors.pdf"},
             {$td->FILE => "split-content-stream-errors.out",
              $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);

# Demonstrate show-xref after check and not after check to illustrate
# that it can dump the real xref or the recovered xref.
$td->runtest("dump bad xref",
             {$td->COMMAND => "qpdf --show-xref bad-xref-entry.pdf"},
             {$td->FILE => "bad-xref-entry.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
# Test @file here too.
open(F, ">args") or die;
print F "--check\n";
print F "--show-xref\n";
close(F);
$td->runtest("dump corrected bad xref",
             {$td->COMMAND => "qpdf \@args bad-xref-entry.pdf"},
             {$td->FILE => "bad-xref-entry-corrected.out",
              $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
unlink "args";

$td->runtest("don't overwrite self",
             {$td->COMMAND => "(echo a.pdf; echo a.pdf) | qpdf \@-"},
             {$td->REGEXP => "input file and output file are the same.*",
              $td->EXIT_STATUS => 2});

$td->runtest("combine show and --pages",
             {$td->COMMAND =>
                  "qpdf --empty --pages minimal.pdf -- --show-pages"},
             {$td->FILE => "show-pages-pages.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

$td->runtest("ignore bad token",
             {$td->COMMAND =>
                  "qpdf --show-xref bad-token-startxref.pdf"},
             {$td->FILE => "bad-token-startxref.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

$td->runtest("recoverable xref errors",
             {$td->COMMAND =>
                  "qpdf --check --show-xref xref-errors.pdf"},
             {$td->FILE => "xref-errors.out",
              $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);

# A file was emailed privately with issue 96. short-O-U.pdf was
# created by copying encryption parameters from that file. It exhibits
# the same behavior as the original file.
$td->runtest("short /O or /U",
             {$td->COMMAND =>
                  "qpdf --password=19723102477 --check short-O-U.pdf"},
             {$td->FILE => "short-O-U.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Newline before endstream ---");
$n_tests += 8;

# From issue 133, http://verapdf.org/software/ is an open source
# package that can verify PDF/A compliance. This could potentially be
# useful for manual or automated verification that qpdf doesn't break
# PDF/A compliance should that ever be desired.

foreach my $d (
    ['--qdf', 'qdf', 'qdf'],
    ['--newline-before-endstream', 'newline', 'nl'],
    ['--qdf --newline-before-endstream', 'newline and qdf', 'nl-qdf'],
    )
{
    my ($flags, $description, $suffix) = @$d;
    $td->runtest("newline before endstream: $description",
                 {$td->COMMAND => "qpdf --static-id --stream-data=preserve" .
                      " $flags streams-with-newlines.pdf a.pdf"},
                 {$td->STRING => "", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("check output",
                 {$td->FILE => "a.pdf"},
                 {$td->FILE => "newline-before-endstream-$suffix.pdf"});
    if ($flags =~ /qdf/)
    {
        $td->runtest("fix-qdf",
                     {$td->COMMAND => "fix-qdf a.pdf"},
                     {$td->FILE => "a.pdf", $td->EXIT_STATUS => 0});
    }
}
show_ntests();
# ----------
$td->notify("--- Split Pages ---");
# sp = split-pages
my @sp_cases = (
    [11, '%d at beginning', '', '%d_split-out.zdf'],
    [11, '%d at end', '--qdf', 'split-out.zdf_%d'],
    [11, '%d in middle', '--encrypt u o 128 --', 'a-%d-split-out.zdf'],
    [11, 'pdf extension', '', 'split-out.Pdf'],
    [4, 'fallback', '--pages 11-pages.pdf 1-3 minimal.pdf --', 'split-out'],
    );
$n_tests += 5;
for (@sp_cases)
{
    $n_tests += 1 + $_->[0];
}

$td->runtest("split page group > 1",
             {$td->COMMAND => "qpdf --static-id --split-pages=5 11-pages.pdf" .
                  " --verbose split-out-group.pdf"},
             {$td->FILE => "split-pages-group.out", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
foreach my $f ('01-05', '06-10', '11-11')
{
    $td->runtest("checkout group $f",
                 {$td->FILE => "split-out-group-$f.pdf"},
                 {$td->FILE => "split-exp-group-$f.pdf"});
}

$td->runtest("no split-pages to stdout",
             {$td->COMMAND => "qpdf --split-pages 11-pages.pdf -"},
             {$td->FILE => "split-pages-stdout.out", $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);

foreach my $d (@sp_cases)
{
    my ($n, $description, $xargs, $out) = @$d;
    $td->runtest("split pages " . $description,
                 {$td->COMMAND =>
                      "qpdf --static-id --split-pages 11-pages.pdf" .
                      " $xargs $out"},
                 {$td->STRING => "", $td->EXIT_STATUS => 0});
    my $pattern = $out;
    my $nlen = length($n);
    if ($pattern =~ m/\%d/)
    {
        $pattern =~ s/\%d/\%0${nlen}d/;
    }
    elsif ($pattern =~ m/\.pdf$/i)
    {
        $pattern =~ s/(\.pdf$)/-%0${nlen}d$1/i;
    }
    else
    {
        $pattern .= "-%0${nlen}d";
    }
    for (my $i = 1; $i <= $n; ++$i)
    {
        my $actual = sprintf($pattern, $i);
        my $expected = $actual;
        $expected =~ s/split-out/split-exp/;
        $td->runtest("checkout output page $i",
                     {$td->FILE => $actual},
                     {$td->FILE => $expected});
    }
}

show_ntests();
# ----------
$td->notify("--- Rotate Pages ---");
$n_tests += 2;
# XXX do absolute, positive, and negative on ranges that include
# inherited and non-inherited.
# Pages 11-15 inherit /Rotate 90
# Pages 1 and 2 have explicit /Rotate 270
# Pages 16 and 17 have explicit /Rotate 180

$td->runtest("page rotation",
             {$td->COMMAND => "qpdf --static-id to-rotate.pdf a.pdf" .
                  " --rotate=+90:1,4,11,16" .
                  " --rotate=180:2,5,12-13" .
                  " --rotate=-90:3,15,17,18"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "rotated.pdf"});

show_ntests();
# ----------
$td->notify("--- Numeric range parsing tests ---");
my @nrange_tests = (
    [",5",
     "qpdf: error at * in numeric range *,5: unexpected separator",
     2],
    ["4,,5",
     "qpdf: error at * in numeric range 4,*,5: unexpected separator",
     2],
    ["4,5,",
     "qpdf: error at * in numeric range 4,5,*: number expected",
     2],
    ["z1,",
     "qpdf: error at * in numeric range z*1,: digit not expected",
     2],
    ["1z,",
     "qpdf: error at * in numeric range 1*z,: z not expected",
     2],
    ["1-5?",
     "qpdf: error at * in numeric range 1-5*?: unexpected character",
     2],
    ["1-30",
     "qpdf: error in numeric range 1-30: number 30 out of range",
     2],
    ["1-10,0,5",
     "qpdf: error in numeric range 1-10,0,5: number 0 out of range",
     2],
    ["1-10,1234,5",
     "qpdf: error in numeric range 1-10,1234,5: number 1234 out of range",
     2],
    ["1,3,5-10,z-13,13,9,z,2",
     "numeric range 1,3,5-10,z-13,13,9,z,2" .
     " -> 1 3 5 6 7 8 9 10 15 14 13 13 9 15 2",
     0],
    );
$n_tests += scalar(@nrange_tests);
foreach my $d (@nrange_tests)
{
    my ($range, $output, $status) = @$d;
    $td->runtest("numeric range $range",
                 {$td->COMMAND => ['qpdf', '-- test-numrange=' . $range],
                  $td->FILTER => "grep 'numeric range'"},
                 {$td->STRING => $output . "\n", $td->EXIT_STATUS => $status},
                 $td->NORMALIZE_NEWLINES);
}

show_ntests();
# ----------
$td->notify("--- Merging and Splitting ---");
$n_tests += 8;

# Select pages from the same file multiple times including selecting
# twice from an encrypted file and specifying the password only the
# first time.  The file 20-pages.pdf is specified with two different
# paths to duplicate a page.
my $pages_options = "--pages page-labels-and-outlines.pdf 1,3,5-7,z" .
    " 20-pages.pdf --password=user z-15" .
    " page-labels-and-outlines.pdf 12" .
    " 20-pages.pdf 10" .
    " ./20-pages.pdf --password=owner 10" .
    " minimal.pdf 1 --";

$td->runtest("merge three files",
             {$td->COMMAND => "qpdf page-labels-and-outlines.pdf a.pdf" .
                  " $pages_options --static-id"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
# Manually verified about this file: make sure that outline entries
# that pointed to pages that were preserved still work in the copy,
# and verify that all pages are as expected.  page-labels-and-outlines
# as well as 20-pages have text on page n (from 1) that shows its page
# position from 0, so page 1 says it's page 0.
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "merge-three-files-1.pdf"});
# Select the same pages but add them to an empty file
$td->runtest("merge three files",
             {$td->COMMAND => "qpdf --empty a.pdf" .
                  " $pages_options --static-id"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
# Manually verified about this file: it has the same pages but does
# not contain outlines, page labels, or other things from the original
# file.
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "merge-three-files-2.pdf"});
$td->runtest("avoid respecification of password",
             {$td->COMMAND =>
                  "qpdf --empty a.pdf --copy-encryption=20-pages.pdf" .
                  " --encryption-file-password=user" .
                  " --pages 20-pages.pdf 1,z -- --static-id"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "pages-copy-encryption.pdf"});
$td->runtest("merge with implicit ranges",
             {$td->COMMAND =>
                  "qpdf --empty a.pdf" .
                  " --pages minimal.pdf 20-pages.pdf --password=user" .
                  " page-labels-and-outlines.pdf --" .
                  " --static-id"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "merge-implicit-ranges.pdf"});
show_ntests();
# ----------
$td->notify("--- PDF From Scratch ---");
$n_tests += 2;

$td->runtest("basic qpdf from scratch",
	     {$td->COMMAND => "pdf_from_scratch 0"},
	     {$td->STRING => "test 0 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "from-scratch-0.pdf"});
show_ntests();
# ----------
$td->notify("--- PCLm ---");
$n_tests += 2;

$td->runtest("write as PCLm",
	     {$td->COMMAND => "test_driver 40 pclm-in.pdf a.pdf"},
	     {$td->STRING => "test 40 done\n", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "pclm-out.pdf"});

show_ntests();
# ----------
$td->notify("--- Precheck streams ---");
$n_tests += 2;

$td->runtest("bad stream",
	     {$td->COMMAND => "qpdf --static-id bad-data.pdf a.pdf"},
	     {$td->FILE => "bad-data.out", $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "bad-data-out.pdf"});

show_ntests();
# ----------
$td->notify("--- Decode levels ---");
$n_tests += 12;

# image-streams.pdf is the output of examples/pdf-create.
# examples/pdf-create validates the actual image data.
foreach my $l (qw(none generalized specialized all))
{
    $td->runtest("image-streams: $l",
                 {$td->COMMAND =>
                      "qpdf image-streams.pdf --compress-streams=n" .
                      " --decode-level=$l a.pdf"},
                 {$td->STRING => "", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("check image-streams: $l",
                 {$td->COMMAND => "test_driver 39 a.pdf"},
                 {$td->FILE => "image-streams-$l.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
}

# Bad JPEG data
$td->runtest("check finds bad jpeg data",
             {$td->COMMAND => "qpdf --check bad-jpeg.pdf"},
             {$td->FILE => "bad-jpeg-check.out",
              $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
$td->runtest("precheck detects bad jpeg data",
             {$td->COMMAND => "qpdf --static-id --decode-level=all" .
                  " bad-jpeg.pdf a.pdf"},
             {$td->FILE => "bad-jpeg.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check file",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "bad-jpeg-out.pdf"});
$td->runtest("get data",
             {$td->COMMAND => "qpdf --show-object=6" .
                  " --filtered-stream-data bad-jpeg.pdf"},
             {$td->FILE => "bad-jpeg-show.out", $td->EXIT_STATUS => 3},
             $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Preserve unreferenced objects ---");
$n_tests += 4;

$td->runtest("drop unused objects",
	     {$td->COMMAND => "qpdf --static-id unreferenced-objects.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "unreferenced-dropped.pdf"});
$td->runtest("keep unused objects",
	     {$td->COMMAND => "qpdf --static-id --preserve-unreferenced" .
                  " unreferenced-objects.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "unreferenced-preserved.pdf"});
show_ntests();
# ----------
$td->notify("--- Copy Foreign Objects ---");
$n_tests += 7;

foreach my $d ([25, 1], [26, 2], [27, 3])
{
    my ($testn, $outn) = @$d;
    $td->runtest("copy objects $outn",
                 {$td->COMMAND => "test_driver $testn" .
                      " copy-foreign-objects-in.pdf minimal.pdf"},
                 {$td->STRING => "test $testn done\n", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("check output",
                 {$td->FILE => "a.pdf"},
                 {$td->FILE => "copy-foreign-objects-out$outn.pdf"});
}
$td->runtest("copy objects error",
             {$td->COMMAND => "test_driver 28" .
                  " copy-foreign-objects-in.pdf minimal.pdf"},
             {$td->FILE => "copy-foreign-objects-errors.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
show_ntests();
# ----------
$td->notify("--- Error Condition Tests ---");
# $n_tests incremented after initialization of badfiles below.

my @badfiles = ("not a PDF file", 			# 1
		"no startxref",				# 2
		"bad primary xref offset",		# 3
		"invalid xref syntax",			# 4
		"invalid xref entry",			# 5
		"free table inconsistency",		# 6
		"no trailer dictionary",		# 7
		"bad secondary xref",			# 8
		"no /Size in trailer",			# 9
		"/Size not integer",			# 10
		"/Prev not integer",			# 11
		"/Size inconsistency",			# 12
		"bad {",				# 13
		"bad }",				# 14
		"bad ]",				# 15
		"bad >>",				# 16
		"odd number of dictionary items",	# 17
		"bad )",				# 18
		"bad >",				# 19
		"invalid hexstring character",		# 20
		"invalid name token",			# 21
		"no /Length for stream dictionary",	# 22
		"/Length not integer",			# 23
		"expected endstream",			# 24
		"bad obj declaration (objid)",		# 25
		"bad obj declaration (generation)",	# 26
		"bad obj declaration (obj)",		# 27
		"expected endobj",			# 28
		"null in name",				# 29
		"invalid stream /Filter",		# 30
		"unknown stream /Filter",		# 31
		"obj/gen mismatch",			# 32
		"invalid stream /Filter and xref",	# 33
		"obj/gen in wrong place",		# 34
		"object stream of wrong type",		# 35
                "bad dictionary key",                   # 36
		);

$n_tests += @badfiles + 3;

# Test 6 contains errors in the free table consistency, but we no
# longer have any consistency check for this since it is not important
# neither Acrobat nor other PDF viewers really care.  Tests 12 and 28
# have error conditions that used to be fatal but are now considered
# non-fatal.
my %badtest_overrides = ();
for(6, 12..15, 17, 22..28, 30..32, 34, 36)
{
    $badtest_overrides{$_} = 0;
}

for (my $i = 1; $i <= scalar(@badfiles); ++$i)
{
    my $status = $badtest_overrides{$i};
    $status = 2 unless defined $status;
    $td->runtest($badfiles[$i-1],
		 {$td->COMMAND => "test_driver 0 bad$i.pdf"},
		 {$td->FILE => "bad$i.out",
		  $td->EXIT_STATUS => $status},
		 $td->NORMALIZE_NEWLINES);
}

$td->runtest("C API: errors",
	     {$td->COMMAND => "qpdf-ctest 2 bad1.pdf '' a.pdf"},
	     {$td->FILE => "c-read-errors.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C API: warnings writing",
	     {$td->COMMAND => "qpdf-ctest 2 bad33.pdf '' a.pdf"},
	     {$td->FILE => "c-write-warnings.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C API: no recovery",
	     {$td->COMMAND => "qpdf-ctest 10 bad33.pdf '' a.pdf"},
	     {$td->FILE => "c-no-recovery.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Recovery Tests ---");
$n_tests += @badfiles + 6;

# Recovery tests.  These are mostly after-the-fact -- when recovery
# was implemented, some degree of recovery was possible on many of the
# files.  Mostly the recovery does not actually repair the error,
# though in some cases it may.  Acrobat Reader would not be able to
# recover any of these files any better.
my %recover_failures = ();
for (1, 7, 16, 18..21, 29, 35)
{
    $recover_failures{$_} = 1;
}
for (my $i = 1; $i <= scalar(@badfiles); ++$i)
{
    my $status = 0;
    if (exists $recover_failures{$i})
    {
	$status = 2;
    }
    $td->runtest("recover " . $badfiles[$i-1],
		 {$td->COMMAND => "test_driver 1 bad$i.pdf"},
		 {$td->FILE => "bad$i-recover.out",
		  $td->EXIT_STATUS => $status},
		 $td->NORMALIZE_NEWLINES);
}

# See if we can recover the cross reference table on a file that has
# been appended to even when it deletes and reuses objects.  We can't
# completely do it in the case of deleted objects, but we can get
# mostly there.
$td->runtest("good replaced page contents",
	     {$td->COMMAND =>
		  "qpdf --static-id -qdf --no-original-object-ids" .
		  " append-page-content.pdf a.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "append-page-content-good.qdf"});
$td->runtest("damaged replaced page contents",
	     {$td->COMMAND =>
		  "qpdf --static-id -qdf --no-original-object-ids" .
		  " append-page-content-damaged.pdf a.pdf"},
	     {$td->FILE => "append-page-content-damaged.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check output",
	     {$td->FILE => "a.pdf"},
	     {$td->FILE => "append-page-content-damaged.qdf"});
$td->runtest("run check on damaged file",
	     {$td->COMMAND => "qpdf --check append-page-content-damaged.pdf"},
	     {$td->FILE => "append-page-content-damaged-check.out",
	      $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check with C API",
	     {$td->COMMAND =>
		  "qpdf-ctest 1 append-page-content-damaged.pdf '' ''"},
	     {$td->FILE => "append-page-content-damaged-c-check.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Basic Parsing Tests ---");
# $n_tests incremented below after initialization of @goodfiles.

my @goodfiles = ("implicit null",			# 1
		 "direct null",				# 2
		 "unresolved null",			# 3
		 "indirect null",			# 4
		 "indirect bool, real",			# 5
		 "direct bool",				# 6
		 "integer",				# 7
		 "real, ASCIIHexDecode",		# 8
		 "string",				# 9
		 "array",				# 10
		 "dictionary",				# 11
		 "stream",				# 12
		 "nesting, strings, names",		# 13
		 "tokenizing pipeline",			# 14
		 "name",				# 15
		 "object-stream",			# 16
		 "hybrid xref",				# 17
		 "hybrid xref old mode",		# 18
		 "xref with prev",			# 19
		 "lots of compressible objects",	# 20
		 "pound in name",                       # 21
		 );

$n_tests += (3 * @goodfiles) + 6;

my %goodtest_overrides = ('14' => 3);
my %goodtest_flags =
    ('18' => '-ignore-xref-streams',
     '20' => '-object-streams=generate',
    );
for (my $i = 1; $i <= scalar(@goodfiles); ++$i)
{
    my $n = $goodtest_overrides{$i} || 1;
    $td->runtest("$goodfiles[$i-1]",
		 {$td->COMMAND => "test_driver $n good$i.pdf"},
		 {$td->FILE => "good$i.out",
		  $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
    my $xflags = $goodtest_flags{$i} || '';
    check_pdf("create qdf",
	      "qpdf --static-id -qdf $xflags good$i.pdf",
	      "good$i.qdf", 0);
}

check_pdf("no normalization",
	  "qpdf -qdf --static-id --normalize-content=n good7.pdf",
	  "good7-not-normalized.qdf",
	  0);

check_pdf("no qdf",
	  "qpdf --static-id good17.pdf",
	  "good17-not-qdf.pdf",
	  0);

check_pdf("no recompression",
	  "qpdf --static-id --stream-data=preserve good17.pdf",
	  "good17-not-recompressed.pdf",
	  0);

show_ntests();
# ----------
$td->notify("--- C API Tests ---");

my @capi = (
    [2, 'no options'],
    [3, 'normalized content'],
    [4, 'ignore xref streams'],
    [5, 'linearized'],
    [6, 'object streams'],
    [7, 'qdf'],
    [8, 'no original object ids'],
    [9, 'uncompressed streams'],
    );
$n_tests += (2 * @capi) + 3;
foreach my $d (@capi)
{
    my ($n, $description) = @$d;
    my $outfile = $description;
    $outfile =~ s/ /-/g;
    $outfile = "c-$outfile.pdf";
    $td->runtest($description,
		 {$td->COMMAND => "qpdf-ctest $n hybrid-xref.pdf '' a.pdf"},
		 {$td->STRING => "", $td->EXIT_STATUS => 0});
    $td->runtest("check $description",
		 {$td->FILE => "a.pdf"},
		 {$td->FILE => $outfile});
}
$td->runtest("write to bad file name",
	     {$td->COMMAND => "qpdf-ctest 2 hybrid-xref.pdf '' /:a:/:b:"},
	     {$td->REGEXP => "error: open /:a:/:b:: .*",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("write damaged to bad file name",
	     {$td->COMMAND => "qpdf-ctest 2 append-page-content-damaged.pdf" .
		  " '' /:a:/:b:"},
	     {$td->REGEXP =>
		  "warning:(?s:.*)\n" .
		  "error: open /:a:/:b:: .*",
		  $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("write damaged",
	     {$td->COMMAND => "qpdf-ctest 2 append-page-content-damaged.pdf" .
		  " '' a.pdf"},
	     {$td->FILE => "c-write-damaged.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Deterministic ID Tests ---");
$n_tests += 11;
foreach my $d ('nn', 'ny', 'yn', 'yy')
{
    my $linearize = ($d =~ m/^y/);
    my $ostream = ($d =~ m/y$/);
    $td->runtest("deterministic ID: linearize/ostream=$d",
                 {$td->COMMAND =>
                      "qpdf -deterministic-id" .
                      ($linearize ? " -linearize" : "") .
                      " -object-streams=" . ($ostream ? "generate" : "disable") .
                      " deterministic-id-in.pdf a.pdf"},
                 {$td->STRING => "",
                  $td->EXIT_STATUS => 0});
    $td->runtest("compare files",
                 {$td->FILE => "a.pdf"},
                 {$td->FILE => "deterministic-id-$d.pdf"});
}

$td->runtest("deterministic ID with encryption",
             {$td->COMMAND => "qpdf -deterministic-id encrypted-with-images.pdf a.pdf"},
             {$td->STRING => "INTERNAL ERROR: QPDFWriter::generateID" .
                  " has no data for deterministic ID." .
                  "  This may happen if deterministic ID and" .
                  " file encryption are requested together.\n",
              $td->EXIT_STATUS => 2},
             $td->NORMALIZE_NEWLINES);
$td->runtest("deterministic ID (C API)",
             {$td->COMMAND =>
                  "qpdf-ctest 19 deterministic-id-in.pdf '' a.pdf"},
             {$td->STRING => "",
              $td->EXIT_STATUS => 0});
$td->runtest("compare files",
             {$td->FILE => "a.pdf"},
             {$td->FILE => "deterministic-id-nn.pdf"});

# ----------
$td->notify("--- Object Stream Tests ---");
$n_tests += (36 * 4) + (12 * 2);
$n_compare_pdfs += 36;

for (my $n = 16; $n <= 19; ++$n)
{
    my $in = "good$n.pdf";
    foreach my $flags ('-object-streams=disable',
		       '-object-streams=preserve',
		       '-object-streams=generate')
    {
	foreach my $qdf ('-qdf', '', '-encrypt "" x 128 --')
	{
	    # 4 tests + 1 compare_pdfs * 36 cases
	    # 2 additional tests * 12 cases
	    $td->runtest("object stream mode",
			 {$td->COMMAND =>
			      "qpdf --static-id $flags $qdf $in a.pdf"},
			 {$td->STRING => "",
			  $td->EXIT_STATUS => 0});
	    compare_pdfs("good$n.pdf", "a.pdf");
	    if ($qdf eq '-qdf')
	    {
		$td->runtest("fix-qdf identity check",
			     {$td->COMMAND => "fix-qdf a.pdf >| b.pdf"},
			     {$td->STRING => "", $td->EXIT_STATUS => 0});
		$td->runtest("compare files",
			     {$td->FILE => "a.pdf"},
			     {$td->FILE => "b.pdf"});
	    }
	    $td->runtest("convert to qdf",
			 {$td->COMMAND =>
			      "qpdf --static-id --no-original-object-ids" .
			      " -qdf -decrypt" .
			      " -object-streams=disable $in a.qdf"},
			 {$td->STRING => "",
			  $td->EXIT_STATUS => 0});
	    $td->runtest("convert output to qdf",
			 {$td->COMMAND =>
			      "qpdf --static-id --no-original-object-ids" .
			      " -qdf -object-streams=disable a.pdf b.qdf"},
			 {$td->STRING => "",
			  $td->EXIT_STATUS => 0});
	    $td->runtest("compare files",
			 {$td->FILE => "a.qdf"},
			 {$td->FILE => "b.qdf"});
	}
    }
    flush_tiff_cache();
}

show_ntests();
# ----------
$td->notify("--- Specific File Tests ---");
$n_tests += 2;

# Special PDF files that caused problems at some point

$td->runtest("damaged stream",
	     {$td->COMMAND => "qpdf --check damaged-stream.pdf"},
	     {$td->FILE => "damaged-stream.out", $td->EXIT_STATUS => 3},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("damaged stream (C)",
	     {$td->COMMAND => "qpdf-ctest 2 damaged-stream.pdf '' a.pdf"},
	     {$td->FILE => "damaged-stream-c-check.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Mutability Tests ---");
$n_tests += 4;

$td->runtest("no normalization",
	     {$td->COMMAND => "test_driver 4 test4-1.pdf"},
	     {$td->FILE => "test4-1.qdf",
	      $td->EXIT_STATUS => 0});

$td->runtest("object ordering",
	     {$td->COMMAND => "test_driver 4 test4-4.pdf"},
	     {$td->FILE => "test4-4.qdf",
	      $td->EXIT_STATUS => 0});

$td->runtest("loop detected",
	     {$td->COMMAND => "test_driver 4 test4-2.pdf"},
	     {$td->FILE => "test4-2.out",
	      $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("stream detected",
	     {$td->COMMAND => "test_driver 4 test4-3.pdf"},
	     {$td->FILE => "test4-3.out",
	      $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Extraction Tests ---");
$n_tests += 11;

$td->runtest("show xref",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-xref"},
	     {$td->FILE => "show-xref.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show pages",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-pages"},
	     {$td->FILE => "show-pages.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show-pages-images",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-pages --with-images"},
	     {$td->FILE => "show-pages-images.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show-page-1",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=5,0"},
	     {$td->FILE => "show-page-1.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show-page-1-content-raw",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=7 --raw-stream-data"},
	     {$td->FILE => "show-page-1-content-raw.out",
	      $td->EXIT_STATUS => 0});

$td->runtest("show-page-1-content-filtered",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=7 --filtered-stream-data"},
	     {$td->FILE => "show-page-1-content-filtered.out",
	      $td->EXIT_STATUS => 0});

$td->runtest("show-page-1-content-normalized",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=7,0 --filtered-stream-data --normalize-content=y"},
	     {$td->FILE => "show-page-1-content-normalized.out",
	      $td->EXIT_STATUS => 0});

$td->runtest("show-page-1-image",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=8 --raw-stream-data"},
	     {$td->FILE => "show-page-1-image.out",
	      $td->EXIT_STATUS => 0});

$td->runtest("unfilterable stream data",
	     {$td->COMMAND => "qpdf unfilterable.pdf" .
		  " --show-object=4 --filtered-stream-data"},
	     {$td->FILE => "show-unfilterable.out",
	      $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show-xref-by-id",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=12"},
	     {$td->FILE => "show-xref-by-id.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("show-xref-by-id-filtered",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf" .
		  " --show-object=12 --filtered-stream-data"},
	     {$td->FILE => "show-xref-by-id-filtered.out",
	      $td->EXIT_STATUS => 0});

show_ntests();
# ----------
$td->notify("--- Clear-text Metadata Tests ---");
$n_tests += 58;

# args: file, exp_encrypted, exp_cleartext
check_metadata("compressed-metadata.pdf", 0, 0);
check_metadata("enc-base.pdf", 0, 1);

foreach my $f (qw(compressed-metadata.pdf enc-base.pdf))
{
    foreach my $w (qw(compress preserve))
    {
	$td->runtest("$w streams ($f)",
		     {$td->COMMAND => "qpdf --stream-data=$w $f a.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("a.pdf", 0, 1);
	$td->runtest("encrypt normally",
		     {$td->COMMAND =>
			  "qpdf --encrypt '' '' 128 -- a.pdf b.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("b.pdf", 1, 0);
	unlink "b.pdf";
	$td->runtest("encrypt V4",
		     {$td->COMMAND =>
			  "qpdf --encrypt '' '' 128 --force-V4 -- a.pdf b.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("b.pdf", 1, 0);
	unlink "b.pdf";
	$td->runtest("encrypt with cleartext metadata",
		     {$td->COMMAND =>
			  "qpdf --encrypt '' '' 128 --cleartext-metadata --" .
			  " a.pdf b.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("b.pdf", 1, 1);
	$td->runtest("preserve encryption",
		     {$td->COMMAND => "qpdf b.pdf c.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("c.pdf", 1, 1);
	unlink "b.pdf", "c.pdf";
	$td->runtest("encrypt with aes and cleartext metadata",
		     {$td->COMMAND =>
			  "qpdf --encrypt '' '' 128" .
			  " --cleartext-metadata --use-aes=y -- a.pdf b.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("b.pdf", 1, 1);
	$td->runtest("preserve encryption",
		     {$td->COMMAND => "qpdf b.pdf c.pdf"},
		     {$td->STRING => "", $td->EXIT_STATUS => 0});
	check_metadata("c.pdf", 1, 1);
	unlink "b.pdf", "c.pdf";
    }
}

show_ntests();
# ----------
$td->notify("--- Linearization Tests ---");
# $n_tests incremented after initialization of @linearized_files and
# @to_linearize.

# *'ed files were linearized with Pdlin.
my @linearized_files =
    ('lin0',			#   not linearized
     'lin1',			# * outlines, page labels, pdlin
     'lin2',			# * lin1 with null and newline
     'lin3',			#   same file saved with acrobat
     'lin4',			# * lin1 with no /PageMode
     'lin5',			#   lin3 with embedded thumbnails
     'lin6',			# * lin5 with pdlin
     'lin7',			#   lin5 with /PageMode /UseThumbs
     'lin8',			# * lin7 with pdlin
     'lin9',			# * shared objects, indirect null
     'badlin1',			#   parameter dictionary errors
     );

my @to_linearize =
    ('lin-special',		# lots of weird cases -- see file comments
     'delete-and-reuse',	# deleted, reused objects
     'lin-delete-and-reuse',	# linearized, then delete and reuse
     'object-stream',		# contains object streams
     'hybrid-xref',	        # contains both xref tables and streams
     'gen1',                    # has objects with generation > 0
     'direct-outlines',         # /Outlines is a direct object
     @linearized_files,		# we should be able to relinearize
     );

$n_tests += @linearized_files + 6;
$n_tests += (3 * @to_linearize * 5) + 6;

foreach my $base (@linearized_files)
{
    $td->runtest("dump linearization: $base",
		 {$td->COMMAND => "qpdf --show-linearization $base.pdf"},
		 {$td->FILE => "$base.out",
		  $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
}

# Check normal modified and linearized modified files, making sure
# that their qdf files are identical.  The next two tests have the
# same expected output files and different input files.
check_pdf("modified",
	  "qpdf --static-id --qdf --no-original-object-ids" .
	  " delete-and-reuse.pdf", "delete-and-reuse.qdf",
	  0);
check_pdf("linearized and modified",
	  "qpdf --static-id --qdf --no-original-object-ids" .
	  " lin-delete-and-reuse.pdf", "delete-and-reuse.qdf", # same output
	  0);

$td->runtest("check linearized and modified",
	     {$td->COMMAND => "qpdf --check lin-delete-and-reuse.pdf"},
	     {$td->FILE => "lin-delete-and-reuse-check.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check multiple modifications",
	     {$td->COMMAND => "qpdf --check delete-and-reuse.pdf"},
	     {$td->FILE => "delete-and-reuse-check.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

foreach my $base (@to_linearize)
{
    foreach my $omode (qw(disable preserve generate))
    {
	my $oarg = "-object-streams=$omode";
        my $sdarg = "";
        if (($base eq 'lin-special') || ($base eq 'object-stream'))
        {
            $sdarg = "--stream-data=uncompress";
        }
	$td->runtest("linearize $base ($omode)",
		     {$td->COMMAND =>
			  "qpdf -linearize $oarg $sdarg" .
                          " --static-id $base.pdf a.pdf"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	$td->runtest("check linearization",
		     {$td->COMMAND => "qpdf --check-linearization a.pdf"},
		     {$td->STRING => "a.pdf: no linearization errors\n",
		      $td->EXIT_STATUS => 0},
		     $td->NORMALIZE_NEWLINES);
	# Relinearizing twice should produce identical results.  We
	# have to do it twice because, if objects changed ordering
	# during the original linearization, the hint tables won't
	# exactly match.  This is because object identifiers are
	# inserted into the hint table in their original order since
	# we don't yet have renumbering information when we compute
	# the table values.
	$td->runtest("relinearize $base 1",
		     {$td->COMMAND =>
			  "qpdf -linearize $sdarg --static-id a.pdf b.pdf"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	$td->runtest("relinearize $base 2",
		     {$td->COMMAND =>
			  "qpdf -linearize $sdarg --static-id b.pdf c.pdf"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	$td->runtest("compare files ($omode)",
		     {$td->FILE => "b.pdf"},
		     {$td->FILE => "c.pdf"});
	if (($base eq 'lin-special') || ($base eq 'object-stream'))
	{
	    $td->runtest("check $base ($omode)",
			 {$td->FILE => "a.pdf"},
			 {$td->FILE => "$base.$omode.exp"});
	}
    }
}

show_ntests();
# ----------
$td->notify("--- Encryption Tests ---");
# $n_tests incremented below

# The enc-file.pdf files were encrypted using Acrobat 5.0, not the
# qpdf library.  The files are decrypted using qpdf, then re-encrypted
# using qpdf with specific flags.  The /P value is checked.  The
# resulting files were saved and manually checked with Acrobat 5.0 to
# ensure that the security settings were as intended.

# The enc-XI-file.pdf files were treated the same way but with Acrobat
# XI instead of Acrobat 5.0.  They were used to create test files with
# newer encryption formats.

# Values: basename, password, encryption flags, /P Encrypt key,
# extract-for-accessibility, extract-for-any-purpose,
# print-low-res, print-high-res, modify-assembly, modify-forms,
# modify-annotate, modify-other, modify-all
my @encrypted_files =
    (['base', ''],
     ['R3,V2', '',
      '-accessibility=n -extract=n -print=full -modify=all', -532,
      0, 0, 1, 1, 1, 1, 1, 1, 1],
     ['R3,V2,U=view', 'view',
      '-accessibility=y -extract=n -print=none -modify=none', -3392,
      1, 0, 0, 0, 0, 0, 0, 0, 0],
     ['R3,V2,O=master', 'master',
      '-accessibility=n -extract=y -print=none -modify=annotate', -2576,
      0, 1, 0, 0, 1, 1, 1, 0, 0],
     ['R3,V2,O=master', '',
      '-accessibility=n -extract=n -print=none -modify=form', -2624,
      0, 0, 0, 0, 1, 1, 0, 0, 0],
     ['R3,V2,U=view,O=master', 'view',
      '-accessibility=n -extract=n -print=none -modify=assembly', -2880,
      0, 0, 0, 0, 1, 0, 0, 0, 0],
     ['R3,V2,U=view,O=master', 'master',
      '-accessibility=n -print=low', -2564,
      0, 1, 1, 0, 1, 1, 1, 1, 1],
     ['R2,V1', '',
      '-print=n -modify=n -extract=n -annotate=n', -64,
      0, 0, 0, 0, 0, 0, 0, 0, 0],
     ['R2,V1,U=view', 'view',
      '-print=y -modify=n -extract=n -annotate=n', -60,
      0, 0, 1, 1, 0, 0, 0, 0, 0],
     ['R2,V1,O=master', 'master',
      '-print=n -modify=y -extract=n -annotate=n', -56,
      0, 0, 0, 0, 1, 0, 0, 1, 0],
     ['R2,V1,O=master', '',
      '-print=n -modify=n -extract=y -annotate=n', -48,
      1, 1, 0, 0, 0, 0, 0, 0, 0],
     ['R2,V1,U=view,O=master', 'view',
      '-print=n -modify=n -extract=n -annotate=y', -32,
      0, 0, 0, 0, 0, 1, 1, 0, 0],
     ['R2,V1,U=view,O=master', 'master',
      '', -4,
      1, 1, 1, 1, 1, 1, 1, 1, 1],
     ['long-password', 'asdf asdf asdf asdf asdf asdf qwer'],
     ['long-password', 'asdf asdf asdf asdf asdf asdf qw'],
     ['XI-base', ''],
     ['XI-R6,V5,O=master', '',
      '-extract=n -print=none -modify=assembly', -2368,
      1, 0, 0, 0, 1, 0, 0, 0, 0],
     ['XI-R6,V5,O=master', 'master',
      '-extract=n -print=none -modify=assembly', -2368,
      1, 0, 0, 0, 1, 0, 0, 0, 0],
     ['XI-R6,V5,U=view,O=master', 'view',
      '-print=low', -2052,
      1, 1, 1, 0, 1, 1, 1, 1, 1],
     ['XI-R6,V5,U=view,O=master', 'master',
      '-print=low', -2052,
      1, 1, 1, 0, 1, 1, 1, 1, 1],
     ['XI-R6,V5,U=view,O=master', 'master',
      '-accessibility=n', -4, # -accessibility=n has no effect
      1, 1, 1, 1, 1, 1, 1, 1, 1],
     ['XI-long-password', 'qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'],
     ['XI-long-password', 'qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcv'],
    );

$n_tests += 5 + (2 * (@encrypted_files)) + (6 * (@encrypted_files - 6)) + 9;

$td->runtest("encrypted file",
	     {$td->COMMAND => "test_driver 2 encrypted-with-images.pdf"},
	     {$td->FILE => "encrypted1.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("preserve encryption",
	     {$td->COMMAND => "qpdf encrypted-with-images.pdf encrypted-with-images.enc"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0});
$td->runtest("recheck encrypted file",
	     {$td->COMMAND => "test_driver 2 encrypted-with-images.enc"},
	     {$td->FILE => "encrypted1.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Test that long passwords that are one character too short fail.  We
# test the truncation cases in the loop below by using passwords
# longer than the supported length.
$td->runtest("significant password characters (V < 5)",
             {$td->COMMAND => "qpdf --check enc-long-password.pdf" .
                  " --password='asdf asdf asdf asdf asdf asdf q'"},
             {$td->REGEXP => ".*invalid password.*", $td->EXIT_STATUS => 2});
$td->runtest("significant password characters (V = 5)",
             {$td->COMMAND => "qpdf --check enc-XI-long-password.pdf" .
                  " --password=qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxc"},
             {$td->REGEXP => ".*invalid password.*", $td->EXIT_STATUS => 2});

my $enc_base = undef;
foreach my $d (@encrypted_files)
{
    my ($file, $pass, $xeflags, $P,
	$accessible, $extract, $printlow, $printhigh,
	$modifyassembly, $modifyform, $modifyannot,
	$modifyother, $modifyall) = @$d;

    my $f = sub { $_[0] ? "allowed" : "not allowed" };
    my $enc_details =
	"extract for accessibility: " . &$f($accessible) . "\n" .
	"extract for any purpose: " . &$f($extract) . "\n" .
	"print low resolution: " . &$f($printlow) . "\n" .
	"print high resolution: " . &$f($printhigh) . "\n" .
	"modify document assembly: " . &$f($modifyassembly) . "\n" .
	"modify forms: " . &$f($modifyform) . "\n" .
	"modify annotations: " . &$f($modifyannot) . "\n" .
	"modify other: " . &$f($modifyother) . "\n" .
	"modify anything: " . &$f($modifyall) . "\n";
    if ($file =~ m/XI-/)
    {
        $enc_details .=
            "stream encryption method: AESv3\n" .
            "string encryption method: AESv3\n" .
            "file encryption method: AESv3\n";
    }

    # Test writing to stdout
    $td->runtest("decrypt $file",
		 {$td->COMMAND =>
		      "qpdf --static-id -qdf --object-streams=disable" .
                      " --no-original-object-ids" .
		      " --password=\"$pass\" enc-$file.pdf -" .
		      " > $file.enc"},
		 {$td->STRING => "",
		  $td->EXIT_STATUS => 0});
    if ($file =~ m/base$/)
    {
        $enc_base = $file;
	$td->runtest("check ID",
		     {$td->COMMAND => "perl check-ID.pl $file.enc"},
		     {$td->STRING => "ID okay\n",
		      $td->EXIT_STATUS => 0},
		     $td->NORMALIZE_NEWLINES);
    }
    else
    {
	$td->runtest("check against base",
		     {$td->COMMAND =>
                          "sh ./diff-encrypted $enc_base.enc $file.enc"},
		     {$td->STRING => "okay\n",
		      $td->EXIT_STATUS => 0},
		     $td->NORMALIZE_NEWLINES);
    }
    if ($file =~ m/^(?:XI-)?R(\d),V(\d)(?:,U=(\w+))?(?:,O=(\w+))?$/)
    {
	my $R = $1;
	my $V = $2;
	my $upass = $3 || "";
	my $opass = $4 || "";
	my $bits = (($V == 5) ? 256 : ($V == 2) ? 128 : 40);

	my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --";
        if (($pass ne $upass) && ($V >= 5))
        {
            # V >= 5 can no longer recover user password with owner
            # password.
            $upass = "";
        }
        my $accessibility_warning = "";
        if (($R > 3) && ($eflags =~ /accessibility=n/))
        {
            $accessibility_warning =
                "qpdf: -accessibility=n is ignored" .
                " for modern encryption formats\n";
        }
	$td->runtest("encrypt $file",
		     {$td->COMMAND =>
			  "qpdf --static-id --no-original-object-ids -qdf" .
			  " $eflags $file.enc $file.enc2"},
		     {$td->STRING => $accessibility_warning,
		      $td->EXIT_STATUS => 0},
                     $td->NORMALIZE_NEWLINES);
	$td->runtest("check /P",
		     {$td->COMMAND =>
			  "qpdf --show-encryption --password=\"$pass\"" .
			  " $file.enc2"},
		     {$td->STRING => "R = $R\nP = $P\n" .
			  "User password = $upass\n$enc_details",
			  $td->EXIT_STATUS => 0},
		     $td->NORMALIZE_NEWLINES);
	$td->runtest("decrypt again",
		     {$td->COMMAND =>
			  "qpdf --static-id --no-original-object-ids -qdf" .
			  " --password=\"$pass\"" .
			  " $file.enc2 $file.enc3"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	$td->runtest("compare",
		     {$td->FILE => "$file.enc"},
		     {$td->FILE => "$file.enc3"});
	$td->runtest("preserve encryption",
		     {$td->COMMAND =>
			  "qpdf --static-id --password=\"$pass\"" .
			  " $file.enc2 $file.enc4"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	$td->runtest("check /P",
		     {$td->COMMAND =>
			  "qpdf --show-encryption --password=\"$pass\"" .
			  " $file.enc4"},
		     {$td->STRING => "R = $R\nP = $P\n" .
			  "User password = $upass\n$enc_details",
			  $td->EXIT_STATUS => 0},
		     $td->NORMALIZE_NEWLINES);
    }
}

$td->runtest("non-encrypted",
	     {$td->COMMAND => "qpdf --show-encryption enc-base.pdf"},
	     {$td->STRING => "File is not encrypted\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

$td->runtest("invalid password",
	     {$td->COMMAND => "qpdf -qdf --password=quack" .
		  " enc-R2,V1,U=view.pdf a.qdf"},
	     {$td->STRING => "enc-R2,V1,U=view.pdf: invalid password\n",
	      $td->EXIT_STATUS => 2},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("C API: invalid password",
	     {$td->COMMAND => "qpdf-ctest 2 enc-R2,V1,U=view.pdf '' a.qdf"},
	     {$td->FILE => "c-invalid-password.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

my @cenc = (
    [11, 'hybrid-xref.pdf', "''", 'r2', "", ""],
    [12, 'hybrid-xref.pdf', "''", 'r3', "", ""],
    [15, 'hybrid-xref.pdf', "''", 'r4', "", ""],
    [17, 'hybrid-xref.pdf', "''", 'r5', "", "owner3"],
    [18, 'hybrid-xref.pdf', "''", 'r6', "", "user4"],
    [13, 'c-r2.pdf', 'user1', 'decrypt with user',
     "user password: user1\n", ""],
    [13, 'c-r3.pdf', 'owner2', 'decrypt with owner',
     "user password: user2\n", ""],
    [13, 'c-r5-in.pdf', 'user3', 'decrypt R5 with user',
     "user password: user3\n", ""],
    [13, 'c-r6-in.pdf', 'owner4', 'decrypt R6 with owner',
     "user password: \n", ""],
    );
$n_tests += 2 * @cenc;

foreach my $d (@cenc)
{
    my ($n, $infile, $pass, $description, $output, $checkpass) = @$d;
    my $outfile = $description;
    $outfile =~ s/ /-/g;
    my $pdf_outfile = "c-$outfile.pdf";
    my $check_outfile = "c-$outfile.out";
    $td->runtest("C API encryption: $description",
		 {$td->COMMAND => "qpdf-ctest $n $infile $pass a.pdf"},
		 {$td->STRING => $output, $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
    if (-f $pdf_outfile)
    {
        $td->runtest("check $description content",
                     {$td->FILE => "a.pdf"},
                     {$td->FILE => $pdf_outfile});
    }
    else
    {
        # QPDF doesn't provide any way to make the random bits in
        # /Perms static, so we have no way to predictably create a
        # /V=5 encrypted file.  It's not worth adding this...the test
        # suite is adequate without having a statically predictable
        # file.
        $td->runtest("check $description",
                     {$td->COMMAND =>
                          "qpdf --check a.pdf --password=$checkpass"},
                     {$td->FILE => $check_outfile, $td->EXIT_STATUS => 0},
                     $td->NORMALIZE_NEWLINES);
    }
}

# Test combinations of linearization and encryption.  Note that we do
# content checks on encrypted and linearized files in various
# combinations below.  Here we are just making sure that they are
# linearized and/or encrypted as desired.

$td->runtest("linearize encrypted file",
	     {$td->COMMAND => "qpdf --linearize encrypted-with-images.pdf a.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --show-encryption a.pdf",
	      $td->FILTER => "grep -v allowed"},
	     {$td->STRING => "R = 3\nP = -4\nUser password = \n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check linearization",
	     {$td->COMMAND => "qpdf --check-linearization a.pdf"},
	     {$td->STRING => "a.pdf: no linearization errors\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("linearize and encrypt file",
	     {$td->COMMAND =>
		  "qpdf --linearize --encrypt user owner 128 --use-aes=y --" .
		  " lin-special.pdf a.pdf"},
	     {$td->STRING => "",
	      $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --show-encryption --password=owner a.pdf",
	      $td->FILTER => "grep -v allowed | grep -v method"},
	     {$td->STRING => "R = 4\nP = -4\nUser password = user\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("check linearization",
	     {$td->COMMAND => "qpdf --check-linearization" .
		  " --password=user a.pdf"},
	     {$td->STRING => "a.pdf: no linearization errors\n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Test AES encryption in various ways.
$n_tests += 18;
$td->runtest("encrypt with AES",
	     {$td->COMMAND => "qpdf --encrypt '' '' 128 --use-aes=y --" .
		  " enc-base.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --show-encryption a.pdf",
	      $td->FILTER => "grep -v allowed | grep -v method"},
	     {$td->STRING => "R = 4\nP = -4\nUser password = \n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("convert original to qdf",
	     {$td->COMMAND => "qpdf --static-id --no-original-object-ids" .
		  " --qdf --min-version=1.6 enc-base.pdf a.qdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("convert encrypted to qdf",
	     {$td->COMMAND => "qpdf --static-id --no-original-object-ids" .
		  " --qdf a.pdf b.qdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("compare files",
	     {$td->FILE => 'a.qdf'},
	     {$td->FILE => 'b.qdf'});
$td->runtest("linearize with AES and object streams",
	     {$td->COMMAND => "qpdf --encrypt '' '' 128 --use-aes=y --" .
		  " --linearize --object-streams=generate enc-base.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --show-encryption a.pdf",
	      $td->FILTER => "grep -v allowed | grep -v method"},
	     {$td->STRING => "R = 4\nP = -4\nUser password = \n",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("linearize original",
	     {$td->COMMAND => "qpdf --linearize --object-streams=generate" .
		  " enc-base.pdf b.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("convert linearized original to qdf",
	     {$td->COMMAND => "qpdf --static-id --no-original-object-ids" .
		  " --qdf --object-streams=generate --min-version=1.6" .
		  " b.pdf a.qdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("convert encrypted to qdf",
	     {$td->COMMAND => "qpdf --static-id --no-original-object-ids" .
		  " --qdf --object-streams=generate a.pdf b.qdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("compare files",
	     {$td->FILE => 'a.qdf'},
	     {$td->FILE => 'b.qdf'});
$td->runtest("force version on aes encrypted",
	     {$td->COMMAND => "qpdf --force-version=1.4 a.pdf b.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check",
	     {$td->COMMAND => "qpdf --check b.pdf"},
	     {$td->FILE => "aes-forced-check.out",
	      $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("make sure there is no xref stream",
	     {$td->COMMAND => "grep /ObjStm b.pdf | wc -l"},
	     {$td->REGEXP => "\\s*0\\s*", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("encrypt with V=5,R=5",
             {$td->COMMAND =>
                  "qpdf --encrypt user owner 256 --force-R5 -- " .
                  "minimal.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --check a.pdf --password=owner"},
	     {$td->FILE => "V5R5.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);
$td->runtest("encrypt with V=5,R=6",
             {$td->COMMAND =>
                  "qpdf --encrypt user owner 256 -- " .
                  "minimal.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check encryption",
	     {$td->COMMAND => "qpdf --check a.pdf --password=user"},
	     {$td->FILE => "V5R6.out", $td->EXIT_STATUS => 0},
	     $td->NORMALIZE_NEWLINES);

# Look at some actual V4 files
$n_tests += 14;
foreach my $d (['--force-V4', 'V4'],
	       ['--cleartext-metadata', 'V4-clearmeta'],
	       ['--use-aes=y', 'V4-aes'],
	       ['--cleartext-metadata --use-aes=y', 'V4-aes-clearmeta'])
{
    my ($args, $out) = @$d;
    $td->runtest("encrypt $args",
		 {$td->COMMAND => "qpdf --static-aes-iv --static-id" .
		      " --encrypt '' '' 128 $args -- enc-base.pdf a.pdf"},
		 {$td->STRING => "", $td->EXIT_STATUS => 0});
    $td->runtest("check output",
		 {$td->FILE => "a.pdf"},
		 {$td->FILE => "$out.pdf"});
    $td->runtest("show encryption",
		 {$td->COMMAND => "qpdf --show-encryption a.pdf"},
		 {$td->FILE => "$out-encryption.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
}
# Crypt Filter
$td->runtest("decrypt with crypt filter",
	     {$td->COMMAND => "qpdf --decrypt --static-id" .
		  " metadata-crypt-filter.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("check output",
	     {$td->FILE => 'a.pdf'},
	     {$td->FILE => 'decrypted-crypt-filter.pdf'});

# Copy encryption parameters
$n_tests += 10;
$td->runtest("create reference qdf",
             {$td->COMMAND =>
                  "qpdf --qdf --no-original-object-ids minimal.pdf a.qdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("create encrypted file",
	     {$td->COMMAND =>
		  "qpdf --encrypt user owner 128 --use-aes=y --extract=n --" .
                  " minimal.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("copy encryption parameters",
             {$td->COMMAND => "test_driver 30 minimal.pdf a.pdf"},
             {$td->STRING => "test 30 done\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check output encryption",
	     {$td->COMMAND => "qpdf --show-encryption b.pdf --password=owner"},
	     {$td->FILE => "copied-encryption.out",
	      $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("convert to qdf",
             {$td->COMMAND =>
                  "qpdf --qdf b.pdf b.qdf" .
                  " --password=owner --no-original-object-ids"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("compare qdf",
             {$td->COMMAND => "sh ./diff-ignore-ID-version a.qdf b.qdf"},
             {$td->STRING => "okay\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("copy encryption with qpdf",
             {$td->COMMAND =>
                  "qpdf --copy-encryption=a.pdf".
                  " --encryption-file-password=user" .
                  " minimal.pdf c.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("check output encryption",
	     {$td->COMMAND => "qpdf --show-encryption c.pdf --password=owner"},
	     {$td->FILE => "copied-encryption.out",
	      $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
$td->runtest("convert to qdf",
             {$td->COMMAND =>
                  "qpdf --qdf c.pdf c.qdf" .
                  " --password=owner --no-original-object-ids"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("compare qdf",
             {$td->COMMAND => "sh ./diff-ignore-ID-version a.qdf c.qdf"},
             {$td->STRING => "okay\n", $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

# Files with attachments
my @attachments = (
    'enc-XI-attachments-base.pdf',
    'enc-XI-R6,V5,U=attachment,encrypted-attachments.pdf',
    'enc-XI-R6,V5,U=view,attachments,cleartext-metadata.pdf');
$n_tests += 4 * @attachments + 3;
foreach my $f (@attachments)
{
    my $pass = '';
    my $tpass = '';
    if ($f =~ m/U=([^,\.]+)/)
    {
        $pass = "--password=$1";
        $tpass = $1;
    }
    $td->runtest("decrypt $f",
                 {$td->COMMAND => "qpdf --decrypt $pass $f a.pdf"},
                 {$td->STRING => "", $td->EXIT_STATUS => 0});
    $td->runtest("extract attachments",
                 {$td->COMMAND => "test_driver 35 a.pdf"},
                 {$td->FILE => "attachments.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("copy $f",
                 {$td->COMMAND => "qpdf $pass $f a.pdf"},
                 {$td->STRING => "", $td->EXIT_STATUS => 0});
    $td->runtest("extract attachments",
                 {$td->COMMAND => "test_driver 35 a.pdf $tpass"},
                 {$td->FILE => "attachments.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
}
$td->runtest("unfilterable with crypt",
             {$td->COMMAND =>
                  "test_driver 36 unfilterable-with-crypt.pdf attachment"},
             {$td->FILE => "unfilterable-with-crypt-before.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);
unlink "a.pdf";
$td->runtest("decrypt file",
             {$td->COMMAND => "qpdf -decrypt --password=attachment" .
                  " unfilterable-with-crypt.pdf a.pdf"},
             {$td->STRING => "", $td->EXIT_STATUS => 0});
$td->runtest("copy of unfilterable with crypt",
             {$td->COMMAND =>
                  "test_driver 36 a.pdf attachment"},
             {$td->FILE => "unfilterable-with-crypt-after.out",
              $td->EXIT_STATUS => 0},
             $td->NORMALIZE_NEWLINES);

show_ntests();
# ----------
$td->notify("--- Content Preservation Tests ---");
# $n_tests incremented below

my @files = ("encrypted-with-images.pdf", # encrypted
	     "inline-images.pdf",
	     "lin-special.pdf",
	     "object-stream.pdf",
	     "hybrid-xref.pdf");
my @flags = (["-qdf",						# 1
	      "qdf"],
	     ["-qdf --normalize-content=n",			# 2
	      "qdf not normalized"],
	     ["-qdf --stream-data=preserve",			# 3
	      "qdf not uncompressed"],
	     ["-qdf --stream-data=preserve --normalize-content=n", # 4
	      "qdf not normalized or uncompressed"],
	     ["--stream-data=uncompress",			# 5
	      "uncompresed"],
	     ["--normalize-content=y",				# 6
	      "normalized"],
	     ["--stream-data=uncompress --normalize-content=y",	# 7
	      "uncompressed and normalized"],
	     ["-decrypt",					# 8
	      "decrypted"],
	     ["-linearize",					# 9
	      "linearized"],
	     ["-encrypt \"\" owner 128 --",			# 10
	      "encrypted"],
	     ["-linearize -encrypt \"\" o 128 --",		# 11
	      "linearized and encrypted"],
	     ["",						# 12
	      "no arguments"],
	     );

$n_tests += 1 + (@files * @flags  * 2 * 3);
$n_compare_pdfs += 1 + (@files * @flags * 2);
$n_acroread += (@files * @flags * 2);

foreach my $file (@files)
{
    my $base = basename($file, '.pdf');

    foreach my $o (qw(disable generate))
    {
	my $n = 0;
	my $oflags = "--object-streams=$o";
	my $odescrip = "os:" . substr($o, 0, 1);
	my $osuf = ($o eq 'generate' ? "-ogen" : "");
	foreach my $d (@flags)
	{
	    my ($flags, $fdescrip) = @$d;
	    ++$n;
	    system("rm -f *.pnm");

	    $td->runtest("$file ($odescrip $fdescrip)",
			 {$td->COMMAND => "qpdf $flags $oflags $file a.pdf"},
			 {$td->STRING => "",
			  $td->EXIT_STATUS => 0});

	    $td->runtest("check status",
			 {$td->COMMAND => "qpdf --check a.pdf"},
			 {$td->FILE => "$base.$n$osuf.check",
			  $td->EXIT_STATUS => 0},
			 $td->NORMALIZE_NEWLINES);

	    $td->runtest("check with C API",
			 {$td->COMMAND => [qw(qpdf-ctest 1 a.pdf), "", ""]},
			 {$td->FILE => "$base.$n$osuf.c-check",
			  $td->EXIT_STATUS => 0},
			 $td->NORMALIZE_NEWLINES);

	    compare_pdfs($file, "a.pdf");

	    if ($have_acroread)
	    {
		# These tests require Adobe Reader > 7.x to work with
		# encrypted files.
		$td->runtest("check with Adobe Reader",
			     {$td->COMMAND =>
				  "acroread -toPostScript -pairs a.pdf 1.ps"},
			     {$td->STRING => "",
			      $td->EXIT_STATUS => 0});
	    }
	}
	flush_tiff_cache();
    }
}

$td->runtest("convert inline-images to qdf",
	     {$td->COMMAND => "qpdf --static-id --no-original-object-ids" .
		  " --qdf inline-images.pdf a.pdf"},
	     {$td->STRING => "", $td->EXIT_STATUS => 0});

compare_pdfs("inline-images.pdf", "a.pdf");

show_ntests();
# ----------
$td->notify("--- fix-qdf Tests ---");
$n_tests += 5;

for (my $n = 1; $n <= 2; ++$n)
{
    $td->runtest("fix-qdf $n",
		 {$td->COMMAND => "fix-qdf fix$n.qdf"},
		 {$td->FILE => "fix$n.qdf.out",
		  $td->EXIT_STATUS => 0});

    $td->runtest("identity fix-qdf $n",
		 {$td->COMMAND => "fix-qdf fix$n.qdf.out"},
		 {$td->FILE => "fix$n.qdf.out",
		  $td->EXIT_STATUS => 0});
}
$td->runtest("fix-qdf with big object stream", # > 255 objects in a stream
             {$td->COMMAND => "fix-qdf big-ostream.pdf"},
             {$td->FILE => "big-ostream.pdf",
              $td->EXIT_STATUS => 0});

show_ntests();
# ----------
$td->notify("--- Large File Tests ---");
my $nlarge = 1;
if (defined $large_file_test_path)
{
    $nlarge = 2;
}
else
{
    $td->notify("--- Skipping tests on actual large files ---");
}
$n_tests += $nlarge * 13;
for (my $large = 0; $large < $nlarge; ++$large)
{
    if ($large)
    {
        $td->notify("--- Running tests on actual large files ---");
    }
    else
    {
        $td->notify("--- Running large file tests on small files ---");
    }
    my $size = ($large ? "large" : "small");
    my $file = $large ? "$large_file_test_path/a.pdf" : "a.pdf";
    $td->runtest("write test file",
                 {$td->COMMAND => "test_large_file write $size '$file'"},
                 {$td->FILE => "large_file.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("read test file",
                 {$td->COMMAND => "test_large_file read $size '$file'"},
                 {$td->FILE => "large_file.out", $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    $td->runtest("check",
                 {$td->COMMAND => "qpdf --suppress-recovery --check '$file'",
                  $td->FILTER => "grep -v checking"},
                 {$td->FILE => "large_file-check-normal.out",
                  $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);

    for my $ostream (0, 1)
    {
        for my $linearize (0, 1)
        {
            if (($ostream == 0) && ($linearize == 0))
            {
                # Original file has no object streams and is not linearized.
                next;
            }
            my $args = "";
            my $omode = $ostream ? "generate" : "disable";
            my $lin = $linearize ? "--linearize" : "";
            my $newfile = "$file-new";

            $td->runtest("transform: ostream=$ostream, linearize=$linearize",
                         {$td->COMMAND =>
                              "qpdf --stream-data=preserve" .
                              " --object-streams=$omode" .
                              " $lin '$file' '$newfile'"},
                         {$td->STRING => "", $td->EXIT_STATUS => 0});
            $td->runtest("read test file",
                         {$td->COMMAND =>
                              "test_large_file read $size '$newfile'"},
                         {$td->FILE => "large_file.out", $td->EXIT_STATUS => 0},
                         $td->NORMALIZE_NEWLINES);
            my $check_out =
                ($linearize
                 ? ($ostream
                    ? "large_file-check-ostream-linearized.out"
                    : "large_file-check-linearized.out")
                 : ($ostream
                    ? "large_file-check-ostream.out"
                    : "large_file-check-normal.out"));
            $td->runtest("check: ostream=$ostream, linearize=$linearize",
                         {$td->COMMAND =>
                              "qpdf --suppress-recovery --check '$newfile'",
                          $td->FILTER => "grep -v checking"},
                         {$td->FILE => $check_out, $td->EXIT_STATUS => 0},
                         $td->NORMALIZE_NEWLINES);
            unlink $newfile;
        }
    }

    # Clobber xref
    open(F, "+<$file") or die;
    seek(F, -50, 2);
    my $pos = tell F;
    my $buf;
    read(F, $buf, 50);
    die unless $buf =~ m/^(.*startxref\n)\d+/s;
    $pos += length($1);
    seek(F, $pos, 0) or die;
    print F "oops" or die;
    close(F);

    my $cmd = +{$td->COMMAND => "test_large_file read $size '$file'"};
    if ($large)
    {
        $cmd->{$td->FILTER} = "sed -e 's,$large_file_test_path/,,'";
    }
    $td->runtest("reconstruct xref table",
                 $cmd,
                 {$td->FILE => "large_file_xref_reconstruct.out",
                  $td->EXIT_STATUS => 0},
                 $td->NORMALIZE_NEWLINES);
    unlink $file;
}
# ----------

cleanup();

# See comments at beginning about calculation of number of tests.  We
# do it strictly based on static values, not as a by-product of
# running the test suite.
$td->report(calc_ntests());

sub calc_ntests
{
    my $result = $n_tests;
    if ($have_acroread)
    {
	$result += $n_acroread;
    }
    if ($compare_images)
    {
	$result += 3 * ($n_compare_pdfs);
    }
    $result;
}

sub show_ntests
{
    if (0)
    {
	$td->emphasize("tests so far: ". calc_ntests());
    }
}

sub check_pdf
{
    my ($description, $command, $output, $status) = @_;
    unlink "a.pdf";
    $td->runtest($description,
		 {$td->COMMAND => "$command a.pdf"},
		 {$td->STRING => "",
		  $td->EXIT_STATUS => $status});
    $td->runtest("check output",
		 {$td->FILE => "a.pdf"},
		 {$td->FILE => $output});
}

sub flush_tiff_cache
{
    system("rm -rf tiff-cache");
}

sub compare_pdfs
{
    return unless $compare_images;

    my ($f1, $f2, $exp) = @_;

    $exp = 0 unless defined $exp;

    system("rm -rf tif1 tif2");

    mkdir "tiff-cache", 0777 unless -d "tiff-cache";

    my $md5_1 = get_md5_checksum($f1);
    my $md5_2 = get_md5_checksum($f2);

    mkdir "tif1", 0777 or die;
    mkdir "tif2", 0777 or die;

    if (-f "tiff-cache/$md5_1.tif")
    {
	$td->runtest("get cached original file image",
		     {$td->COMMAND => "cp tiff-cache/$md5_1.tif tif1/a.tif"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
    }
    else
    {
        # We discard gs's stderr since it has sometimes been known to
        # complain about files that are not bad.  In particular, gs
        # 9.04 can't handle empty xref sections such as those found in
        # the hybrid xref cases.  We don't really care whether gs
        # complains or not as long as it creates correct images.  If
        # it doesn't create correct images, the test will fail, and we
        # can run manually to see the error message.  If it does, then
        # we don't care about the warning.
	$td->runtest("convert original file to image",
		     {$td->COMMAND =>
			  "(cd tif1;" .
			  " gs 2>$devNull -q -dNOPAUSE -sDEVICE=tiff24nc" .
			  " -sOutputFile=a.tif - < ../$f1)"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	copy("tif1/a.tif", "tiff-cache/$md5_1.tif");
    }

    if (-f "tiff-cache/$md5_2.tif")
    {
	$td->runtest("get cached new file image",
		     {$td->COMMAND => "cp tiff-cache/$md5_2.tif tif2/a.tif"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
    }
    else
    {
	$td->runtest("convert new file to image",
		     {$td->COMMAND =>
			  "(cd tif2;" .
			  " gs 2>$devNull -q -dNOPAUSE -sDEVICE=tiff24nc" .
			  " -sOutputFile=a.tif - < ../$f2)"},
		     {$td->STRING => "",
		      $td->EXIT_STATUS => 0});
	copy("tif2/a.tif", "tiff-cache/$md5_2.tif");
    }

    $td->runtest("compare images",
		 {$td->COMMAND => "tiffcmp -t tif1/a.tif tif2/a.tif"},
		 {$td->REGEXP => ".*",
		  $td->EXIT_STATUS => $exp});

    system("rm -rf tif1 tif2");
}

sub check_metadata
{
    my ($file, $exp_encrypted, $exp_cleartext) = @_;
    my $out = "encrypted=$exp_encrypted; cleartext=$exp_cleartext\n" .
	"test 6 done\n";
    $td->runtest("check metadata: $file",
		 {$td->COMMAND => "test_driver 6 $file"},
		 {$td->STRING => $out, $td->EXIT_STATUS => 0},
		 $td->NORMALIZE_NEWLINES);
}

sub get_md5_checksum
{
    my $file = shift;
    open(F, "<$file") or fatal("can't open $file: $!");
    binmode F;
    my $digest = Digest::MD5->new->addfile(*F)->hexdigest;
    close(F);
    $digest;
}

sub cleanup
{
    system("rm -rf *.ps *.pnm ?.pdf ?.qdf *.enc* tif1 tif2 tiff-cache");
    system("rm -rf *split-out*");
}