From 1d6da60048b3984565b09172cddb9f83949ccb45 Mon Sep 17 00:00:00 2001 From: m-holger Date: Thu, 27 Nov 2025 19:31:48 +0000 Subject: [PATCH] Expose global settings in `QPDFJob` / the CLI --- include/qpdf/QPDFJob.hh | 20 ++++++++++++++++++++ include/qpdf/auto_job_c_global.hh | 12 ++++++++++++ include/qpdf/auto_job_c_limits.hh | 0 include/qpdf/global.hh | 8 ++++---- job.sums | 19 ++++++++++--------- job.yml | 20 ++++++++++++++++++++ libqpdf/QPDFJob_argv.cc | 16 ++++++++++++++++ libqpdf/QPDFJob_config.cc | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------- libqpdf/QPDFJob_json.cc | 14 ++++++++++++++ libqpdf/qpdf/auto_job_decl.hh | 3 +++ libqpdf/qpdf/auto_job_help.hh | 41 +++++++++++++++++++++++++++++++++++++++++ libqpdf/qpdf/auto_job_init.hh | 7 +++++++ libqpdf/qpdf/auto_job_json_decl.hh | 2 ++ libqpdf/qpdf/auto_job_json_init.hh | 18 ++++++++++++++++++ libqpdf/qpdf/auto_job_schema.hh | 7 +++++++ libtests/objects.cc | 42 ++++++++++++++++++++++++++++++++++++++++++ manual/cli.rst | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ manual/qpdf.1 | 45 +++++++++++++++++++++++++++++++++++++++++++++ qpdf/qtest/arg-parsing.test | 9 ++++++++- qpdf/qtest/global.test | 45 +++++++++++++++++++++++++++++++++++++++++++++ qpdf/qtest/qpdf/global.pdf | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ qpdf/qtest/qpdf/global1.out | 7 +++++++ qpdf/qtest/qpdf/global2.out | 8 ++++++++ qpdf/qtest/qpdf/global3.out | 5 +++++ qpdf/qtest/qpdf/global4.out | 9 +++++++++ qpdf/qtest/qpdf/global_damaged.pdf | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 26 files changed, 693 insertions(+), 24 deletions(-) create mode 100644 include/qpdf/auto_job_c_global.hh create mode 100644 include/qpdf/auto_job_c_limits.hh create mode 100644 qpdf/qtest/global.test create mode 100644 qpdf/qtest/qpdf/global.pdf create mode 100644 qpdf/qtest/qpdf/global1.out create mode 100644 qpdf/qtest/qpdf/global2.out create mode 100644 qpdf/qtest/qpdf/global3.out create mode 100644 qpdf/qtest/qpdf/global4.out create mode 100644 qpdf/qtest/qpdf/global_damaged.pdf diff --git a/include/qpdf/QPDFJob.hh b/include/qpdf/QPDFJob.hh index 6039ca6..3ea9b0c 100644 --- a/include/qpdf/QPDFJob.hh +++ b/include/qpdf/QPDFJob.hh @@ -306,6 +306,24 @@ class QPDFJob Config* config; }; + class GlobalConfig + { + friend class QPDFJob; + friend class Config; + + public: + QPDF_DLL + Config* endGlobal(); + +#include + + GlobalConfig(Config*); // for qpdf internal use only + GlobalConfig(GlobalConfig const&) = delete; + + private: + Config* config; + }; + class Config { friend class QPDFJob; @@ -331,6 +349,8 @@ class QPDFJob QPDF_DLL std::shared_ptr addAttachment(); QPDF_DLL + std::shared_ptr global(); + QPDF_DLL std::shared_ptr pages(); QPDF_DLL std::shared_ptr overlay(); diff --git a/include/qpdf/auto_job_c_global.hh b/include/qpdf/auto_job_c_global.hh new file mode 100644 index 0000000..8a2dc6b --- /dev/null +++ b/include/qpdf/auto_job_c_global.hh @@ -0,0 +1,12 @@ +// +// This file is automatically generated by generate_auto_job. +// Edits will be automatically overwritten if the build is +// run in maintainer mode. +// +// clang-format off +// +QPDF_DLL GlobalConfig* noDefaultLimits(); +QPDF_DLL GlobalConfig* parserMaxContainerSize(std::string const& parameter); +QPDF_DLL GlobalConfig* parserMaxContainerSizeDamaged(std::string const& parameter); +QPDF_DLL GlobalConfig* parserMaxErrors(std::string const& parameter); +QPDF_DLL GlobalConfig* parserMaxNesting(std::string const& parameter); diff --git a/include/qpdf/auto_job_c_limits.hh b/include/qpdf/auto_job_c_limits.hh new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/include/qpdf/auto_job_c_limits.hh diff --git a/include/qpdf/global.hh b/include/qpdf/global.hh index 7801905..f18abfd 100644 --- a/include/qpdf/global.hh +++ b/include/qpdf/global.hh @@ -145,7 +145,7 @@ namespace qpdf::global set_uint32(qpdf_p_parser_max_errors, value); } - /// @brief Retrieves the maximum number of objectstop-level allowed in a container while + /// @brief Retrieves the maximum number of top-level objects allowed in a container while /// parsing. /// /// The limit applies when the PDF document's xref table is undamaged and the object itself @@ -160,7 +160,8 @@ namespace qpdf::global return get_uint32(qpdf_p_parser_max_container_size); } - /// @brief Sets the maximum number oftop-level objects allowed in a container while parsing. + /// @brief Sets the maximum number of top-level objects allowed in a container while + /// parsing. /// /// The limit applies when the PDF document's xref table is undamaged and the object itself /// can be parsed without errors. The default limit is 4,294,967,295. @@ -178,8 +179,7 @@ namespace qpdf::global /// parsing objects. /// /// The limit applies when the PDF document's xref table is damaged or the object itself is - /// damaged. The limit also applies when parsing trailer dictionaries and xref streams. The - /// default limit is 5,000. + /// damaged. The limit also applies when parsing xref streams. The default limit is 5,000. /// /// @return The maximum number of top-level objects allowed in a container while parsing /// objects. diff --git a/job.sums b/job.sums index 5502dfb..d2b52d8 100644 --- a/job.sums +++ b/job.sums @@ -4,17 +4,18 @@ generate_auto_job 8e3175a515aa8837d8a01bba0346b04b3d777d70330ba5b7d52f691316054a include/qpdf/auto_job_c_att.hh 4c2b171ea00531db54720bf49a43f8b34481586ae7fb6cbf225099ee42bc5bb4 include/qpdf/auto_job_c_copy_att.hh 50609012bff14fd82f0649185940d617d05d530cdc522185c7f3920a561ccb42 include/qpdf/auto_job_c_enc.hh 28446f3c32153a52afa239ea40503e6cc8ac2c026813526a349e0cd4ae17ddd5 +include/qpdf/auto_job_c_global.hh f1dc365206d033a0d6b19b6e561cc244fbd5b49a8d9604b5b646a5fd92895a5a include/qpdf/auto_job_c_main.hh b865eb827356554763bb8349eadfcbc5cb260f80e025a5e229467c525007356d include/qpdf/auto_job_c_pages.hh 09ca15649cc94fdaf6d9bdae28a20723f2a66616bf15aa86d83df31051d82506 include/qpdf/auto_job_c_uo.hh 9c2f98a355858dd54d0bba444b73177a59c9e56833e02fa6406f429c07f39e62 -job.yml e136726a6c7e43736b1f75f4de347fa50baf5f38ed1da58647ce2e980751fb29 -libqpdf/qpdf/auto_job_decl.hh 34ba07d3891c3e5cdd8712f991e508a0652c9db314c5d5bcdf4421b76e6f6e01 -libqpdf/qpdf/auto_job_help.hh d0cca031e99f10caa3f4b70ea574b36b0af63d24de333e7d6f0bf835e959f0be -libqpdf/qpdf/auto_job_init.hh 02c526c37ad4051cac956ac7c12ae1d020517264f3f3d3beabb066ae2529e4bf -libqpdf/qpdf/auto_job_json_decl.hh 04965f6321e54b8b3b1dd2ca101d763a22ab44fa81c69e4b6fc0fd6bb7f50f92 -libqpdf/qpdf/auto_job_json_init.hh b49378f00d521a9f3e0ce9086e30b082bc6ef8e43c845e2a3c99857b72448307 -libqpdf/qpdf/auto_job_schema.hh f6a3e8b663714bba50b594f5e31437bbcb96ca4609d2c150c3bbc172e3b000fa +job.yml 131922d22086d9f4710743e18229cc1e956268197bcae8e1aae30f3be42877be +libqpdf/qpdf/auto_job_decl.hh d612a02839e4f20a80e1c6a3ba09c17187fccddc3581ec7ebb1e3919ffd6801d +libqpdf/qpdf/auto_job_help.hh 00ac90c621b6c0529d7bad9ea596f57595517901c8d33f49d2812fbea52dfb41 +libqpdf/qpdf/auto_job_init.hh 889dde948e0ab53616584976d9520ab7ab3773c787d241f8a107f5e2f9f2112f +libqpdf/qpdf/auto_job_json_decl.hh 7dbb83ddadcea39bfd1faa4ca061e1e3c3134d693b8ae634b463e7e19dc8bd0a +libqpdf/qpdf/auto_job_json_init.hh 3c5f3d07a85e89dd7ecd79342c18e1f0ad580fc57758abb434aa9c9ae277c01e +libqpdf/qpdf/auto_job_schema.hh eb21c99d3a4dc40b333fd1b19d5de52f8813c74a1d4ca830ea4c3311c120d63e manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580 -manual/cli.rst b7bd5e34495d3f9156ff6242988dba73a2e5dce33d71f75ec1415514a3843f35 -manual/qpdf.1 d5785d23e77b02a77180419d87787002dc244d82d586d56008ab603299f565fd +manual/cli.rst 08e9e7a18d2b0d05102a072f82eabf9ede6bfb1fb797be307ea680eed93ea60f +manual/qpdf.1 19a45f8de6b7c0584fe4395c4ae98b92147a2875e45dbdf729c70e644ccca295 manual/qpdf.1.in 436ecc85d45c4c9e2dbd1725fb7f0177fb627179469f114561adf3cb6cbb677b diff --git a/job.yml b/job.yml index 6cdcaf3..5e560f6 100644 --- a/job.yml +++ b/job.yml @@ -84,12 +84,24 @@ options: - zopfli optional_choices: json-help: json_version + - table: global + prefix: Global + config: c_global + positional: false + bare: + - no-default-limits + required_parameter: + parser-max-container-size: level + parser-max-container-size-damaged: level + parser-max-errors: level + parser-max-nesting: level - table: main config: c_main manual: - add-attachment - copy-attachments-from - encrypt + - global - overlay - pages - underlay @@ -112,6 +124,7 @@ options: - filtered-stream-data - flatten-rotation - generate-appearances + - global - ignore-xref-streams - is-encrypted - json-input @@ -399,6 +412,13 @@ json: - null json-stream-data: json-stream-prefix: + # global options + global: + no-default-limits: + parser-max-container-size: + parser-max-container-size-damaged: + parser-max-errors: + parser-max-nesting: # other options update-from-json: allow-weak-crypto: diff --git a/libqpdf/QPDFJob_argv.cc b/libqpdf/QPDFJob_argv.cc index f49b7b5..52a74ce 100644 --- a/libqpdf/QPDFJob_argv.cc +++ b/libqpdf/QPDFJob_argv.cc @@ -31,6 +31,7 @@ namespace std::shared_ptr c_main; std::shared_ptr c_copy_att; std::shared_ptr c_att; + std::shared_ptr c_global; std::shared_ptr c_pages; std::shared_ptr c_uo; std::shared_ptr c_enc; @@ -418,6 +419,21 @@ ArgParser::argEndSetPageLabels() } void +ArgParser::argGlobal() +{ + accumulated_args.clear(); + c_global = c_main->global(); + ap.selectOptionTable(O_GLOBAL); +} + +void +ArgParser::argEndGlobal() +{ + c_global->endGlobal(); + c_global = nullptr; +} + +void ArgParser::argJobJsonHelp() { *QPDFLogger::defaultLogger()->getInfo() diff --git a/libqpdf/QPDFJob_config.cc b/libqpdf/QPDFJob_config.cc index d318d2d..273b5cb 100644 --- a/libqpdf/QPDFJob_config.cc +++ b/libqpdf/QPDFJob_config.cc @@ -4,31 +4,53 @@ #include #include #include +#include +#include #include #include -static void -int_usage(std::string_view option, std::integral auto min, std::integral auto max) +[[noreturn]] static void +int_usage(std::string_view option, std::integral auto max, std::integral auto min) { + qpdf_expect(min < max); throw QPDFUsage( "invalid "s.append(option) + ": must be a number between " + std::to_string(min) + " and " + std::to_string(max)); } static int -to_int(std::string_view option, std::string const& value, int min, int max) +to_int(std::string_view option, std::string const& value, int max, int min) { - int result = 0; + qpdf_expect(min < max); try { - result = std::stoi(value); + int result = std::stoi(value); if (result < min || result > max) { - int_usage(option, min, max); + int_usage(option, max, min); } + return result; } catch (std::exception&) { - int_usage(option, min, max); + int_usage(option, max, min); + } +} + +static uint32_t +to_uint32( + std::string_view option, + std::string const& value, + uint32_t max = std::numeric_limits::max(), + uint32_t min = 0) +{ + qpdf_expect(min < max); + try { + auto result = std::stoll(value); + if (std::cmp_less(result, min) || std::cmp_greater(result, max)) { + int_usage(option, max, min); + } + return static_cast(result); + } catch (std::exception&) { + int_usage(option, max, min); } - return result; } void @@ -156,14 +178,14 @@ QPDFJob::Config::compressStreams(std::string const& parameter) QPDFJob::Config* QPDFJob::Config::compressionLevel(std::string const& parameter) { - o.m->compression_level = to_int("compression-level", parameter, 1, 9); + o.m->compression_level = to_int("compression-level", parameter, 9, 1); return this; } QPDFJob::Config* QPDFJob::Config::jpegQuality(std::string const& parameter) { - o.m->jpeg_quality = to_int("jpeg-quality", parameter, 0, 100); + o.m->jpeg_quality = to_int("jpeg-quality", parameter, 100, 0); return this; } @@ -1140,6 +1162,60 @@ QPDFJob::Config::encrypt( return std::shared_ptr(new EncConfig(this)); } +QPDFJob::GlobalConfig::GlobalConfig(Config* c) : + config(c) +{ +} + +std::shared_ptr +QPDFJob::Config::global() +{ + return std::make_shared(this); +} + +QPDFJob::Config* +QPDFJob::GlobalConfig::endGlobal() +{ + return config; +} + +QPDFJob::GlobalConfig* +QPDFJob::GlobalConfig::noDefaultLimits() +{ + global::Options::default_limits(false); + return this; +} + +QPDFJob::GlobalConfig* +QPDFJob::GlobalConfig::parserMaxContainerSize(const std::string& parameter) +{ + global::Limits::parser_max_container_size( + false, to_uint32("parser-max-container-size", parameter, 4'294'967'295)); + return this; +} + +QPDFJob::GlobalConfig* +QPDFJob::GlobalConfig::parserMaxContainerSizeDamaged(const std::string& parameter) +{ + global::Limits::parser_max_container_size( + true, to_uint32("parser-max-container-size-damaged", parameter, 4'294'967'295)); + return this; +} + +QPDFJob::GlobalConfig* +QPDFJob::GlobalConfig::parserMaxErrors(const std::string& parameter) +{ + global::Limits::parser_max_errors(to_uint32("parser-max-errors", parameter)); + return this; +} + +QPDFJob::GlobalConfig* +QPDFJob::GlobalConfig::parserMaxNesting(const std::string& parameter) +{ + global::Limits::parser_max_nesting(to_uint32("parser-max-nesting", parameter)); + return this; +} + QPDFJob::Config* QPDFJob::Config::setPageLabels(const std::vector& specs) { diff --git a/libqpdf/QPDFJob_json.cc b/libqpdf/QPDFJob_json.cc index 21d5e17..8332ee0 100644 --- a/libqpdf/QPDFJob_json.cc +++ b/libqpdf/QPDFJob_json.cc @@ -63,6 +63,7 @@ namespace std::shared_ptr c_main; std::shared_ptr c_copy_att; std::shared_ptr c_att; + std::shared_ptr c_global; std::shared_ptr c_pages; std::shared_ptr c_uo; std::shared_ptr c_enc; @@ -620,6 +621,19 @@ Handlers::beginSetPageLabelsArray(JSON) } void +Handlers::beginGlobal(JSON) +{ + this->c_global = c_main->global(); +} + +void +Handlers::endGlobal() +{ + c_global->endGlobal(); + c_global = nullptr; +} + +void QPDFJob::initializeFromJson(std::string const& json, bool partial) { std::list errors; diff --git a/libqpdf/qpdf/auto_job_decl.hh b/libqpdf/qpdf/auto_job_decl.hh index 6016509..b4b693c 100644 --- a/libqpdf/qpdf/auto_job_decl.hh +++ b/libqpdf/qpdf/auto_job_decl.hh @@ -5,6 +5,7 @@ // // clang-format off // +static constexpr char const* O_GLOBAL = "global"; static constexpr char const* O_PAGES = "pages"; static constexpr char const* O_ENCRYPTION = "encryption"; static constexpr char const* O_40_BIT_ENCRYPTION = "40-bit encryption"; @@ -21,11 +22,13 @@ void argShowCrypto(); void argJobJsonHelp(); void argZopfli(); void argJsonHelp(std::string const&); +void argEndGlobal(); void argPositional(std::string const&); void argAddAttachment(); void argCopyAttachmentsFrom(); void argEmpty(); void argEncrypt(); +void argGlobal(); void argOverlay(); void argPages(); void argReplaceInput(); diff --git a/libqpdf/qpdf/auto_job_help.hh b/libqpdf/qpdf/auto_job_help.hh index 1379f88..84c8e45 100644 --- a/libqpdf/qpdf/auto_job_help.hh +++ b/libqpdf/qpdf/auto_job_help.hh @@ -1004,6 +1004,46 @@ Update a PDF file from a JSON file. Please see the "qpdf JSON" chapter of the manual for information about how to use this option. )"); +ap.addHelpTopic("global", "options for changing the behaviour of qpdf", R"(The options below modify the overall behaviour of qpdf. This includes modifying +implementation limits and changing modes of operation. +)"); +ap.addOptionHelp("--global", "global", "begin setting global options and limits", R"(--global [options] -- + +Begin setting global options and limits. +)"); +ap.addOptionHelp("--no-default-limits", "global", "disable optional default limits", R"(Disables all optional default limits. Explicitly set limits are unaffected. Some +limits, especially limits designed to prevent stack overflow, cannot be removed +with this option but can be modified. Where this is the case it is mentioned +in the entry for the relevant option. +)"); +ap.addOptionHelp("--parser-max-nesting", "global", "set the maximum nesting level while parsing objects", R"(--parser-max-nesting=n + +Set the maximum nesting level while parsing objects. The maximum nesting level +is not disabled by --no-default-limits. Defaults to 499. +)"); +ap.addOptionHelp("--parser-max-errors", "global", "set the maximum number of errors while parsing", R"(--parser-max-errors=n + +Set the maximum number of errors allowed while parsing an indirect object. +A value of 0 means that no maximum is imposed. Defaults to 15. +)"); +ap.addOptionHelp("--parser-max-container-size", "global", "set the maximum container size while parsing", R"(--parser-max-container-size=n + +Set the maximum number of top-level objects allowed in a container while +parsing. The limit applies when the PDF document's xref table is undamaged +and the object itself can be parsed without errors. The default limit +is 4,294,967,295. See also --parser-max-container-size-damaged. +)"); +ap.addOptionHelp("--parser-max-container-size-damaged", "global", "set the maximum container size while parsing damaged files", R"(--parser-max-container-size-damaged=n + +Set the maximum number of top-level objects allowed in a container while +parsing. The limit applies when the PDF document's xref table is damaged +or the object itself is damaged. The limit also applies when parsing +xref streams. The default limit is 5,000. +See also --parser-max-container-size. +)"); +} +static void add_help_9(QPDFArgParser& ap) +{ ap.addHelpTopic("testing", "options for testing or debugging", R"(The options below are useful when writing automated test code that includes files created by qpdf or when testing qpdf itself. )"); @@ -1039,6 +1079,7 @@ static void add_help(QPDFArgParser& ap) add_help_6(ap); add_help_7(ap); add_help_8(ap); + add_help_9(ap); ap.addHelpFooter("For detailed help, visit the qpdf manual: https://qpdf.readthedocs.io\n"); } diff --git a/libqpdf/qpdf/auto_job_init.hh b/libqpdf/qpdf/auto_job_init.hh index 7b6c99f..3f38cfb 100644 --- a/libqpdf/qpdf/auto_job_init.hh +++ b/libqpdf/qpdf/auto_job_init.hh @@ -34,6 +34,12 @@ this->ap.addBare("show-crypto", b(&ArgParser::argShowCrypto)); this->ap.addBare("job-json-help", b(&ArgParser::argJobJsonHelp)); this->ap.addBare("zopfli", b(&ArgParser::argZopfli)); this->ap.addChoices("json-help", p(&ArgParser::argJsonHelp), false, json_version_choices); +this->ap.registerOptionTable("global", b(&ArgParser::argEndGlobal)); +this->ap.addBare("no-default-limits", [this](){c_global->noDefaultLimits();}); +this->ap.addRequiredParameter("parser-max-container-size", [this](std::string const& x){c_global->parserMaxContainerSize(x);}, "level"); +this->ap.addRequiredParameter("parser-max-container-size-damaged", [this](std::string const& x){c_global->parserMaxContainerSizeDamaged(x);}, "level"); +this->ap.addRequiredParameter("parser-max-errors", [this](std::string const& x){c_global->parserMaxErrors(x);}, "level"); +this->ap.addRequiredParameter("parser-max-nesting", [this](std::string const& x){c_global->parserMaxNesting(x);}, "level"); this->ap.selectMainOptionTable(); this->ap.addPositional(p(&ArgParser::argPositional)); this->ap.addBare("add-attachment", b(&ArgParser::argAddAttachment)); @@ -50,6 +56,7 @@ this->ap.addBare("externalize-inline-images", [this](){c_main->externalizeInline this->ap.addBare("filtered-stream-data", [this](){c_main->filteredStreamData();}); this->ap.addBare("flatten-rotation", [this](){c_main->flattenRotation();}); this->ap.addBare("generate-appearances", [this](){c_main->generateAppearances();}); +this->ap.addBare("global", b(&ArgParser::argGlobal)); this->ap.addBare("ignore-xref-streams", [this](){c_main->ignoreXrefStreams();}); this->ap.addBare("is-encrypted", [this](){c_main->isEncrypted();}); this->ap.addBare("json-input", [this](){c_main->jsonInput();}); diff --git a/libqpdf/qpdf/auto_job_json_decl.hh b/libqpdf/qpdf/auto_job_json_decl.hh index 58d55e1..92e7698 100644 --- a/libqpdf/qpdf/auto_job_json_decl.hh +++ b/libqpdf/qpdf/auto_job_json_decl.hh @@ -24,6 +24,8 @@ void beginJsonKeyArray(JSON); void endJsonKeyArray(); void beginJsonObjectArray(JSON); void endJsonObjectArray(); +void beginGlobal(JSON); +void endGlobal(); void beginAddAttachmentArray(JSON); void endAddAttachmentArray(); void beginAddAttachment(JSON); diff --git a/libqpdf/qpdf/auto_job_json_init.hh b/libqpdf/qpdf/auto_job_json_init.hh index 8612ca9..270d4b5 100644 --- a/libqpdf/qpdf/auto_job_json_init.hh +++ b/libqpdf/qpdf/auto_job_json_init.hh @@ -272,6 +272,24 @@ popHandler(); // key: jsonStreamData pushKey("jsonStreamPrefix"); addParameter([this](std::string const& p) { c_main->jsonStreamPrefix(p); }); popHandler(); // key: jsonStreamPrefix +pushKey("global"); +beginDict(bindJSON(&Handlers::beginGlobal), bindBare(&Handlers::endGlobal)); // .global +pushKey("noDefaultLimits"); +addBare([this]() { c_global->noDefaultLimits(); }); +popHandler(); // key: noDefaultLimits +pushKey("parserMaxContainerSize"); +addParameter([this](std::string const& p) { c_global->parserMaxContainerSize(p); }); +popHandler(); // key: parserMaxContainerSize +pushKey("parserMaxContainerSizeDamaged"); +addParameter([this](std::string const& p) { c_global->parserMaxContainerSizeDamaged(p); }); +popHandler(); // key: parserMaxContainerSizeDamaged +pushKey("parserMaxErrors"); +addParameter([this](std::string const& p) { c_global->parserMaxErrors(p); }); +popHandler(); // key: parserMaxErrors +pushKey("parserMaxNesting"); +addParameter([this](std::string const& p) { c_global->parserMaxNesting(p); }); +popHandler(); // key: parserMaxNesting +popHandler(); // key: global pushKey("updateFromJson"); addParameter([this](std::string const& p) { c_main->updateFromJson(p); }); popHandler(); // key: updateFromJson diff --git a/libqpdf/qpdf/auto_job_schema.hh b/libqpdf/qpdf/auto_job_schema.hh index 7acabc4..839bcfd 100644 --- a/libqpdf/qpdf/auto_job_schema.hh +++ b/libqpdf/qpdf/auto_job_schema.hh @@ -90,6 +90,13 @@ static constexpr char const* JOB_SCHEMA_DATA = R"({ ], "jsonStreamData": "how to handle streams in json output", "jsonStreamPrefix": "prefix for json stream data files", + "global": { + "noDefaultLimits": "disable optional default limits", + "parserMaxContainerSize": "set the maximum container size while parsing", + "parserMaxContainerSizeDamaged": "set the maximum container size while parsing damaged files", + "parserMaxErrors": "set the maximum number of errors while parsing", + "parserMaxNesting": "set the maximum nesting level while parsing objects" + }, "updateFromJson": "update a PDF from qpdf JSON", "allowWeakCrypto": "allow insecure cryptographic algorithms", "keepFilesOpen": "manage keeping multiple files open", diff --git a/libtests/objects.cc b/libtests/objects.cc index 4335602..c9020b7 100644 --- a/libtests/objects.cc +++ b/libtests/objects.cc @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -237,6 +238,47 @@ test_2(QPDF& pdf, char const* arg2) } catch (std::exception&) { } assert(qpdf::global::limit_errors() == 2); + + // Test global settings using the QPDFJob interface + QPDFJob j; + j.config() + ->inputFile("minimal.pdf") + ->global() + ->parserMaxNesting("111") + ->parserMaxErrors("112") + ->parserMaxContainerSize("113") + ->parserMaxContainerSizeDamaged("114") + ->noDefaultLimits() + ->endGlobal() + ->outputFile("a.pdf"); + auto qpdf = j.createQPDF(); + assert(parser_max_nesting() == 111); + assert(parser_max_errors() == 112); + assert(parser_max_container_size() == 113); + assert(parser_max_container_size_damaged() == 114); + assert(!default_limits()); + + // Test global settings using the JobJSON + QPDFJob jj; + jj.initializeFromJson(R"( + { + "inputFile": "minimal.pdf", + "global": { + "parserMaxNesting": "211", + "parserMaxErrors": "212", + "parserMaxContainerSize": "213", + "parserMaxContainerSizeDamaged": "214", + "noDefaultLimits": "" + }, + "outputFile": "a.pdf" + } + )"); + qpdf = j.createQPDF(); + assert(parser_max_nesting() == 211); + assert(parser_max_errors() == 212); + assert(parser_max_container_size() == 213); + assert(parser_max_container_size_damaged() == 214); + assert(!default_limits()); } void diff --git a/manual/cli.rst b/manual/cli.rst index ecc31e4..e68b348 100644 --- a/manual/cli.rst +++ b/manual/cli.rst @@ -3762,6 +3762,110 @@ Related Options For a information about how to use this option, please see :ref:`json`. +.. _global-options: + +Global Options +-------------- + +.. help-topic global: options for changing the behaviour of qpdf + + The options below modify the overall behaviour of qpdf. This includes modifying + implementation limits and changing modes of operation. + +The options below modify the overall behaviour of qpdf. This includes modifying implementation +limits and changing modes of operation. + +Related Options +~~~~~~~~~~~~~~~ + +.. qpdf:option:: --global [options] -- + + .. help: begin setting global options and limits + + Begin setting global options and limits. + +Begin setting global options and limits. + + +Global Limits +~~~~~~~~~~~~~ + +qpdf uses a number of global limits to protect itself from damaged and specially constructed PDF +files. Without these limits such files can cause qpdf to crash and/or to consume excessive +processor and memory resources. Very few legitimate PDF files exceed these limits, however +where necessary the limits can be modified or entirely removed by the following options. + +.. qpdf:option:: --no-default-limits + + .. help: disable optional default limits + + Disables all optional default limits. Explicitly set limits are unaffected. Some + limits, especially limits designed to prevent stack overflow, cannot be removed + with this option but can be modified. Where this is the case it is mentioned + in the entry for the relevant option. + +Disables all optional default limits. Explicitly set limits are unaffected. Some limits, +especially limits designed to prevent stack overflow, cannot be removed with this option +but can be modified. Where this is the case it is mentioned in the entry for the relevant +option. + +Parser Limits +............. + +.. qpdf:option:: --parser-max-nesting=n + + .. help: set the maximum nesting level while parsing objects + + Set the maximum nesting level while parsing objects. The maximum nesting level + is not disabled by --no-default-limits. Defaults to 499. + +Set the maximum nesting level while parsing objects. The maximum nesting level is not +disabled by :qpdf:ref:`--no-default-limits`. Defaults to 499. + + +.. qpdf:option:: --parser-max-errors=n + + .. help: set the maximum number of errors while parsing + + Set the maximum number of errors allowed while parsing an indirect object. + A value of 0 means that no maximum is imposed. Defaults to 15. + +Set the maximum number of errors allowed while parsing an indirect object. +A value of 0 means that no maximum is imposed. Defaults to 15. + +.. qpdf:option:: --parser-max-container-size=n + + .. help: set the maximum container size while parsing + + Set the maximum number of top-level objects allowed in a container while + parsing. The limit applies when the PDF document's xref table is undamaged + and the object itself can be parsed without errors. The default limit + is 4,294,967,295. See also --parser-max-container-size-damaged. + + +Set the maximum number of top-level objects allowed in a container while +parsing. The limit applies when the PDF document's xref table is undamaged +and the object itself can be parsed without errors. The default limit +is 4,294,967,295. See also :qpdf:ref:`--parser-max-container-size-damaged`. + + +.. qpdf:option:: --parser-max-container-size-damaged=n + + .. help: set the maximum container size while parsing damaged files + + Set the maximum number of top-level objects allowed in a container while + parsing. The limit applies when the PDF document's xref table is damaged + or the object itself is damaged. The limit also applies when parsing + xref streams. The default limit is 5,000. + See also --parser-max-container-size. + + +Set the maximum number of top-level objects allowed in a container while +parsing. The limit applies when the PDF document's xref table is damaged +or the object itself is damaged. The limit also applies when parsing +xref streams. The default limit is 5,000. +See also :qpdf:ref:`--parser-max-container-size`. + .. _test-options: Options for Testing or Debugging diff --git a/manual/qpdf.1 b/manual/qpdf.1 index cc7e90a..0bfb1d0 100644 --- a/manual/qpdf.1 +++ b/manual/qpdf.1 @@ -1191,6 +1191,51 @@ how to use this option. Update a PDF file from a JSON file. Please see the "qpdf JSON" chapter of the manual for information about how to use this option. +.SH GLOBAL (options for changing the behaviour of qpdf) +The options below modify the overall behaviour of qpdf. This includes modifying +implementation limits and changing modes of operation. +.PP +Related Options: +.TP +.B --global \-\- begin setting global options and limits +--global [options] -- + +Begin setting global options and limits. +.TP +.B --no-default-limits \-\- disable optional default limits +Disables all optional default limits. Explicitly set limits are unaffected. Some +limits, especially limits designed to prevent stack overflow, cannot be removed +with this option but can be modified. Where this is the case it is mentioned +in the entry for the relevant option. +.TP +.B --parser-max-nesting \-\- set the maximum nesting level while parsing objects +--parser-max-nesting=n + +Set the maximum nesting level while parsing objects. The maximum nesting level +is not disabled by --no-default-limits. Defaults to 499. +.TP +.B --parser-max-errors \-\- set the maximum number of errors while parsing +--parser-max-errors=n + +Set the maximum number of errors allowed while parsing an indirect object. +A value of 0 means that no maximum is imposed. Defaults to 15. +.TP +.B --parser-max-container-size \-\- set the maximum container size while parsing +--parser-max-container-size=n + +Set the maximum number of top-level objects allowed in a container while +parsing. The limit applies when the PDF document's xref table is undamaged +and the object itself can be parsed without errors. The default limit +is 4,294,967,295. See also --parser-max-container-size-damaged. +.TP +.B --parser-max-container-size-damaged \-\- set the maximum container size while parsing damaged files +--parser-max-container-size-damaged=n + +Set the maximum number of top-level objects allowed in a container while +parsing. The limit applies when the PDF document's xref table is damaged +or the object itself is damaged. The limit also applies when parsing +xref streams. The default limit is 5,000. +See also --parser-max-container-size. .SH TESTING (options for testing or debugging) The options below are useful when writing automated test code that includes files created by qpdf or when testing qpdf itself. diff --git a/qpdf/qtest/arg-parsing.test b/qpdf/qtest/arg-parsing.test index 4da865e..a5eefd3 100644 --- a/qpdf/qtest/arg-parsing.test +++ b/qpdf/qtest/arg-parsing.test @@ -15,7 +15,7 @@ cleanup(); my $td = new TestDriver('arg-parsing'); -my $n_tests = 32; +my $n_tests = 33; $td->runtest("required argument", {$td->COMMAND => "qpdf --password minimal.pdf"}, @@ -187,5 +187,12 @@ $td->runtest("bad jpeg-quality", $td->EXIT_STATUS => 2}, $td->NORMALIZE_NEWLINES); +$td->runtest("bad objects-container-max-container-size", + {$td->COMMAND => "qpdf --global --parser-max-container-size=-1 -- minimal.pdf"}, + {$td->REGEXP => + "invalid parser-max-container-size: must be a number between 0 and 4294967295", + $td->EXIT_STATUS => 2}, + $td->NORMALIZE_NEWLINES); + cleanup(); $td->report($n_tests); diff --git a/qpdf/qtest/global.test b/qpdf/qtest/global.test new file mode 100644 index 0000000..32e0200 --- /dev/null +++ b/qpdf/qtest/global.test @@ -0,0 +1,45 @@ +#!/usr/bin/env perl +require 5.008; +use warnings; +use strict; +use File::Copy; + +unshift(@INC, '.'); +require qpdf_test_helpers; + +chdir("qpdf") or die "chdir testdir failed: $!\n"; + +require TestDriver; + +cleanup(); + +my $td = new TestDriver('global'); + +my $n_tests = 4; + +$td->runtest("parser-max-nesting", + {$td->COMMAND => "qpdf --global --parser-max-nesting=4 -- global.pdf a.pdf"}, + {$td->FILE => "global1.out", + $td->EXIT_STATUS => 3}, + $td->NORMALIZE_NEWLINES); + +$td->runtest("parser-max-errors", + {$td->COMMAND => "qpdf --global --parser-max-errors=2 -- global_damaged.pdf a.pdf"}, + {$td->FILE => "global2.out", + $td->EXIT_STATUS => 3}, + $td->NORMALIZE_NEWLINES); + +$td->runtest("parser-max-container-size", + {$td->COMMAND => "qpdf --global --parser-max-container-size=3 -- global.pdf a.pdf"}, + {$td->FILE => "global3.out", + $td->EXIT_STATUS => 3}, + $td->NORMALIZE_NEWLINES); + +$td->runtest("parser-max-container-size-damaged", + {$td->COMMAND => "qpdf --global --parser-max-container-size-damaged=9 -- global_damaged.pdf a.pdf"}, + {$td->FILE => "global4.out", + $td->EXIT_STATUS => 3}, + $td->NORMALIZE_NEWLINES); + +cleanup(); +$td->report($n_tests); diff --git a/qpdf/qtest/qpdf/global.pdf b/qpdf/qtest/qpdf/global.pdf new file mode 100644 index 0000000..76a6750 --- /dev/null +++ b/qpdf/qtest/qpdf/global.pdf @@ -0,0 +1,80 @@ +%PDF-1.3 +1 0 obj +<< + /Type /Catalog + /Pages 2 0 R +>> +endobj + +2 0 obj +<< + /Type /Pages + /Kids [ + 3 0 R + ] + /Count 1 +>> +endobj + +3 0 obj +<< + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 612 792] + /Contents 4 0 R + /Resources << + /ProcSet 5 0 R + /Font << + /F1 6 0 R + >> + >> +>> +endobj + +4 0 obj +<< + /Length 44 +>> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj + +5 0 obj +[ + /PDF + /Text +] +endobj + +6 0 obj +<< + /Type /Font + /Subtype /Type1 + /Name /F1 + /BaseFont /Helvetica + /Encoding /WinAnsiEncoding +>> +endobj + +xref +0 7 +0000000000 65535 f +0000000009 00000 n +0000000063 00000 n +0000000135 00000 n +0000000307 00000 n +0000000403 00000 n +0000000438 00000 n +trailer << + /Size 7 + /Root 1 0 R + /Nesting [ [ [ [ [ /1 /2 /3 /4 /5 /6 /7 /8 /9 /10 ] ] ] ] ] +>> +startxref +556 +%%EOF diff --git a/qpdf/qtest/qpdf/global1.out b/qpdf/qtest/qpdf/global1.out new file mode 100644 index 0000000..88f05cb --- /dev/null +++ b/qpdf/qtest/qpdf/global1.out @@ -0,0 +1,7 @@ +WARNING: global.pdf (trailer, offset 759): limits error(parser-max-nesting): ignoring excessively deeply nested data structure +WARNING: global.pdf: file is damaged +WARNING: global.pdf (offset 712): expected trailer dictionary +WARNING: global.pdf: Attempting to reconstruct cross-reference table +WARNING: global.pdf (trailer, offset 759): limits error(parser-max-nesting): ignoring excessively deeply nested data structure +WARNING: global.pdf: unable to find trailer dictionary while recovering damaged file +qpdf: operation succeeded with warnings; resulting file may have some problems diff --git a/qpdf/qtest/qpdf/global2.out b/qpdf/qtest/qpdf/global2.out new file mode 100644 index 0000000..6ba4e86 --- /dev/null +++ b/qpdf/qtest/qpdf/global2.out @@ -0,0 +1,8 @@ +WARNING: global_damaged.pdf: file is damaged +WARNING: global_damaged.pdf (offset 55): xref not found +WARNING: global_damaged.pdf: Attempting to reconstruct cross-reference table +WARNING: global_damaged.pdf (trailer, offset 764): unknown token while reading object; treating as null +WARNING: global_damaged.pdf (trailer, offset 767): unknown token while reading object; treating as null +WARNING: global_damaged.pdf (trailer, offset 767): limits error(parser-max-errors): too many errors during parsing; treating object as null +WARNING: global_damaged.pdf: unable to find trailer dictionary while recovering damaged file +qpdf: operation succeeded with warnings; resulting file may have some problems diff --git a/qpdf/qtest/qpdf/global3.out b/qpdf/qtest/qpdf/global3.out new file mode 100644 index 0000000..703a2d8 --- /dev/null +++ b/qpdf/qtest/qpdf/global3.out @@ -0,0 +1,5 @@ +WARNING: global.pdf (trailer, offset 770): limits error(parser-max-container-size): encountered an array or dictionary with more than 3 elements during xref recovery; giving up on reading object +WARNING: global.pdf: file is damaged +WARNING: global.pdf (offset 712): expected trailer dictionary +WARNING: global.pdf: Attempting to reconstruct cross-reference table +qpdf: operation succeeded with warnings; resulting file may have some problems diff --git a/qpdf/qtest/qpdf/global4.out b/qpdf/qtest/qpdf/global4.out new file mode 100644 index 0000000..7d2d16d --- /dev/null +++ b/qpdf/qtest/qpdf/global4.out @@ -0,0 +1,9 @@ +WARNING: global_damaged.pdf: file is damaged +WARNING: global_damaged.pdf (offset 55): xref not found +WARNING: global_damaged.pdf: Attempting to reconstruct cross-reference table +WARNING: global_damaged.pdf (trailer, offset 764): unknown token while reading object; treating as null +WARNING: global_damaged.pdf (trailer, offset 767): unknown token while reading object; treating as null +WARNING: global_damaged.pdf (trailer, offset 770): unknown token while reading object; treating as null +WARNING: global_damaged.pdf (trailer, offset 788): limits error(parser-max-container-size-damaged): encountered errors while parsing an array or dictionary with more than 9 elements; giving up on reading object +WARNING: global_damaged.pdf: unable to find trailer dictionary while recovering damaged file +qpdf: operation succeeded with warnings; resulting file may have some problems diff --git a/qpdf/qtest/qpdf/global_damaged.pdf b/qpdf/qtest/qpdf/global_damaged.pdf new file mode 100644 index 0000000..ceda53d --- /dev/null +++ b/qpdf/qtest/qpdf/global_damaged.pdf @@ -0,0 +1,80 @@ +%PDF-1.3 +1 0 obj +<< + /Type /Catalog + /Pages 2 0 R +>> +endobj + +2 0 obj +<< + /Type /Pages + /Kids [ + 3 0 R + ] + /Count 1 +>> +endobj + +3 0 obj +<< + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 612 792] + /Contents 4 0 R + /Resources << + /ProcSet 5 0 R + /Font << + /F1 6 0 R + >> + >> +>> +endobj + +4 0 obj +<< + /Length 44 +>> +stream +BT + /F1 24 Tf + 72 720 Td + (Potato) Tj +ET +endstream +endobj + +5 0 obj +[ + /PDF + /Text +] +endobj + +6 0 obj +<< + /Type /Font + /Subtype /Type1 + /Name /F1 + /BaseFont /Helvetica + /Encoding /WinAnsiEncoding +>> +endobj + +xref +0 7 +0000000000 65535 f +0000000009 00000 n +0000000063 00000 n +0000000135 00000 n +0000000307 00000 n +0000000403 00000 n +0000000438 00000 n +trailer << + /Size 7 + /Root 1 0 R + /Nesting [ [ [ [ [ /1 !2 !3 !4 /5 /6 /7 /8 /9 /10 ] ] ] ] ] +>> +startxref +55 +%%EOF -- libgit2 0.21.4