QPDFJob: add test cases

This commit is contained in:
Jay Berkenbilt 2022-01-31 15:56:05 -05:00
parent e3506253f1
commit 47f33cec25
51 changed files with 802 additions and 2 deletions

View File

@ -2,6 +2,7 @@
#include <qpdf/JSONHandler.hh>
#include <qpdf/QPDFUsage.hh>
#include <qpdf/QUtil.hh>
#include <qpdf/QTC.hh>
#include <memory>
#include <stdexcept>
@ -108,6 +109,7 @@ Handlers::addBare(bare_handler_t fn)
jh->addBoolHandler([this, fn](std::string const& path, bool v){
if (! v)
{
QTC::TC("qpdf", "QPDFJob json bare not true");
usage(path + ": value must be true");
}
else
@ -140,12 +142,14 @@ Handlers::addChoices(char const** choices,
{
if (strcmp(*i, p) == 0)
{
QTC::TC("qpdf", "QPDFJob json choice match");
matches = true;
break;
}
}
if (! matches)
{
QTC::TC("qpdf", "QPDFJob json choice mismatch");
std::ostringstream msg;
msg << path + ": unexpected value; expected one of ";
bool first = true;
@ -307,6 +311,7 @@ Handlers::beginOutputOptionsEncrypt(JSON j)
{
if (key_len != 0)
{
QTC::TC("qpdf", "QPDFJob json encrypt duplicate key length");
usage("exactly one of 40bit, 128bit, or 256bit must be given");
}
key_len = QUtil::string_to_int(key.c_str());
@ -322,12 +327,14 @@ Handlers::beginOutputOptionsEncrypt(JSON j)
});
if (key_len == 0)
{
QTC::TC("qpdf", "QPDFJob json encrypt no key length");
usage("exactly one of 40bit, 128bit, or 256bit must be given;"
" an empty dictionary may be supplied for one of them"
" to set the key length without imposing any restrictions");
}
if (! (user_password_seen && owner_password_seen))
{
QTC::TC("qpdf", "QPDFJob json encrypt missing password");
usage("the user and owner password are both required; use the empty"
" string for the user password if you don't want a password");
}
@ -550,6 +557,7 @@ Handlers::beginOptionsPages(JSON j)
});
if (! file_seen)
{
QTC::TC("qpdf", "QPDFJob json pages no file");
usage("file is required in page specification");
}
this->c_pages->pageSpec(

View File

@ -630,3 +630,10 @@ qpdf check password password correct 0
qpdf check password not encrypted 0
QPDFJob_config password file 0
QPDFJob_config password stdin 0
QPDFJob json bare not true 0
QPDFJob json choice mismatch 0
QPDFJob json choice match 0
QPDFJob json encrypt no key length 0
QPDFJob json encrypt duplicate key length 0
QPDFJob json encrypt missing password 0
QPDFJob json pages no file 0

View File

@ -370,6 +370,93 @@ foreach my $f (@dangling)
{$td->FILE => "a.pdf"},
{$td->FILE => "$f-dangling-out.pdf"});
}
show_ntests();
# ----------
$td->notify("--- QPDFJob Tests ---");
open(F, ">auto-txt") or die;
print F "from file";
close(F);
my @bad_json = (
"bare-option-false",
"choice-mismatch",
"encrypt-duplicate-key-length",
"encrypt-missing-password",
"encrypt-no-key-length",
"pages-no-file",
"schema-error",
"json-error"
);
my @good_json = (
"choice-match",
"input-file-password",
"empty-input",
"replace-input",
"encrypt-40",
"encrypt-128",
"encrypt-256-with-restrictions",
"add-attachments",
"copy-attachments",
"underlay-overlay",
"underlay-overlay-password",
"misc-options",
);
$n_tests += 4 + scalar(@bad_json) + (2 * scalar(@good_json));
foreach my $i (@bad_json)
{
$td->runtest("QPDFJob bad json: $i",
{$td->COMMAND => "qpdf --job-json-file=bad-json-$i.json"},
{$td->FILE => "bad-$i-json.out", $td->EXIT_STATUS => 2},
$td->NORMALIZE_NEWLINES);
}
foreach my $i (@good_json)
{
if ($i eq 'replace-input')
{
copy("minimal.pdf", 'a.pdf');
}
$td->runtest("QPDFJob good json: $i",
{$td->COMMAND => "qpdf --job-json-file=job-json-$i.json"},
{$td->STRING => "", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
if ($i =~ m/encrypt-256/)
{
$td->runtest("check encryption $i",
{$td->COMMAND =>
"qpdf a.pdf --password=u" .
" --job-json-file=job-show-encryption.json"},
{$td->FILE => "job-json-$i.out", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
}
else
{
$td->runtest("check good json $i output",
{$td->FILE => "a.pdf"},
{$td->FILE => "job-json-$i.pdf"});
}
}
$td->runtest("QPDFJob json partial",
{$td->COMMAND => "test_driver 83 - job-partial.json"},
{$td->FILE => "job-partial-json.out", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
$td->runtest("QPDFJob API",
{$td->COMMAND => "test_driver 84 -"},
{$td->FILE => "job-api.out", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
$td->runtest("check output",
{$td->FILE => "a.pdf"},
{$td->FILE => "test84.pdf"});
$td->runtest("json output from job",
{$td->COMMAND => "qpdf --job-json-file=job-json-output.json"},
{$td->FILE => "job-json-output.out.json", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
show_ntests();
# ----------
$td->notify("--- Form Tests ---");

View File

@ -0,0 +1,10 @@
qpdf: error with job-json file bad-json-bare-option-false.json: .output.options.qdf: value must be true
Run qpdf--job-json-help for information on the file format.
For help:
qpdf--help=usage usage information
qpdf--help=topic help on a topic
qpdf--help=--option help on an option
qpdf--help general help and a topic list

View File

@ -0,0 +1,10 @@
qpdf: error with job-json file bad-json-choice-mismatch.json: .output.options.objectStreams: unexpected value; expected one of disable, preserve, generate
Run qpdf--job-json-help for information on the file format.
For help:
qpdf--help=usage usage information
qpdf--help=topic help on a topic
qpdf--help=--option help on an option
qpdf--help general help and a topic list

View File

@ -0,0 +1,10 @@
qpdf: error with job-json file bad-json-encrypt-duplicate-key-length.json: exactly one of 40bit, 128bit, or 256bit must be given
Run qpdf--job-json-help for information on the file format.
For help:
qpdf--help=usage usage information
qpdf--help=topic help on a topic
qpdf--help=--option help on an option
qpdf--help general help and a topic list

View File

@ -0,0 +1,10 @@
qpdf: error with job-json file bad-json-encrypt-missing-password.json: the user and owner password are both required; use the empty string for the user password if you don't want a password
Run qpdf--job-json-help for information on the file format.
For help:
qpdf--help=usage usage information
qpdf--help=topic help on a topic
qpdf--help=--option help on an option
qpdf--help general help and a topic list

View File

@ -0,0 +1,10 @@
qpdf: error with job-json file bad-json-encrypt-no-key-length.json: exactly one of 40bit, 128bit, or 256bit must be given; an empty dictionary may be supplied for one of them to set the key length without imposing any restrictions
Run qpdf--job-json-help for information on the file format.
For help:
qpdf--help=usage usage information
qpdf--help=topic help on a topic
qpdf--help=--option help on an option
qpdf--help general help and a topic list

View File

@ -0,0 +1,7 @@
{
"output": {
"options": {
"qdf": false
}
}
}

View File

@ -0,0 +1,7 @@
{
"output": {
"options": {
"objectStreams": "potato"
}
}
}

View File

@ -0,0 +1,14 @@
{
"output": {
"options": {
"encrypt": {
"userPassword": "",
"ownerPassword": "someOwnerThing",
"256bit": {
},
"128bit": {
}
}
}
}
}

View File

@ -0,0 +1,11 @@
{
"output": {
"options": {
"encrypt": {
"userPassword": "",
"256bit": {
}
}
}
}
}

View File

@ -0,0 +1,10 @@
{
"output": {
"options": {
"encrypt": {
"userPassword": "",
"ownerPassword": "someOwnerThing"
}
}
}
}

View File

@ -0,0 +1,10 @@
qpdf: error with job-json file bad-json-json-error.json: JSON: offset 130: unexpected dictionary end delimiter
Run qpdf--job-json-help for information on the file format.
For help:
qpdf--help=usage usage information
qpdf--help=topic help on a topic
qpdf--help=--option help on an option
qpdf--help general help and a topic list

View File

@ -0,0 +1,10 @@
{
"output": {
"options": {
"encrypt": {
"userPassword": "",
"ownerPassword": "someOwnerThing",
}
}
}
}

View File

@ -0,0 +1,9 @@
{
"options": {
"pages": [
{
"range": "1-z"
}
]
}
}

View File

@ -0,0 +1,12 @@
{
"output": {
"potato": {
"encrypt": {
"userPassword": "",
"ownerPassword": "someOwnerThing",
"256bit": {
}
}
}
}
}

View File

@ -0,0 +1,10 @@
qpdf: error with job-json file bad-json-pages-no-file.json: file is required in page specification
Run qpdf--job-json-help for information on the file format.
For help:
qpdf--help=usage usage information
qpdf--help=topic help on a topic
qpdf--help=--option help on an option
qpdf--help general help and a topic list

View File

@ -0,0 +1,11 @@
qpdf: error with job-json file bad-json-schema-error.json: qpdf: job json has errors:
json key ".output": key "potato" is not present in schema but appears in object
Run qpdf--job-json-help for information on the file format.
For help:
qpdf--help=usage usage information
qpdf--help=topic help on a topic
qpdf--help=--option help on an option
qpdf--help general help and a topic list

View File

@ -0,0 +1,8 @@
normal
error caught by check
finished config
usage: an input file name is required
error caught by run
finished config
usage: an input file name is required
test 84 done

View File

@ -0,0 +1,27 @@
{
"input": {
"file": "minimal.pdf"
},
"output": {
"file": "a.pdf",
"options": {
"staticId": true
}
},
"options": {
"addAttachment": [
{
"file": "auto-txt",
"moddate": "D:20220131134246-05'00'",
"creationdate": "D:20220131134246-05'00'"
},
{
"file": "auto-txt",
"moddate": "D:20220131134246-05'00'",
"creationdate": "D:20220131134246-05'00'",
"filename": "auto2",
"key": "auto2-key"
}
]
}
}

Binary file not shown.

View File

@ -0,0 +1,12 @@
{
"input": {
"file": "minimal.pdf"
},
"output": {
"file": "a.pdf",
"options": {
"deterministicId": true,
"objectStreams": "generate"
}
}
}

Binary file not shown.

View File

@ -0,0 +1,26 @@
{
"input": {
"file": "minimal.pdf"
},
"output": {
"file": "a.pdf",
"options": {
"staticId": true
}
},
"options": {
"copyAttachmentsFrom": [
{
"file": "job-json-add-attachments.pdf"
},
{
"file": "20-pages.pdf",
"password": "user"
},
{
"file": "job-json-add-attachments.pdf",
"prefix": "p-"
}
]
}
}

Binary file not shown.

View File

@ -0,0 +1,23 @@
{
"input": {
"empty": true
},
"output": {
"file": "a.pdf",
"options": {
"staticId": true
}
},
"options": {
"pages": [
{
"file": "minimal.pdf"
},
{
"file": "20-pages.pdf",
"password": "user",
"range": "1-5"
}
]
}
}

Binary file not shown.

View File

@ -0,0 +1,19 @@
{
"input": {
"file": "fxo-blue.pdf"
},
"output": {
"file": "a.pdf",
"options": {
"staticId": true,
"staticAesIv": true,
"encrypt": {
"userPassword": "u",
"ownerPassword": "o",
"128bit": {
"useAes": "y"
}
}
}
}
}

Binary file not shown.

View File

@ -0,0 +1,20 @@
{
"input": {
"file": "minimal.pdf"
},
"output": {
"file": "a.pdf",
"options": {
"staticId": true,
"staticAesIv": true,
"encrypt": {
"userPassword": "u",
"ownerPassword": "o",
"256bit": {
"print": "low",
"modify": "form"
}
}
}
}
}

View File

@ -0,0 +1,16 @@
R = 6
P = -2092
User password = u
Supplied password is user password
extract for accessibility: allowed
extract for any purpose: allowed
print low resolution: allowed
print high resolution: not allowed
modify document assembly: allowed
modify forms: allowed
modify annotations: not allowed
modify other: not allowed
modify anything: not allowed
stream encryption method: AESv3
string encryption method: AESv3
file encryption method: AESv3

View File

@ -0,0 +1,19 @@
{
"input": {
"file": "minimal.pdf"
},
"output": {
"file": "a.pdf",
"options": {
"staticId": true,
"encrypt": {
"userPassword": "u",
"ownerPassword": "o",
"40bit": {}
}
}
},
"options": {
"allowWeakCrypto": true
}
}

View File

@ -0,0 +1,39 @@
%PDF-1.3
%¿÷¢þ
1 0 obj
<< /Pages 2 0 R /Type /Catalog >>
endobj
2 0 obj
<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >>
endobj
3 0 obj
<< /Contents 4 0 R /MediaBox [ 0 0 612 792 ] /Parent 2 0 R /Resources << /Font << /F1 5 0 R >> /ProcSet 6 0 R >> /Type /Page >>
endobj
4 0 obj
<< /Length 48 /Filter /FlateDecode >>
stream
´æ@!Ò©¯ e\ †Në §ŽK3Ù3­GAG5â|'j4oeendstream
endobj
5 0 obj
<< /BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font >>
endobj
6 0 obj
[ /PDF /Text ]
endobj
7 0 obj
<< /Filter /Standard /Length 40 /O <853fee3f6550fc3bc212797eaed99cc9be53347583a738e25fdfb1242bf93366> /P -4 /R 2 /U <5c78313a3f3f17efd751ded6e1f69b40e384ea413e49df3af00c031a9f4185c0> /V 1 >>
endobj
xref
0 8
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000123 00000 n
0000000266 00000 n
0000000384 00000 n
0000000491 00000 n
0000000521 00000 n
trailer << /Root 1 0 R /Size 8 /ID [<31415926535897932384626433832795><31415926535897932384626433832795>] /Encrypt 7 0 R >>
startxref
727
%%EOF

View File

@ -0,0 +1,14 @@
{
"input": {
"file": "20-pages.pdf",
"password": "user"
},
"output": {
"file": "a.pdf",
"options": {
"staticId": true,
"staticAesIv": true,
"compressStreams": "n"
}
}
}

Binary file not shown.

View File

@ -0,0 +1,13 @@
{
"input": {
"file": "minimal.pdf"
},
"output": {
"file": "a.pdf",
"options": {
"staticId": true,
"linearize": true,
"compressStreams": "n"
}
}
}

Binary file not shown.

View File

@ -0,0 +1,16 @@
{
"input": {
"file": "minimal.pdf"
},
"inspect": {
"json": "1",
"jsonKey": [
"pages",
"objects"
],
"jsonObject": [
"trailer",
"5"
]
}
}

View File

@ -0,0 +1,28 @@
{
"objects": {
"5 0 R": [
"/PDF",
"/Text"
],
"trailer": {
"/Root": "1 0 R",
"/Size": 7
}
},
"pages": [
{
"contents": [
"4 0 R"
],
"images": [],
"label": null,
"object": "3 0 R",
"outlines": [],
"pageposfrom1": 1
}
],
"parameters": {
"decodelevel": "generalized"
},
"version": 1
}

View File

@ -0,0 +1,12 @@
{
"input": {
"file": "a.pdf"
},
"output": {
"replaceInput": true,
"options": {
"staticId": true,
"objectStreams": "generate"
}
}
}

Binary file not shown.

View File

@ -0,0 +1,23 @@
{
"input": {
"file": "minimal.pdf"
},
"output": {
"file": "a.pdf",
"options": {
"staticId": true
}
},
"options": {
"underlay": {
"file": "20-pages.pdf",
"password": "user",
"from": "5"
},
"overlay": {
"file": "job-json-encrypt-128.pdf",
"password": "o",
"from": "7"
}
}
}

Binary file not shown.

View File

@ -0,0 +1,23 @@
{
"input": {
"file": "20-pages.pdf",
"password": "owner"
},
"output": {
"file": "a.pdf",
"options": {
"staticId": true,
"decrypt": true
}
},
"options": {
"underlay": {
"file": "fxo-green.pdf"
},
"overlay": {
"file": "fxo-red.pdf",
"from": "1,2",
"repeat": "3"
}
}
}

Binary file not shown.

View File

@ -0,0 +1,3 @@
calling initializeFromJson
usage: an input file name is required
test 83 done

View File

@ -0,0 +1,12 @@
{
"output": {
"options": {
"encrypt": {
"userPassword": "",
"ownerPassword": "",
"256bit": {
}
}
}
}
}

View File

@ -0,0 +1,5 @@
{
"inspect": {
"showEncryption": true
}
}

101
qpdf/qtest/qpdf/test84.pdf Normal file
View File

@ -0,0 +1,101 @@
%PDF-1.3
%¿÷¢þ
%QDF-1.0
%% Original object ID: 1 0
1 0 obj
<<
/Pages 2 0 R
/Type /Catalog
>>
endobj
%% Original object ID: 2 0
2 0 obj
<<
/Count 1
/Kids [
3 0 R
]
/Type /Pages
>>
endobj
%% Page 1
%% Original object ID: 3 0
3 0 obj
<<
/Contents 4 0 R
/MediaBox [
0
0
612
792
]
/Parent 2 0 R
/Resources <<
/Font <<
/F1 6 0 R
>>
/ProcSet 7 0 R
>>
/Type /Page
>>
endobj
%% Contents for page 1
%% Original object ID: 4 0
4 0 obj
<<
/Length 5 0 R
>>
stream
BT
/F1 24 Tf
72 720 Td
(Potato) Tj
ET
endstream
endobj
5 0 obj
44
endobj
%% Original object ID: 6 0
6 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
%% Original object ID: 5 0
7 0 obj
[
/PDF
/Text
]
endobj
xref
0 8
0000000000 65535 f
0000000052 00000 n
0000000133 00000 n
0000000242 00000 n
0000000484 00000 n
0000000583 00000 n
0000000629 00000 n
0000000774 00000 n
trailer <<
/Root 1 0 R
/Size 8
/ID [<cd76a1aff58b062d8141f81511edbe38><cd76a1aff58b062d8141f81511edbe38>]
>>
startxref
809
%%EOF

View File

@ -20,6 +20,8 @@
#include <qpdf/QPDFWriter.hh>
#include <qpdf/QPDFSystemError.hh>
#include <qpdf/QIntC.hh>
#include <qpdf/QPDFJob.hh>
#include <qpdf/QPDFUsage.hh>
#include <iostream>
#include <sstream>
#include <algorithm>
@ -3140,6 +3142,81 @@ static void test_82(QPDF& pdf, char const* arg2)
assert(! str.isOrHasName("/Marvin"));
}
static void test_83(QPDF& pdf, char const* arg2)
{
// Test QPDFJob json with partial = false. For testing with
// partial = true, we just use qpdf --job-json-file.
QPDFJob j;
PointerHolder<char> file_buf;
size_t size;
QUtil::read_file_into_memory(arg2, file_buf, size);
try
{
std::cout << "calling initializeFromJson" << std::endl;
j.initializeFromJson(std::string(file_buf.getPointer(), size));
std::cout << "called initializeFromJson" << std::endl;
}
catch (QPDFUsage& e)
{
std::cerr << "usage: " << e.what() << std::endl;
}
catch (std::exception& e)
{
std::cerr << "exception: " << e.what() << std::endl;
}
}
static void test_84(QPDF& pdf, char const* arg2)
{
// Test QPDFJob API
std::cout << "normal" << std::endl;
{
QPDFJob j;
j.config()
->inputFile("minimal.pdf")
->outputFile("a.pdf")
->qdf()
->deterministicId()
->objectStreams("preserve")
->checkConfiguration();
j.run();
}
std::cout << "error caught by check" << std::endl;
try
{
QPDFJob j;
j.config()
->outputFile("a.pdf")
->qdf();
std::cout << "finished config" << std::endl;
j.checkConfiguration();
assert(false);
}
catch (QPDFUsage& e)
{
std::cout << "usage: " << e.what() << std::endl;
}
std::cout << "error caught by run" << std::endl;
try
{
QPDFJob j;
j.config()
->outputFile("a.pdf")
->qdf();
std::cout << "finished config" << std::endl;
j.run();
assert(false);
}
catch (QPDFUsage& e)
{
std::cout << "usage: " << e.what() << std::endl;
}
}
void runtest(int n, char const* filename1, char const* arg2)
{
// Most tests here are crafted to work on specific files. Look at
@ -3206,7 +3283,7 @@ void runtest(int n, char const* filename1, char const* arg2)
pdf.processMemoryFile((std::string(filename1) + ".pdf").c_str(),
p, size);
}
else if ((n == 61) || (n == 81))
else if ((n == 61) || (n == 81) || (n == 83) || (n == 84))
{
// Ignore filename argument entirely
}
@ -3253,7 +3330,8 @@ void runtest(int n, char const* filename1, char const* arg2)
{68, test_68}, {69, test_69}, {70, test_70}, {71, test_71},
{72, test_72}, {73, test_73}, {74, test_74}, {75, test_75},
{76, test_76}, {77, test_77}, {78, test_78}, {79, test_79},
{80, test_80}, {81, test_81}, {82, test_82},
{80, test_80}, {81, test_81}, {82, test_82}, {83, test_83},
{84, test_84},
};
auto fn = test_functions.find(n);