This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/qpid-proton.git
commit aaf4c2ce7669c19453bc09c8f49147b73ed37f5e Author: Andrew Stitcher <[email protected]> AuthorDate: Mon Aug 24 21:43:23 2026 -0400 PROTON-2967: Decode AMQP values iteratively rather than recursively pn_decoder_decode() recursed once per level of nesting, so the C stack it consumed grew with the nesting depth of the value being decoded. Decode iteratively instead. The state a recursive decoder keeps in its stack frames - how many children of the enclosing container are still to come, and, for an array, the constructor its elements share - is now kept in the container's own node, in the scratch space the encoder also uses. The tree being built is therefore also the decoder's stack, so the only bound on nesting is the node array: PNI_NID_MAX, and any limit set with pn_data_set_decode_limits(). Every open node - list, map, array and described alike - carries a count of the children still to be decoded, which lets a single loop close each node as its last child arrives. The AMQP standard is contradictory about whether described types are allowed to be directly nested: In the descriptive text for described format code it says "prmitive format code" which would disallow a described type. The formal syntax allows it, merely specifying "constructor". The section on transactions requires the message body to the transaction coordinator to be an amqp value (a described type) with a direct value of another described type for declaring and discharging transactions. So to sanity check incoming AMQP values we only allow a single directly nested described type. This is all that is required per the standard, and it's hard to see a valid use for more deeply nested described types. Assisted-By: Claude Opus 5 <[email protected]> --- c/src/core/codec.c | 14 +- c/src/core/data.h | 27 +++- c/src/core/decoder.c | 403 +++++++++++++++++++++++++++++--------------------- c/tests/data_test.cpp | 187 +++++++++++++++++++++++ 4 files changed, 453 insertions(+), 178 deletions(-) diff --git a/c/src/core/codec.c b/c/src/core/codec.c index 2b1f861a1..e90449b9b 100644 --- a/c/src/core/codec.c +++ b/c/src/core/codec.c @@ -1457,16 +1457,6 @@ pn_type_t pn_data_type(pn_data_t *data) } } -pn_type_t pni_data_parent_type(pn_data_t *data) -{ - pni_node_t *node = pn_data_node(data, data->parent); - if (node) { - return node->type; - } else { - return PN_INVALID; - } -} - size_t pn_data_siblings(pn_data_t *data) { pni_node_t *node = pn_data_node(data, data->parent); @@ -1674,9 +1664,9 @@ int pn_data_put_array(pn_data_t *data, bool described, pn_type_t type) return 0; } -void pni_data_set_array_type(pn_data_t *data, pn_type_t type) +void pni_data_set_parent_array_type(pn_data_t *data, pn_type_t type) { - pni_node_t *array = pni_data_current(data); + pni_node_t *array = pn_data_node(data, data->parent); if (array) { array->array_type = (uint8_t)type; } diff --git a/c/src/core/data.h b/c/src/core/data.h index d7338010b..0189be0fe 100644 --- a/c/src/core/data.h +++ b/c/src/core/data.h @@ -34,6 +34,18 @@ typedef uint16_t pni_nid_t; #define PN_ARRAY_DESCRIBED 26 // Internal type: described array #define PN_DEFER 27 // Internal type: node used only in pn_data_fill/vfill +/* + * Bookkeeping the decoder keeps in a compound node while it is being decoded. + * It is dead once the node's last child has been decoded, so it can live in + * the node's scratch space. remaining is a pni_nid_t because a node can never + * have more children than the node array can hold. + */ +typedef struct { + pni_nid_t remaining; /* children still to be decoded */ + uint8_t typecode; /* constructor shared by all elements (arrays only) */ + uint8_t unused; +} pni_decoder_state_t; + /* * Value payload for a pni_node_t. * @@ -79,7 +91,8 @@ typedef union { pni_nid_t down; // offset 0: 2 bytes pni_nid_t children_count; // offset 2: 2 bytes union { - uint32_t as_u32; + uint32_t as_u32; /* encoder: where the node's header was written */ + pni_decoder_state_t as_decoder_state; /* decoder: what is left to decode */ } scratch; // offset 4: 4 bytes } as_compound; // 8 bytes } pni_node_payload_t; @@ -147,6 +160,14 @@ static inline pni_node_t * pn_data_node(pn_data_t *data, pni_nid_t nd) return nd ? (data->nodes + nd - 1) : NULL; } +/* The type of the node we are currently inside, PN_INVALID at the top level. + * This can be an internal type, e.g. PN_ARRAY_DESCRIBED. */ +static inline pn_type_t pni_data_parent_type(pn_data_t *data) +{ + pni_node_t *node = pn_data_node(data, data->parent); + return node ? (pn_type_t) node->type : PN_INVALID; +} + static inline pni_nid_t pni_node_get_down(pni_node_t *node) { if (!node) return 0; @@ -238,6 +259,10 @@ static inline void pni_node_inc_children(pni_node_t *node) } } +/* Set the element type of the array we are currently inside (data->parent). + * The decoder must create an array node before it has read the constructor + * that gives the element type, so it fills the type in afterwards. */ +void pni_data_set_parent_array_type(pn_data_t *data, pn_type_t type); int pni_data_traverse(pn_data_t *data, int (*enter)(void *ctx, pn_data_t *data, pni_node_t *node), int (*exit)(void *ctx, pn_data_t *data, pni_node_t *node), diff --git a/c/src/core/decoder.c b/c/src/core/decoder.c index 5d03212f2..a525ecb1c 100644 --- a/c/src/core/decoder.c +++ b/c/src/core/decoder.c @@ -27,6 +27,21 @@ #include <string.h> +/* + * Decoding is iterative rather than recursive. + * + * Nesting depth is bounded only by the input, so a decoder that recursed once + * per level would consume C stack in proportion to it. The state that a + * recursive decoder would keep in its stack frames - how many children of the + * enclosing container are still to come, and, for an array, the constructor + * its elements share - is instead kept in the container's own node, in the + * scratch space the encoder also uses (pni_decoder_state()). + * + * The tree being built is therefore also the decoder's stack: the only bound + * on nesting is the node array, which is bounded by PNI_NID_MAX and by any + * limit set with pn_data_set_decode_limits(). + */ + void pn_decoder_initialize(pn_decoder_t *decoder) { decoder->input = NULL; @@ -87,14 +102,6 @@ static inline size_t pn_decoder_remaining(pn_decoder_t *decoder) return decoder->input + decoder->size - decoder->position; } -typedef union { - uint32_t i; - uint32_t a[2]; - uint64_t l; - float f; - double d; -} conv_t; - static inline pn_type_t pn_code2type(uint8_t code) { switch (code) @@ -169,19 +176,80 @@ static inline pn_type_t pn_code2type(uint8_t code) } } -static int pni_decoder_decode_type(pn_decoder_t *decoder, pn_data_t *data, uint8_t *code); -static int pni_decoder_single_described(pn_decoder_t *decoder, pn_data_t *data); -static int pni_decoder_single(pn_decoder_t *decoder, pn_data_t *data); -void pni_data_set_array_type(pn_data_t *data, pn_type_t type); +// Typecodes that introduce children which have to be decoded in turn. +// PNE_LIST0 is not one of them: it is an empty list, so it decodes in a single +// step like a scalar. +static inline bool pni_decoder_is_container_code(uint8_t code) +{ + switch (code) + { + case PNE_ARRAY8: + case PNE_ARRAY32: + case PNE_LIST8: + case PNE_LIST32: + case PNE_MAP8: + case PNE_MAP32: + return true; + default: + return false; + } +} + +// Everything else (bar the descriptor prefix) decodes to a single childless node. +static inline bool pni_decoder_is_scalar_code(uint8_t code) +{ + return code != PNE_DESCRIPTOR && !pni_decoder_is_container_code(code); +} + +/* + * The decoder's stack. + * + * "Open" nodes are the container and described nodes we have entered and not + * yet left; depth counts them and is a local of pni_decoder_decode_value(). + * The innermost open node - the one we are putting children into - is + * data->parent, and it carries the state saying what is left to decode. + * + * NB the returned pointer is invalidated by any pn_data_put_*(), which may + * reallocate the node array; always re-fetch it after putting a node. + */ +static inline pni_decoder_state_t *pni_decoder_state(pn_data_t *data) +{ + return &pn_data_node(data, data->parent)->u.as_compound.scratch.as_decoder_state; +} + +// One fewer child for the open node to wait for. Called before putting that +// child, so a node's count reaches 0 exactly as its last child is added. +static inline void pni_decoder_dec_remaining_children(pn_data_t *data, unsigned depth) +{ + if (depth > 0) pni_decoder_state(data)->remaining--; +} + +// Array elements are encoded without a constructor of their own, so they are +// decoded differently from every other value. +static inline bool pni_decoder_in_array(pn_data_t *data, unsigned depth) +{ + if (depth == 0) return false; + pn_type_t type = pni_data_parent_type(data); + return type == PN_ARRAY || type == PN_ARRAY_DESCRIBED; +} + +typedef union { + uint32_t i; + uint32_t a[2]; + uint64_t l; + float f; + double d; +} conv_t; -static int pni_decoder_decode_value(pn_decoder_t *decoder, pn_data_t *data, uint8_t code) +// Decode a value that has no children to decode: any scalar, or an empty list. +// The constructor has already been read; code is it. +static int pni_decoder_decode_scalar(pn_decoder_t *decoder, pn_data_t *data, uint8_t code) { int err; conv_t conv; pn_decimal128_t dec128; pn_uuid_t uuid; size_t size; - size_t count; switch (code) { @@ -336,104 +404,6 @@ static int pni_decoder_decode_value(pn_decoder_t *decoder, pn_data_t *data, uint case PNE_LIST0: err = pn_data_put_list(data); break; - case PNE_ARRAY8: - case PNE_ARRAY32: - case PNE_LIST8: - case PNE_LIST32: - case PNE_MAP8: - case PNE_MAP32: { - size_t min_expected_size = 0; - switch (code) - { - case PNE_ARRAY8: - min_expected_size += 1; // Array has a constructor of at least 1 byte - PN_FALLTHROUGH; - case PNE_LIST8: - case PNE_MAP8: - min_expected_size += 1; // All these types have a count - if (pn_decoder_remaining(decoder) < min_expected_size+1) return PN_UNDERFLOW; - size = pn_decoder_readf8(decoder); - // size must be at least big enough for count or count+constructor - if (size < min_expected_size) { - return pn_error_format(pn_data_error(data), PN_ARG_ERR, - "%s size %zu too small to hold its own header", - pn_type_name(pn_code2type(code)), size); - } - if (pn_decoder_remaining(decoder) < size) return PN_UNDERFLOW; - count = pn_decoder_readf8(decoder); - break; - case PNE_ARRAY32: - min_expected_size += 1; // Array has a constructor of at least 1 byte - PN_FALLTHROUGH; - case PNE_LIST32: - case PNE_MAP32: - min_expected_size += 4; // All these types have a count - if (pn_decoder_remaining(decoder) < min_expected_size+4) return PN_UNDERFLOW; - size = pn_decoder_readf32(decoder); - // size must be at least big enough for count or count+constructor - if (size < min_expected_size) { - return pn_error_format(pn_data_error(data), PN_ARG_ERR, - "%s size %zu too small to hold its own header", - pn_type_name(pn_code2type(code)), size); - } - if (pn_decoder_remaining(decoder) < size) return PN_UNDERFLOW; - count = pn_decoder_readf32(decoder); - break; - default: - return pn_error_format(pn_data_error(data), PN_ARG_ERR, "internal error"); - } - - switch (code) - { - case PNE_ARRAY8: - case PNE_ARRAY32: - { - uint8_t next = *decoder->position; - bool described = (next == PNE_DESCRIPTOR); - err = pn_data_put_array(data, described, (pn_type_t) 0); - if (err) return err; - - pn_data_enter(data); - uint8_t acode; - int e = pni_decoder_decode_type(decoder, data, &acode); - if (e) return e; - pn_type_t type = pn_code2type(acode); - if ((int)type < 0) { - return pn_error_format(pn_data_error(data), (int) type, "unrecognized array element typecode: %u", acode); - } - for (size_t i = 0; i < count; i++) - { - e = pni_decoder_decode_value(decoder, data, acode); - if (e) return e; - } - pn_data_exit(data); - - pni_data_set_array_type(data, type); - } - return 0; - case PNE_LIST8: - case PNE_LIST32: - err = pn_data_put_list(data); - if (err) return err; - break; - case PNE_MAP8: - case PNE_MAP32: - err = pn_data_put_map(data); - if (err) return err; - break; - default: - return pn_error_format(pn_data_error(data), PN_ARG_ERR, "internal error"); - } - pn_data_enter(data); - for (size_t i = 0; i < count; i++) - { - int e = pni_decoder_single(decoder, data); - if (e) return e; - } - pn_data_exit(data); - - return 0; - } default: return pn_error_format(pn_data_error(data), PN_ARG_ERR, "unrecognized typecode: %u", code); } @@ -441,87 +411,190 @@ static int pni_decoder_decode_value(pn_decoder_t *decoder, pn_data_t *data, uint return err; } -pn_type_t pni_data_parent_type(pn_data_t *data); - -static int pni_decoder_decode_type(pn_decoder_t *decoder, pn_data_t *data, uint8_t *code) +// Decode the value of a descriptor. Descriptors are restricted to scalars: a +// compound descriptor buys nothing and is a nesting path we would rather not +// have to bound. +static int pni_decoder_decode_descriptor(pn_decoder_t *decoder, pn_data_t *data) { - int err; + if (!pn_decoder_remaining(decoder)) return PN_UNDERFLOW; + + uint8_t code = *decoder->position++; - if (!pn_decoder_remaining(decoder)) { - return PN_UNDERFLOW; + if (!pni_decoder_is_scalar_code(code)) { + return pn_error_format(pn_data_error(data), PN_ARG_ERR, "invalid descriptor value typecode: %u", code); } - uint8_t next = *decoder->position++; + return pni_decoder_decode_scalar(decoder, data, code); +} - if (next != PNE_DESCRIPTOR) { - *code = next; - return 0; - } +// How many descriptors may prefix a single value: @d1:@d2:value is accepted, +// another level of chaining is not. +#define PNI_DECODER_MAX_DESCRIPTORS 2 - pn_type_t parent_type = pni_data_parent_type(data); - if (parent_type != PN_ARRAY && parent_type != PN_ARRAY_DESCRIBED) { - err = pn_data_put_described(data); - if (err) return err; +/* + * Read the constructor of the next value: the descriptors prefixing it, if + * any, and then its format code, which is returned in *code. + * + * Each descriptor puts a PN_DESCRIBED node and enters it. Such a node holds + * exactly two children: the descriptor value, decoded here, and the value it + * describes. The node is left open for that value, which the caller decodes + * and which closes the node. + */ +static int pni_decoder_decode_constructor(pn_decoder_t *decoder, pn_data_t *data, + unsigned *depth, uint8_t *code) +{ + unsigned descriptors = 0; + + while (true) { + if (!pn_decoder_remaining(decoder)) return PN_UNDERFLOW; + + uint8_t next = *decoder->position++; + if (next != PNE_DESCRIPTOR) { + *code = next; + return 0; + } + + if (++descriptors > PNI_DECODER_MAX_DESCRIPTORS) { + return pn_error_format(pn_data_error(data), PN_ARG_ERR, "nested described type depth exceeded"); + } - // pni_decoder_single has the corresponding exit + pni_decoder_dec_remaining_children(data, *depth); + int err = pn_data_put_described(data); + if (err) return err; pn_data_enter(data); + (*depth)++; + + // Of the node's two children the descriptor is decoded right here, leaving + // just the value it describes for the caller. + pni_decoder_state(data)->remaining = 1; + err = pni_decoder_decode_descriptor(decoder, data); + if (err) return err; } +} - err = pni_decoder_single_described(decoder, data); - if (err) return err; +/* + * Put an array node, enter it and read the constructor its elements share. + * + * An array may be prefixed by a descriptor, which describes the array as a + * whole rather than any one element. It is held as the array node's first + * child, so it is added before the element count is recorded and does not + * count towards it. + */ +static int pni_decoder_open_array(pn_decoder_t *decoder, pn_data_t *data, unsigned *depth, pni_nid_t count) +{ + // The header check in pni_decoder_open_container leaves at least the one + // constructor byte an array must have, so this peek is in bounds. + bool described = (*decoder->position == PNE_DESCRIPTOR); - err = pni_decoder_decode_type(decoder, data, code); + int err = pn_data_put_array(data, described, (pn_type_t) 0); if (err) return err; + pn_data_enter(data); + (*depth)++; - return 0; -} + if (described) { + decoder->position++; + err = pni_decoder_decode_descriptor(decoder, data); + if (err) return err; + } -size_t pn_data_siblings(pn_data_t *data); + if (!pn_decoder_remaining(decoder)) return PN_UNDERFLOW; + uint8_t element_code = *decoder->position++; -// We disallow using any compound type as a described descriptor to avoid recursion -// in decoding. Although these seem syntactically valid they don't seem to be of any -// conceivable use! -static inline bool pni_allowed_descriptor_code(uint8_t code) -{ - return - code != PNE_DESCRIPTOR && - code != PNE_ARRAY8 && code != PNE_ARRAY32 && - code != PNE_LIST8 && code != PNE_LIST32 && - code != PNE_MAP8 && code != PNE_MAP32; + if (element_code == PNE_DESCRIPTOR) { + return pn_error_format(pn_data_error(data), PN_ARG_ERR, + "chained descriptor not supported for a described array"); + } + + pn_type_t element_type = pn_code2type(element_code); + if ((int) element_type < 0) { + return pn_error_format(pn_data_error(data), (int) element_type, + "unrecognized array element typecode: %u", element_code); + } + // The array node had to be created before its element type could be known. + pni_data_set_parent_array_type(data, element_type); + + pni_decoder_state_t *state = pni_decoder_state(data); + state->remaining = count; + state->typecode = element_code; + return 0; } -int pni_decoder_single_described(pn_decoder_t *decoder, pn_data_t *data) +/* + * Read a container header - the byte size and the child count - then put the + * container node and enter it. Its children are decoded by the main loop; the + * state left in the node says how many of them are still to come. + */ +static int pni_decoder_open_container(pn_decoder_t *decoder, pn_data_t *data, unsigned *depth, uint8_t code) { - if (!pn_decoder_remaining(decoder)) { - return PN_UNDERFLOW; + const pn_type_t type = pn_code2type(code); // PN_LIST, PN_MAP or PN_ARRAY + size_t width; // bytes in each of the size and count fields + + switch (code) + { + case PNE_LIST8: case PNE_MAP8: case PNE_ARRAY8: width = 1; break; + case PNE_LIST32: case PNE_MAP32: case PNE_ARRAY32: width = 4; break; + default: + return pn_error_format(pn_data_error(data), PN_ARG_ERR, "internal error"); } - uint8_t code = *decoder->position++;; + // What the size field has to cover: the count, and for an array at least one + // byte of the constructor its elements share. + const size_t min_size = width + (type == PN_ARRAY ? 1 : 0); - if (!pni_allowed_descriptor_code(code)) { - return pn_error_format(pn_data_error(data), PN_ARG_ERR, "invalid descriptor value typecode: %u", code); + if (pn_decoder_remaining(decoder) < width + min_size) return PN_UNDERFLOW; + size_t size = (width == 1) ? pn_decoder_readf8(decoder) : pn_decoder_readf32(decoder); + if (size < min_size) { + return pn_error_format(pn_data_error(data), PN_ARG_ERR, + "%s size %zu too small to hold its own header", + pn_type_name(type), size); } + if (pn_decoder_remaining(decoder) < size) return PN_UNDERFLOW; + size_t count = (width == 1) ? pn_decoder_readf8(decoder) : pn_decoder_readf32(decoder); - int err = pni_decoder_decode_value(decoder, data, code); - if (err) return err; + if (type == PN_ARRAY) return pni_decoder_open_array(decoder, data, depth, (pni_nid_t) count); - if (pni_data_parent_type(data) == PN_DESCRIBED && pn_data_siblings(data) > 1) { - pn_data_exit(data); - } + int err = (type == PN_LIST) ? pn_data_put_list(data) : pn_data_put_map(data); + if (err) return err; + pn_data_enter(data); + (*depth)++; + pni_decoder_state(data)->remaining = (pni_nid_t) count; return 0; } -int pni_decoder_single(pn_decoder_t *decoder, pn_data_t *data) +// Decode one complete value - with all of its descendants - into data. +static int pni_decoder_decode_value(pn_decoder_t *decoder, pn_data_t *data) { - uint8_t code; - int err = pni_decoder_decode_type(decoder, data, &code); - if (err) return err; - err = pni_decoder_decode_value(decoder, data, code); - if (err) return err; - if (pni_data_parent_type(data) == PN_DESCRIBED && pn_data_siblings(data) > 1) { - pn_data_exit(data); + unsigned depth = 0; // open nodes: how deep into the value we currently are + + while (true) + { + uint8_t code = 0; + int err; + + if (pni_decoder_in_array(data, depth)) { + code = pni_decoder_state(data)->typecode; // every element shares it + } else { + err = pni_decoder_decode_constructor(decoder, data, &depth, &code); + if (err) return err; + } + + // Whatever we decode next is one of the children the open node is waiting for. + pni_decoder_dec_remaining_children(data, depth); + + err = pni_decoder_is_container_code(code) + ? pni_decoder_open_container(decoder, data, &depth, code) + : pni_decoder_decode_scalar(decoder, data, code); + if (err) return err; + + // Leave every node that now has all of its children - a container opened + // empty is complete as soon as it is opened. Back at depth 0 the one value + // we were asked for is complete. + while (depth > 0 && pni_decoder_state(data)->remaining == 0) { + pn_data_exit(data); + depth--; + } + if (depth == 0) return 0; } - return 0; } ssize_t pn_decoder_decode(pn_decoder_t *decoder, const char *src, size_t size, pn_data_t *dst) @@ -530,7 +603,7 @@ ssize_t pn_decoder_decode(pn_decoder_t *decoder, const char *src, size_t size, p decoder->size = size; decoder->position = src; - int err = pni_decoder_single(decoder, dst); + int err = pni_decoder_decode_value(decoder, dst); if (err == PN_UNDERFLOW) return pn_error_format(pn_data_error(dst), PN_UNDERFLOW, "not enough data to decode"); diff --git a/c/tests/data_test.cpp b/c/tests/data_test.cpp index b7a2c7675..4a6e04d20 100644 --- a/c/tests/data_test.cpp +++ b/c/tests/data_test.cpp @@ -22,14 +22,31 @@ #include "./pn_test.hpp" #include "core/data.h" +#include "core/value_dump.h" #include <proton/codec.h> #include <proton/error.h> #include <cstdarg> +#include <cstdint> +#include <cstring> +#include <string> +#include <vector> using namespace pn_test; +// Compare the semantic content of two encoded AMQP byte buffers using pn_value_dump(), +// the same textifying path used for frame tracing. This tolerates the encoder legitimately +// choosing a different (but equivalent) encoding width on re-encode, e.g. LIST0 instead of +// an empty LIST8. +static void check_roundtrip(pn_bytes_t initial, pn_bytes_t final_bytes) { + char initial_buf[256]; + char final_buf[256]; + pn_value_dump(initial, initial_buf, sizeof(initial_buf)); + pn_value_dump(final_bytes, final_buf, sizeof(final_buf)); + CHECK(std::string(initial_buf) == std::string(final_buf)); +} + // Check that pn_data_set_decode_limits() enforces a node-count cap. TEST_CASE("data_decode_node_limit") { auto_free<pn_data_t, pn_data_free> data(pn_data(0)); @@ -227,6 +244,176 @@ TEST_CASE("data_described_list") { CHECK("@open(16) [channel-max=965], \"extra\"" == inspect(data)); } +TEST_CASE("data_decode_single_nested_described_type") { + auto_free<pn_data_t, pn_data_free> data(pn_data(0)); + + // described(amqp-value) whose value is itself a described list + // 0x00 0x53 0x77 0x00 0x53 0x31 0x45 + const uint8_t encoded[] = { + 0x00, 0x53, 0x77, + 0x00, 0x53, 0x31, + 0x45 + }; + + ssize_t dec = pn_data_decode(data, (const char *) encoded, sizeof(encoded)); + CHECK(dec == (ssize_t) sizeof(encoded)); + CHECK(pn_data_errno(data) == 0); + + char roundtrip[32]; + int enc = pn_data_encode(data, roundtrip, sizeof(roundtrip)); + REQUIRE(enc > 0); + check_roundtrip(pn_bytes(sizeof(encoded), (const char *) encoded), pn_bytes((size_t) enc, roundtrip)); +} + +TEST_CASE("data_decode_rejects_deep_described_chain") { + auto_free<pn_data_t, pn_data_free> data(pn_data(0)); + + // three chained described constructors: only one nested described value is allowed. + const uint8_t encoded[] = { + 0x00, 0x53, 0x77, + 0x00, 0x53, 0x31, + 0x00, 0x53, 0x32, + 0x40 + }; + + ssize_t dec = pn_data_decode(data, (const char *) encoded, sizeof(encoded)); + CHECK(dec == PN_ARG_ERR); +} + +TEST_CASE("data_decode_described_empty_list") { + auto_free<pn_data_t, pn_data_free> data(pn_data(0)); + + // @ulong(0x77):list() where the empty list is encoded as LIST8 (not the + // compact LIST0 form), to exercise the container-open/immediately-empty path. + // 0x00 0x53 0x77 0xc0 0x01 0x00 + const uint8_t encoded[] = { + 0x00, 0x53, 0x77, 0xc0, 0x01, 0x00 + }; + + ssize_t dec = pn_data_decode(data, (const char *) encoded, sizeof(encoded)); + CHECK(dec == (ssize_t) sizeof(encoded)); + CHECK(pn_data_errno(data) == 0); + + char roundtrip[32]; + int enc = pn_data_encode(data, roundtrip, sizeof(roundtrip)); + REQUIRE(enc > 0); + check_roundtrip(pn_bytes(sizeof(encoded), (const char *) encoded), pn_bytes((size_t) enc, roundtrip)); +} + +TEST_CASE("data_decode_described_empty_list_in_list") { + auto_free<pn_data_t, pn_data_free> data(pn_data(0)); + + // [ @ulong(0x77):list(), 5 ] + const uint8_t encoded[] = { + 0xc0, 0x09, 0x02, + 0x00, 0x53, 0x77, 0xc0, 0x01, 0x00, + 0x52, 0x05 + }; + + ssize_t dec = pn_data_decode(data, (const char *) encoded, sizeof(encoded)); + CHECK(dec == (ssize_t) sizeof(encoded)); + CHECK(pn_data_errno(data) == 0); + CHECK("[@amqp-value(119) [], 5]" == inspect(data)); + + char roundtrip[32]; + int enc = pn_data_encode(data, roundtrip, sizeof(roundtrip)); + REQUIRE(enc > 0); + check_roundtrip(pn_bytes(sizeof(encoded), (const char *) encoded), pn_bytes((size_t) enc, roundtrip)); +} + +TEST_CASE("data_decode_described_empty_map") { + auto_free<pn_data_t, pn_data_free> data(pn_data(0)); + + // @ulong(0x77):map() where the empty map is encoded as MAP8. + // 0x00 0x53 0x77 0xc1 0x01 0x00 + const uint8_t encoded[] = { + 0x00, 0x53, 0x77, 0xc1, 0x01, 0x00 + }; + + ssize_t dec = pn_data_decode(data, (const char *) encoded, sizeof(encoded)); + CHECK(dec == (ssize_t) sizeof(encoded)); + CHECK(pn_data_errno(data) == 0); + + char roundtrip[32]; + int enc = pn_data_encode(data, roundtrip, sizeof(roundtrip)); + REQUIRE(enc > 0); + check_roundtrip(pn_bytes(sizeof(encoded), (const char *) encoded), pn_bytes((size_t) enc, roundtrip)); +} + +// Hand-build `depth` nested AMQP list32 frames as raw wire bytes: [0xd0][size:4 BE][count:4 BE], +// innermost count 0, each outer one wrapping the next as its single element. This lets us drive +// the decoder to a nesting depth no pn_data_t-based construction can conveniently reach, without +// relying on any recursive helper on the encode side. +static std::vector<uint8_t> build_nested_list32(uint32_t depth) { + const size_t per = 9; // 1 tag + 4 size + 4 count + std::vector<uint8_t> buf(depth * per); + uint8_t *p = buf.data(); + for (uint32_t i = 0; i < depth; i++) { + uint32_t count = (i == depth - 1) ? 0 : 1; + uint32_t size = 4 + (uint32_t) ((depth - 1 - i) * per); // covers count(4) + nested content + *p++ = 0xd0; // PNE_LIST32 + p[0] = (uint8_t) (size >> 24); p[1] = (uint8_t) (size >> 16); p[2] = (uint8_t) (size >> 8); p[3] = (uint8_t) size; + p += 4; + p[0] = (uint8_t) (count >> 24); p[1] = (uint8_t) (count >> 16); p[2] = (uint8_t) (count >> 8); p[3] = (uint8_t) count; + p += 4; + } + return buf; +} + +TEST_CASE("data_decode_deep_nesting") { + auto_free<pn_data_t, pn_data_free> data(pn_data(0)); + + // Depth well beyond what any fixed recursion-depth heuristic would allow, but + // comfortably under the absolute PNI_NID_MAX node-id ceiling (2^16 - 1), so the + // only limit in play is the (disabled) node-count limit below. The iterative + // decoder keeps the nesting in its own heap-backed node storage, so a value + // this deeply nested decodes normally. + const uint32_t depth = 20000; + std::vector<uint8_t> buf = build_nested_list32(depth); + + pn_data_set_decode_limits(data, 0, 0); // unlimited: isolate depth handling from node budget + ssize_t dec = pn_data_decode(data, (const char *) buf.data(), buf.size()); + CHECK(dec == (ssize_t) buf.size()); + CHECK(pn_data_errno(data) == 0); +} + +TEST_CASE("data_decode_rejects_deeply_nested_values_on_node_limit") { + auto_free<pn_data_t, pn_data_free> data(pn_data(0)); + + // Same deep-nesting shape as above, but now with a tiny node budget: decoding + // must fail cleanly with PN_OUT_OF_MEMORY rather than fail some other way or + // succeed, proving that node count is what bounds nesting. + const uint32_t depth = 20000; + std::vector<uint8_t> buf = build_nested_list32(depth); + + pn_data_set_decode_limits(data, 10, 0); + ssize_t dec = pn_data_decode(data, (const char *) buf.data(), buf.size()); + CHECK(dec == PN_OUT_OF_MEMORY); + CHECK(pn_data_errno(data) == PN_OUT_OF_MEMORY); +} + +TEST_CASE("data_decode_into_entered_container") { + auto_free<pn_data_t, pn_data_free> data(pn_data(0)); + + // Decoding appends to wherever the pn_data_t is positioned, including inside + // a container the caller has entered: exactly one value is decoded and the + // caller's position is left as it was. + REQUIRE(pn_data_put_list(data) == 0); + REQUIRE(pn_data_enter(data)); + + const uint8_t encoded[] = { + 0xc0, 0x04, 0x02, 0x52, 0x05, 0x40 // [5, null] + }; + + ssize_t dec = pn_data_decode(data, (const char *) encoded, sizeof(encoded)); + CHECK(dec == (ssize_t) sizeof(encoded)); + CHECK(pn_data_errno(data) == 0); + + REQUIRE(pn_data_exit(data)); + pn_data_rewind(data); + CHECK("[[5, null]]" == inspect(data)); +} + TEST_CASE("data_map") { auto_free<pn_data_t, pn_data_free> data(pn_data(1)); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
