diff --git a/include/qpdf/Constants.h b/include/qpdf/Constants.h index fe19448..8c0d08c 100644 --- a/include/qpdf/Constants.h +++ b/include/qpdf/Constants.h @@ -244,4 +244,50 @@ enum qpdf_page_label_e { pl_roman_upper, }; +/** + * @enum qpdf_result_e + * @brief Enum representing result codes for qpdf C-API functions. + * + * Results <= qpdf_r_no_warn indicate success without warnings, + * qpdf_r_no_warn < result <= qpdf_r_success indicates success with warnings, and + * qpdf_r_success < result indicates failure. + */ +enum qpdf_result_e { + /* success */ + qpdf_r_ok = 0, + qpdf_r_no_warn = 0xff, /// any result <= qpdf_no_warn indicates success without warning + qpdf_r_success = 0xffff, /// any result <= qpdf_r_success indicates success + /* failure */ + qpdf_r_bad_parameter = 0x10000, + + qpdf_r_no_warn_mask = 0x7fffff00, + qpdf_r_success_mask = 0x7fff0000, +}; + +/** + * @enum qpdf_param_e + * @brief This enumeration defines various parameters and configuration options for qpdf C-API + * functions. + * + * The enum values are grouped into sections based on their functionality, such as global + * options or global limits. For the meaning of individual parameters see `qpdf/global.cc` + */ +enum qpdf_param_e { + /* global state */ + qpdf_p_limit_errors = 0x10020, + + /* global options */ + qpdf_p_default_limits = 0x11100, + /* global limits */ + + /* object - parser limits */ + qpdf_p_parser_max_nesting = 0x13000, + qpdf_p_parser_max_errors, + qpdf_p_parser_max_container_size, + qpdf_p_parser_max_container_size_damaged, + + /* next section = 0x20000 */ + qpdf_enum_max = 0x7fffffff, +}; + #endif /* QPDFCONSTANTS_H */ 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/QUtil.hh b/include/qpdf/QUtil.hh index 8202ce6..dd5acae 100644 --- a/include/qpdf/QUtil.hh +++ b/include/qpdf/QUtil.hh @@ -20,8 +20,10 @@ #ifndef QUTIL_HH #define QUTIL_HH +#include #include #include + #include #include #include @@ -441,6 +443,25 @@ namespace QUtil QPDF_DLL bool is_number(char const*); + /// @brief Handles the result code from qpdf functions. + /// + /// **For qpdf internal use only - not part of the public API** + /// @par + /// Depending on the result code, either continues execution or throws an + /// exception in case of an invalid parameter. + /// + /// @param result The result code of type qpdf_result_e, indicating success or failure status. + /// @param context A string describing the context where this function is invoked, used for + /// error reporting if an exception is thrown. + /// + /// @throws std::logic_error If the result code is `qpdf_bad_parameter`, indicating an invalid + /// parameter was supplied to a function. The exception message will + /// include the provided context for easier debugging. + /// + /// @since 12.3 + QPDF_DLL + void handle_result_code(qpdf_result_e result, std::string_view context); + // This method parses the numeric range syntax used by the qpdf command-line tool. May throw // std::runtime_error. A numeric range is as comma-separated list of groups. A group may be a // number specification or a range of number specifications separated by a dash. A number 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 new file mode 100644 index 0000000..0f71843 --- /dev/null +++ b/include/qpdf/global.hh @@ -0,0 +1,212 @@ +// Copyright (c) 2005-2021 Jay Berkenbilt +// Copyright (c) 2022-2025 Jay Berkenbilt and Manfred Holger +// +// This file is part of qpdf. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License +// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +// or implied. See the License for the specific language governing permissions and limitations under +// the License. +// +// Versions of qpdf prior to version 7 were released under the terms of version 2.0 of the Artistic +// License. At your option, you may continue to consider qpdf to be licensed under those terms. +// Please see the manual for additional information. + +#ifndef GLOBAL_HH +#define GLOBAL_HH + +#include + +#include +#include + +#include + +namespace qpdf::global +{ + /// Helper function to translate result codes into C++ exceptions - for qpdf internal use only. + inline void + handle_result(qpdf_result_e result) + { + if (result != qpdf_r_ok) { + QUtil::handle_result_code(result, "qpdf::global"); + } + } + + /// Helper function to wrap calls to qpdf_global_get_uint32 - for qpdf internal use only. + inline uint32_t + get_uint32(qpdf_param_e param) + { + uint32_t value; + handle_result(qpdf_global_get_uint32(param, &value)); + return value; + } + + /// Helper function to wrap calls to qpdf_global_set_uint32 - for qpdf internal use only. + inline void + set_uint32(qpdf_param_e param, uint32_t value) + { + handle_result(qpdf_global_set_uint32(param, value)); + } + + /// @brief Retrieves the number of limit errors. + /// + /// Returns the number a global limit was exceeded. This item is read only. + /// + /// @return The number of limit errors. + /// + /// @since 12.3 + uint32_t inline limit_errors() + { + return get_uint32(qpdf_p_limit_errors); + } + + namespace options + { + /// @brief Retrieves whether default limits are enabled. + /// + /// @return True if default limits are enabled. + /// + /// @since 12.3 + bool inline default_limits() + { + return get_uint32(qpdf_p_default_limits) != 0; + } + + /// @brief Disable all optional default limits if `false` is passed. + /// + /// This function disables all optional default limits if `false` is passed. Once default + /// values have been disabled they cannot be re-enabled. Passing `true` has no effect. This + /// function will leave any limits that have been explicitly set unchanged. Some limits, + /// such as limits imposed to avoid stack overflows, cannot be disabled but can be changed. + /// + /// @param value A boolean indicating whether to disable (false) the default limits. + /// + /// @since 12.3 + void inline default_limits(bool value) + { + set_uint32(qpdf_p_default_limits, value ? QPDF_TRUE : QPDF_FALSE); + } + + } // namespace options + + namespace limits + { + /// @brief Retrieves the maximum nesting level while parsing objects. + /// + /// @return The maximum nesting level while parsing objects. + /// + /// @note The maximum nesting level cannot be disabled by calling `default_limit(false)`. + /// + /// @since 12.3 + uint32_t inline parser_max_nesting() + { + return get_uint32(qpdf_p_parser_max_nesting); + } + + /// @brief Sets the maximum nesting level while parsing objects. + /// + /// @param value The maximum nesting level to set. + /// + /// @note The maximum nesting level cannot be disabled by calling `default_limit(false)`. + /// + /// @since 12.3 + void inline parser_max_nesting(uint32_t value) + { + set_uint32(qpdf_p_parser_max_nesting, value); + } + + /// @brief Retrieves the maximum number of errors allowed while parsing objects. + /// + /// A value of 0 means that there is no maximum imposed. + /// + /// @return The maximum number of errors allowed while parsing objects. + /// + /// @since 12.3 + uint32_t inline parser_max_errors() + { + return get_uint32(qpdf_p_parser_max_errors); + } + + /// Sets the maximum number of errors allowed while parsing objects. + /// + /// A value of 0 means that there is no maximum imposed. + /// + /// @param value The maximum number of errors allowed while parsing objects to set. + /// + /// @since 12.3 + void inline parser_max_errors(uint32_t value) + { + set_uint32(qpdf_p_parser_max_errors, value); + } + + /// @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 + /// can be parsed without errors. The default limit is 4,294,967,295. + /// + /// @return The maximum number of top-level objects allowed in a container while parsing + /// objects. + /// + /// @since 12.3 + uint32_t inline parser_max_container_size() + { + return get_uint32(qpdf_p_parser_max_container_size); + } + + /// @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. + /// + /// @param value The maximum number of top-level objects allowed in a container while + /// parsing objects to set. + /// + /// @since 12.3 + void inline parser_max_container_size(uint32_t value) + { + set_uint32(qpdf_p_parser_max_container_size, value); + } + + /// @brief Retrieves the maximum number of top-level objects allowed in a container while + /// 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 xref streams. The default limit is 5,000. + /// + /// @return The maximum number of top-level objects allowed in a container while parsing + /// objects. + /// + /// @since 12.3 + uint32_t inline parser_max_container_size_damaged() + { + return get_uint32(qpdf_p_parser_max_container_size_damaged); + } + + /// @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 damaged or the object itself is + /// damaged. The limit also applies when parsing trailer dictionaries and xref streams. The + /// default limit is 5,000. + /// + /// @param value The maximum number of top-level objects allowed in a container while + /// parsing objects to set. + /// + /// @since 12.3 + void inline parser_max_container_size_damaged(uint32_t value) + { + set_uint32(qpdf_p_parser_max_container_size_damaged, value); + } + } // namespace limits + +} // namespace qpdf::global + +#endif // GLOBAL_HH diff --git a/include/qpdf/qpdf-c.h b/include/qpdf/qpdf-c.h index a2ba876..70f5acd 100644 --- a/include/qpdf/qpdf-c.h +++ b/include/qpdf/qpdf-c.h @@ -119,6 +119,8 @@ #include #include #include + +#include #include #ifdef __cplusplus @@ -1000,6 +1002,50 @@ extern "C" { /* removePage() */ QPDF_DLL QPDF_ERROR_CODE qpdf_remove_page(qpdf_data qpdf, qpdf_oh page); + + /* GLOBAL OPTIONS AND SETTINGS */ + + QPDF_DLL + /** + * @brief Retrieves a 32-bit unsigned integer value associated with a global option or limit. + * + * This function allows querying of specific parameters, identified by the qpdf_param_e enum, + * and retrieves their associated unsigned 32-bit integer values. The result will be stored in + * the variable pointed to by `value`. For details about the available parameters and their + * meanings see `qpdf/global.hh`. + * + * @param param[in] The parameter for which the value is being retrieved. This must be a valid + * value from the qpdf_param_e enumeration. + * @param value[out] A pointer to a uint32_t to store the retrieved value. This must be a valid, + * non-null pointer. + * + * @return An enumeration of type qpdf_result_e indicating the result of the operation. Possible + * values include success or specific error statuses related to the retrieval process. + * + * @since 12.3 + */ + enum qpdf_result_e qpdf_global_get_uint32(enum qpdf_param_e param, uint32_t* value); + + QPDF_DLL + /** + * @brief Sets a global option or limit for the qpdf library to a specified value. + * + * This function is used to configure global options or limits for the qpdf library based on the + * provided parameter and value. The behavior depends on the specific `param` provided and its + * valid range of values. For details about the available parameters and their meanings see + * `qpdf/global.hh`. + * + * @param param[in] The parameter to be set. Must be one of the values defined in the + * qpdf_param_e enumeration. + * @param value[in] The value to assign to the specified parameter. Interpretation of this value + * depends on the parameter being set. + * + * @return An enumeration of type qpdf_result_e indicating the result of the operation. Possible + * values include success or specific error statuses related to the retrieval process. + * + * @since 12.3 + */ + enum qpdf_result_e qpdf_global_set_uint32(enum qpdf_param_e param, uint32_t value); #ifdef __cplusplus } 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.cc b/libqpdf/QPDFJob.cc index b9663c2..8820423 100644 --- a/libqpdf/QPDFJob.cc +++ b/libqpdf/QPDFJob.cc @@ -25,6 +25,7 @@ #include #include #include +#include #include // JOB_SCHEMA_DATA @@ -485,6 +486,11 @@ QPDFJob::writeQPDF(QPDF& pdf) *m->log->getWarn() << m->message_prefix << ": operation succeeded with warnings\n"; } } + if (!m->d_cfg.suppress_warnings() && global::Limits::errors()) { + *m->log->getWarn() << m->message_prefix + << ": some configurable limits were exceeded; for more details " + "see https://qpdf.readthedocs.io/en/stable/cli.html#global-limits\n"; + } if (m->report_mem_usage) { // Call get_max_memory_usage before generating output. When debugging, it's easier if print // statements from get_max_memory_usage are not interleaved with the output. 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..c4dca6d 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) +{ + 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/QPDFParser.cc b/libqpdf/QPDFParser.cc index 7b6d855..464b0b5 100644 --- a/libqpdf/QPDFParser.cc +++ b/libqpdf/QPDFParser.cc @@ -15,7 +15,7 @@ using namespace qpdf; using ObjectPtr = std::shared_ptr; -static uint32_t const& max_nesting{global::Limits::objects_max_nesting()}; +static uint32_t const& max_nesting{global::Limits::parser_max_nesting()}; // The ParseGuard class allows QPDFParser to detect re-entrant parsing. It also provides // special access to allow the parser to create unresolved objects and dangling references. @@ -437,17 +437,16 @@ QPDFParser::parseRemainder(bool content_stream) case QPDFTokenizer::tt_array_open: case QPDFTokenizer::tt_dict_open: if (stack.size() > max_nesting) { - warn("ignoring excessively deeply nested data structure"); - return {}; - } else { - b_contents = false; - stack.emplace_back( - input, - (tokenizer.getType() == QPDFTokenizer::tt_array_open) ? st_array - : st_dictionary_key); - frame = &stack.back(); - continue; + limits_error( + "parser-max-nesting", "ignoring excessively deeply nested data structure"); } + b_contents = false; + stack.emplace_back( + input, + (tokenizer.getType() == QPDFTokenizer::tt_array_open) ? st_array + : st_dictionary_key); + frame = &stack.back(); + continue; case QPDFTokenizer::tt_bool: addScalar(tokenizer.getValue() == "true"); @@ -586,11 +585,11 @@ template void QPDFParser::addScalar(Args&&... args) { - auto limit = Limits::objects_max_container_size(bad_count || sanity_checks); - if (frame->olist.size() > limit || frame->dict.size() > limit) { + auto limit = Limits::parser_max_container_size(bad_count || sanity_checks); + if (frame->olist.size() >= limit || frame->dict.size() >= limit) { // Stop adding scalars. We are going to abort when the close token or a bad token is // encountered. - max_bad_count = 0; + max_bad_count = 1; check_too_many_bad_tokens(); // always throws Error() } auto obj = QPDFObject::create(std::forward(args)...); @@ -644,25 +643,30 @@ QPDFParser::fixMissingKeys() void QPDFParser::check_too_many_bad_tokens() { - auto limit = Limits::objects_max_container_size(bad_count || sanity_checks); - if (frame->olist.size() > limit || frame->dict.size() > limit) { + auto limit = Limits::parser_max_container_size(bad_count || sanity_checks); + if (frame->olist.size() >= limit || frame->dict.size() >= limit) { if (bad_count) { - warn( + limits_error( + "parser-max-container-size-damaged", "encountered errors while parsing an array or dictionary with more than " + - std::to_string(limit) + " elements; giving up on reading object"); - throw Error(); + std::to_string(limit) + " elements; giving up on reading object"); } - warn( + limits_error( + "parser-max-container-size", "encountered an array or dictionary with more than " + std::to_string(limit) + - " elements during xref recovery; giving up on reading object"); + " elements during xref recovery; giving up on reading object"); } - if (max_bad_count && --max_bad_count > 0 && good_count > 4) { + if (max_bad_count && --max_bad_count == 0) { + limits_error( + "parser-max-errors", "too many errors during parsing; treating object as null"); + } + if (good_count > 4) { good_count = 0; bad_count = 1; return; } if (++bad_count > 5 || - (frame->state != st_array && QIntC::to_size(max_bad_count) < frame->olist.size())) { + (frame->state != st_array && std::cmp_less(max_bad_count, frame->olist.size()))) { // Give up after 5 errors in close proximity or if the number of missing dictionary keys // exceeds the remaining number of allowable total errors. warn("too many errors; giving up on reading object"); @@ -672,6 +676,14 @@ QPDFParser::check_too_many_bad_tokens() } void +QPDFParser::limits_error(std::string const& limit, std::string const& msg) +{ + Limits::error(); + warn("limits error("s + limit + "): " + msg); + throw Error(); +} + +void QPDFParser::warn(QPDFExc const& e) const { // If parsing on behalf of a QPDF object and want to give a warning, we can warn through the diff --git a/libqpdf/QUtil.cc b/libqpdf/QUtil.cc index c879941..ffbbac2 100644 --- a/libqpdf/QUtil.cc +++ b/libqpdf/QUtil.cc @@ -40,6 +40,7 @@ #endif using namespace qpdf; +using namespace std::literals; // First element is 24 static unsigned short pdf_doc_low_to_unicode[] = { @@ -2068,3 +2069,15 @@ QUtil::is_hex_digit(char c) { return util::is_hex_digit(c); } + +void +QUtil::handle_result_code(qpdf_result_e result, std::string_view context) +{ + if (result == qpdf_r_ok) { + return; + } + qpdf::util::assertion( + result == qpdf_r_bad_parameter, + "unexpected result code received from function in "s.append(context)); + throw std::logic_error("invalid parameter supplied to function in "s.append(context)); +} diff --git a/libqpdf/global.cc b/libqpdf/global.cc index 33f778d..418a544 100644 --- a/libqpdf/global.cc +++ b/libqpdf/global.cc @@ -1,5 +1,83 @@ #include +#include + using namespace qpdf; +using namespace qpdf::global; + +Limits Limits::l; +Options Options::o; + +void +Limits::parser_max_container_size(bool damaged, uint32_t value) +{ + if (damaged) { + l.parser_max_container_size_damaged_set_ = true; + l.parser_max_container_size_damaged_ = value; + } else { + l.parser_max_container_size_ = value; + } +} + +void +Limits::disable_defaults() +{ + if (!l.parser_max_errors_set_) { + l.parser_max_errors_ = 0; + } + if (!l.parser_max_container_size_damaged_set_) { + l.parser_max_container_size_damaged_ = std::numeric_limits::max(); + } +} + +qpdf_result_e +qpdf_global_get_uint32(qpdf_param_e param, uint32_t* value) +{ + qpdf_expect(value); + switch (param) { + case qpdf_p_default_limits: + *value = Options::default_limits(); + return qpdf_r_ok; + case qpdf_p_limit_errors: + *value = Limits::errors(); + return qpdf_r_ok; + case qpdf_p_parser_max_nesting: + *value = Limits::parser_max_nesting(); + return qpdf_r_ok; + case qpdf_p_parser_max_errors: + *value = Limits::parser_max_errors(); + return qpdf_r_ok; + case qpdf_p_parser_max_container_size: + *value = Limits::parser_max_container_size(false); + return qpdf_r_ok; + case qpdf_p_parser_max_container_size_damaged: + *value = Limits::parser_max_container_size(true); + return qpdf_r_ok; + default: + return qpdf_r_bad_parameter; + } +} -global::Limits global::Limits::l; +qpdf_result_e +qpdf_global_set_uint32(qpdf_param_e param, uint32_t value) +{ + switch (param) { + case qpdf_p_default_limits: + Options::default_limits(value); + return qpdf_r_ok; + case qpdf_p_parser_max_nesting: + Limits::parser_max_nesting(value); + return qpdf_r_ok; + case qpdf_p_parser_max_errors: + Limits::parser_max_errors(value); + return qpdf_r_ok; + case qpdf_p_parser_max_container_size: + Limits::parser_max_container_size(false, value); + return qpdf_r_ok; + case qpdf_p_parser_max_container_size_damaged: + Limits::parser_max_container_size(true, value); + return qpdf_r_ok; + default: + return qpdf_r_bad_parameter; + } +} diff --git a/libqpdf/qpdf/QPDFParser.hh b/libqpdf/qpdf/QPDFParser.hh index 062b315..e108a20 100644 --- a/libqpdf/qpdf/QPDFParser.hh +++ b/libqpdf/qpdf/QPDFParser.hh @@ -124,6 +124,7 @@ class QPDFParser void check_too_many_bad_tokens(); void warnDuplicateKey(); void fixMissingKeys(); + [[noreturn]] void limits_error(std::string const& limit, std::string const& msg); void warn(qpdf_offset_t offset, std::string const& msg) const; void warn(std::string const& msg) const; void warn(QPDFExc const&) const; @@ -149,7 +150,7 @@ class QPDFParser // it only gets incremented or reset when a bad token is encountered. int bad_count{0}; // Number of bad tokens (remaining) before giving up. - uint32_t max_bad_count{Limits::objects_max_errors()}; + uint32_t max_bad_count{Limits::parser_max_errors()}; // Number of good tokens since last bad token. Irrelevant if bad_count == 0. int good_count{0}; // Start offset including any leading whitespace. 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/libqpdf/qpdf/global_private.hh b/libqpdf/qpdf/global_private.hh index 334f351..66b5324 100644 --- a/libqpdf/qpdf/global_private.hh +++ b/libqpdf/qpdf/global_private.hh @@ -1,57 +1,109 @@ - #ifndef GLOBAL_PRIVATE_HH #define GLOBAL_PRIVATE_HH -#include +#include -#include #include -namespace qpdf +namespace qpdf::global { - namespace global + class Limits { - class Limits - { - public: - Limits(Limits const&) = delete; - Limits(Limits&&) = delete; - Limits& operator=(Limits const&) = delete; - Limits& operator=(Limits&&) = delete; - - static uint32_t const& - objects_max_nesting() - { - return l.objects_max_nesting_; - } + public: + Limits(Limits const&) = delete; + Limits(Limits&&) = delete; + Limits& operator=(Limits const&) = delete; + Limits& operator=(Limits&&) = delete; - static uint32_t const& - objects_max_errors() - { - return l.objects_max_errors_; - } + static uint32_t const& + parser_max_nesting() + { + return l.parser_max_nesting_; + } + + static void + parser_max_nesting(uint32_t value) + { + l.parser_max_nesting_ = value; + } + + static uint32_t const& + parser_max_errors() + { + return l.parser_max_errors_; + } + + static void + parser_max_errors(uint32_t value) + { + l.parser_max_errors_set_ = true; + l.parser_max_errors_ = value; + } + + static uint32_t const& + parser_max_container_size(bool damaged) + { + return damaged ? l.parser_max_container_size_damaged_ : l.parser_max_container_size_; + } - static uint32_t const& - objects_max_container_size(bool damaged) - { - return damaged ? l.objects_max_container_size_damaged_ - : l.objects_max_container_size_; + static void parser_max_container_size(bool damaged, uint32_t value); + + /// Record a limit error. + static void + error() + { + if (l.errors_ < std::numeric_limits::max()) { + ++l.errors_; } + } + + static uint32_t const& + errors() + { + return l.errors_; + } + + static void disable_defaults(); + + private: + Limits() = default; + ~Limits() = default; - private: - Limits() = default; - ~Limits() = default; + static Limits l; - static Limits l; + uint32_t errors_{0}; - uint32_t objects_max_nesting_{499}; - uint32_t objects_max_errors_{15}; - uint32_t objects_max_container_size_{std::numeric_limits::max()}; - uint32_t objects_max_container_size_damaged_{5'000}; - }; + uint32_t parser_max_nesting_{499}; + uint32_t parser_max_errors_{15}; + bool parser_max_errors_set_{false}; + uint32_t parser_max_container_size_{std::numeric_limits::max()}; + uint32_t parser_max_container_size_damaged_{5'000}; + bool parser_max_container_size_damaged_set_{false}; + }; + + class Options + { + public: + static bool + default_limits() + { + return static_cast(o.default_limits_); + } + + static void + default_limits(bool value) + { + if (!value) { + o.default_limits_ = false; + Limits::disable_defaults(); + } + } - } // namespace global + private: + static Options o; -} // namespace qpdf + bool default_limits_{true}; + }; +} // namespace qpdf::global #endif // GLOBAL_PRIVATE_HH diff --git a/libtests/objects.cc b/libtests/objects.cc index 52da62c..c9020b7 100644 --- a/libtests/objects.cc +++ b/libtests/objects.cc @@ -5,8 +5,10 @@ #include #include +#include #include #include +#include #include #include @@ -153,6 +155,132 @@ test_1(QPDF& pdf, char const* arg2) assert(QPDFObjectHandle(d).getDictAsMap().size() == 4); } +static void +test_2(QPDF& pdf, char const* arg2) +{ + // Test global limits. + using namespace qpdf::global::options; + using namespace qpdf::global::limits; + + // Check default values + assert(parser_max_nesting() == 499); + assert(parser_max_errors() == 15); + assert(parser_max_container_size() == std::numeric_limits::max()); + assert(parser_max_container_size_damaged() == 5'000); + assert(default_limits()); + + // Test disabling optional default limits + default_limits(false); + assert(parser_max_nesting() == 499); + assert(parser_max_errors() == 0); + assert(parser_max_container_size() == std::numeric_limits::max()); + assert(parser_max_container_size_damaged() == std::numeric_limits::max()); + assert(!default_limits()); + + // Check disabling default limits is irreversible + default_limits(true); + assert(!default_limits()); + + // Test setting limits + parser_max_nesting(11); + parser_max_errors(12); + parser_max_container_size(13); + parser_max_container_size_damaged(14); + + assert(parser_max_nesting() == 11); + assert(parser_max_errors() == 12); + assert(parser_max_container_size() == 13); + assert(parser_max_container_size_damaged() == 14); + + // Check disabling default limits does not override explicit limits + default_limits(false); + assert(parser_max_nesting() == 11); + assert(parser_max_errors() == 12); + assert(parser_max_container_size() == 13); + assert(parser_max_container_size_damaged() == 14); + + // Test parameter checking + QUtil::handle_result_code(qpdf_r_ok, ""); + bool thrown = false; + try { + qpdf::global::handle_result(qpdf_r_success_mask); + } catch (std::logic_error const&) { + thrown = true; + } + assert(thrown); + thrown = false; + try { + qpdf::global::get_uint32(qpdf_param_e(42)); + } catch (std::logic_error const&) { + thrown = true; + } + assert(thrown); + thrown = false; + try { + qpdf::global::set_uint32(qpdf_param_e(42), 42); + } catch (std::logic_error const&) { + thrown = true; + } + assert(thrown); + + /* Test limit errors */ + assert(qpdf::global::limit_errors() == 0); + QPDFObjectHandle::parse("[[[[]]]]"); + assert(qpdf::global::limit_errors() == 0); + parser_max_nesting(3); + try { + QPDFObjectHandle::parse("[[[[[]]]]]"); + } catch (std::exception&) { + } + assert(qpdf::global::limit_errors() == 1); + try { + QPDFObjectHandle::parse("[[[[[]]]]]"); + } 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 runtest(int n, char const* filename1, char const* arg2) { @@ -160,9 +288,7 @@ runtest(int n, char const* filename1, char const* arg2) // the test suite to see how the test is invoked to find the file // that the test is supposed to operate on. - std::set ignore_filename = { - 1, - }; + std::set ignore_filename = {1, 2}; QPDF pdf; std::shared_ptr file_buf; @@ -176,9 +302,7 @@ runtest(int n, char const* filename1, char const* arg2) } std::map test_functions = { - {0, test_0}, - {1, test_1}, - }; + {0, test_0}, {1, test_1}, {2, test_2}}; auto fn = test_functions.find(n); if (fn == test_functions.end()) { diff --git a/libtests/qtest/objects.test b/libtests/qtest/objects.test index f6fde63..4959f37 100644 --- a/libtests/qtest/objects.test +++ b/libtests/qtest/objects.test @@ -11,7 +11,7 @@ require TestDriver; my $td = new TestDriver('objects'); -my $n_tests = 2; +my $n_tests = 3; $td->runtest("integer type checks", {$td->COMMAND => "objects 0 minimal.pdf"}, @@ -23,4 +23,9 @@ $td->runtest("dictionary checks", {$td->STRING => => "test 1 done\n", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +$td->runtest("global object limits", + {$td->COMMAND => "objects 2 -"}, + {$td->STRING => => "test 2 done\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + $td->report($n_tests); 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/manual/release-notes.rst b/manual/release-notes.rst index 53b6483..8e2c3c4 100644 --- a/manual/release-notes.rst +++ b/manual/release-notes.rst @@ -33,7 +33,7 @@ more detail. - Bug fixes - - Set `is_different` flag in `QPDFFormFieldObjectHelper::getTopLevelField` to + - Set ``is_different`` flag in ``QPDFFormFieldObjectHelper::getTopLevelField`` to false if the field is a top-level field. Previously the flag was only set if the field is a top-level field. @@ -58,7 +58,15 @@ more detail. - Add new ``Buffer`` methods ``move``, ``view``, ``data``, ``size`` and ``empty``. The new methods present the ``Buffer`` as a ``char`` (rather than ``unsigned char``) container and facilitate the efficient moving - of its content into a `std::string``. + of its content into a ``std::string``. + + - Add various new functions in the ``qpdf::`global`` namespace to access + and set/modify global settings and limits. See :ref:`global-options` + and header file ``qpdf/global.hh`` for further detail. + + - Add new C-API functions ``qpdf_global_get_uint32`` and + ``qpdf_global_set_uint32`` to access and set/modify various global + settings and limits. - Build fixes @@ -74,6 +82,9 @@ more detail. - Option :qpdf:ref:`--check` now includes additional basic checks of the AcroForm, Dests, Outlines, and PageLabels structures. + - Add new option :qpdf:ref:`--global` to set or modify various global + options and limits. See :ref:`global-options` for further detail. + - Fix completion scripts and handling to avoid leaking arguments into the environment during completion and to correctly handle ``bashcompinit`` for zsh users. 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..7de4e16 --- /dev/null +++ b/qpdf/qtest/qpdf/global1.out @@ -0,0 +1,8 @@ +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 +qpdf: some configurable limits were exceeded; for more details see https://qpdf.readthedocs.io/en/stable/cli.html#global-limits diff --git a/qpdf/qtest/qpdf/global2.out b/qpdf/qtest/qpdf/global2.out new file mode 100644 index 0000000..a2c7d37 --- /dev/null +++ b/qpdf/qtest/qpdf/global2.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 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 +qpdf: some configurable limits were exceeded; for more details see https://qpdf.readthedocs.io/en/stable/cli.html#global-limits diff --git a/qpdf/qtest/qpdf/global3.out b/qpdf/qtest/qpdf/global3.out new file mode 100644 index 0000000..32d8508 --- /dev/null +++ b/qpdf/qtest/qpdf/global3.out @@ -0,0 +1,6 @@ +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 +qpdf: some configurable limits were exceeded; for more details see https://qpdf.readthedocs.io/en/stable/cli.html#global-limits diff --git a/qpdf/qtest/qpdf/global4.out b/qpdf/qtest/qpdf/global4.out new file mode 100644 index 0000000..d4741b5 --- /dev/null +++ b/qpdf/qtest/qpdf/global4.out @@ -0,0 +1,10 @@ +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 +qpdf: some configurable limits were exceeded; for more details see https://qpdf.readthedocs.io/en/stable/cli.html#global-limits 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 diff --git a/qpdf/qtest/qpdf/issue-146.out b/qpdf/qtest/qpdf/issue-146.out index 40fbe4c..5a2ad11 100644 --- a/qpdf/qtest/qpdf/issue-146.out +++ b/qpdf/qtest/qpdf/issue-146.out @@ -1,7 +1,7 @@ WARNING: issue-146.pdf: file is damaged WARNING: issue-146.pdf: can't find startxref WARNING: issue-146.pdf: Attempting to reconstruct cross-reference table -WARNING: issue-146.pdf (trailer, offset 695): ignoring excessively deeply nested data structure +WARNING: issue-146.pdf (trailer, offset 695): limits error(parser-max-nesting): ignoring excessively deeply nested data structure WARNING: issue-146.pdf (object 1 0, offset 92): expected endobj WARNING: issue-146.pdf (object 7 0, offset 146): unknown token while reading object; treating as null WARNING: issue-146.pdf (object 7 0, offset 168): expected endobj diff --git a/qpdf/qtest/qpdf/issue-202.out b/qpdf/qtest/qpdf/issue-202.out index 913c379..35b8c7a 100644 --- a/qpdf/qtest/qpdf/issue-202.out +++ b/qpdf/qtest/qpdf/issue-202.out @@ -1,9 +1,10 @@ -WARNING: issue-202.pdf (trailer, offset 55770): ignoring excessively deeply nested data structure +WARNING: issue-202.pdf (trailer, offset 55770): limits error(parser-max-nesting): ignoring excessively deeply nested data structure WARNING: issue-202.pdf: file is damaged WARNING: issue-202.pdf (offset 54769): expected trailer dictionary WARNING: issue-202.pdf: Attempting to reconstruct cross-reference table -WARNING: issue-202.pdf (trailer, offset 55770): ignoring excessively deeply nested data structure +WARNING: issue-202.pdf (trailer, offset 55770): limits error(parser-max-nesting): ignoring excessively deeply nested data structure WARNING: issue-202.pdf (object 222 0, offset 50101): dictionary has duplicated key /Creator; last occurrence overrides earlier ones WARNING: issue-202.pdf (object 222 0, offset 50101): dictionary has duplicated key /Producer; last occurrence overrides earlier ones WARNING: issue-202.pdf: unable to find trailer dictionary while recovering damaged file qpdf: operation succeeded with warnings; resulting file may have some problems +qpdf: some configurable limits were exceeded; for more details see https://qpdf.readthedocs.io/en/stable/cli.html#global-limits