Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package aws-c-mqtt for openSUSE:Factory checked in at 2026-05-26 16:34:59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/aws-c-mqtt (Old) and /work/SRC/openSUSE:Factory/.aws-c-mqtt.new.2084 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "aws-c-mqtt" Tue May 26 16:34:59 2026 rev:18 rq:1355151 version:0.16.0 Changes: -------- --- /work/SRC/openSUSE:Factory/aws-c-mqtt/aws-c-mqtt.changes 2026-03-27 06:36:39.702212956 +0100 +++ /work/SRC/openSUSE:Factory/.aws-c-mqtt.new.2084/aws-c-mqtt.changes 2026-05-26 16:35:13.210946473 +0200 @@ -1,0 +2,9 @@ +Fri May 22 07:55:57 UTC 2026 - John Paul Adrian Glaubitz <[email protected]> + +- Update to version 0.16.0 + * Metrics metadata by @xiazhvera in (#422) + * builder -> v0.9.92 and clang-latest by @sbSteveK in (#424) + * Refactor IoT metrics parsing by @xiazhvera in (#423) + * Keep metadata param order by @xiazhvera in (#426) + +------------------------------------------------------------------- Old: ---- v0.15.2.tar.gz New: ---- v0.16.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ aws-c-mqtt.spec ++++++ --- /var/tmp/diff_new_pack.Buh9KS/_old 2026-05-26 16:35:14.122984206 +0200 +++ /var/tmp/diff_new_pack.Buh9KS/_new 2026-05-26 16:35:14.126984371 +0200 @@ -18,7 +18,7 @@ %global library_version 1_0_0 Name: aws-c-mqtt -Version: 0.15.2 +Version: 0.16.0 Release: 0 Summary: AWS C99 implementation of the MQTT 3.1.1 specification License: Apache-2.0 ++++++ v0.15.2.tar.gz -> v0.16.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/.github/workflows/ci.yml new/aws-c-mqtt-0.16.0/.github/workflows/ci.yml --- old/aws-c-mqtt-0.15.2/.github/workflows/ci.yml 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/.github/workflows/ci.yml 2026-05-18 20:49:17.000000000 +0200 @@ -6,7 +6,7 @@ - 'main' env: - BUILDER_VERSION: v0.9.73 + BUILDER_VERSION: v0.9.92 BUILDER_SOURCE: releases BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net PACKAGE_NAME: aws-c-mqtt @@ -55,6 +55,7 @@ - clang-11 - clang-15 - clang-17 + - clang-latest - gcc-4.8 - gcc-5 - gcc-6 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/include/aws/mqtt/client.h new/aws-c-mqtt-0.16.0/include/aws/mqtt/client.h --- old/aws-c-mqtt-0.15.2/include/aws/mqtt/client.h 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/include/aws/mqtt/client.h 2026-05-18 20:49:17.000000000 +0200 @@ -697,8 +697,6 @@ * Sets IoT SDK metrics configuration for the connection. * These metrics will be appended to the username field during connection. * - * NOTE: DO NOT USE METADATA. Metadata will not be set. - * * \param connection The connection object * \param metrics The IoT SDK metrics configuration (pass NULL to disable metrics) * \returns AWS_OP_SUCCESS if successful, AWS_OP_ERR otherwise diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/include/aws/mqtt/mqtt.h new/aws-c-mqtt-0.16.0/include/aws/mqtt/mqtt.h --- old/aws-c-mqtt-0.15.2/include/aws/mqtt/mqtt.h 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/include/aws/mqtt/mqtt.h 2026-05-18 20:49:17.000000000 +0200 @@ -127,6 +127,12 @@ * Library name string (SDK attribute) */ struct aws_byte_cursor library_name; + + /** + * Metadata entries, key value pair to set in metrics "Metadata" field + */ + size_t metadata_count; + const struct aws_mqtt_metadata_entry *metadata_entries; }; AWS_EXTERN_C_BEGIN diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/include/aws/mqtt/private/mqtt_iot_metrics.h new/aws-c-mqtt-0.16.0/include/aws/mqtt/private/mqtt_iot_metrics.h --- old/aws-c-mqtt-0.15.2/include/aws/mqtt/private/mqtt_iot_metrics.h 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/include/aws/mqtt/private/mqtt_iot_metrics.h 2026-05-18 20:49:17.000000000 +0200 @@ -16,6 +16,8 @@ struct aws_byte_cursor library_name; + struct aws_array_list metadata_entries; + struct aws_byte_buf storage; }; @@ -54,4 +56,17 @@ AWS_MQTT_API int aws_mqtt_validate_iot_metrics(const struct aws_mqtt_iot_metrics *metrics); +/** + * Checks if username should be included in the CONNECT packet. + * Returns true if either username is provided or metrics are configured. + * + * @param username The username cursor (can be NULL) + * @param metrics_storage The metrics storage (can be NULL) + * @return true if username should be included, false otherwise + */ +AWS_MQTT_API +bool aws_mqtt_has_non_empty_username( + const struct aws_byte_cursor *username, + const struct aws_mqtt_iot_metrics_storage *metrics_storage); + #endif /* AWS_MQTT_IOT_METRICS_H */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/source/client.c new/aws-c-mqtt-0.16.0/source/client.c --- old/aws-c-mqtt-0.15.2/source/client.c 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/source/client.c 2026-05-18 20:49:17.000000000 +0200 @@ -635,12 +635,13 @@ &connect, topic_cur, connection->will.qos, connection->will.retain, payload_cur); } - if (connection->username || connection->metrics_storage) { - struct aws_byte_cursor username_cur; - AWS_ZERO_STRUCT(username_cur); - if (connection->username) { - username_cur = aws_byte_cursor_from_string(connection->username); - } + struct aws_byte_cursor username_cur; + AWS_ZERO_STRUCT(username_cur); + if (connection->username) { + username_cur = aws_byte_cursor_from_string(connection->username); + } + + if (aws_mqtt_has_non_empty_username(&username_cur, connection->metrics_storage)) { /* Apply metrics to username if configured */ if (connection->metrics_storage) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/source/mqtt_iot_metrics.c new/aws-c-mqtt-0.16.0/source/mqtt_iot_metrics.c --- old/aws-c-mqtt-0.15.2/source/mqtt_iot_metrics.c 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/source/mqtt_iot_metrics.c 2026-05-18 20:49:17.000000000 +0200 @@ -15,86 +15,324 @@ const size_t AWS_IOT_MAX_USERNAME_SIZE = UINT16_MAX; const size_t DEFAULT_QUERY_PARAM_COUNT = 10; +/********************************************************************************************************************* + * Helper functions for parsing and merging key-value pairs + ********************************************************************************************************************/ + /** - * Builds final username with query parameters appended. + * Find a parameter by key in a list of aws_uri_param. * - * @param base_username The original username cursor - * @param base_username_length Length of base username to use (may differ from cursor length) - * @param params_list List of query parameters to append - * @param output_username [Optional] Buffer to write result. Caller must init/cleanup the buffer. - * @param out_final_username_size [Optional] Outputs the final username size - * - * @return AWS_OP_SUCCESS on success, AWS_OP_ERR on failure + * @param params_list List of aws_uri_param to search + * @param key The key to search for + * @param out_index [Optional] If not NULL and key is found, will be set to the index of the found parameter + * @return Pointer to the found parameter, or NULL if not found */ -int s_build_username_query( - const struct aws_byte_cursor *base_username, - size_t base_username_length, +static struct aws_uri_param *s_find_param_by_key( const struct aws_array_list *params_list, - struct aws_byte_buf *output_username, - size_t *out_final_username_size) { + const struct aws_byte_cursor key, + size_t *out_index) { - AWS_ASSERT(base_username); - AWS_ASSERT(params_list); + size_t params_count = aws_array_list_length(params_list); + for (size_t i = 0; i < params_count; ++i) { + struct aws_uri_param *param = NULL; + aws_array_list_get_at_ptr(params_list, (void **)¶m, i); + if (aws_byte_cursor_eq(¶m->key, &key)) { + if (out_index) { + *out_index = i; + } + return param; + } + } + return NULL; +} - if (output_username) { - if (!aws_byte_buf_write(output_username, base_username->ptr, base_username_length)) { - return AWS_OP_ERR; +/** + * Add a parameter to the list only if a parameter with the same key doesn't already exist. + * + * @param params_list List of aws_uri_param to add to + * @param key The key for the new parameter + * @param value The value for the new parameter + * @return true if the parameter was added, false if a parameter with the same key already exists + */ +static bool s_add_param_if_not_exists( + struct aws_array_list *params_list, + const struct aws_byte_cursor key, + const struct aws_byte_cursor value) { + + if (s_find_param_by_key(params_list, key, NULL) != NULL) { + return false; + } + + struct aws_uri_param param = { + .key = key, + .value = value, + }; + aws_array_list_push_back(params_list, ¶m); + return true; +} + +/** + * Parse key-value entries from a aws_byte_cursor using a specified delimiter. + * Each entry is expected to be in the format "key=value" or just "key". (e.g., "key1=value1&key2=value2") + * + * @param content The content to parse + * @param delimiter The delimiter cursor (e.g., ";" or "&"), length of delimiter must > 0. + * @param out_entries List to populate with parsed aws_uri_param entries. Must be initialized by caller. + * @return AWS_OP_SUCCESS on success, AWS_OP_ERR on failure + */ +static int s_parse_delimited_entries( + const struct aws_byte_cursor content, + const struct aws_byte_cursor delimiter, + struct aws_array_list *out_entries) { + + AWS_ASSERT(delimiter.len > 0); + + struct aws_byte_cursor equals_delim = aws_byte_cursor_from_c_str("="); + struct aws_byte_cursor entry_cursor; + AWS_ZERO_STRUCT(entry_cursor); + + while (aws_byte_cursor_next_split_on_cursor(&content, delimiter, &entry_cursor)) { + /* Skip empty entries */ + if (entry_cursor.len == 0) { + continue; + } + + /* Parse key=value from entry_cursor */ + struct aws_uri_param entry_param; + AWS_ZERO_STRUCT(entry_param); + + /* Use a copy to avoid modifying entry_cursor which tracks iteration state */ + struct aws_byte_cursor working_cursor = entry_cursor; + struct aws_byte_cursor equals_pos; + if (aws_byte_cursor_find_exact(&working_cursor, &equals_delim, &equals_pos) == AWS_OP_SUCCESS) { + size_t equals_offset = equals_pos.ptr - working_cursor.ptr; + entry_param.key = aws_byte_cursor_advance(&working_cursor, equals_offset); + /* Skip the "=" delimiter */ + aws_byte_cursor_advance(&working_cursor, 1); + entry_param.value = working_cursor; + } else { + /* No equals sign, treat entire entry as key */ + entry_param.key = entry_cursor; } + + aws_array_list_push_back(out_entries, &entry_param); } - if (out_final_username_size) { - *out_final_username_size = base_username_length; + return AWS_OP_SUCCESS; +} + +/** + * Merge new entries into an existing entries list without overwriting existing keys. + * + * @param existing_metadata_list List of existing aws_uri_param entries (will be modified) + * @param new_entries Array of new entries to merge + * @param new_entries_count Number of new entries + */ +static void s_merge_metadata_entries_no_overwrite( + struct aws_array_list *existing_metadata_list, + const struct aws_mqtt_metadata_entry *new_entries_list, + const size_t new_entries_list_count) { + + for (size_t i = 0; i < new_entries_list_count; ++i) { + const struct aws_mqtt_metadata_entry *new_entry = &new_entries_list[i]; + s_add_param_if_not_exists(existing_metadata_list, new_entry->key, new_entry->value); } +} - struct aws_byte_cursor query_delim = aws_byte_cursor_from_c_str("?"); - struct aws_byte_cursor query_param_amp = aws_byte_cursor_from_c_str("&"); +/** + * Build a delimited key-value string from a list of parameters. + * Can be used to either calculate the size, build the string, or both. + * + * Format: {prefix}{key1}={value1}{delimiter}{key2}={value2}...{suffix} + * If params_list is empty, returns empty string (no prefix/suffix). + * + * @param params_list List of aws_uri_param entries + * @param prefix [Optional] String to prepend (e.g., "?" for query strings, "(" for metadata) + * @param suffix [Optional] String to append (e.g., ")" for metadata) + * @param entry_delimiter Delimiter between entries (e.g., "&" for query strings, ";" for metadata) + * @param output_buf [Optional] Buffer to write the result to. Must be initialized with sufficient capacity. + * @param out_size [Optional] If not NULL, will be set to the size of the result string + * @return AWS_OP_SUCCESS on success, AWS_OP_ERR on failure + */ +static int s_build_delimited_params( + const struct aws_array_list *params_list, + const struct aws_byte_cursor *prefix, + const struct aws_byte_cursor *suffix, + const struct aws_byte_cursor entry_delimiter, + struct aws_byte_buf *output_buf, + size_t *out_size) { + + AWS_ASSERT(entry_delimiter.len > 0); + + size_t local_out_size = 0; struct aws_byte_cursor key_value_delim = aws_byte_cursor_from_c_str("="); size_t params_count = aws_array_list_length(params_list); + + /* If params_list is empty, return empty string (no prefix/suffix) */ + if (params_count == 0) { + if (out_size) { + *out_size = 0; + } + return AWS_OP_SUCCESS; + } + + if (prefix) { + local_out_size += prefix->len; + } + if (suffix) { + local_out_size += suffix->len; + } + + if (output_buf && prefix && prefix->len > 0) { + if (aws_byte_buf_append(output_buf, prefix)) { + return AWS_OP_ERR; + } + } + for (size_t i = 0; i < params_count; ++i) { struct aws_uri_param param; AWS_ZERO_STRUCT(param); aws_array_list_get_at(params_list, ¶m, i); - if (output_username) { - if (i == 0) { - aws_byte_buf_append(output_username, &query_delim); - } else { - aws_byte_buf_append(output_username, &query_param_amp); + /* Add delimiter between entries (not before the first one) */ + if (i > 0) { + if (output_buf) { + if (aws_byte_buf_append(output_buf, &entry_delimiter)) { + return AWS_OP_ERR; + } } + local_out_size += entry_delimiter.len; } - if (out_final_username_size) { - *out_final_username_size += 1; + /* Append key */ + if (output_buf && param.key.len > 0) { + if (aws_byte_buf_append(output_buf, ¶m.key)) { + return AWS_OP_ERR; + } } + local_out_size += param.key.len; - if (output_username) { - if (param.key.len > 0) { - // append key if key exists - if (aws_byte_buf_append(output_username, ¶m.key)) { + /* Append =value if value exists */ + if (param.value.len > 0) { + if (output_buf) { + if (aws_byte_buf_append(output_buf, &key_value_delim) || + aws_byte_buf_append(output_buf, ¶m.value)) { return AWS_OP_ERR; } } + local_out_size += 1 + param.value.len; /* '=' + value */ + } + } - // append value if value exists - if (param.value.len > 0) - // Note: If value exists without a key, append "=" and value (e.g., "?=abc"). - // Please note server treats "a=", "a", and "=a" equivalently. - if ((aws_byte_buf_append(output_username, &key_value_delim)) || - aws_byte_buf_append(output_username, ¶m.value)) { - return AWS_OP_ERR; - } + if (output_buf && suffix && suffix->len > 0) { + if (aws_byte_buf_append(output_buf, suffix)) { + return AWS_OP_ERR; } + } + + if (out_size) { + *out_size = local_out_size; + } - if (out_final_username_size) { - *out_final_username_size += param.key.len + (param.value.len > 0 ? 1 : 0) + param.value.len; + return AWS_OP_SUCCESS; +} + +/** + * Build the metadata value string from entries: (key1=value1;key2=value2;...) + * Can be used to either calculate the size, build the string, or both. + * + * @param entries List of aws_uri_param entries + * @param output_buf [Optional] Buffer to write the metadata value to. Must be initialized with sufficient capacity. + * @param out_size [Optional] If not NULL, will be set to the size of the metadata value string + * @return AWS_OP_SUCCESS on success, AWS_OP_ERR on failure + */ +static int s_build_metadata_value( + const struct aws_array_list *entries, + struct aws_byte_buf *output_buf, + size_t *out_size) { + + struct aws_byte_cursor open_paren = aws_byte_cursor_from_c_str("("); + struct aws_byte_cursor close_paren = aws_byte_cursor_from_c_str(")"); + struct aws_byte_cursor semicolon_delim = aws_byte_cursor_from_c_str(";"); + + return s_build_delimited_params(entries, &open_paren, &close_paren, semicolon_delim, output_buf, out_size); +} + +/** + * Builds final username with query parameters appended. + * + * @param base_username The original username cursor + * @param base_username_length Length of base username to use (may differ from cursor length) + * @param params_list List of query parameters to append + * @param output_username [Optional] Buffer to write result. Caller must init/cleanup the buffer. + * @param out_final_username_size [Optional] Outputs the final username size + * + * @return AWS_OP_SUCCESS on success, AWS_OP_ERR on failure + */ +static int s_build_username_query( + const struct aws_byte_cursor base_username, + const size_t base_username_length, + const struct aws_array_list *params_list, + struct aws_byte_buf *output_username, + size_t *out_final_username_size) { + + /* Write base username first */ + if (output_username) { + if (!aws_byte_buf_write(output_username, base_username.ptr, base_username_length)) { + return AWS_OP_ERR; } } + if (out_final_username_size) { + *out_final_username_size = base_username_length; + } + + /* Build query parameters using the generic helper */ + struct aws_byte_cursor query_prefix = aws_byte_cursor_from_c_str("?"); + struct aws_byte_cursor ampersand_delim = aws_byte_cursor_from_c_str("&"); + + size_t params_size = 0; + if (s_build_delimited_params(params_list, &query_prefix, NULL, ampersand_delim, output_username, ¶ms_size)) { + return AWS_OP_ERR; + } + + if (out_final_username_size) { + *out_final_username_size += params_size; + } + return AWS_OP_SUCCESS; } -// TODO Future Work: we ignored the metadata field for now, will add them in future support +/** + * Appends SDK metrics to an MQTT username for IoT service telemetry. + * + * This function transforms a username by appending SDK metrics as query parameters. + * The IoT service uses the last '?' in the username to identify the metrics section. + * + * Step-by-step process: + * 1. Parse existing username: Find the last '?' in the original username and parse query params into + * username_params_list + * 2. Add "SDK" and "Platform" parameters if not already present + * 3. Handle Metadata entries if metadata entries are provided + * a. Check if existing "Metadata" parameter exists in username_params_list + * b. If existing Metadata found with valid format: + * - Parse existing entries into a key-value metadata_param_list + * - Remove existing Metadata from username_params_list (will be rebuilt later) + * c. If existing Metadata has an invalid format: + * - Log debug message and skip adding metrics metadata entries + * - Keep original Metadata value unchanged, skip (d) + * d. Append metrics metadata entries to metadata_param_list + * - Add new metadata entries into metadata_param_list (existing keys take precedence, won't be overwritten) + * - build metadata value string from metadata_param_list + * - Add Metadata parameter to username_params_list + * 4. Build final username from username_params_list + * + * Example transformation: + * Input: "myuser?existing=value" + * Metrics: {library_name="MySDK/1.0", metadata=[{key="ver", value="1.0"}]} + * Output: "myuser?existing=value&SDK=MySDK/1.0&Platform=Darwin&Metadata=(ver=1.0)" + */ int aws_mqtt_append_sdk_metrics_to_username( struct aws_allocator *allocator, const struct aws_byte_cursor *original_username, @@ -128,22 +366,29 @@ } int result = AWS_OP_ERR; - // The length of the base username part not including query parameters size_t base_username_length = 0; struct aws_byte_cursor question_mark_str = aws_byte_cursor_from_c_str("?"); struct aws_byte_cursor sdk_str = aws_byte_cursor_from_c_str("SDK"); struct aws_byte_cursor platform_str = aws_byte_cursor_from_c_str("Platform"); + struct aws_byte_cursor metadata_str = aws_byte_cursor_from_c_str("Metadata"); + + struct aws_array_list username_params_list; + aws_array_list_init_dynamic( + &username_params_list, allocator, DEFAULT_QUERY_PARAM_COUNT, sizeof(struct aws_uri_param)); - struct aws_array_list params_list; - aws_array_list_init_dynamic(¶ms_list, allocator, DEFAULT_QUERY_PARAM_COUNT, sizeof(struct aws_uri_param)); + struct aws_byte_buf metadata_value_buf; + AWS_ZERO_STRUCT(metadata_value_buf); - // Looking for any existing query in the original username + struct aws_array_list metadata_param_list; + AWS_ZERO_STRUCT(metadata_param_list); + + /* Parse existing query parameters from the original username */ if (local_original_username.len > 0) { struct aws_byte_cursor question_mark_find = local_original_username; bool found_query = false; - // Find the last question mark. The IoT service will trim string after last question mark and handle it as - // metrics metadata + /* Find the last question mark. The IoT service will trim string after last question mark and handle it as + * metrics metadata */ while (AWS_OP_SUCCESS == aws_byte_cursor_find_exact(&question_mark_find, &question_mark_str, &question_mark_find)) { // Advance cursor to skip the "?" character @@ -154,53 +399,98 @@ if (found_query) { // Trim out the query delimiter from the base username base_username_length = question_mark_find.ptr - 1 - local_original_username.ptr; - aws_query_string_params(question_mark_find, ¶ms_list); + aws_query_string_params(question_mark_find, &username_params_list); } else { base_username_length = local_original_username.len; } } - // Verify if the username already contains "SDK" and "Platform" fields. - bool found_sdk = false; - bool found_platform = false; - - size_t params_count = aws_array_list_length(¶ms_list); - for (size_t i = 0; i < params_count; ++i) { - struct aws_uri_param param; - AWS_ZERO_STRUCT(param); - aws_array_list_get_at(¶ms_list, ¶m, i); - if (aws_byte_cursor_eq(¶m.key, &sdk_str)) { - found_sdk = true; - } else if (aws_byte_cursor_eq(¶m.key, &platform_str)) { - found_platform = true; + /* Add SDK parameter if not already present */ + struct aws_byte_cursor sdk_value = + metrics->library_name.len > 0 ? metrics->library_name : aws_byte_cursor_from_c_str("IoTDeviceSDK/C"); + s_add_param_if_not_exists(&username_params_list, sdk_str, sdk_value); + + /* Add Platform parameter if not already present */ + s_add_param_if_not_exists(&username_params_list, platform_str, aws_get_platform_build_os_string()); + + /* Handle metadata entries: parse existing, merge with new, and rebuild */ + bool has_new_metadata = metrics->metadata_entries != NULL && metrics->metadata_count > 0; + + if (has_new_metadata) { + struct aws_byte_cursor semicolon_delim = aws_byte_cursor_from_c_str(";"); + bool should_merge_metadata = true; + + aws_array_list_init_dynamic( + &metadata_param_list, allocator, DEFAULT_QUERY_PARAM_COUNT, sizeof(struct aws_uri_param)); + + /* Find existing Metadata parameter */ + size_t existing_metadata_index = 0; + struct aws_uri_param *existing_metadata_param = + s_find_param_by_key(&username_params_list, metadata_str, &existing_metadata_index); + + if (existing_metadata_param != NULL && existing_metadata_param->value.len > 0) { + /* Extract content without parentheses: (key1=value1;key2=value2) -> key1=value1;key2=value2 */ + struct aws_byte_cursor existing_content = existing_metadata_param->value; + if (existing_content.len >= 2 && existing_content.ptr[0] == '(' && + existing_content.ptr[existing_content.len - 1] == ')') { + aws_byte_cursor_advance(&existing_content, 1); + existing_content.len -= 1; + + /* Parse existing metadata entries */ + if (existing_content.len > 0) { + s_parse_delimited_entries(existing_content, semicolon_delim, &metadata_param_list); + } + } else { + /* Wrong format: keep the original metadata value unchanged and don't append new metadata */ + AWS_LOGF_DEBUG( + AWS_LS_MQTT_GENERAL, + "Existing Metadata parameter has invalid format (expected parentheses). " + "Keeping original value and skipping new metadata entries."); + should_merge_metadata = false; + } } - } - if (!found_sdk) { - struct aws_uri_param sdk_params = { - .key = sdk_str, - .value = - metrics->library_name.len > 0 ? metrics->library_name : aws_byte_cursor_from_c_str("IoTDeviceSDK/C"), - }; - aws_array_list_push_back(¶ms_list, &sdk_params); - } + if (should_merge_metadata) { + /* Merge new metadata entries (won't overwrite existing keys) */ + s_merge_metadata_entries_no_overwrite( + &metadata_param_list, metrics->metadata_entries, metrics->metadata_count); + + /* Calculate size needed for metadata value string first */ + size_t metadata_value_size = 0; + s_build_metadata_value(&metadata_param_list, NULL, &metadata_value_size); + + /* Initialize buffer and build the metadata value string */ + if (aws_byte_buf_init(&metadata_value_buf, allocator, metadata_value_size)) { + goto cleanup; + } + + if (s_build_metadata_value(&metadata_param_list, &metadata_value_buf, NULL)) { + goto cleanup; + } - if (!found_platform) { - struct aws_uri_param platform_params = { - .key = platform_str, - .value = aws_get_platform_build_os_string(), - }; - aws_array_list_push_back(¶ms_list, &platform_params); + /* Add or replace Metadata parameter in username_params_list */ + struct aws_uri_param metadata_params = { + .key = metadata_str, + .value = aws_byte_cursor_from_buf(&metadata_value_buf), + }; + if (existing_metadata_param != NULL) { + /* Replace existing Metadata parameter at the same index */ + aws_array_list_set_at(&username_params_list, &metadata_params, existing_metadata_index); + } else { + aws_array_list_push_back(&username_params_list, &metadata_params); + } + } } - // Rebuild metrics string from params_list - // First parse to get final username size + /* Rebuild metrics string from username_params_list */ + // Calculate the final username size first. size_t total_size = 0; - s_build_username_query(&local_original_username, base_username_length, ¶ms_list, NULL, &total_size); + s_build_username_query(local_original_username, base_username_length, &username_params_list, NULL, &total_size); if (total_size > AWS_IOT_MAX_USERNAME_SIZE) { AWS_LOGF_ERROR( - AWS_LL_DEBUG, "Failed to append SDK metrics to username: resulting username exceeds max username size."); + AWS_LS_MQTT_GENERAL, + "Failed to append SDK metrics to username: resulting username exceeds max username size."); aws_raise_error(AWS_ERROR_INVALID_ARGUMENT); goto cleanup; } @@ -209,16 +499,23 @@ goto cleanup; } - // build final output username if (s_build_username_query( - &local_original_username, base_username_length, ¶ms_list, output_username, out_full_username_size)) { + local_original_username, + base_username_length, + &username_params_list, + output_username, + out_full_username_size)) { goto cleanup; } result = AWS_OP_SUCCESS; cleanup: - aws_array_list_clean_up(¶ms_list); + aws_byte_buf_clean_up(&metadata_value_buf); + aws_array_list_clean_up(&username_params_list); + if (aws_array_list_is_valid(&metadata_param_list)) { + aws_array_list_clean_up(&metadata_param_list); + } if (result == AWS_OP_ERR) { aws_byte_buf_clean_up(output_username); @@ -238,6 +535,15 @@ size_t storage_size = 0; storage_size += metrics->library_name.len; + /* Add storage for metadata entries */ + if (metrics->metadata_entries != NULL && metrics->metadata_count > 0) { + for (size_t i = 0; i < metrics->metadata_count; ++i) { + const struct aws_mqtt_metadata_entry *entry = &metrics->metadata_entries[i]; + storage_size += entry->key.len; + storage_size += entry->value.len; + } + } + return storage_size; } @@ -270,13 +576,49 @@ storage_view->library_name = metrics_storage->library_name; } + /* Copy metadata entries */ + if (metrics_options->metadata_entries != NULL && metrics_options->metadata_count > 0) { + if (aws_array_list_init_dynamic( + &metrics_storage->metadata_entries, + allocator, + metrics_options->metadata_count, + sizeof(struct aws_mqtt_metadata_entry))) { + goto cleanup_storage; + } + + for (size_t i = 0; i < metrics_options->metadata_count; ++i) { + struct aws_mqtt_metadata_entry entry; + entry.key = metrics_options->metadata_entries[i].key; + entry.value = metrics_options->metadata_entries[i].value; + + /* Copy key into storage buffer and update cursor */ + if (entry.key.len > 0) { + if (aws_byte_buf_append_and_update(&metrics_storage->storage, &entry.key)) { + goto cleanup_storage; + } + } + + /* Copy value into storage buffer and update cursor */ + if (entry.value.len > 0) { + if (aws_byte_buf_append_and_update(&metrics_storage->storage, &entry.value)) { + goto cleanup_storage; + } + } + + aws_array_list_push_back(&metrics_storage->metadata_entries, &entry); + } + + /* Set storage_view to point to the metadata entries array */ + storage_view->metadata_count = aws_array_list_length(&metrics_storage->metadata_entries); + storage_view->metadata_entries = metrics_storage->metadata_entries.data; + } + return metrics_storage; cleanup_storage: - // TODO Future Work: add metadata entries once we implemented the metadata feature - // if (aws_array_list_is_valid(&metrics_storage->metadata_entries)) { - // aws_array_list_clean_up(&metrics_storage->metadata_entries); - // } + if (aws_array_list_is_valid(&metrics_storage->metadata_entries)) { + aws_array_list_clean_up(&metrics_storage->metadata_entries); + } aws_byte_buf_clean_up(&metrics_storage->storage); aws_mem_release(allocator, metrics_storage); @@ -288,6 +630,10 @@ return; } + if (aws_array_list_is_valid(&metrics_storage->metadata_entries)) { + aws_array_list_clean_up(&metrics_storage->metadata_entries); + } + aws_byte_buf_clean_up(&metrics_storage->storage); aws_mem_release(metrics_storage->allocator, metrics_storage); @@ -302,5 +648,23 @@ return AWS_OP_ERR; } + /* Validate metadata entries */ + if (metrics->metadata_entries != NULL && metrics->metadata_count > 0) { + for (size_t i = 0; i < metrics->metadata_count; ++i) { + if (aws_mqtt_validate_utf8_text(metrics->metadata_entries[i].key)) { + return AWS_OP_ERR; + } + if (aws_mqtt_validate_utf8_text(metrics->metadata_entries[i].value)) { + return AWS_OP_ERR; + } + } + } + return AWS_OP_SUCCESS; } + +bool aws_mqtt_has_non_empty_username( + const struct aws_byte_cursor *username, + const struct aws_mqtt_iot_metrics_storage *metrics_storage) { + return (username != NULL && username->len > 0) || metrics_storage != NULL; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/source/v5/mqtt5_options_storage.c new/aws-c-mqtt-0.16.0/source/v5/mqtt5_options_storage.c --- old/aws-c-mqtt-0.15.2/source/v5/mqtt5_options_storage.c 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/source/v5/mqtt5_options_storage.c 2026-05-18 20:49:17.000000000 +0200 @@ -645,7 +645,9 @@ size_t storage_size = 0; storage_size += view->client_id.len; - if (view->username != NULL) { + + if (aws_mqtt_has_non_empty_username(view->username, options ? options->metrics_storage : NULL)) { + if (options) { size_t username_size = 0; aws_mqtt_append_sdk_metrics_to_username( @@ -699,17 +701,19 @@ return AWS_OP_ERR; } - if (view->username != NULL) { - storage->username = *view->username; + if (aws_mqtt_has_non_empty_username( + view->username, client_options_storage ? client_options_storage->metrics_storage : NULL)) { + if (view->username) { + storage->username = *view->username; + } struct aws_byte_buf metrics_username_buf; AWS_ZERO_STRUCT(metrics_username_buf); /* Apply metrics to username if configured */ if (client_options_storage) { - struct aws_byte_cursor username_cur = storage->username; if (aws_mqtt_append_sdk_metrics_to_username( allocator, - &username_cur, + &storage->username, client_options_storage->metrics_storage ? &client_options_storage->metrics_storage->storage_view : NULL, &metrics_username_buf, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/tests/CMakeLists.txt new/aws-c-mqtt-0.16.0/tests/CMakeLists.txt --- old/aws-c-mqtt-0.15.2/tests/CMakeLists.txt 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/tests/CMakeLists.txt 2026-05-18 20:49:17.000000000 +0200 @@ -115,6 +115,7 @@ # Connection state tests add_test_case(mqtt_connection_set_metrics_valid) add_test_case(mqtt_connection_set_metrics_null) +add_test_case(mqtt_connection_set_metrics_with_null_username) add_test_case(mqtt_connection_set_metrics_invalid_utf8_library) add_test_case(mqtt_connection_set_metrics_modify_on_reconnect) @@ -149,6 +150,13 @@ add_test_case(mqtt_append_sdk_metrics_long_strings) add_test_case(mqtt_append_sdk_metrics_invalid_utf8) add_test_case(mqtt_append_sdk_metrics_multiple_question_mark) +add_test_case(mqtt_append_sdk_metrics_with_metadata) +add_test_case(mqtt_append_sdk_metrics_with_metadata_invalid_utf8) +add_test_case(mqtt_append_sdk_metrics_with_metadata_invalid_utf8_value) +add_test_case(mqtt_iot_metrics_storage_with_metadata) +add_test_case(mqtt_iot_metrics_storage_empty_metadata) +add_test_case(mqtt_append_sdk_metrics_existing_metadata) +add_test_case(mqtt_append_sdk_metrics_existing_metadata_no_new) # topic aliasing add_test_case(mqtt5_inbound_topic_alias_register_failure) @@ -414,6 +422,7 @@ # Mqtt5 Metrics tests add_test_case(mqtt5_client_set_metrics_valid) add_test_case(mqtt5_client_set_metrics_null) +add_test_case(mqtt5_client_set_metrics_with_null_username) add_test_case(rate_limiter_token_bucket_init_invalid) add_test_case(rate_limiter_token_bucket_regeneration_integral) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/tests/shared_utils_tests.c new/aws-c-mqtt-0.16.0/tests/shared_utils_tests.c --- old/aws-c-mqtt-0.15.2/tests/shared_utils_tests.c 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/tests/shared_utils_tests.c 2026-05-18 20:49:17.000000000 +0200 @@ -175,7 +175,7 @@ struct aws_byte_buf expected_buf; AWS_ZERO_STRUCT(expected_buf); aws_test_mqtt_build_expected_metrics( - allocator, NULL, aws_byte_cursor_from_c_str("IoTDeviceSDK/C"), NULL, &expected_buf); + allocator, NULL, aws_byte_cursor_from_c_str("IoTDeviceSDK/C"), NULL, NULL, 0, &expected_buf); ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&output_cursor, &expected_buf)); @@ -192,9 +192,6 @@ struct aws_mqtt_iot_metrics metrics = { .library_name = aws_byte_cursor_from_c_str("TEST_SDK_STRING"), - // TODO: add metadata entries when enabled - // .metadata_entries = NULL, - // .metadata_count = 0, }; struct aws_byte_buf output_username; @@ -211,7 +208,7 @@ struct aws_byte_buf expected_buf; AWS_ZERO_STRUCT(expected_buf); aws_test_mqtt_build_expected_metrics( - allocator, &original_username, aws_byte_cursor_from_c_str("TEST_SDK_STRING"), NULL, &expected_buf); + allocator, &original_username, aws_byte_cursor_from_c_str("TEST_SDK_STRING"), NULL, NULL, 0, &expected_buf); ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&output_cursor, &expected_buf)); @@ -228,9 +225,6 @@ struct aws_mqtt_iot_metrics metrics = { .library_name = aws_byte_cursor_from_c_str("NewSDK"), - // TODO: add metadata entries when enabled - // .metadata_entries = NULL, - // .metadata_count = 0, }; struct aws_byte_buf output_username; @@ -272,7 +266,7 @@ struct aws_byte_buf expected_buf; AWS_ZERO_STRUCT(expected_buf); aws_test_mqtt_build_expected_metrics( - allocator, &original_username, aws_byte_cursor_from_c_str("SDK/Test-1.0"), NULL, &expected_buf); + allocator, &original_username, aws_byte_cursor_from_c_str("SDK/Test-1.0"), NULL, NULL, 0, &expected_buf); ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&output_cursor, &expected_buf)); @@ -304,7 +298,7 @@ struct aws_byte_buf expected_buf; AWS_ZERO_STRUCT(expected_buf); aws_test_mqtt_build_expected_metrics( - allocator, &base_username, aws_byte_cursor_from_c_str("SDK/Test-1.0"), NULL, &expected_buf); + allocator, &base_username, aws_byte_cursor_from_c_str("SDK/Test-1.0"), NULL, NULL, 0, &expected_buf); ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&output_cursor, &expected_buf)); @@ -376,3 +370,270 @@ } AWS_TEST_CASE(mqtt_append_sdk_metrics_invalid_utf8, s_test_mqtt_append_sdk_metrics_invalid_utf8) + +static int s_test_mqtt_append_sdk_metrics_with_metadata(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt_metadata_entry metadata_entries[] = { + { + .key = aws_byte_cursor_from_c_str("CustomKey1"), + .value = aws_byte_cursor_from_c_str("CustomValue1"), + }, + { + .key = aws_byte_cursor_from_c_str("CustomKey2"), + .value = aws_byte_cursor_from_c_str("CustomValue2"), + }, + }; + + struct aws_mqtt_iot_metrics metrics = { + .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), + .metadata_count = AWS_ARRAY_SIZE(metadata_entries), + .metadata_entries = metadata_entries, + }; + + struct aws_byte_buf output_username; + AWS_ZERO_STRUCT(output_username); + + struct aws_byte_cursor original_username = aws_byte_cursor_from_c_str("testuser"); + + ASSERT_SUCCESS( + aws_mqtt_append_sdk_metrics_to_username(allocator, &original_username, &metrics, &output_username, NULL)); + + struct aws_byte_cursor output_cursor = aws_byte_cursor_from_buf(&output_username); + + /* Verify the output contains the metadata in the format: &Metadata=(key1=value1;key2=value2) */ + struct aws_byte_cursor metadata_format = + aws_byte_cursor_from_c_str("&Metadata=(CustomKey1=CustomValue1;CustomKey2=CustomValue2)"); + struct aws_byte_cursor found; + + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&output_cursor, &metadata_format, &found)); + + aws_byte_buf_clean_up(&output_username); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_append_sdk_metrics_with_metadata, s_test_mqtt_append_sdk_metrics_with_metadata) + +static int s_test_mqtt_append_sdk_metrics_with_metadata_invalid_utf8(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + /* Invalid UTF-8 sequence in metadata key */ + struct aws_mqtt_metadata_entry metadata_entries[] = { + { + .key = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("InvalidKey\xFF\xFE"), + .value = aws_byte_cursor_from_c_str("ValidValue"), + }, + }; + + struct aws_mqtt_iot_metrics metrics = { + .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), + .metadata_count = AWS_ARRAY_SIZE(metadata_entries), + .metadata_entries = metadata_entries, + }; + + struct aws_byte_buf output_username; + AWS_ZERO_STRUCT(output_username); + + struct aws_byte_cursor original_username = aws_byte_cursor_from_c_str("testuser"); + + /* Should fail due to invalid UTF-8 in metadata key */ + ASSERT_FAILS( + aws_mqtt_append_sdk_metrics_to_username(allocator, &original_username, &metrics, &output_username, NULL)); + ASSERT_INT_EQUALS(aws_last_error(), AWS_ERROR_INVALID_UTF8); + + aws_byte_buf_clean_up(&output_username); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt_append_sdk_metrics_with_metadata_invalid_utf8, + s_test_mqtt_append_sdk_metrics_with_metadata_invalid_utf8) + +static int s_test_mqtt_append_sdk_metrics_with_metadata_invalid_utf8_value(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + /* Invalid UTF-8 sequence in metadata value */ + struct aws_mqtt_metadata_entry metadata_entries[] = { + { + .key = aws_byte_cursor_from_c_str("ValidKey"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("InvalidValue\xFF\xFE"), + }, + }; + + struct aws_mqtt_iot_metrics metrics = { + .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), + .metadata_count = AWS_ARRAY_SIZE(metadata_entries), + .metadata_entries = metadata_entries, + }; + + struct aws_byte_buf output_username; + AWS_ZERO_STRUCT(output_username); + + struct aws_byte_cursor original_username = aws_byte_cursor_from_c_str("testuser"); + + /* Should fail due to invalid UTF-8 in metadata value */ + ASSERT_FAILS( + aws_mqtt_append_sdk_metrics_to_username(allocator, &original_username, &metrics, &output_username, NULL)); + ASSERT_INT_EQUALS(aws_last_error(), AWS_ERROR_INVALID_UTF8); + + aws_byte_buf_clean_up(&output_username); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE( + mqtt_append_sdk_metrics_with_metadata_invalid_utf8_value, + s_test_mqtt_append_sdk_metrics_with_metadata_invalid_utf8_value) + +static int s_test_mqtt_iot_metrics_storage_with_metadata(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt_metadata_entry metadata_entries[] = { + { + .key = aws_byte_cursor_from_c_str("Key1"), + .value = aws_byte_cursor_from_c_str("Value1"), + }, + { + .key = aws_byte_cursor_from_c_str("Key2"), + .value = aws_byte_cursor_from_c_str("Value2"), + }, + }; + + struct aws_mqtt_iot_metrics metrics = { + .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), + .metadata_count = AWS_ARRAY_SIZE(metadata_entries), + .metadata_entries = metadata_entries, + }; + + struct aws_mqtt_iot_metrics_storage *storage = aws_mqtt_iot_metrics_storage_new(allocator, &metrics); + ASSERT_NOT_NULL(storage); + + /* Verify the storage view has the correct values */ + ASSERT_TRUE(aws_byte_cursor_eq_c_str(&storage->storage_view.library_name, "TestSDK/1.0")); + ASSERT_INT_EQUALS(2, storage->storage_view.metadata_count); + ASSERT_NOT_NULL(storage->storage_view.metadata_entries); + + /* Verify metadata entries are correctly stored */ + ASSERT_TRUE(aws_byte_cursor_eq_c_str(&storage->storage_view.metadata_entries[0].key, "Key1")); + ASSERT_TRUE(aws_byte_cursor_eq_c_str(&storage->storage_view.metadata_entries[0].value, "Value1")); + ASSERT_TRUE(aws_byte_cursor_eq_c_str(&storage->storage_view.metadata_entries[1].key, "Key2")); + ASSERT_TRUE(aws_byte_cursor_eq_c_str(&storage->storage_view.metadata_entries[1].value, "Value2")); + + aws_mqtt_iot_metrics_storage_destroy(storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_iot_metrics_storage_with_metadata, s_test_mqtt_iot_metrics_storage_with_metadata) + +static int s_test_mqtt_iot_metrics_storage_empty_metadata(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + struct aws_mqtt_iot_metrics metrics = { + .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), + .metadata_count = 0, + .metadata_entries = NULL, + }; + + struct aws_mqtt_iot_metrics_storage *storage = aws_mqtt_iot_metrics_storage_new(allocator, &metrics); + ASSERT_NOT_NULL(storage); + + /* Verify the storage view has the correct values */ + ASSERT_TRUE(aws_byte_cursor_eq_c_str(&storage->storage_view.library_name, "TestSDK/1.0")); + ASSERT_INT_EQUALS(0, storage->storage_view.metadata_count); + + aws_mqtt_iot_metrics_storage_destroy(storage); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_iot_metrics_storage_empty_metadata, s_test_mqtt_iot_metrics_storage_empty_metadata) + +static int s_test_mqtt_append_sdk_metrics_existing_metadata(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + /* New metadata entries to add */ + struct aws_mqtt_metadata_entry metadata_entries[] = { + { + .key = aws_byte_cursor_from_c_str("NewKey"), + .value = aws_byte_cursor_from_c_str("NewValue"), + }, + }; + + struct aws_mqtt_iot_metrics metrics = { + .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), + .metadata_count = AWS_ARRAY_SIZE(metadata_entries), + .metadata_entries = metadata_entries, + }; + + struct aws_byte_buf output_username; + AWS_ZERO_STRUCT(output_username); + + /* Username already contains SDK, Platform, and Metadata fields */ + struct aws_byte_cursor original_username = aws_byte_cursor_from_c_str( + "testuser?SDK=ExistingSDK&Platform=ExistingPlatform&Metadata=(ExistingKey1=ExistingValue1;ExistingKey2=" + "ExistingValue2)"); + + ASSERT_SUCCESS( + aws_mqtt_append_sdk_metrics_to_username(allocator, &original_username, &metrics, &output_username, NULL)); + + struct aws_byte_cursor output_cursor = aws_byte_cursor_from_buf(&output_username); + + /* Verify the output contains merged metadata: + * ((ExistingKey1=ExistingValue1;ExistingKey2=ExistingValue2;NewKey=NewValue) */ + struct aws_byte_cursor merged_metadata = aws_byte_cursor_from_c_str( + "Metadata=(ExistingKey1=ExistingValue1;ExistingKey2=ExistingValue2;NewKey=NewValue)"); + struct aws_byte_cursor found; + + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&output_cursor, &merged_metadata, &found)); + + /* Verify SDK and Platform are preserved (not duplicated) */ + struct aws_byte_cursor sdk_check = aws_byte_cursor_from_c_str("SDK=ExistingSDK"); + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&output_cursor, &sdk_check, &found)); + + struct aws_byte_cursor platform_check = aws_byte_cursor_from_c_str("Platform=ExistingPlatform"); + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&output_cursor, &platform_check, &found)); + + aws_byte_buf_clean_up(&output_username); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_append_sdk_metrics_existing_metadata, s_test_mqtt_append_sdk_metrics_existing_metadata) + +static int s_test_mqtt_append_sdk_metrics_existing_metadata_no_new(struct aws_allocator *allocator, void *ctx) { + (void)ctx; + + /* No new metadata entries */ + struct aws_mqtt_iot_metrics metrics = { + .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), + .metadata_count = 0, + .metadata_entries = NULL, + }; + + struct aws_byte_buf output_username; + AWS_ZERO_STRUCT(output_username); + + /* Username already contains Metadata field */ + struct aws_byte_cursor original_username = aws_byte_cursor_from_c_str( + "testuser?SDK=ExistingSDK&Platform=ExistingPlatform&Metadata=(ExistingKey=ExistingValue)"); + + ASSERT_SUCCESS( + aws_mqtt_append_sdk_metrics_to_username(allocator, &original_username, &metrics, &output_username, NULL)); + + struct aws_byte_cursor output_cursor = aws_byte_cursor_from_buf(&output_username); + + /* Verify the existing metadata is preserved */ + struct aws_byte_cursor existing_metadata = aws_byte_cursor_from_c_str("Metadata=(ExistingKey=ExistingValue)"); + struct aws_byte_cursor found; + + ASSERT_SUCCESS(aws_byte_cursor_find_exact(&output_cursor, &existing_metadata, &found)); + + aws_byte_buf_clean_up(&output_username); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(mqtt_append_sdk_metrics_existing_metadata_no_new, s_test_mqtt_append_sdk_metrics_existing_metadata_no_new) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/tests/v3/connection_state_test.c new/aws-c-mqtt-0.16.0/tests/v3/connection_state_test.c --- old/aws-c-mqtt-0.15.2/tests/v3/connection_state_test.c 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/tests/v3/connection_state_test.c 2026-05-18 20:49:17.000000000 +0200 @@ -4115,10 +4115,15 @@ /** * helper function to test client with different metrics by checking the received username field + * @param allocator The allocator to use + * @param metrics The metrics to set (can be NULL to disable metrics) + * @param username The username to set (can be NULL to skip setting username) + * @param ctx The test context */ static int s_create_mqtt_connection_and_set_metrics( struct aws_allocator *allocator, struct aws_mqtt_iot_metrics *metrics, + struct aws_byte_cursor *username, void *ctx) { struct mqtt_connection_state_test *state_test_data = ctx; @@ -4131,8 +4136,9 @@ .on_connection_complete = aws_test311_on_connection_complete_fn, }; - struct aws_byte_cursor username = aws_byte_cursor_from_c_str("testuser"); - ASSERT_SUCCESS(aws_mqtt_client_connection_set_login(state_test_data->mqtt_connection, &username, NULL)); + if (username != NULL) { + ASSERT_SUCCESS(aws_mqtt_client_connection_set_login(state_test_data->mqtt_connection, username, NULL)); + } ASSERT_SUCCESS(aws_mqtt_client_connection_set_metrics(state_test_data->mqtt_connection, metrics)); @@ -4154,9 +4160,16 @@ struct aws_byte_buf expected_buf; AWS_ZERO_STRUCT(expected_buf); if (metrics) { - aws_test_mqtt_build_expected_metrics(allocator, &username, metrics->library_name, NULL, &expected_buf); - } else { - aws_byte_buf_init_copy_from_cursor(&expected_buf, allocator, username); + aws_test_mqtt_build_expected_metrics( + allocator, + username, + metrics->library_name, + NULL, + metrics->metadata_entries, + metrics->metadata_count, + &expected_buf); + } else if (username != NULL) { + aws_byte_buf_init_copy_from_cursor(&expected_buf, allocator, *username); } ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&received_packet->username, &expected_buf)); @@ -4172,14 +4185,26 @@ static int s_test_mqtt_connection_set_metrics_valid_fn(struct aws_allocator *allocator, void *ctx) { (void)allocator; + /* Create metadata entries */ + struct aws_mqtt_metadata_entry metadata_entries[] = { + { + .key = aws_byte_cursor_from_c_str("key1"), + .value = aws_byte_cursor_from_c_str("value1"), + }, + { + .key = aws_byte_cursor_from_c_str("key2"), + .value = aws_byte_cursor_from_c_str("value2"), + }, + }; + struct aws_mqtt_iot_metrics metrics = { .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), - // TODO: enable metadata testing when metadata support is added - // .metadata_entries = NULL, - // .metadata_count = 0, + .metadata_count = AWS_ARRAY_SIZE(metadata_entries), + .metadata_entries = metadata_entries, }; - ASSERT_SUCCESS(s_create_mqtt_connection_and_set_metrics(allocator, &metrics, ctx)); + struct aws_byte_cursor username = aws_byte_cursor_from_c_str("testuser"); + ASSERT_SUCCESS(s_create_mqtt_connection_and_set_metrics(allocator, &metrics, &username, ctx)); return AWS_OP_SUCCESS; } @@ -4196,7 +4221,8 @@ */ static int s_test_mqtt_connection_set_metrics_null_fn(struct aws_allocator *allocator, void *ctx) { - ASSERT_SUCCESS(s_create_mqtt_connection_and_set_metrics(allocator, NULL, ctx)); + struct aws_byte_cursor username = aws_byte_cursor_from_c_str("testuser"); + ASSERT_SUCCESS(s_create_mqtt_connection_and_set_metrics(allocator, NULL, &username, ctx)); return AWS_OP_SUCCESS; } @@ -4208,6 +4234,38 @@ s_clean_up_mqtt_server_fn, &test_data) +static int s_test_mqtt_connection_set_metrics_with_null_username_fn(struct aws_allocator *allocator, void *ctx) { + + /* Create metadata entries */ + struct aws_mqtt_metadata_entry metadata_entries[] = { + { + .key = aws_byte_cursor_from_c_str("key1"), + .value = aws_byte_cursor_from_c_str("value1"), + }, + { + .key = aws_byte_cursor_from_c_str("key2"), + .value = aws_byte_cursor_from_c_str("value2"), + }, + }; + + struct aws_mqtt_iot_metrics metrics = { + .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), + .metadata_count = AWS_ARRAY_SIZE(metadata_entries), + .metadata_entries = metadata_entries, + }; + + ASSERT_SUCCESS(s_create_mqtt_connection_and_set_metrics(allocator, &metrics, NULL, ctx)); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE_FIXTURE( + mqtt_connection_set_metrics_with_null_username, + s_setup_mqtt_server_fn, + s_test_mqtt_connection_set_metrics_with_null_username_fn, + s_clean_up_mqtt_server_fn, + &test_data) + /** * Test that aws_mqtt_client_connection_set_metrics rejects invalid UTF-8 in library name */ @@ -4277,7 +4335,7 @@ /* verify the username and metrics is setup properly */ struct aws_byte_buf expected_buf; AWS_ZERO_STRUCT(expected_buf); - aws_test_mqtt_build_expected_metrics(allocator, &username1, metrics1.library_name, NULL, &expected_buf); + aws_test_mqtt_build_expected_metrics(allocator, &username1, metrics1.library_name, NULL, NULL, 0, &expected_buf); ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&received_packet1->username, &expected_buf)); aws_byte_buf_clean_up(&expected_buf); @@ -4298,7 +4356,7 @@ /* verify the username and metrics is setup properly */ AWS_ZERO_STRUCT(expected_buf); - aws_test_mqtt_build_expected_metrics(allocator, &username2, metrics2.library_name, NULL, &expected_buf); + aws_test_mqtt_build_expected_metrics(allocator, &username2, metrics2.library_name, NULL, NULL, 0, &expected_buf); ASSERT_TRUE(aws_byte_cursor_eq_byte_buf(&received_packet2->username, &expected_buf)); aws_byte_buf_clean_up(&expected_buf); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/tests/v3/mqtt311_testing_utils.c new/aws-c-mqtt-0.16.0/tests/v3/mqtt311_testing_utils.c --- old/aws-c-mqtt-0.15.2/tests/v3/mqtt311_testing_utils.c 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/tests/v3/mqtt311_testing_utils.c 2026-05-18 20:49:17.000000000 +0200 @@ -583,13 +583,20 @@ static const struct aws_byte_cursor SDK_ATT_STR = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("?SDK="); static const struct aws_byte_cursor PLATFORM_ATT_STR = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("&Platform="); +static const struct aws_byte_cursor METADATA_ATT_STR = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("&Metadata=("); +static const struct aws_byte_cursor METADATA_CLOSE_PAREN = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL(")"); +static const struct aws_byte_cursor METADATA_KEY_VALUE_DELIM = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("="); +static const struct aws_byte_cursor METADATA_ENTRY_DELIM = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL(";"); void aws_test_mqtt_build_expected_metrics( struct aws_allocator *allocator, const struct aws_byte_cursor *original_username, const struct aws_byte_cursor sdk, const struct aws_byte_cursor *platform, + const struct aws_mqtt_metadata_entry *metadata_entries, + size_t metadata_count, struct aws_byte_buf *expected_buf) { + struct aws_byte_cursor platform_to_use = platform ? *platform : aws_get_platform_build_os_string(); if (original_username) { aws_byte_buf_init_copy_from_cursor(expected_buf, allocator, *original_username); @@ -600,4 +607,23 @@ aws_byte_buf_append_dynamic(expected_buf, &sdk); aws_byte_buf_append_dynamic(expected_buf, &PLATFORM_ATT_STR); aws_byte_buf_append_dynamic(expected_buf, &platform_to_use); + + /* Append metadata if present */ + if (metadata_entries != NULL && metadata_count > 0) { + aws_byte_buf_append_dynamic(expected_buf, &METADATA_ATT_STR); + + for (size_t i = 0; i < metadata_count; ++i) { + const struct aws_mqtt_metadata_entry *entry = &metadata_entries[i]; + aws_byte_buf_append_dynamic(expected_buf, &entry->key); + aws_byte_buf_append_dynamic(expected_buf, &METADATA_KEY_VALUE_DELIM); + aws_byte_buf_append_dynamic(expected_buf, &entry->value); + + /* Add semicolon separator between entries (not after the last one) */ + if (i < metadata_count - 1) { + aws_byte_buf_append_dynamic(expected_buf, &METADATA_ENTRY_DELIM); + } + } + + aws_byte_buf_append_dynamic(expected_buf, &METADATA_CLOSE_PAREN); + } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/tests/v3/mqtt311_testing_utils.h new/aws-c-mqtt-0.16.0/tests/v3/mqtt311_testing_utils.h --- old/aws-c-mqtt-0.15.2/tests/v3/mqtt311_testing_utils.h 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/tests/v3/mqtt311_testing_utils.h 2026-05-18 20:49:17.000000000 +0200 @@ -151,11 +151,25 @@ void aws_test311_on_connection_termination_fn(void *userdata); +/** + * Build expected metrics string with optional metadata support. + * Format: username?SDK=<sdk>&Platform=<platform>[&Metadata=(key1=value1;key2=value2)] + * + * @param allocator The allocator to use + * @param original_username The original username (can be NULL) + * @param sdk The SDK string + * @param platform The platform string (can be NULL to use default) + * @param metadata_entries Array of metadata entries (can be NULL if metadata_count is 0) + * @param metadata_count Number of metadata entries (0 for no metadata) + * @param expected_buf Output buffer for the expected metrics string + */ void aws_test_mqtt_build_expected_metrics( struct aws_allocator *allocator, const struct aws_byte_cursor *original_username, const struct aws_byte_cursor sdk, const struct aws_byte_cursor *platform, + const struct aws_mqtt_metadata_entry *metadata_entries, + size_t metadata_count, struct aws_byte_buf *expected_buf); AWS_EXTERN_C_END diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/tests/v5/mqtt5_client_tests.c new/aws-c-mqtt-0.16.0/tests/v5/mqtt5_client_tests.c --- old/aws-c-mqtt-0.15.2/tests/v5/mqtt5_client_tests.c 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/tests/v5/mqtt5_client_tests.c 2026-05-18 20:49:17.000000000 +0200 @@ -6659,6 +6659,7 @@ static int s_mqtt5_client_metrics_in_username_fn( struct aws_allocator *allocator, struct aws_mqtt_iot_metrics *metrics, + struct aws_byte_cursor *original_username, void *ctx) { (void)ctx; @@ -6668,14 +6669,11 @@ aws_mqtt5_client_test_init_default_options(&test_options); test_options.client_options.metrics = metrics; - /* Set up username and metrics */ - struct aws_byte_cursor original_username = aws_byte_cursor_from_c_str("test_user"); - struct aws_mqtt5_packet_connect_view connect_view = { .keep_alive_interval_seconds = 30, .client_id = aws_byte_cursor_from_string(g_default_client_id), .clean_start = true, - .username = &original_username}; + .username = original_username}; test_options.connect_options = connect_view; @@ -6769,21 +6767,59 @@ } static int s_test_mqtt5_client_set_metrics_valid(struct aws_allocator *allocator, void *ctx) { + + struct aws_mqtt_metadata_entry metadata_entries[] = { + { + .key = aws_byte_cursor_from_c_str("lang"), + .value = aws_byte_cursor_from_c_str("C"), + }, + { + .key = aws_byte_cursor_from_c_str("version"), + .value = aws_byte_cursor_from_c_str("1.0.0"), + }, + }; + struct aws_mqtt_iot_metrics metrics = { - .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0") - // TODO: enable when metadata is supported - // .metadata_entries = NULL, - // .metadata_count = 0, + .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), + .metadata_count = AWS_ARRAY_SIZE(metadata_entries), + .metadata_entries = metadata_entries, }; - return s_mqtt5_client_metrics_in_username_fn(allocator, &metrics, ctx); + struct aws_byte_cursor username = aws_byte_cursor_from_c_str("test_user"); + + return s_mqtt5_client_metrics_in_username_fn(allocator, &metrics, &username, ctx); } AWS_TEST_CASE(mqtt5_client_set_metrics_valid, s_test_mqtt5_client_set_metrics_valid) +static int s_test_mqtt5_client_set_metrics_with_null_username(struct aws_allocator *allocator, void *ctx) { + + struct aws_mqtt_metadata_entry metadata_entries[] = { + { + .key = aws_byte_cursor_from_c_str("lang"), + .value = aws_byte_cursor_from_c_str("C"), + }, + { + .key = aws_byte_cursor_from_c_str("version"), + .value = aws_byte_cursor_from_c_str("1.0.0"), + }, + }; + + struct aws_mqtt_iot_metrics metrics = { + .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), + .metadata_count = AWS_ARRAY_SIZE(metadata_entries), + .metadata_entries = metadata_entries, + }; + + return s_mqtt5_client_metrics_in_username_fn(allocator, &metrics, NULL, ctx); +} + +AWS_TEST_CASE(mqtt5_client_set_metrics_with_null_username, s_test_mqtt5_client_set_metrics_with_null_username) + static int s_test_mqtt5_client_set_metrics_null(struct aws_allocator *allocator, void *ctx) { - return s_mqtt5_client_metrics_in_username_fn(allocator, NULL, ctx); + struct aws_byte_cursor username = aws_byte_cursor_from_c_str("test_user"); + return s_mqtt5_client_metrics_in_username_fn(allocator, NULL, &username, ctx); } AWS_TEST_CASE(mqtt5_client_set_metrics_null, s_test_mqtt5_client_set_metrics_null) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/aws-c-mqtt-0.15.2/tests/v5/mqtt5_to_mqtt3_adapter_tests.c new/aws-c-mqtt-0.16.0/tests/v5/mqtt5_to_mqtt3_adapter_tests.c --- old/aws-c-mqtt-0.15.2/tests/v5/mqtt5_to_mqtt3_adapter_tests.c 2026-03-18 21:44:05.000000000 +0100 +++ new/aws-c-mqtt-0.16.0/tests/v5/mqtt5_to_mqtt3_adapter_tests.c 2026-05-18 20:49:17.000000000 +0200 @@ -4428,8 +4428,21 @@ struct aws_mqtt_client_connection *connection = fixture.connection; + struct aws_mqtt_metadata_entry metadata_entries[] = { + { + .key = aws_byte_cursor_from_c_str("lang"), + .value = aws_byte_cursor_from_c_str("C"), + }, + { + .key = aws_byte_cursor_from_c_str("version"), + .value = aws_byte_cursor_from_c_str("1.0.0"), + }, + }; + struct aws_mqtt_iot_metrics metrics = { .library_name = aws_byte_cursor_from_c_str("TestSDK/1.0"), + .metadata_count = AWS_ARRAY_SIZE(metadata_entries), + .metadata_entries = metadata_entries, }; ASSERT_SUCCESS(aws_mqtt_client_connection_set_metrics(connection, &metrics)); @@ -4499,8 +4512,15 @@ /* Invalid UTF-8 sequence */ struct aws_byte_cursor invalid_utf8_library = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("TestSDK\xFF\xFE"); + struct aws_mqtt_metadata_entry metadata_entries[] = {{ + .key = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("key1\xFF\xFE"), + .value = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("value1\xFF\xFE"), + }}; + struct aws_mqtt_iot_metrics metrics = { .library_name = invalid_utf8_library, + .metadata_count = AWS_ARRAY_SIZE(metadata_entries), + .metadata_entries = metadata_entries, }; ASSERT_FAILS(aws_mqtt_client_connection_set_metrics(connection, &metrics));
