From 34db6e3057d1b0c5afdcc60b570fdbe8ce711f8a Mon Sep 17 00:00:00 2001 From: m-holger Date: Sat, 15 Nov 2025 12:10:44 +0000 Subject: [PATCH] Introduce `global` namespace for managing qpdf global options and limits; add support for configurable global limits via `qpdf_global_get_uint32` and `qpdf_global_set_uint32`. --- include/qpdf/Constants.h | 43 +++++++++++++++++++++++++++++++++++++++++++ include/qpdf/QUtil.hh | 21 +++++++++++++++++++++ include/qpdf/global.hh | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ include/qpdf/qpdf-c.h | 46 ++++++++++++++++++++++++++++++++++++++++++++++ libqpdf/QUtil.cc | 13 +++++++++++++ libqpdf/global.cc | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- libqpdf/qpdf/global_private.hh | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------- libtests/objects.cc | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ libtests/qtest/objects.test | 7 ++++++- 9 files changed, 551 insertions(+), 47 deletions(-) create mode 100644 include/qpdf/global.hh diff --git a/include/qpdf/Constants.h b/include/qpdf/Constants.h index fe19448..4f62fad 100644 --- a/include/qpdf/Constants.h +++ b/include/qpdf/Constants.h @@ -244,4 +244,47 @@ 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_no_warn 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 options */ + qpdf_p_default_limits = 0x10100, + /* global limits */ + + /* object - parser limits */ + qpdf_p_objects_max_nesting = 0x11000, + qpdf_p_objects_max_errors, + qpdf_p_objects_max_container_size, + qpdf_p_objects_max_container_size_damaged, + + /* next section = 0x20000 */ + qpdf_enum_max = 0x7fffffff, +}; + #endif /* QPDFCONSTANTS_H */ diff --git a/include/qpdf/QUtil.hh b/include/qpdf/QUtil.hh index 8202ce6..68b5cf1 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, checks 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/global.hh b/include/qpdf/global.hh new file mode 100644 index 0000000..042e979 --- /dev/null +++ b/include/qpdf/global.hh @@ -0,0 +1,200 @@ +// 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)); + } + + 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 objects_max_nesting() + { + return get_uint32(qpdf_p_objects_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 objects_max_nesting(uint32_t value) + { + set_uint32(qpdf_p_objects_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 objects_max_errors() + { + return get_uint32(qpdf_p_objects_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 objects_max_errors(uint32_t value) + { + set_uint32(qpdf_p_objects_max_errors, value); + } + + /// @brief Retrieves the maximum number of objectstop-level 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 objects_max_container_size() + { + return get_uint32(qpdf_p_objects_max_container_size); + } + + /// @brief Sets the maximum number oftop-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 objects_max_container_size(uint32_t value) + { + set_uint32(qpdf_p_objects_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 trailer dictionaries and 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 objects_max_container_size_damaged() + { + return get_uint32(qpdf_p_objects_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 objects_max_container_size_damaged(uint32_t value) + { + set_uint32(qpdf_p_objects_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/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..6bca03b 100644 --- a/libqpdf/global.cc +++ b/libqpdf/global.cc @@ -1,5 +1,80 @@ #include +#include + using namespace qpdf; +using namespace qpdf::global; + +Limits Limits::l; +Options Options::o; + +void +Limits::objects_max_container_size(bool damaged, uint32_t value) +{ + if (damaged) { + l.objects_max_container_size_damaged_set_ = true; + l.objects_max_container_size_damaged_ = value; + } else { + l.objects_max_container_size_ = value; + } +} + +void +Limits::disable_defaults() +{ + if (!l.objects_max_errors_set_) { + l.objects_max_errors_ = 0; + } + if (!l.objects_max_container_size_damaged_set_) { + l.objects_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_objects_max_nesting: + *value = Limits::objects_max_nesting(); + return qpdf_r_ok; + case qpdf_p_objects_max_errors: + *value = Limits::objects_max_errors(); + return qpdf_r_ok; + case qpdf_p_objects_max_container_size: + *value = Limits::objects_max_container_size(false); + return qpdf_r_ok; + case qpdf_p_objects_max_container_size_damaged: + *value = Limits::objects_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_objects_max_nesting: + Limits::objects_max_nesting(value); + return qpdf_r_ok; + case qpdf_p_objects_max_errors: + Limits::objects_max_errors(value); + return qpdf_r_ok; + case qpdf_p_objects_max_container_size: + Limits::objects_max_container_size(false, value); + return qpdf_r_ok; + case qpdf_p_objects_max_container_size_damaged: + Limits::objects_max_container_size(true, value); + return qpdf_r_ok; + default: + return qpdf_r_bad_parameter; + } +} diff --git a/libqpdf/qpdf/global_private.hh b/libqpdf/qpdf/global_private.hh index 334f351..3a81256 100644 --- a/libqpdf/qpdf/global_private.hh +++ b/libqpdf/qpdf/global_private.hh @@ -1,57 +1,92 @@ - #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() { - 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_; - } + return l.objects_max_nesting_; + } - static uint32_t const& - objects_max_errors() - { - return l.objects_max_errors_; - } + static void + objects_max_nesting(uint32_t value) + { + l.objects_max_nesting_ = value; + } - static uint32_t const& - objects_max_container_size(bool damaged) - { - return damaged ? l.objects_max_container_size_damaged_ - : l.objects_max_container_size_; - } + static uint32_t const& + objects_max_errors() + { + return l.objects_max_errors_; + } - private: - Limits() = default; - ~Limits() = default; + static void + objects_max_errors(uint32_t value) + { + l.objects_max_errors_set_ = true; + l.objects_max_errors_ = value; + } + + static uint32_t const& + objects_max_container_size(bool damaged) + { + return damaged ? l.objects_max_container_size_damaged_ : l.objects_max_container_size_; + } - static Limits l; + static void objects_max_container_size(bool damaged, uint32_t value); - 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}; - }; + static void disable_defaults(); + + private: + Limits() = default; + ~Limits() = default; + + static Limits l; + + uint32_t objects_max_nesting_{499}; + uint32_t objects_max_errors_{15}; + bool objects_max_errors_set_{false}; + uint32_t objects_max_container_size_{std::numeric_limits::max()}; + uint32_t objects_max_container_size_damaged_{5'000}; + bool objects_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..60d80b4 100644 --- a/libtests/objects.cc +++ b/libtests/objects.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -153,6 +154,75 @@ 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(objects_max_nesting() == 499); + assert(objects_max_errors() == 15); + assert(objects_max_container_size() == std::numeric_limits::max()); + assert(objects_max_container_size_damaged() == 5'000); + assert(default_limits()); + + // Test disabling optional default limits + default_limits(false); + assert(objects_max_nesting() == 499); + assert(objects_max_errors() == 0); + assert(objects_max_container_size() == std::numeric_limits::max()); + assert(objects_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 + objects_max_nesting(11); + objects_max_errors(12); + objects_max_container_size(13); + objects_max_container_size_damaged(14); + + assert(objects_max_nesting() == 11); + assert(objects_max_errors() == 12); + assert(objects_max_container_size() == 13); + assert(objects_max_container_size_damaged() == 14); + + // Check disabling default limits does not override explicit limits + default_limits(false); + assert(objects_max_nesting() == 11); + assert(objects_max_errors() == 12); + assert(objects_max_container_size() == 13); + assert(objects_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); +} + void runtest(int n, char const* filename1, char const* arg2) { @@ -160,9 +230,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 +244,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); -- libgit2 0.21.4