From 63aac224697210e74868f77e373d5cef47b038e9 Mon Sep 17 00:00:00 2001 From: Jon Ringle Date: Tue, 13 Jan 2026 07:47:30 -0500 Subject: [PATCH 1/4] zcbor: add streaming encode support Add an encode mode that writes CBOR bytes directly to a caller-provided callback instead of an intermediate output buffer. This enables low-RAM producers (e.g. UART streaming) to emit CBOR without needing to precompute container sizes. The encode implementation routes all byte writes through helper macros that either append to the payload buffer (existing behavior) or call the streaming callback (streaming mode). When streaming is enabled, list/map containers are terminated using indefinite-length encoding (break 0xFF) to avoid backtracking and header resizing. Also add helper APIs for iterator-driven repeated-field encoding and chunked indefinite-length text string encoding, plus error reporting for streaming write/provider failures. Backward compatible: NULL callback retains existing buffered encoding. --- include/zcbor_common.h | 40 ++++++++ include/zcbor_decode.h | 11 +- include/zcbor_encode.h | 89 +++++++++++++++- src/zcbor_common.c | 44 ++++++-- src/zcbor_encode.c | 228 +++++++++++++++++++++++++++++++++++++---- src/zcbor_print.c | 2 + 6 files changed, 380 insertions(+), 34 deletions(-) diff --git a/include/zcbor_common.h b/include/zcbor_common.h index 6d0f8362..faf9f272 100644 --- a/include/zcbor_common.h +++ b/include/zcbor_common.h @@ -152,8 +152,46 @@ struct zcbor_state_constant { #ifdef ZCBOR_MAP_SMART_SEARCH uint8_t *map_search_elem_state_end; /**< The end of the @ref map_search_elem_state buffer. */ #endif + int (*stream_write)(void *user_data, const uint8_t *data, size_t len); + void *stream_user_data; + size_t stream_bytes_written; /**< Total bytes written in streaming mode. */ + + const void *stream_providers; }; +/* Alignment helper for state storage. */ +#if defined(__cplusplus) && (__cplusplus >= 201103L) +#define ZCBOR_ALIGNAS(type) alignas(type) +#define ZCBOR_ALIGNOF(type) alignof(type) +#define ZCBOR_STATIC_ASSERT(cond, msg) static_assert((cond), msg) +#elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) +#define ZCBOR_ALIGNAS(type) _Alignas(type) +#define ZCBOR_ALIGNOF(type) _Alignof(type) +#define ZCBOR_STATIC_ASSERT(cond, msg) _Static_assert((cond), msg) +#else +/* Best-effort fallback for older toolchains. */ +#if defined(__GNUC__) +#define ZCBOR_ALIGNAS(type) __attribute__((aligned(__alignof__(type)))) +#define ZCBOR_ALIGNOF(type) __alignof__(type) +#else +#define ZCBOR_ALIGNAS(type) +#define ZCBOR_ALIGNOF(type) 0 +#endif +#define ZCBOR_STATIC_ASSERT(cond, msg) /* no static assert available */ +#endif + +/* We store struct zcbor_state_constant in state storage that is aligned as + * zcbor_state_t. Ensure that is sufficient for the constant state object. + */ +ZCBOR_STATIC_ASSERT((ZCBOR_ALIGNOF(zcbor_state_t) >= ZCBOR_ALIGNOF(struct zcbor_state_constant)), + "zcbor_state_t alignment must be >= zcbor_state_constant alignment."); + +/** Number of zcbor_state_t slots required to store a struct zcbor_state_constant + * object at the end of the state array. + */ +#define ZCBOR_CONST_STATE_SLOTS \ + ((sizeof(struct zcbor_state_constant) + sizeof(zcbor_state_t) - 1) / sizeof(zcbor_state_t)) + #ifdef ZCBOR_CANONICAL #define ZCBOR_ENFORCE_CANONICAL_DEFAULT true #else @@ -285,6 +323,8 @@ do { \ #define ZCBOR_ERR_MAP_FLAGS_NOT_AVAILABLE 20 #define ZCBOR_ERR_INVALID_VALUE_ENCODING 21 ///! When ZCBOR_CANONICAL is defined, and the incoming data is not encoded with minimal length, or uses indefinite length array. #define ZCBOR_ERR_CONSTANT_STATE_MISSING 22 +#define ZCBOR_ERR_STREAM_WRITE_FAILED 23 ///! Streaming callback returned error or wrote fewer bytes than requested +#define ZCBOR_ERR_STREAM_READ_FAILED 24 ///! Streaming provider callback returned error or invalid data #define ZCBOR_ERR_UNKNOWN 31 /** The largest possible elem_count. */ diff --git a/include/zcbor_decode.h b/include/zcbor_decode.h index 90319d21..5e4fb1ce 100644 --- a/include/zcbor_decode.h +++ b/include/zcbor_decode.h @@ -44,10 +44,15 @@ void zcbor_new_decode_state(zcbor_state_t *state_array, size_t n_states, * including elements in nested unordered maps. */ #define ZCBOR_STATE_D(name, num_backups, payload, payload_size, elem_count, n_flags) \ -zcbor_state_t name[((num_backups) + 2 + ZCBOR_FLAG_STATES(n_flags))]; \ +ZCBOR_ALIGNAS(zcbor_state_t) uint8_t name##_storage[ \ + (((num_backups) + 1 + ZCBOR_FLAG_STATES(n_flags) + ZCBOR_CONST_STATE_SLOTS) * sizeof(zcbor_state_t)) \ +]; \ +zcbor_state_t *name = (zcbor_state_t *)name##_storage; \ do { \ - zcbor_new_decode_state(name, ZCBOR_ARRAY_SIZE(name), payload, payload_size, elem_count, \ - (uint8_t *)&name[(num_backups) + 1], ZCBOR_FLAG_STATES(n_flags) * sizeof(zcbor_state_t)); \ + zcbor_new_decode_state(name, ((num_backups) + 1 + ZCBOR_FLAG_STATES(n_flags) + ZCBOR_CONST_STATE_SLOTS), \ + payload, payload_size, elem_count, \ + (uint8_t *)&name[(num_backups) + 1], \ + ZCBOR_FLAG_STATES(n_flags) * sizeof(zcbor_state_t)); \ } while(0) diff --git a/include/zcbor_encode.h b/include/zcbor_encode.h index 9bd9383c..bd0e21c6 100644 --- a/include/zcbor_encode.h +++ b/include/zcbor_encode.h @@ -40,9 +40,13 @@ void zcbor_new_encode_state(zcbor_state_t *state_array, size_t n_states, * @param[in] elem_count The starting elem_count (typically 1). */ #define ZCBOR_STATE_E(name, num_backups, payload, payload_size, elem_count) \ -zcbor_state_t name[((num_backups) + 2)]; \ +ZCBOR_ALIGNAS(zcbor_state_t) uint8_t name##_storage[ \ + (((num_backups) + 1 + ZCBOR_CONST_STATE_SLOTS) * sizeof(zcbor_state_t)) \ +]; \ +zcbor_state_t *name = (zcbor_state_t *)name##_storage; \ do { \ - zcbor_new_encode_state(name, ZCBOR_ARRAY_SIZE(name), payload, payload_size, elem_count); \ + zcbor_new_encode_state(name, ((num_backups) + 1 + ZCBOR_CONST_STATE_SLOTS), \ + payload, payload_size, elem_count); \ } while(0) @@ -188,6 +192,29 @@ bool zcbor_multi_encode_minmax(size_t min_encode, size_t max_encode, const size_t *num_encode, zcbor_encoder_t encoder, zcbor_state_t *state, const void *input, size_t input_len); +/** + * @brief Iterator callback for streaming encode of repeated fields. + * + * Return values: + * - 1: produced one element, stored at *elem_out + * - 0: done (normal end) + * - <0: error (negative errno-style) + * + * The pointer stored in *elem_out must remain valid until the next call to next() + * (or until encoding is finished), whichever comes first. + */ +typedef int (*zcbor_repeat_next_fn)(void *ctx, const void **elem_out); + +/** + * @brief Encode a repeated field by iterating elements from a callback. + * + * This is intended for streaming encode where the caller does not want (or + * cannot afford) to materialize an array + count upfront. + */ +bool zcbor_multi_encode_iter_minmax(size_t min_encode, size_t max_encode, + zcbor_encoder_t encoder, zcbor_state_t *state, + zcbor_repeat_next_fn next, void *ctx); + /* Supplementary string (bstr/tstr) encoding functions: */ @@ -231,6 +258,64 @@ bool zcbor_bstr_start_encode(zcbor_state_t *state); */ bool zcbor_bstr_end_encode(zcbor_state_t *state, struct zcbor_string *result); +/** + * @brief Chunk iterator for streaming encode of indefinite-length text/byte strings. + * + * Return values: + * - 1: produced one chunk (*ptr, *len) + * - 0: done (normal end) + * - <0: error (negative errno-style) + */ +typedef int (*zcbor_next_chunk_fn)(void *ctx, const uint8_t **ptr, size_t *len); + +/* Encode an indefinite-length tstr using a chunk iterator. */ +bool zcbor_tstr_encode_indefinite_chunks(zcbor_state_t *state, + zcbor_next_chunk_fn next_chunk, void *ctx); +bool zcbor_bstr_encode_indefinite_chunks(zcbor_state_t *state, + zcbor_next_chunk_fn next_chunk, void *ctx); + +/** Stream write callback type. + * + * @param user_data User-provided context (e.g., UART device pointer) + * @param data Pointer to data to write + * @param len Number of bytes to write + * @return Number of bytes written (must equal len for success), or negative on error + */ +typedef int (*zcbor_stream_write_fn)(void *user_data, const uint8_t *data, size_t len); + +/** Initialize encoding state for streaming mode. + * + * Bytes are written directly via the callback instead of buffering. + * In streaming mode, arrays and maps use indefinite-length encoding + * (0x9F/0xBF + items + 0xFF) to avoid backtracking. + * + * @param state_array Array of states (must have at least 2 elements for constant_state) + * @param n_states Number of states in array + * @param stream_write Callback function to write bytes + * @param stream_user_data User data passed to callback (e.g., UART device pointer) + * @param elem_count Starting element count (typically 1) + */ +void zcbor_new_encode_state_streaming(zcbor_state_t *state_array, size_t n_states, + zcbor_stream_write_fn stream_write, void *stream_user_data, size_t elem_count); + +size_t zcbor_stream_bytes_written(const zcbor_state_t *state); + +int zcbor_stream_entry_function(void *input, zcbor_state_t *states, size_t n_states, + zcbor_encoder_t func, zcbor_stream_write_fn stream_write, void *stream_user_data, + size_t elem_count, size_t *bytes_written_out); + +static inline void zcbor_set_stream_providers(zcbor_state_t *state, const void *providers) +{ + if (state && state->constant_state) { + state->constant_state->stream_providers = providers; + } +} + +static inline const void *zcbor_get_stream_providers(const zcbor_state_t *state) +{ + return (state && state->constant_state) ? state->constant_state->stream_providers : NULL; +} + #ifdef __cplusplus } #endif diff --git a/src/zcbor_common.c b/src/zcbor_common.c index 5bf959e6..89d1ce2b 100644 --- a/src/zcbor_common.c +++ b/src/zcbor_common.c @@ -14,9 +14,6 @@ _Static_assert((sizeof(size_t) == sizeof(void *)), "This code needs size_t to be the same length as pointers."); -_Static_assert((sizeof(zcbor_state_t) >= sizeof(struct zcbor_state_constant)), - "This code needs zcbor_state_t to be at least as large as zcbor_backups_t."); - bool zcbor_new_backup(zcbor_state_t *state, size_t new_elem_count) { ZCBOR_CHECK_ERROR(); @@ -150,14 +147,41 @@ void zcbor_new_state(zcbor_state_t *state_array, size_t n_states, #endif state_array[0].constant_state = NULL; - if (n_states < 2) { + if (n_states < (1 + ZCBOR_CONST_STATE_SLOTS)) { return; } - /* Use the last state as a struct zcbor_state_constant object. */ - state_array[0].constant_state = (struct zcbor_state_constant *)&state_array[n_states - 1]; + /* Store the constant state object in the tail of the state array. + * + * The state array layout is: + * [0] : live state + * [1 .. backups] : backups + * [.. optional flags ..] : map smart search scratch (decode only, if flags points into the array) + * [tail] : struct zcbor_state_constant (may span multiple slots) + */ + const size_t const_state_slots = ZCBOR_CONST_STATE_SLOTS; + const size_t const_state_idx = n_states - const_state_slots; + + state_array[0].constant_state = (struct zcbor_state_constant *)&state_array[const_state_idx]; state_array[0].constant_state->backup_list = NULL; - state_array[0].constant_state->num_backups = n_states - 2; + + /* Backups live in state_array[1..num_backups]. If flags points inside the array, + * it indicates the start of the scratch region, and backups stop just before it. + */ + size_t backup_end_idx = const_state_idx - 1; + const uint8_t *const base = (const uint8_t *)state_array; + const uint8_t *const tail = (const uint8_t *)&state_array[const_state_idx]; + if (flags) { + const uint8_t *const f = flags; + if ((f >= base) && (f < tail)) { + size_t flags_idx = (size_t)(f - base) / sizeof(zcbor_state_t); + if (flags_idx > 0) { + backup_end_idx = flags_idx - 1; + } + } + } + + state_array[0].constant_state->num_backups = (backup_end_idx >= 1) ? backup_end_idx : 0; state_array[0].constant_state->current_backup = 0; state_array[0].constant_state->error = ZCBOR_SUCCESS; #ifdef ZCBOR_STOP_ON_ERROR @@ -168,7 +192,11 @@ void zcbor_new_state(zcbor_state_t *state_array, size_t n_states, #ifdef ZCBOR_MAP_SMART_SEARCH state_array[0].constant_state->map_search_elem_state_end = flags + flags_bytes; #endif - if (n_states > 2) { + state_array[0].constant_state->stream_write = NULL; + state_array[0].constant_state->stream_user_data = NULL; + state_array[0].constant_state->stream_bytes_written = 0; + state_array[0].constant_state->stream_providers = NULL; + if (state_array[0].constant_state->num_backups > 0) { state_array[0].constant_state->backup_list = &state_array[1]; } } diff --git a/src/zcbor_encode.c b/src/zcbor_encode.c index 1240fd9b..bc80b187 100644 --- a/src/zcbor_encode.c +++ b/src/zcbor_encode.c @@ -15,6 +15,43 @@ _Static_assert((sizeof(size_t) == sizeof(void *)), "This code needs size_t to be the same length as pointers."); +#define ZCBOR_IS_STREAMING(state) ((state)->constant_state && (state)->constant_state->stream_write) + +#define ZCBOR_WRITE_BYTE(state, byte) \ + do { \ + if (ZCBOR_IS_STREAMING(state)) { \ + uint8_t b = (byte); \ + int ret = state->constant_state->stream_write( \ + state->constant_state->stream_user_data, &b, 1); \ + if (ret != 1) { \ + ZCBOR_ERR(ZCBOR_ERR_STREAM_WRITE_FAILED); \ + } \ + state->constant_state->stream_bytes_written++; \ + } else { \ + ZCBOR_CHECK_PAYLOAD(); \ + *(state->payload_mut) = (byte); \ + state->payload_mut++; \ + } \ + } while(0) + +#define ZCBOR_WRITE_BYTES(state, data, len) \ + do { \ + if (ZCBOR_IS_STREAMING(state)) { \ + int ret = state->constant_state->stream_write( \ + state->constant_state->stream_user_data, (data), (len)); \ + if (ret != (int)(len)) { \ + ZCBOR_ERR(ZCBOR_ERR_STREAM_WRITE_FAILED); \ + } \ + state->constant_state->stream_bytes_written += (len); \ + } else { \ + if ((state->payload + (len)) > state->payload_end) { \ + ZCBOR_ERR(ZCBOR_ERR_NO_PAYLOAD); \ + } \ + memcpy(state->payload_mut, (data), (len)); \ + state->payload_mut += (len); \ + } \ + } while(0) + static uint8_t log2ceil(size_t val) { @@ -44,13 +81,11 @@ static bool encode_header_byte(zcbor_state_t *state, zcbor_major_type_t major_type, uint8_t additional) { ZCBOR_CHECK_ERROR(); - ZCBOR_CHECK_PAYLOAD(); zcbor_assert_state(additional < 32, "Unsupported additional value: %d\r\n", additional); - *(state->payload_mut) = (uint8_t)((major_type << 5) | (additional & 0x1F)); + ZCBOR_WRITE_BYTE(state, (uint8_t)((major_type << 5) | (additional & 0x1F))); zcbor_trace(state, "value_encode"); - state->payload_mut++; return true; } @@ -62,21 +97,16 @@ static bool value_encode_len(zcbor_state_t *state, zcbor_major_type_t major_type { uint8_t *u8_result = (uint8_t *)result; - if ((state->payload + 1 + result_len) > state->payload_end) { - ZCBOR_ERR(ZCBOR_ERR_NO_PAYLOAD); - } - if (!encode_header_byte(state, major_type, get_additional(result_len, u8_result[0]))) { ZCBOR_FAIL(); } #ifdef ZCBOR_BIG_ENDIAN - memcpy(state->payload_mut, u8_result, result_len); - state->payload_mut += result_len; + ZCBOR_WRITE_BYTES(state, u8_result, result_len); #else for (; result_len > 0; result_len--) { - *(state->payload_mut++) = u8_result[result_len - 1]; + ZCBOR_WRITE_BYTE(state, u8_result[result_len - 1]); } #endif /* ZCBOR_BIG_ENDIAN */ @@ -211,10 +241,12 @@ bool zcbor_size_encode(zcbor_state_t *state, const size_t *input) static bool str_start_encode(zcbor_state_t *state, const struct zcbor_string *input, zcbor_major_type_t major_type) { - if (input->value && ((zcbor_header_len_ptr(&input->len, sizeof(input->len)) - + input->len + (size_t)state->payload) - > (size_t)state->payload_end)) { - ZCBOR_ERR(ZCBOR_ERR_NO_PAYLOAD); + if (!ZCBOR_IS_STREAMING(state)) { + if (input->value && ((zcbor_header_len_ptr(&input->len, sizeof(input->len)) + + input->len + (size_t)state->payload) + > (size_t)state->payload_end)) { + ZCBOR_ERR(ZCBOR_ERR_NO_PAYLOAD); + } } if (!value_encode(state, major_type, &input->len, sizeof(input->len))) { ZCBOR_FAIL(); @@ -270,19 +302,25 @@ bool zcbor_bstr_end_encode(zcbor_state_t *state, struct zcbor_string *result) static bool str_encode(zcbor_state_t *state, const struct zcbor_string *input, zcbor_major_type_t major_type) { - ZCBOR_CHECK_PAYLOAD(); /* To make the size_t cast below safe. */ - if (input->len > (size_t)(state->payload_end - state->payload)) { - ZCBOR_ERR(ZCBOR_ERR_NO_PAYLOAD); + if (!state->constant_state || !state->constant_state->stream_write) { + ZCBOR_CHECK_PAYLOAD(); /* To make the size_t cast below safe. */ + if (input->len > (size_t)(state->payload_end - state->payload)) { + ZCBOR_ERR(ZCBOR_ERR_NO_PAYLOAD); + } } if (!str_start_encode(state, input, major_type)) { ZCBOR_FAIL(); } - if (state->payload_mut != input->value) { - /* Use memmove since string might be encoded into the same space + if (state->constant_state && state->constant_state->stream_write) { + ZCBOR_WRITE_BYTES(state, input->value, input->len); + } else { + /* Buffer mode: use memmove since string might be encoded into the same space * because of bstrx_cbor_start_encode/bstrx_cbor_end_encode. */ - memmove(state->payload_mut, input->value, input->len); + if (state->payload_mut != input->value) { + memmove(state->payload_mut, input->value, input->len); + } + state->payload += input->len; } - state->payload += input->len; return true; } @@ -331,6 +369,14 @@ static bool list_map_start_encode(zcbor_state_t *state, size_t max_num, zcbor_major_type_t major_type) { #ifdef ZCBOR_CANONICAL + /* In streaming mode, always use indefinite-length to avoid backtracking. */ + if (state->constant_state && state->constant_state->stream_write) { + if (!encode_header_byte(state, major_type, ZCBOR_VALUE_IS_INDEFINITE_LENGTH)) { + ZCBOR_FAIL(); + } + return true; + } + if (!zcbor_new_backup(state, 0)) { ZCBOR_FAIL(); } @@ -367,6 +413,14 @@ static bool list_map_end_encode(zcbor_state_t *state, size_t max_num, zcbor_major_type_t major_type) { #ifdef ZCBOR_CANONICAL + /* In streaming mode, terminate indefinite-length containers with break (0xFF). */ + if (state->constant_state && state->constant_state->stream_write) { + if (!encode_header_byte(state, ZCBOR_MAJOR_TYPE_SIMPLE, ZCBOR_VALUE_IS_INDEFINITE_LENGTH)) { + ZCBOR_FAIL(); + } + return true; + } + size_t list_count = ((major_type == ZCBOR_MAJOR_TYPE_LIST) ? state->elem_count : (state->elem_count / 2)); @@ -581,6 +635,96 @@ bool zcbor_multi_encode_minmax(size_t min_encode, size_t max_encode, } } +bool zcbor_multi_encode_iter_minmax(size_t min_encode, size_t max_encode, + zcbor_encoder_t encoder, zcbor_state_t *state, + zcbor_repeat_next_fn next, void *ctx) +{ + ZCBOR_CHECK_ERROR(); + + if (!next) { + ZCBOR_ERR(ZCBOR_ERR_STREAM_READ_FAILED); + } + + size_t count = 0; + for (; count < max_encode; count++) { + const void *elem = NULL; + int rc = next(ctx, &elem); + if (rc == 0) { + break; /* done */ + } + if (rc < 0 || !elem) { + ZCBOR_ERR(ZCBOR_ERR_STREAM_READ_FAILED); + } + + if (!encoder(state, elem)) { + ZCBOR_FAIL(); + } + } + + if (count < min_encode) { + ZCBOR_ERR(ZCBOR_ERR_ITERATIONS); + } + + zcbor_log("Encoded %zu elements (iter).\n", count); + return true; +} + +static bool encode_indefinite_chunks(zcbor_state_t *state, uint8_t major_type, + bool (*chunk_put)(zcbor_state_t *state, const struct zcbor_string *zs), + zcbor_next_chunk_fn next_chunk, void *ctx) +{ + ZCBOR_CHECK_ERROR(); + + if (!next_chunk || !chunk_put) { + ZCBOR_ERR(ZCBOR_ERR_STREAM_READ_FAILED); + } + + /* Start indefinite-length container. */ + if (!encode_header_byte(state, major_type, ZCBOR_VALUE_IS_INDEFINITE_LENGTH)) { + ZCBOR_FAIL(); + } + + while (true) { + const uint8_t *ptr = NULL; + size_t len = 0; + int rc = next_chunk(ctx, &ptr, &len); + if (rc == 0) { + break; /* done */ + } + if (rc < 0 || (!ptr && len != 0)) { + ZCBOR_ERR(ZCBOR_ERR_STREAM_READ_FAILED); + } + + const struct zcbor_string zs = { .value = ptr, .len = len }; + if (!chunk_put(state, &zs)) { + ZCBOR_FAIL(); + } + } + + /* Break (0xFF). */ + if (!encode_header_byte(state, ZCBOR_MAJOR_TYPE_SIMPLE, ZCBOR_VALUE_IS_INDEFINITE_LENGTH)) { + ZCBOR_FAIL(); + } + + return true; +} + +bool zcbor_tstr_encode_indefinite_chunks(zcbor_state_t *state, + zcbor_next_chunk_fn next_chunk, void *ctx) +{ + return encode_indefinite_chunks(state, ZCBOR_MAJOR_TYPE_TSTR, + (bool (*)(zcbor_state_t *, const struct zcbor_string *))zcbor_tstr_encode, + next_chunk, ctx); +} + +bool zcbor_bstr_encode_indefinite_chunks(zcbor_state_t *state, + zcbor_next_chunk_fn next_chunk, void *ctx) +{ + return encode_indefinite_chunks(state, ZCBOR_MAJOR_TYPE_BSTR, + (bool (*)(zcbor_state_t *, const struct zcbor_string *))zcbor_bstr_encode, + next_chunk, ctx); +} + bool zcbor_multi_encode(const size_t num_encode, zcbor_encoder_t encoder, zcbor_state_t *state, const void *input, size_t result_len) @@ -601,3 +745,45 @@ void zcbor_new_encode_state(zcbor_state_t *state_array, size_t n_states, { zcbor_new_state(state_array, n_states, payload, payload_len, elem_count, NULL, 0); } + +void zcbor_new_encode_state_streaming(zcbor_state_t *state_array, size_t n_states, + zcbor_stream_write_fn stream_write, void *stream_user_data, size_t elem_count) +{ + /* Initialize with dummy buffer (not used in streaming mode) */ + static uint8_t dummy_buffer[1]; + zcbor_new_state(state_array, n_states, dummy_buffer, sizeof(dummy_buffer), elem_count, NULL, 0); + + if (state_array[0].constant_state) { + state_array[0].constant_state->stream_write = stream_write; + state_array[0].constant_state->stream_user_data = stream_user_data; + state_array[0].constant_state->stream_bytes_written = 0; + state_array[0].constant_state->stream_providers = NULL; + } +} + +size_t zcbor_stream_bytes_written(const zcbor_state_t *state) +{ + if (!state || !state->constant_state || !state->constant_state->stream_write) { + return 0; + } + return state->constant_state->stream_bytes_written; +} + +int zcbor_stream_entry_function(void *input, zcbor_state_t *states, size_t n_states, + zcbor_encoder_t func, zcbor_stream_write_fn stream_write, void *stream_user_data, + size_t elem_count, size_t *bytes_written_out) +{ + zcbor_new_encode_state_streaming(states, n_states, stream_write, stream_user_data, elem_count); + + bool ret = func(states, input); + if (!ret) { + int err = zcbor_pop_error(states); + err = (err == ZCBOR_SUCCESS) ? ZCBOR_ERR_UNKNOWN : err; + return err; + } + + if (bytes_written_out) { + *bytes_written_out = zcbor_stream_bytes_written(&states[0]); + } + return ZCBOR_SUCCESS; +} diff --git a/src/zcbor_print.c b/src/zcbor_print.c index d62f2780..11be182a 100644 --- a/src/zcbor_print.c +++ b/src/zcbor_print.c @@ -93,6 +93,8 @@ const char *zcbor_error_str(int error) ZCBOR_ERR_CASE(ZCBOR_ERR_MAP_FLAGS_NOT_AVAILABLE) ZCBOR_ERR_CASE(ZCBOR_ERR_INVALID_VALUE_ENCODING) ZCBOR_ERR_CASE(ZCBOR_ERR_CONSTANT_STATE_MISSING) + ZCBOR_ERR_CASE(ZCBOR_ERR_STREAM_WRITE_FAILED) + ZCBOR_ERR_CASE(ZCBOR_ERR_STREAM_READ_FAILED) } #undef ZCBOR_ERR_CASE From 4fb059244655f20e1998a15d42069ef37229d131 Mon Sep 17 00:00:00 2001 From: Jon Ringle Date: Tue, 13 Jan 2026 12:16:53 -0500 Subject: [PATCH 2/4] zcbor: generator support for streaming encode entrypoints Add optional generation of streaming encode entrypoints (cbor_stream_encode_) and provider structs for repeated fields (iterator) and tstr chunking. Keep default generated output unchanged unless --stream-encode-functions is enabled, and keep repeated fields as fixed arrays unless --repeated-as-pointers is enabled. --- zcbor/zcbor.py | 286 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 271 insertions(+), 15 deletions(-) diff --git a/zcbor/zcbor.py b/zcbor/zcbor.py index ea649a01..79b16643 100755 --- a/zcbor/zcbor.py +++ b/zcbor/zcbor.py @@ -1863,11 +1863,14 @@ class CddlTypes(NamedTuple): class CodeGenerator(CddlXcoder): """Class for generating C code that encode/decodes CBOR and validates it according to the CDDL. """ - def __init__(self, mode, entry_type_names, default_bit_size, *args, **kwargs): + def __init__(self, mode, entry_type_names, default_bit_size, repeated_as_pointers, + stream_encode_functions, *args, **kwargs): super(CodeGenerator, self).__init__(*args, **kwargs) self.mode = mode self.entry_type_names = entry_type_names self.default_bit_size = default_bit_size + self.repeated_as_pointers = repeated_as_pointers + self.stream_encode_functions = stream_encode_functions @classmethod def from_cddl(cddl_class, mode, *args, **kwargs): @@ -1890,7 +1893,14 @@ def is_cbor(self): return res def init_args(self): - return (self.mode, self.entry_type_names, self.default_bit_size, self.default_max_qty) + return ( + self.mode, + self.entry_type_names, + self.default_bit_size, + self.repeated_as_pointers, + self.stream_encode_functions, + self.default_max_qty, + ) def delegate_type_condition(self): """Whether to use the C type of the first child as this type's C type""" @@ -2046,8 +2056,12 @@ def add_var_name(self, var_type, full=False, anonymous=False): f"Expected single var: {var_type!r}" if not anonymous or var_type[-1][-1] != "}": var_name = self.var_name() - array_part = f"[{self.max_qty}]" if full and self.max_qty != 1 else "" - var_type[-1] += f" {var_name}{array_part}" + if full and self.max_qty != 1 and self.repeated_as_pointers: + # Repeated fields become pointer + *_count (caller-owned storage). + var_type[-1] += f" *{var_name}" + else: + array_part = f"[{self.max_qty}]" if full and self.max_qty != 1 else "" + var_type[-1] += f" {var_name}{array_part}" var_type = add_semicolon(var_type) return var_type @@ -2346,6 +2360,30 @@ def xcode_single_func_prim(self, union_int=None): """Make a string from the list returned by single_func_prim()""" return xcode_statement(*self.single_func_prim(self.val_access(), union_int)) + def xcode_tstr_stream_chunks(self): + if self.mode != "encode" or not self.stream_encode_functions: + return None + if self.value is not None or self.key is not None or self.cbor: + return None + if self.count_var_condition(): + return None + + prov_name = self.var_name(with_prefix=True, observe_skipped=False) + "_tstr_chunks" + prov_access = "((const struct cbor_stream_providers *)zcbor_get_stream_providers(state))" + has_prov = f"({prov_access} && {prov_access}->{prov_name}.next_chunk)" + stream_call = ( + f"zcbor_tstr_encode_indefinite_chunks(state, " + f"{prov_access}->{prov_name}.next_chunk, {prov_access}->{prov_name}.ctx)" + ) + return f"({has_prov} ? ({stream_call}) : ({self.xcode_single_func_prim()}))" + + def xcode_tstr(self): + """Encode TSTR, with optional streaming chunk provider support.""" + stream_chunks = self.xcode_tstr_stream_chunks() + if stream_chunks is not None: + return stream_chunks + return self.xcode_single_func_prim() + def list_counts(self): """Recursively sum the total minimum and maximum element count for this element.""" retval = ({ @@ -2385,9 +2423,18 @@ def xcode_list(self): "zcbor_map_end_decode", "zcbor_map_end_encode"] assert self.type in ["LIST", "MAP"], \ "Expected LIST or MAP type, was %s." % self.type - _, max_counts = zip( - *(child.list_counts() for child in self.value)) if self.value else ((0,), (0,)) - count_arg = f', {str(sum(max_counts))}' if self.mode == 'encode' else '' + + # Default: definite-length. When generating streaming encode entrypoints, use + # indefinite-length containers. + if self.mode == 'encode' and self.stream_encode_functions: + count_arg = ', ZCBOR_VALUE_IS_INDEFINITE_LENGTH' + elif self.mode == 'encode': + _, max_counts = zip( + *(child.list_counts() for child in self.value)) if self.value else ((0,), (0,)) + count_arg = f', {str(sum(max_counts))}' + else: + count_arg = '' + with_children = "(%s && ((%s) || (%s, false)) && %s)" % ( f"{start_func}(state{count_arg})", f"{newl_ind}&& ".join(child.full_xcode() for child in self.value), @@ -2447,7 +2494,27 @@ def xcode_union(self): [child.enum_var_name() for child in self.value], [child.full_xcode() for child in self.value]) + def xcode_bstr_stream_chunks(self): + if self.mode != "encode" or not self.stream_encode_functions: + return None + if self.value is not None or self.key is not None or self.cbor: + return None + if self.count_var_condition(): + return None + + prov_name = self.var_name(with_prefix=True, observe_skipped=False) + "_bstr_chunks" + prov_access = "((const struct cbor_stream_providers *)zcbor_get_stream_providers(state))" + has_prov = f"({prov_access} && {prov_access}->{prov_name}.next_chunk)" + stream_call = ( + f"zcbor_bstr_encode_indefinite_chunks(state, " + f"{prov_access}->{prov_name}.next_chunk, {prov_access}->{prov_name}.ctx)" + ) + return f"({has_prov} ? ({stream_call}) : ({self.xcode_single_func_prim()}))" + def xcode_bstr(self): + stream_chunks = self.xcode_bstr_stream_chunks() + if stream_chunks is not None: + return stream_chunks if self.cbor and not self.cbor.is_entry_type(): access_arg = f', {deref_if_not_null(self.val_access())}' if self.mode == 'decode' \ else '' @@ -2544,7 +2611,7 @@ def repeated_xcode(self, union_int=None): "NINT": lambda: self.xcode_single_func_prim(val_union_int), "FLOAT": self.xcode_single_func_prim, "BSTR": self.xcode_bstr, - "TSTR": self.xcode_single_func_prim, + "TSTR": self.xcode_tstr, "BOOL": self.xcode_single_func_prim, "NIL": self.xcode_single_func_prim, "UNDEF": self.xcode_single_func_prim, @@ -2603,10 +2670,36 @@ def full_xcode(self, union_int=None): minmax = "_minmax" if self.mode == "encode" else "" mode = self.mode + + # Keep encode max_qty overrideable (streaming write entrypoints only) when the CDDL + # omitted a max and we fell back to --default-max-qty. + if self.mode == "encode" and self.stream_encode_functions and self.max_qty == self.default_max_qty: + max_qty = "DEFAULT_MAX_QTY" + else: + max_qty = self.max_qty + + # Optional streaming encode: provider iterator overrides pointer+count. + if self.mode == "encode" and self.stream_encode_functions: + prov_name = self.var_name(with_prefix=True, observe_skipped=False) + prov_access = f"((const struct cbor_stream_providers *)zcbor_get_stream_providers(state))" + has_prov = f"({prov_access} && {prov_access}->{prov_name}.next)" + iter_call = ( + f"zcbor_multi_encode_iter_minmax({self.min_qty}, {max_qty}, " + f"(zcbor_encoder_t *){func}, state, {prov_access}->{prov_name}.next, " + f"{prov_access}->{prov_name}.ctx)" + ) + ptr_call = ( + f"zcbor_multi_{mode}{minmax}({self.min_qty}, {max_qty}, &{self.count_var_access()}, " + f"(zcbor_{mode}r_t *){func}, " + f"{xcode_args('*' + arg if arg != 'NULL' and self.result_len() != '0' else arg)}, " + f"{self.result_len()})" + ) + return f"({has_prov} ? ({iter_call}) : ({ptr_call}))" + return ( f"zcbor_multi_{mode}{minmax}(%s, %s, &%s, (zcbor_{mode}r_t *)%s, %s, %s)" % (self.min_qty, - self.max_qty, + max_qty, self.count_var_access(), func, xcode_args("*" + arg if arg != "NULL" and self.result_len() != "0" else arg), @@ -2652,10 +2745,12 @@ def public_xcode_func_sig(self): class CodeRenderer(): - def __init__(self, entry_types, modes, print_time, default_max_qty, git_sha='', file_header=''): + def __init__(self, entry_types, modes, print_time, default_max_qty, git_sha='', file_header='', + stream_encode_functions=False): self.entry_types = entry_types self.print_time = print_time self.default_max_qty = default_max_qty + self.stream_encode_functions = stream_encode_functions self.sorted_types = dict() self.functions = dict() @@ -2838,7 +2933,7 @@ def render_c_file(self, header_file_name, mode): #include "{header_file_name}" #include "zcbor_print.h" -#if DEFAULT_MAX_QTY != {self.default_max_qty} +#if {"ZCBOR_GENERATED_DEFAULT_MAX_QTY" if self.stream_encode_functions else "DEFAULT_MAX_QTY"} != {self.default_max_qty} #error "The type file was generated with a different default_max_qty than this file" #endif @@ -2849,10 +2944,40 @@ def render_c_file(self, header_file_name, mode): {linesep.join([self.render_function(xcoder, mode) for xcoder in self.functions[mode]])} {linesep.join([self.render_entry_function(xcoder, mode) for xcoder in self.entry_types[mode]])} + +{self.render_write_impls(mode) if self.stream_encode_functions and mode == "encode" else ""} """ + def render_write_impls(self, mode): + if mode != "encode": + return "" + return (linesep * 2).join([ + f"""int cbor_stream_encode_{xcoder.var_name(with_prefix=True, observe_skipped=False)}( +\t\tzcbor_stream_write_fn stream_write, void *stream_user_data, +\t\tconst {xcoder.type_name() if struct_ptr_name(mode) in xcoder.full_xcode() else "void"} *input, +\t\tconst struct cbor_stream_providers *prov, +\t\tsize_t *bytes_written_out) +{{ +\tzcbor_state_t states[8]; +\tzcbor_new_encode_state_streaming(states, sizeof(states) / sizeof(states[0]), +\t\tstream_write, stream_user_data, 1); +\tzcbor_set_stream_providers(&states[0], prov); +\tbool ok = {xcoder.xcode_func_name()}(&states[0], input); +\tif (!ok) {{ +\t\tint err = zcbor_pop_error(&states[0]); +\t\treturn (err == ZCBOR_SUCCESS) ? ZCBOR_ERR_UNKNOWN : err; +\t}} +\tif (bytes_written_out) {{ +\t\t*bytes_written_out = zcbor_stream_bytes_written(&states[0]); +\t}} +\treturn ZCBOR_SUCCESS; +}}""" + for xcoder in self.entry_types[mode] + ]) + def render_h_file(self, type_def_file, header_guard, mode): """Render the entire generated header file contents.""" + write_includes = '#include "zcbor_encode.h"' if (self.stream_encode_functions and mode == "encode") else "" return \ f"""/*{self.render_file_header(" *")} */ @@ -2864,18 +2989,23 @@ def render_h_file(self, type_def_file, header_guard, mode): #include #include #include +{write_includes} #include "{type_def_file}" #ifdef __cplusplus extern "C" {{ #endif -#if DEFAULT_MAX_QTY != {self.default_max_qty} +#if {"ZCBOR_GENERATED_DEFAULT_MAX_QTY" if self.stream_encode_functions else "DEFAULT_MAX_QTY"} != {self.default_max_qty} #error "The type file was generated with a different default_max_qty than this file" #endif {(linesep * 2).join([f"{xcoder.public_xcode_func_sig()};" for xcoder in self.entry_types[mode]])} +{self.render_write_types(mode) if self.stream_encode_functions and mode == "encode" else ""} + +{self.render_write_decls(mode) if self.stream_encode_functions and mode == "encode" else ""} + #ifdef __cplusplus }} @@ -2884,6 +3014,108 @@ def render_h_file(self, type_def_file, header_guard, mode): #endif /* {header_guard} */ """ + def render_write_types(self, mode): + if mode != "encode": + return "" + repeats, tstrs, bstrs = self.collect_stream_provider_names() + + repeat_fields = linesep.join([f"\tstruct zcbor_iter_provider {name};" for name in repeats]) or "\t/* no repeated providers */" + tstr_fields = linesep.join([f"\tstruct zcbor_tstr_chunk_provider {name};" for name in tstrs]) or "\t/* no tstr providers */" + bstr_fields = linesep.join([f"\tstruct zcbor_bstr_chunk_provider {name};" for name in bstrs]) or "\t/* no bstr providers */" + + return f"""/* Streaming encode helpers (generated by zcbor.py --stream-encode-functions) */ +struct zcbor_iter_provider {{ + void *ctx; + zcbor_repeat_next_fn next; +}}; + +struct zcbor_tstr_chunk_provider {{ + void *ctx; + zcbor_next_chunk_fn next_chunk; +}}; + +struct zcbor_bstr_chunk_provider {{ + void *ctx; + zcbor_next_chunk_fn next_chunk; +}}; + +/* Global provider struct (schema-wide). */ +struct cbor_stream_providers {{ + /* Repeated fields */ +{repeat_fields} + + /* Text string fields */ +{tstr_fields} + + /* Byte string fields */ +{bstr_fields} +}};""" + + def collect_stream_provider_names(self): + """Collect provider field names for the whole schema (encode side only). + + These names are stable, unique (with_prefix=True), and map directly to the + var_name() used in generated encode code. + """ + repeats = set() + tstrs = set() + bstrs = set() + + visited = set() + + def walk(elem): + # Avoid infinite recursion on self-referential types. + if id(elem) in visited: + return + visited.add(id(elem)) + + if elem.count_var_condition(): + repeats.add(elem.var_name(with_prefix=True, observe_skipped=False)) + + # Variable (non-literal) tstr values. Exclude map keys and fixed values. + # Don't generate *_chunks for repeated string elements; chunk providers apply to a + # single string value, not each element of a repeated list. + if (not elem.count_var_condition() + and elem.type == "TSTR" and elem.value is None and elem.key is None and not elem.cbor): + tstrs.add(elem.var_name(with_prefix=True, observe_skipped=False) + "_tstr_chunks") + + # Variable (non-literal) bstr values. Exclude map keys and CBOR-encoded bstrs. + if (not elem.count_var_condition() + and elem.type == "BSTR" and elem.value is None and elem.key is None and not elem.cbor): + bstrs.add(elem.var_name(with_prefix=True, observe_skipped=False) + "_bstr_chunks") + + if elem.type in ["LIST", "MAP", "GROUP", "UNION"]: + for child in elem.value: + walk(child) + + if elem.cbor: + walk(elem.cbor) + + if elem.key: + walk(elem.key) + + # Follow named types. + if elem.type == "OTHER" and elem.value in elem.my_types: + walk(elem.my_types[elem.value]) + + # Walk from all top-level types; this makes provider struct schema-wide. + for root in self.sorted_types["encode"]: + walk(root) + + return (sorted(repeats), sorted(tstrs), sorted(bstrs)) + + def render_write_decls(self, mode): + if mode != "encode": + return "" + return (linesep * 2).join([ + f"""int cbor_stream_encode_{xcoder.var_name(with_prefix=True, observe_skipped=False)}( + zcbor_stream_write_fn stream_write, void *stream_user_data, + const {xcoder.type_name() if struct_ptr_name(mode) in xcoder.full_xcode() else "void"} *input, + const struct cbor_stream_providers *prov, + size_t *bytes_written_out);""" + for xcoder in self.entry_types[mode] + ]) + def render_type_file(self, header_guard, mode): body = ( linesep + linesep).join( @@ -2912,7 +3144,13 @@ def render_type_file(self, header_guard, mode): * * See `zcbor --help` for more information about --default-max-qty */ -#define DEFAULT_MAX_QTY {self.default_max_qty} +{("#define ZCBOR_GENERATED_DEFAULT_MAX_QTY " + str(self.default_max_qty) + linesep + + linesep + + "/* Allow build-system override. */" + linesep + + "#ifndef DEFAULT_MAX_QTY" + linesep + + "#define DEFAULT_MAX_QTY ZCBOR_GENERATED_DEFAULT_MAX_QTY" + linesep + + "#endif") + if self.stream_encode_functions else ("#define DEFAULT_MAX_QTY " + str(self.default_max_qty))} {body} @@ -3044,6 +3282,22 @@ def parse_args(): to allow it to be configurable when building the code. This is not always possible, as sometimes the value is needed for internal computations. If so, the script will raise an exception.""") + code_parser.add_argument( + "--repeated-as-pointers", required=False, action="store_true", default=False, + help="""Represent repeated fields (max_qty > 1) as pointer + count instead of embedding a +fixed-size array in the generated types. + +This can significantly reduce the size of top-level unions/structs at the cost of requiring the +caller to provide storage for decode, and a readable array for encode.""") + code_parser.add_argument( + "--stream-encode-functions", required=False, + action="store_true", default=False, dest="stream_encode_functions", + help="""Also generate streaming encode entrypoints (cbor_stream_encode_) for each entry type. +These use zcbor_new_encode_state_streaming() and are intended for low-RAM UART write paths. + +Streaming encode entrypoints support: + - repeated fields via zcbor_multi_encode_iter_minmax() (iterator callback) + - tstr fields via zcbor_tstr_encode_indefinite_chunks() (chunk iterator)""") code_parser.add_argument( "--output-c", "--oc", required=False, type=str, help="""Path to output C file. If both --decode and --encode are specified, _decode and @@ -3213,7 +3467,8 @@ def process_code(args): for mode in modes: cddl_res[mode] = CodeGenerator.from_cddl( mode, cddl_contents, args.default_max_qty, mode, args.entry_types, - args.default_bit_size, short_names=args.short_names) + args.default_bit_size, args.repeated_as_pointers, args.stream_encode_functions, + short_names=args.short_names) # Parsing is done, pretty print the result. verbose_print(args.verbose, "Parsed CDDL types:") @@ -3268,7 +3523,8 @@ def add_mode_to_fname(filename, mode): for entry in args.entry_types] for mode in modes}, modes=modes, print_time=args.time_header, default_max_qty=args.default_max_qty, git_sha=git_sha, - file_header=args.file_header + file_header=args.file_header, + stream_encode_functions=args.stream_encode_functions ) c_code_dir = C_SRC_PATH From 1d3720aa208936b4a5545c29e3909a086a3a59d2 Mon Sep 17 00:00:00 2001 From: Jon Ringle Date: Tue, 13 Jan 2026 12:31:35 -0500 Subject: [PATCH 3/4] docs: document streaming encode code generation Document the optional streaming encode entrypoints and the --stream-encode-functions codegen option. Update the architecture overview to mention the generated provider structs and streaming entrypoints. --- ARCHITECTURE.md | 2 ++ README.md | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 56fd40e7..9188d362 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -175,6 +175,8 @@ There are up to 4 files constructed: - The H file with all the struct definitions (the type file). If both decoding and encoding files are generated for the same CDDL, they can share the same type file. - An optional cmake file for building the generated code together with the zcbor C libraries. +When `zcbor code` is invoked with `--stream-encode-functions`, CodeRenderer also emits streaming encode entrypoints (`cbor_stream_encode_`) and the associated provider structs used to supply repeated elements and chunked `tstr` values. + CodeRenderer conducts some pruning and deduplication of the list of types and functions received from CodeGenerator. diff --git a/README.md b/README.md index 713ad357..e3bc1a39 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,23 @@ The tests require [Zephyr](https://github.com/zephyrproject-rtos/zephyr) (if you The generated C code is C++ compatible. +Streaming encode entrypoints +---------------------------- + +For encode-heavy, low-RAM paths, `zcbor code` can optionally generate streaming encode entrypoints: + +- `cbor_stream_encode_(zcbor_stream_write_fn stream_write, void *stream_user_data, const *input, const struct cbor_stream_providers *prov, size_t *bytes_written_out)` + +Enable them with `--stream-encode-functions`. + +These entrypoints write CBOR via the supplied callback (no output buffer), and can stream: + +- repeated fields via iterator callbacks in `struct cbor_stream_providers` +- `bstr` values via chunk callbacks (indefinite-length byte strings) +- `tstr` values via chunk callbacks (indefinite-length text strings) + +When streaming entrypoints are enabled, generated encode code uses indefinite-length containers where needed. + Build system ------------ @@ -436,7 +453,8 @@ zcbor code --help ``` usage: zcbor code [-h] -c CDDL [--no-prelude] [-v] - [--default-max-qty DEFAULT_MAX_QTY] [--output-c OUTPUT_C] + [--default-max-qty DEFAULT_MAX_QTY] [--repeated-as-pointers] + [--stream-encode-functions] [--output-c OUTPUT_C] [--output-h OUTPUT_H] [--output-h-types OUTPUT_H_TYPES] [--copy-sources] [--output-cmake OUTPUT_CMAKE] -t ENTRY_TYPES [ENTRY_TYPES ...] [-d] [-e] [--time-header] @@ -477,6 +495,13 @@ options: as sometimes the value is needed for internal computations. If so, the script will raise an exception. + --repeated-as-pointers + Represent repeated fields (max_qty > 1) as pointer + + count instead of embedding fixed-size arrays in the + generated types. + --stream-encode-functions + Also generate streaming encode entrypoints + (cbor_stream_encode_) for each entry type. --output-c OUTPUT_C, --oc OUTPUT_C Path to output C file. If both --decode and --encode are specified, _decode and _encode will be appended to From 0948a1e534652d2221ff6000edcd92dc72ffc57b Mon Sep 17 00:00:00 2001 From: Jon Ringle Date: Wed, 14 Jan 2026 08:58:14 -0500 Subject: [PATCH 4/4] tests/encode: add streaming encode coverage - Add STREAMING=1 variants for test1_suit, test2_simple, test3_corner_cases, and test4_senml. - Exercise streaming encode paths via stream_write helpers and cbor_stream_encode_* entrypoints. --- tests/encode/test1_suit/CMakeLists.txt | 4 + tests/encode/test1_suit/src/main.c | 64 ++++++++ tests/encode/test1_suit/testcase.yaml | 9 ++ tests/encode/test2_simple/CMakeLists.txt | 5 +- tests/encode/test2_simple/src/main.c | 141 ++++++++++++++++++ tests/encode/test2_simple/testcase.yaml | 19 +++ .../encode/test3_corner_cases/CMakeLists.txt | 4 + tests/encode/test3_corner_cases/src/main.c | 122 +++++++++++++++ tests/encode/test3_corner_cases/testcase.yaml | 19 +++ tests/encode/test4_senml/CMakeLists.txt | 4 + tests/encode/test4_senml/src/main.c | 47 ++++++ tests/encode/test4_senml/testcase.yaml | 19 +++ tests/unit/test1_unit_tests/src/main.c | 4 +- 13 files changed, 459 insertions(+), 2 deletions(-) diff --git a/tests/encode/test1_suit/CMakeLists.txt b/tests/encode/test1_suit/CMakeLists.txt index 65f93f68..01f51d62 100644 --- a/tests/encode/test1_suit/CMakeLists.txt +++ b/tests/encode/test1_suit/CMakeLists.txt @@ -23,6 +23,10 @@ set(py_command ${bit_arg} ) +if (STREAMING) + list(APPEND py_command --stream-encode-functions) +endif() + execute_process( COMMAND ${py_command} COMMAND_ERROR_IS_FATAL ANY diff --git a/tests/encode/test1_suit/src/main.c b/tests/encode/test1_suit/src/main.c index b70cf803..1934b516 100644 --- a/tests/encode/test1_suit/src/main.c +++ b/tests/encode/test1_suit/src/main.c @@ -9,6 +9,70 @@ #include "manifest3_encode.h" #include "zcbor_print.h" +#ifdef STREAMING +#include + +struct stream_ctx { + uint8_t *buf; + size_t size; + size_t pos; +}; + +static int stream_write(void *user_data, const uint8_t *data, size_t len) +{ + struct stream_ctx *ctx = (struct stream_ctx *)user_data; + + if (!ctx || !data) { + return -1; + } + if (len == 0) { + return 0; + } + if (ctx->pos + len > ctx->size) { + return -1; + } + + memcpy(&ctx->buf[ctx->pos], data, len); + ctx->pos += len; + return (int)len; +} + +static int stream_encode_SUIT_Command_Sequence(uint8_t *payload, size_t payload_len, + const struct SUIT_Command_Sequence *input, size_t *out_len) +{ + struct stream_ctx ctx = { + .buf = payload, + .size = payload_len, + .pos = 0, + }; + int rc = cbor_stream_encode_SUIT_Command_Sequence(stream_write, &ctx, input, NULL, out_len); + + if (rc == ZCBOR_SUCCESS) { + zassert_equal(*out_len, ctx.pos, NULL); + } + return rc; +} + +static int stream_encode_SUIT_Outer_Wrapper(uint8_t *payload, size_t payload_len, + const struct SUIT_Outer_Wrapper *input, size_t *out_len) +{ + struct stream_ctx ctx = { + .buf = payload, + .size = payload_len, + .pos = 0, + }; + int rc = cbor_stream_encode_SUIT_Outer_Wrapper(stream_write, &ctx, input, NULL, out_len); + + if (rc == ZCBOR_SUCCESS) { + zassert_equal(*out_len, ctx.pos, NULL); + } + return rc; +} + +#define cbor_encode_SUIT_Command_Sequence stream_encode_SUIT_Command_Sequence +#define cbor_encode_SUIT_Outer_Wrapper stream_encode_SUIT_Outer_Wrapper +#endif /* STREAMING */ + /* draft-ietf-suit-manifest-02 Example 0 */ uint8_t test_vector0_02[] = { 0xa2, 0x01, 0x58, 0x54, 0xd2, 0x84, 0x43, 0xa1, 0x01, diff --git a/tests/encode/test1_suit/testcase.yaml b/tests/encode/test1_suit/testcase.yaml index 0120b824..d8ecbb5e 100644 --- a/tests/encode/test1_suit/testcase.yaml +++ b/tests/encode/test1_suit/testcase.yaml @@ -7,3 +7,12 @@ tests: - qemu_malta/qemu_malta/be tags: zcbor encode canonical test1 extra_args: CANONICAL=CANONICAL + + zcbor.encode.test1_suit.streaming_canonical: + platform_allow: + - native_sim + - native_sim/native/64 + - mps2/an521/cpu0 + - qemu_malta/qemu_malta/be + tags: zcbor encode streaming canonical test1 + extra_args: STREAMING=1 CANONICAL=CANONICAL diff --git a/tests/encode/test2_simple/CMakeLists.txt b/tests/encode/test2_simple/CMakeLists.txt index 0ff39a35..d34756dc 100644 --- a/tests/encode/test2_simple/CMakeLists.txt +++ b/tests/encode/test2_simple/CMakeLists.txt @@ -22,6 +22,10 @@ set(py_command --file-header ${CMAKE_CURRENT_LIST_DIR}/file_header_copyright.txt ) +if (STREAMING) + list(APPEND py_command --stream-encode-functions) +endif() + execute_process( COMMAND ${py_command} COMMAND_ERROR_IS_FATAL ANY @@ -31,4 +35,3 @@ include(${PROJECT_BINARY_DIR}/pet.cmake) target_link_libraries(pet PRIVATE zephyr_interface) target_link_libraries(app PRIVATE pet) - diff --git a/tests/encode/test2_simple/src/main.c b/tests/encode/test2_simple/src/main.c index 1a849965..37fadb12 100644 --- a/tests/encode/test2_simple/src/main.c +++ b/tests/encode/test2_simple/src/main.c @@ -13,6 +13,65 @@ #endif #include +#ifdef STREAMING +#include +#include + +struct stream_ctx { + uint8_t *buf; + size_t size; + size_t pos; +}; + +static int stream_write(void *user_data, const uint8_t *data, size_t len) +{ + struct stream_ctx *ctx = (struct stream_ctx *)user_data; + + if (!ctx) { + return -1; + } + if (len == 0) { + return 0; + } + if (!data) { + return -1; + } + if (ctx->pos + len > ctx->size) { + return -1; + } + + memcpy(&ctx->buf[ctx->pos], data, len); + ctx->pos += len; + return (int)len; +} +#endif /* STREAMING */ + +#ifdef STREAMING +struct bstr_chunk_ctx { + int idx; + const uint8_t *chunks[2]; + size_t lens[2]; +}; + +static int bstr_next_chunk(void *user_ctx, const uint8_t **ptr, size_t *len) +{ + struct bstr_chunk_ctx *c = (struct bstr_chunk_ctx *)user_ctx; + + if (!c || !ptr || !len) { + return -EINVAL; + } + + if (c->idx >= 2) { + return 0; + } + + *ptr = c->chunks[c->idx]; + *len = c->lens[c->idx]; + c->idx++; + return 1; +} +#endif /* STREAMING */ + /* This test uses generated code to encode a 'Pet' instance. It populates the * generated struct, and runs the generated encoding function, then checks that @@ -41,7 +100,19 @@ ZTEST(cbor_encode_test2, test_pet) size_t out_len; /* Check that encoding succeeded. */ +#ifdef STREAMING + struct stream_ctx ctx = { + .buf = output, + .size = sizeof(output), + .pos = 0, + }; + + int rc = cbor_stream_encode_Pet(stream_write, &ctx, &pet, NULL, &out_len); + zassert_equal(ZCBOR_SUCCESS, rc, NULL); + zassert_equal(out_len, ctx.pos, NULL); +#else zassert_equal(ZCBOR_SUCCESS, cbor_encode_Pet(output, sizeof(output), &pet, &out_len), NULL); +#endif /* Check that the resulting length is correct. */ zassert_equal(sizeof(exp_output), out_len, NULL); @@ -59,7 +130,18 @@ ZTEST(cbor_encode_test2, test_pet) ZTEST(cbor_encode_test2, test_pet_raw) { uint8_t payload[100] = {0}; +#ifdef STREAMING + struct stream_ctx ctx = { + .buf = payload, + .size = sizeof(payload), + .pos = 0, + }; + zcbor_state_t states[4]; + zcbor_new_encode_state_streaming(states, 4, stream_write, &ctx, 1); + zcbor_state_t *state = states; +#else ZCBOR_STATE_E(state, 4, payload, sizeof(payload), 1); +#endif uint8_t exp_output[] = { LIST(3), @@ -97,10 +179,69 @@ ZTEST(cbor_encode_test2, test_pet_raw) /* Check that encoding succeeded. */ zassert_true(res, NULL); /* Check that the resulting length is correct. */ +#ifdef STREAMING + zassert_equal(sizeof(exp_output), ctx.pos, "%d != %d\r\n", + sizeof(exp_output), ctx.pos); +#else zassert_equal(sizeof(exp_output), state->payload - payload, "%d != %d\r\n", sizeof(exp_output), state->payload - payload); +#endif /* Check the payload contents. */ zassert_mem_equal(exp_output, payload, sizeof(exp_output), NULL); } +#ifdef STREAMING +ZTEST(cbor_encode_test2, test_bstr_indefinite_chunks) +{ + /* Expect: 0x5f (bstr indefinite), 0x43 01 02 03, 0x42 04 05, 0xff (break) */ + uint8_t output[16]; + struct stream_ctx ctx = { + .buf = output, + .size = sizeof(output), + .pos = 0, + }; + + const uint8_t c1[] = { 0x01, 0x02, 0x03 }; + const uint8_t c2[] = { 0x04, 0x05 }; + struct bstr_chunk_ctx chunks = { + .idx = 0, + .chunks = { c1, c2 }, + .lens = { sizeof(c1), sizeof(c2) }, + }; + + zcbor_state_t s[4]; + zcbor_new_encode_state_streaming(s, 4, stream_write, &ctx, 1); + + zassert_true(zcbor_bstr_encode_indefinite_chunks(s, bstr_next_chunk, &chunks), NULL); + zassert_equal(ctx.pos, 1 + 1 + sizeof(c1) + 1 + sizeof(c2) + 1, NULL); + + const uint8_t exp[] = { 0x5f, 0x43, 0x01, 0x02, 0x03, 0x42, 0x04, 0x05, 0xff }; + zassert_mem_equal(output, exp, ctx.pos, NULL); +} +#endif /* STREAMING */ + +#ifdef STREAMING +ZTEST(cbor_encode_test2, test_streaming_container_headers) +{ + /* In streaming mode, lists/maps must be indefinite-length (0x9f/0xbf ... 0xff). */ + uint8_t output[32]; + struct stream_ctx ctx = { + .buf = output, + .size = sizeof(output), + .pos = 0, + }; + + zcbor_state_t s[4]; + zcbor_new_encode_state_streaming(s, 4, stream_write, &ctx, 1); + + zassert_true(zcbor_list_start_encode(s, 3), NULL); + zassert_true(zcbor_uint32_put(s, 1), NULL); + zassert_true(zcbor_list_end_encode(s, 3), NULL); + + zassert_true(ctx.pos >= 2, NULL); + zassert_equal(output[0], 0x9f, NULL); + zassert_equal(output[ctx.pos - 1], 0xff, NULL); +} +#endif /* STREAMING */ + ZTEST_SUITE(cbor_encode_test2, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/encode/test2_simple/testcase.yaml b/tests/encode/test2_simple/testcase.yaml index a373f987..92e3f2dd 100644 --- a/tests/encode/test2_simple/testcase.yaml +++ b/tests/encode/test2_simple/testcase.yaml @@ -6,6 +6,25 @@ tests: - mps2/an521/cpu0 - qemu_malta/qemu_malta/be tags: zcbor encode test + + zcbor.encode.test2_simple.streaming: + platform_allow: + - native_sim + - native_sim/native/64 + - mps2/an521/cpu0 + - qemu_malta/qemu_malta/be + tags: zcbor encode streaming test + extra_args: STREAMING=1 + + zcbor.encode.test2_simple.streaming_canonical: + platform_allow: + - native_sim + - native_sim/native/64 + - mps2/an521/cpu0 + - qemu_malta/qemu_malta/be + tags: zcbor encode streaming canonical test + extra_args: STREAMING=1 CANONICAL=CANONICAL + zcbor.encode.test2_simple.canonical: platform_allow: - native_sim diff --git a/tests/encode/test3_corner_cases/CMakeLists.txt b/tests/encode/test3_corner_cases/CMakeLists.txt index 6b6e7a0a..d834ba3b 100644 --- a/tests/encode/test3_corner_cases/CMakeLists.txt +++ b/tests/encode/test3_corner_cases/CMakeLists.txt @@ -59,6 +59,10 @@ set(py_command --short-names ) +if (STREAMING) + list(APPEND py_command --stream-encode-functions) +endif() + execute_process( COMMAND ${py_command} COMMAND_ERROR_IS_FATAL ANY diff --git a/tests/encode/test3_corner_cases/src/main.c b/tests/encode/test3_corner_cases/src/main.c index 61cf5a0c..fb9cbb7b 100644 --- a/tests/encode/test3_corner_cases/src/main.c +++ b/tests/encode/test3_corner_cases/src/main.c @@ -12,6 +12,128 @@ #endif #include +#ifdef STREAMING +#include + +struct stream_ctx { + uint8_t *buf; + size_t size; + size_t pos; +}; + +static int stream_write(void *user_data, const uint8_t *data, size_t len) +{ + struct stream_ctx *ctx = (struct stream_ctx *)user_data; + + if (!ctx || !data) { + return -1; + } + if (len == 0) { + return 0; + } + if (ctx->pos + len > ctx->size) { + return -1; + } + + memcpy(&ctx->buf[ctx->pos], data, len); + ctx->pos += len; + return (int)len; +} + +#define STREAM_ENCODE_WRAPPER(func, type) \ +static int stream_encode_##func(uint8_t *payload, size_t payload_len, \ + const type *input, size_t *out_len) \ +{ \ + struct stream_ctx ctx = { \ + .buf = payload, \ + .size = payload_len, \ + .pos = 0, \ + }; \ + int rc = cbor_stream_encode_##func(stream_write, &ctx, input, NULL, out_len); \ + if (rc == ZCBOR_SUCCESS) { \ + zassert_equal(*out_len, ctx.pos, NULL); \ + } \ + return rc; \ +} + +STREAM_ENCODE_WRAPPER(NestedListMap, struct NestedListMap) +STREAM_ENCODE_WRAPPER(NestedMapListMap, struct NestedMapListMap) +STREAM_ENCODE_WRAPPER(Numbers, struct Numbers) +STREAM_ENCODE_WRAPPER(Numbers2, struct Numbers2) +STREAM_ENCODE_WRAPPER(NumberMap, struct NumberMap) +STREAM_ENCODE_WRAPPER(TaggedUnion, struct TaggedUnion_r) +STREAM_ENCODE_WRAPPER(Strings, struct Strings) +STREAM_ENCODE_WRAPPER(Simple2, struct Simple2) +STREAM_ENCODE_WRAPPER(Optional, struct Optional) +STREAM_ENCODE_WRAPPER(Union, struct Union_r) +STREAM_ENCODE_WRAPPER(Map, struct Map) +STREAM_ENCODE_WRAPPER(Level1, struct Level1) +STREAM_ENCODE_WRAPPER(Range, struct Range) +STREAM_ENCODE_WRAPPER(ValueRange, struct ValueRange) +STREAM_ENCODE_WRAPPER(SingleBstr, struct zcbor_string) +STREAM_ENCODE_WRAPPER(SingleInt_uint52, void) +STREAM_ENCODE_WRAPPER(SingleInt2, uint32_t) +STREAM_ENCODE_WRAPPER(Unabstracted, struct Unabstracted) +STREAM_ENCODE_WRAPPER(QuantityRange, struct QuantityRange) +STREAM_ENCODE_WRAPPER(DoubleMap, struct DoubleMap) +STREAM_ENCODE_WRAPPER(Floats, struct Floats) +STREAM_ENCODE_WRAPPER(Floats2, struct Floats2) +STREAM_ENCODE_WRAPPER(CBORBstr, struct CBORBstr) +STREAM_ENCODE_WRAPPER(MapLength, struct MapLength) +STREAM_ENCODE_WRAPPER(UnionInt2, struct UnionInt2) +STREAM_ENCODE_WRAPPER(Intmax1, void) +STREAM_ENCODE_WRAPPER(Intmax2, struct Intmax2) +STREAM_ENCODE_WRAPPER(InvalidIdentifiers, struct InvalidIdentifiers) +STREAM_ENCODE_WRAPPER(MapUnionPrimAlias, struct MapUnionPrimAlias) +STREAM_ENCODE_WRAPPER(EmptyContainer, struct EmptyContainer) +STREAM_ENCODE_WRAPPER(SingleElemList, struct SingleElemList) +STREAM_ENCODE_WRAPPER(Choice1, struct Choice1_r) +STREAM_ENCODE_WRAPPER(Choice2, struct Choice2_r) +STREAM_ENCODE_WRAPPER(Choice3, struct Choice3_r) +STREAM_ENCODE_WRAPPER(Choice4, struct Choice4_r) +STREAM_ENCODE_WRAPPER(Choice5, struct Choice5_r) +STREAM_ENCODE_WRAPPER(OptList, struct OptList) + +#undef STREAM_ENCODE_WRAPPER + +#define cbor_encode_NestedListMap stream_encode_NestedListMap +#define cbor_encode_NestedMapListMap stream_encode_NestedMapListMap +#define cbor_encode_Numbers stream_encode_Numbers +#define cbor_encode_Numbers2 stream_encode_Numbers2 +#define cbor_encode_NumberMap stream_encode_NumberMap +#define cbor_encode_TaggedUnion stream_encode_TaggedUnion +#define cbor_encode_Strings stream_encode_Strings +#define cbor_encode_Simple2 stream_encode_Simple2 +#define cbor_encode_Optional stream_encode_Optional +#define cbor_encode_Union stream_encode_Union +#define cbor_encode_Map stream_encode_Map +#define cbor_encode_Level1 stream_encode_Level1 +#define cbor_encode_Range stream_encode_Range +#define cbor_encode_ValueRange stream_encode_ValueRange +#define cbor_encode_SingleBstr stream_encode_SingleBstr +#define cbor_encode_SingleInt_uint52 stream_encode_SingleInt_uint52 +#define cbor_encode_SingleInt2 stream_encode_SingleInt2 +#define cbor_encode_Unabstracted stream_encode_Unabstracted +#define cbor_encode_QuantityRange stream_encode_QuantityRange +#define cbor_encode_DoubleMap stream_encode_DoubleMap +#define cbor_encode_Floats stream_encode_Floats +#define cbor_encode_Floats2 stream_encode_Floats2 +#define cbor_encode_CBORBstr stream_encode_CBORBstr +#define cbor_encode_MapLength stream_encode_MapLength +#define cbor_encode_UnionInt2 stream_encode_UnionInt2 +#define cbor_encode_Intmax1 stream_encode_Intmax1 +#define cbor_encode_Intmax2 stream_encode_Intmax2 +#define cbor_encode_InvalidIdentifiers stream_encode_InvalidIdentifiers +#define cbor_encode_MapUnionPrimAlias stream_encode_MapUnionPrimAlias +#define cbor_encode_EmptyContainer stream_encode_EmptyContainer +#define cbor_encode_SingleElemList stream_encode_SingleElemList +#define cbor_encode_Choice1 stream_encode_Choice1 +#define cbor_encode_Choice2 stream_encode_Choice2 +#define cbor_encode_Choice3 stream_encode_Choice3 +#define cbor_encode_Choice4 stream_encode_Choice4 +#define cbor_encode_Choice5 stream_encode_Choice5 +#define cbor_encode_OptList stream_encode_OptList +#endif /* STREAMING */ ZTEST(cbor_encode_test3, test_numbers) { diff --git a/tests/encode/test3_corner_cases/testcase.yaml b/tests/encode/test3_corner_cases/testcase.yaml index a49b690f..fd4c71e0 100644 --- a/tests/encode/test3_corner_cases/testcase.yaml +++ b/tests/encode/test3_corner_cases/testcase.yaml @@ -6,6 +6,16 @@ tests: - mps2/an521/cpu0 - qemu_malta/qemu_malta/be tags: zcbor encode test3 + + zcbor.encode.test3_corner_cases.streaming: + platform_allow: + - native_sim + - native_sim/native/64 + - mps2/an521/cpu0 + - qemu_malta/qemu_malta/be + tags: zcbor encode streaming test3 + extra_args: STREAMING=1 + zcbor.encode.test3_corner_cases.canonical: platform_allow: - native_sim @@ -14,3 +24,12 @@ tests: - qemu_malta/qemu_malta/be tags: zcbor encode canonical test3 extra_args: CANONICAL=CANONICAL + + zcbor.encode.test3_corner_cases.streaming_canonical: + platform_allow: + - native_sim + - native_sim/native/64 + - mps2/an521/cpu0 + - qemu_malta/qemu_malta/be + tags: zcbor encode streaming canonical test3 + extra_args: STREAMING=1 CANONICAL=CANONICAL diff --git a/tests/encode/test4_senml/CMakeLists.txt b/tests/encode/test4_senml/CMakeLists.txt index d1cf0961..14291897 100644 --- a/tests/encode/test4_senml/CMakeLists.txt +++ b/tests/encode/test4_senml/CMakeLists.txt @@ -20,6 +20,10 @@ set(py_command ${bit_arg} ) +if (STREAMING) + list(APPEND py_command --stream-encode-functions) +endif() + execute_process( COMMAND ${py_command} COMMAND_ERROR_IS_FATAL ANY diff --git a/tests/encode/test4_senml/src/main.c b/tests/encode/test4_senml/src/main.c index 1ba0f7a0..2109dae2 100644 --- a/tests/encode/test4_senml/src/main.c +++ b/tests/encode/test4_senml/src/main.c @@ -12,6 +12,53 @@ #endif #include +#ifdef STREAMING +#include + +struct stream_ctx { + uint8_t *buf; + size_t size; + size_t pos; +}; + +static int stream_write(void *user_data, const uint8_t *data, size_t len) +{ + struct stream_ctx *ctx = (struct stream_ctx *)user_data; + + if (!ctx || !data) { + return -1; + } + if (len == 0) { + return 0; + } + if (ctx->pos + len > ctx->size) { + return -1; + } + + memcpy(&ctx->buf[ctx->pos], data, len); + ctx->pos += len; + return (int)len; +} + +static int stream_encode_lwm2m_senml(uint8_t *payload, size_t payload_len, + struct lwm2m_senml *input, size_t *out_len) +{ + struct stream_ctx ctx = { + .buf = payload, + .size = payload_len, + .pos = 0, + }; + int rc = cbor_stream_encode_lwm2m_senml(stream_write, &ctx, input, NULL, out_len); + + if (rc == ZCBOR_SUCCESS) { + zassert_equal(*out_len, ctx.pos, NULL); + } + return rc; +} + +#define cbor_encode_lwm2m_senml stream_encode_lwm2m_senml +#endif /* STREAMING */ + ZTEST(cbor_encode_test4, test_senml) { struct lwm2m_senml input = { diff --git a/tests/encode/test4_senml/testcase.yaml b/tests/encode/test4_senml/testcase.yaml index 57c91fcb..155ac536 100644 --- a/tests/encode/test4_senml/testcase.yaml +++ b/tests/encode/test4_senml/testcase.yaml @@ -6,6 +6,16 @@ tests: - mps2/an521/cpu0 - qemu_malta/qemu_malta/be tags: zcbor encode test4 + + zcbor.encode.test4_senml.streaming: + platform_allow: + - native_sim + - native_sim/native/64 + - mps2/an521/cpu0 + - qemu_malta/qemu_malta/be + tags: zcbor encode streaming test4 + extra_args: STREAMING=1 + zcbor.encode.test4_senml.canonical: platform_allow: - native_sim @@ -14,3 +24,12 @@ tests: - qemu_malta/qemu_malta/be tags: zcbor encode canonical test4 extra_args: CANONICAL=CANONICAL + + zcbor.encode.test4_senml.streaming_canonical: + platform_allow: + - native_sim + - native_sim/native/64 + - mps2/an521/cpu0 + - qemu_malta/qemu_malta/be + tags: zcbor encode streaming canonical test4 + extra_args: STREAMING=1 CANONICAL=CANONICAL diff --git a/tests/unit/test1_unit_tests/src/main.c b/tests/unit/test1_unit_tests/src/main.c index 990a0a25..ac3daf90 100644 --- a/tests/unit/test1_unit_tests/src/main.c +++ b/tests/unit/test1_unit_tests/src/main.c @@ -1024,10 +1024,12 @@ ZTEST(zcbor_unit_tests, test_error_str) test_str(ZCBOR_ERR_MAP_FLAGS_NOT_AVAILABLE); test_str(ZCBOR_ERR_INVALID_VALUE_ENCODING); test_str(ZCBOR_ERR_CONSTANT_STATE_MISSING); + test_str(ZCBOR_ERR_STREAM_WRITE_FAILED); + test_str(ZCBOR_ERR_STREAM_READ_FAILED); test_str(ZCBOR_ERR_UNKNOWN); zassert_mem_equal(zcbor_error_str(-1), "ZCBOR_ERR_UNKNOWN", sizeof("ZCBOR_ERR_UNKNOWN"), NULL); zassert_mem_equal(zcbor_error_str(-10), "ZCBOR_ERR_UNKNOWN", sizeof("ZCBOR_ERR_UNKNOWN"), NULL); - zassert_mem_equal(zcbor_error_str(ZCBOR_ERR_CONSTANT_STATE_MISSING + 1), "ZCBOR_ERR_UNKNOWN", sizeof("ZCBOR_ERR_UNKNOWN"), NULL); + zassert_mem_equal(zcbor_error_str(ZCBOR_ERR_STREAM_READ_FAILED + 1), "ZCBOR_ERR_UNKNOWN", sizeof("ZCBOR_ERR_UNKNOWN"), NULL); zassert_mem_equal(zcbor_error_str(100000), "ZCBOR_ERR_UNKNOWN", sizeof("ZCBOR_ERR_UNKNOWN"), NULL); }