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]

Reply via email to