mirror of
https://github.com/qpdf/qpdf.git
synced 2024-11-13 08:16:29 +00:00
QPDFJob: add test cases
This commit is contained in:
parent
e3506253f1
commit
47f33cec25
@ -2,6 +2,7 @@
|
|||||||
#include <qpdf/JSONHandler.hh>
|
#include <qpdf/JSONHandler.hh>
|
||||||
#include <qpdf/QPDFUsage.hh>
|
#include <qpdf/QPDFUsage.hh>
|
||||||
#include <qpdf/QUtil.hh>
|
#include <qpdf/QUtil.hh>
|
||||||
|
#include <qpdf/QTC.hh>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
@ -108,6 +109,7 @@ Handlers::addBare(bare_handler_t fn)
|
|||||||
jh->addBoolHandler([this, fn](std::string const& path, bool v){
|
jh->addBoolHandler([this, fn](std::string const& path, bool v){
|
||||||
if (! v)
|
if (! v)
|
||||||
{
|
{
|
||||||
|
QTC::TC("qpdf", "QPDFJob json bare not true");
|
||||||
usage(path + ": value must be true");
|
usage(path + ": value must be true");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -140,12 +142,14 @@ Handlers::addChoices(char const** choices,
|
|||||||
{
|
{
|
||||||
if (strcmp(*i, p) == 0)
|
if (strcmp(*i, p) == 0)
|
||||||
{
|
{
|
||||||
|
QTC::TC("qpdf", "QPDFJob json choice match");
|
||||||
matches = true;
|
matches = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (! matches)
|
if (! matches)
|
||||||
{
|
{
|
||||||
|
QTC::TC("qpdf", "QPDFJob json choice mismatch");
|
||||||
std::ostringstream msg;
|
std::ostringstream msg;
|
||||||
msg << path + ": unexpected value; expected one of ";
|
msg << path + ": unexpected value; expected one of ";
|
||||||
bool first = true;
|
bool first = true;
|
||||||
@ -307,6 +311,7 @@ Handlers::beginOutputOptionsEncrypt(JSON j)
|
|||||||
{
|
{
|
||||||
if (key_len != 0)
|
if (key_len != 0)
|
||||||
{
|
{
|
||||||
|
QTC::TC("qpdf", "QPDFJob json encrypt duplicate key length");
|
||||||
usage("exactly one of 40bit, 128bit, or 256bit must be given");
|
usage("exactly one of 40bit, 128bit, or 256bit must be given");
|
||||||
}
|
}
|
||||||
key_len = QUtil::string_to_int(key.c_str());
|
key_len = QUtil::string_to_int(key.c_str());
|
||||||
@ -322,12 +327,14 @@ Handlers::beginOutputOptionsEncrypt(JSON j)
|
|||||||
});
|
});
|
||||||
if (key_len == 0)
|
if (key_len == 0)
|
||||||
{
|
{
|
||||||
|
QTC::TC("qpdf", "QPDFJob json encrypt no key length");
|
||||||
usage("exactly one of 40bit, 128bit, or 256bit must be given;"
|
usage("exactly one of 40bit, 128bit, or 256bit must be given;"
|
||||||
" an empty dictionary may be supplied for one of them"
|
" an empty dictionary may be supplied for one of them"
|
||||||
" to set the key length without imposing any restrictions");
|
" to set the key length without imposing any restrictions");
|
||||||
}
|
}
|
||||||
if (! (user_password_seen && owner_password_seen))
|
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"
|
usage("the user and owner password are both required; use the empty"
|
||||||
" string for the user password if you don't want a password");
|
" string for the user password if you don't want a password");
|
||||||
}
|
}
|
||||||
@ -550,6 +557,7 @@ Handlers::beginOptionsPages(JSON j)
|
|||||||
});
|
});
|
||||||
if (! file_seen)
|
if (! file_seen)
|
||||||
{
|
{
|
||||||
|
QTC::TC("qpdf", "QPDFJob json pages no file");
|
||||||
usage("file is required in page specification");
|
usage("file is required in page specification");
|
||||||
}
|
}
|
||||||
this->c_pages->pageSpec(
|
this->c_pages->pageSpec(
|
||||||
|
@ -630,3 +630,10 @@ qpdf check password password correct 0
|
|||||||
qpdf check password not encrypted 0
|
qpdf check password not encrypted 0
|
||||||
QPDFJob_config password file 0
|
QPDFJob_config password file 0
|
||||||
QPDFJob_config password stdin 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
|
||||||
|
@ -370,6 +370,93 @@ foreach my $f (@dangling)
|
|||||||
{$td->FILE => "a.pdf"},
|
{$td->FILE => "a.pdf"},
|
||||||
{$td->FILE => "$f-dangling-out.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();
|
show_ntests();
|
||||||
# ----------
|
# ----------
|
||||||
$td->notify("--- Form Tests ---");
|
$td->notify("--- Form Tests ---");
|
||||||
|
10
qpdf/qtest/qpdf/bad-bare-option-false-json.out
Normal file
10
qpdf/qtest/qpdf/bad-bare-option-false-json.out
Normal 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
|
||||||
|
|
10
qpdf/qtest/qpdf/bad-choice-mismatch-json.out
Normal file
10
qpdf/qtest/qpdf/bad-choice-mismatch-json.out
Normal 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
|
||||||
|
|
10
qpdf/qtest/qpdf/bad-encrypt-duplicate-key-length-json.out
Normal file
10
qpdf/qtest/qpdf/bad-encrypt-duplicate-key-length-json.out
Normal 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
|
||||||
|
|
10
qpdf/qtest/qpdf/bad-encrypt-missing-password-json.out
Normal file
10
qpdf/qtest/qpdf/bad-encrypt-missing-password-json.out
Normal 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
|
||||||
|
|
10
qpdf/qtest/qpdf/bad-encrypt-no-key-length-json.out
Normal file
10
qpdf/qtest/qpdf/bad-encrypt-no-key-length-json.out
Normal 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
|
||||||
|
|
7
qpdf/qtest/qpdf/bad-json-bare-option-false.json
Normal file
7
qpdf/qtest/qpdf/bad-json-bare-option-false.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"output": {
|
||||||
|
"options": {
|
||||||
|
"qdf": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
qpdf/qtest/qpdf/bad-json-choice-mismatch.json
Normal file
7
qpdf/qtest/qpdf/bad-json-choice-mismatch.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"output": {
|
||||||
|
"options": {
|
||||||
|
"objectStreams": "potato"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
qpdf/qtest/qpdf/bad-json-encrypt-duplicate-key-length.json
Normal file
14
qpdf/qtest/qpdf/bad-json-encrypt-duplicate-key-length.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"output": {
|
||||||
|
"options": {
|
||||||
|
"encrypt": {
|
||||||
|
"userPassword": "",
|
||||||
|
"ownerPassword": "someOwnerThing",
|
||||||
|
"256bit": {
|
||||||
|
},
|
||||||
|
"128bit": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
qpdf/qtest/qpdf/bad-json-encrypt-missing-password.json
Normal file
11
qpdf/qtest/qpdf/bad-json-encrypt-missing-password.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"output": {
|
||||||
|
"options": {
|
||||||
|
"encrypt": {
|
||||||
|
"userPassword": "",
|
||||||
|
"256bit": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
qpdf/qtest/qpdf/bad-json-encrypt-no-key-length.json
Normal file
10
qpdf/qtest/qpdf/bad-json-encrypt-no-key-length.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"output": {
|
||||||
|
"options": {
|
||||||
|
"encrypt": {
|
||||||
|
"userPassword": "",
|
||||||
|
"ownerPassword": "someOwnerThing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
qpdf/qtest/qpdf/bad-json-error-json.out
Normal file
10
qpdf/qtest/qpdf/bad-json-error-json.out
Normal 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
|
||||||
|
|
10
qpdf/qtest/qpdf/bad-json-json-error.json
Normal file
10
qpdf/qtest/qpdf/bad-json-json-error.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"output": {
|
||||||
|
"options": {
|
||||||
|
"encrypt": {
|
||||||
|
"userPassword": "",
|
||||||
|
"ownerPassword": "someOwnerThing",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
qpdf/qtest/qpdf/bad-json-pages-no-file.json
Normal file
9
qpdf/qtest/qpdf/bad-json-pages-no-file.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"options": {
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"range": "1-z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
12
qpdf/qtest/qpdf/bad-json-schema-error.json
Normal file
12
qpdf/qtest/qpdf/bad-json-schema-error.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"output": {
|
||||||
|
"potato": {
|
||||||
|
"encrypt": {
|
||||||
|
"userPassword": "",
|
||||||
|
"ownerPassword": "someOwnerThing",
|
||||||
|
"256bit": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
qpdf/qtest/qpdf/bad-pages-no-file-json.out
Normal file
10
qpdf/qtest/qpdf/bad-pages-no-file-json.out
Normal 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
|
||||||
|
|
11
qpdf/qtest/qpdf/bad-schema-error-json.out
Normal file
11
qpdf/qtest/qpdf/bad-schema-error-json.out
Normal 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
|
||||||
|
|
8
qpdf/qtest/qpdf/job-api.out
Normal file
8
qpdf/qtest/qpdf/job-api.out
Normal 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
|
27
qpdf/qtest/qpdf/job-json-add-attachments.json
Normal file
27
qpdf/qtest/qpdf/job-json-add-attachments.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
qpdf/qtest/qpdf/job-json-add-attachments.pdf
Normal file
BIN
qpdf/qtest/qpdf/job-json-add-attachments.pdf
Normal file
Binary file not shown.
12
qpdf/qtest/qpdf/job-json-choice-match.json
Normal file
12
qpdf/qtest/qpdf/job-json-choice-match.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"input": {
|
||||||
|
"file": "minimal.pdf"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"file": "a.pdf",
|
||||||
|
"options": {
|
||||||
|
"deterministicId": true,
|
||||||
|
"objectStreams": "generate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
qpdf/qtest/qpdf/job-json-choice-match.pdf
Normal file
BIN
qpdf/qtest/qpdf/job-json-choice-match.pdf
Normal file
Binary file not shown.
26
qpdf/qtest/qpdf/job-json-copy-attachments.json
Normal file
26
qpdf/qtest/qpdf/job-json-copy-attachments.json
Normal 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-"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
qpdf/qtest/qpdf/job-json-copy-attachments.pdf
Normal file
BIN
qpdf/qtest/qpdf/job-json-copy-attachments.pdf
Normal file
Binary file not shown.
23
qpdf/qtest/qpdf/job-json-empty-input.json
Normal file
23
qpdf/qtest/qpdf/job-json-empty-input.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
BIN
qpdf/qtest/qpdf/job-json-empty-input.pdf
Normal file
BIN
qpdf/qtest/qpdf/job-json-empty-input.pdf
Normal file
Binary file not shown.
19
qpdf/qtest/qpdf/job-json-encrypt-128.json
Normal file
19
qpdf/qtest/qpdf/job-json-encrypt-128.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
qpdf/qtest/qpdf/job-json-encrypt-128.pdf
Normal file
BIN
qpdf/qtest/qpdf/job-json-encrypt-128.pdf
Normal file
Binary file not shown.
20
qpdf/qtest/qpdf/job-json-encrypt-256-with-restrictions.json
Normal file
20
qpdf/qtest/qpdf/job-json-encrypt-256-with-restrictions.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
qpdf/qtest/qpdf/job-json-encrypt-256-with-restrictions.out
Normal file
16
qpdf/qtest/qpdf/job-json-encrypt-256-with-restrictions.out
Normal 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
|
19
qpdf/qtest/qpdf/job-json-encrypt-40.json
Normal file
19
qpdf/qtest/qpdf/job-json-encrypt-40.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
39
qpdf/qtest/qpdf/job-json-encrypt-40.pdf
Normal file
39
qpdf/qtest/qpdf/job-json-encrypt-40.pdf
Normal 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Ù3GAG5â‚|'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
|
14
qpdf/qtest/qpdf/job-json-input-file-password.json
Normal file
14
qpdf/qtest/qpdf/job-json-input-file-password.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"input": {
|
||||||
|
"file": "20-pages.pdf",
|
||||||
|
"password": "user"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"file": "a.pdf",
|
||||||
|
"options": {
|
||||||
|
"staticId": true,
|
||||||
|
"staticAesIv": true,
|
||||||
|
"compressStreams": "n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
qpdf/qtest/qpdf/job-json-input-file-password.pdf
Normal file
BIN
qpdf/qtest/qpdf/job-json-input-file-password.pdf
Normal file
Binary file not shown.
13
qpdf/qtest/qpdf/job-json-misc-options.json
Normal file
13
qpdf/qtest/qpdf/job-json-misc-options.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"input": {
|
||||||
|
"file": "minimal.pdf"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"file": "a.pdf",
|
||||||
|
"options": {
|
||||||
|
"staticId": true,
|
||||||
|
"linearize": true,
|
||||||
|
"compressStreams": "n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
qpdf/qtest/qpdf/job-json-misc-options.pdf
Normal file
BIN
qpdf/qtest/qpdf/job-json-misc-options.pdf
Normal file
Binary file not shown.
16
qpdf/qtest/qpdf/job-json-output.json
Normal file
16
qpdf/qtest/qpdf/job-json-output.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"input": {
|
||||||
|
"file": "minimal.pdf"
|
||||||
|
},
|
||||||
|
"inspect": {
|
||||||
|
"json": "1",
|
||||||
|
"jsonKey": [
|
||||||
|
"pages",
|
||||||
|
"objects"
|
||||||
|
],
|
||||||
|
"jsonObject": [
|
||||||
|
"trailer",
|
||||||
|
"5"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
28
qpdf/qtest/qpdf/job-json-output.out.json
Normal file
28
qpdf/qtest/qpdf/job-json-output.out.json
Normal 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
|
||||||
|
}
|
12
qpdf/qtest/qpdf/job-json-replace-input.json
Normal file
12
qpdf/qtest/qpdf/job-json-replace-input.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"input": {
|
||||||
|
"file": "a.pdf"
|
||||||
|
},
|
||||||
|
"output": {
|
||||||
|
"replaceInput": true,
|
||||||
|
"options": {
|
||||||
|
"staticId": true,
|
||||||
|
"objectStreams": "generate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
qpdf/qtest/qpdf/job-json-replace-input.pdf
Normal file
BIN
qpdf/qtest/qpdf/job-json-replace-input.pdf
Normal file
Binary file not shown.
23
qpdf/qtest/qpdf/job-json-underlay-overlay-password.json
Normal file
23
qpdf/qtest/qpdf/job-json-underlay-overlay-password.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
qpdf/qtest/qpdf/job-json-underlay-overlay-password.pdf
Normal file
BIN
qpdf/qtest/qpdf/job-json-underlay-overlay-password.pdf
Normal file
Binary file not shown.
23
qpdf/qtest/qpdf/job-json-underlay-overlay.json
Normal file
23
qpdf/qtest/qpdf/job-json-underlay-overlay.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
qpdf/qtest/qpdf/job-json-underlay-overlay.pdf
Normal file
BIN
qpdf/qtest/qpdf/job-json-underlay-overlay.pdf
Normal file
Binary file not shown.
3
qpdf/qtest/qpdf/job-partial-json.out
Normal file
3
qpdf/qtest/qpdf/job-partial-json.out
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
calling initializeFromJson
|
||||||
|
usage: an input file name is required
|
||||||
|
test 83 done
|
12
qpdf/qtest/qpdf/job-partial.json
Normal file
12
qpdf/qtest/qpdf/job-partial.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"output": {
|
||||||
|
"options": {
|
||||||
|
"encrypt": {
|
||||||
|
"userPassword": "",
|
||||||
|
"ownerPassword": "",
|
||||||
|
"256bit": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
qpdf/qtest/qpdf/job-show-encryption.json
Normal file
5
qpdf/qtest/qpdf/job-show-encryption.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"inspect": {
|
||||||
|
"showEncryption": true
|
||||||
|
}
|
||||||
|
}
|
101
qpdf/qtest/qpdf/test84.pdf
Normal file
101
qpdf/qtest/qpdf/test84.pdf
Normal 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
|
@ -20,6 +20,8 @@
|
|||||||
#include <qpdf/QPDFWriter.hh>
|
#include <qpdf/QPDFWriter.hh>
|
||||||
#include <qpdf/QPDFSystemError.hh>
|
#include <qpdf/QPDFSystemError.hh>
|
||||||
#include <qpdf/QIntC.hh>
|
#include <qpdf/QIntC.hh>
|
||||||
|
#include <qpdf/QPDFJob.hh>
|
||||||
|
#include <qpdf/QPDFUsage.hh>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@ -3140,6 +3142,81 @@ static void test_82(QPDF& pdf, char const* arg2)
|
|||||||
assert(! str.isOrHasName("/Marvin"));
|
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)
|
void runtest(int n, char const* filename1, char const* arg2)
|
||||||
{
|
{
|
||||||
// Most tests here are crafted to work on specific files. Look at
|
// 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(),
|
pdf.processMemoryFile((std::string(filename1) + ".pdf").c_str(),
|
||||||
p, size);
|
p, size);
|
||||||
}
|
}
|
||||||
else if ((n == 61) || (n == 81))
|
else if ((n == 61) || (n == 81) || (n == 83) || (n == 84))
|
||||||
{
|
{
|
||||||
// Ignore filename argument entirely
|
// 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},
|
{68, test_68}, {69, test_69}, {70, test_70}, {71, test_71},
|
||||||
{72, test_72}, {73, test_73}, {74, test_74}, {75, test_75},
|
{72, test_72}, {73, test_73}, {74, test_74}, {75, test_75},
|
||||||
{76, test_76}, {77, test_77}, {78, test_78}, {79, test_79},
|
{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);
|
auto fn = test_functions.find(n);
|
||||||
|
Loading…
Reference in New Issue
Block a user