diff --git a/.gitlab/ci/test.yml b/.gitlab/ci/test.yml index 58cd4af..a577b36 100644 --- a/.gitlab/ci/test.yml +++ b/.gitlab/ci/test.yml @@ -8,6 +8,7 @@ host_tests: needs: [] artifacts: paths: + - "**/build*/build.log" - "**/coverage.xml" - "**/coverage.html" expire_in: 1 week @@ -28,8 +29,10 @@ host_tests: git worktree add --force mqtt HEAD || (echo "Worktree failed; using local clone" && git clone --local --no-hardlinks . mqtt) git worktree list || true cd mqtt - idf-ci build run -t linux -p test/host + idf-ci build run -t linux -p test cd test/host ./build_linux_coverage/host_mqtt_client_test.elf -r junit -o junit.xml + cd ../mqtt_utils_host_test + ./build_linux_default/mqtt_utils_host_test.elf cd ../.. gcovr --gcov-ignore-parse-errors -g -k -r . --html coverage.html -x coverage.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f86e9d..7f4b8ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,3 +12,6 @@ idf_component_register(SRCS "${srcs}" PRIV_REQUIRES esp_timer http_parser esp_hw_support heap KCONFIG ${CMAKE_CURRENT_LIST_DIR}/Kconfig ) + +add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/lib/mqtt_utils) +target_link_libraries(${COMPONENT_LIB} PUBLIC idf::mqtt::utils) diff --git a/cmake/CPM.cmake b/cmake/CPM.cmake new file mode 100644 index 0000000..8474873 --- /dev/null +++ b/cmake/CPM.cmake @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MIT +# +# SPDX-FileCopyrightText: Copyright (c) 2019-2023 Lars Melchior and contributors + +set(CPM_DOWNLOAD_VERSION 0.42.0) +set(CPM_HASH_SUM "2020b4fc42dba44817983e06342e682ecfc3d2f484a581f11cc5731fbe4dce8a") + +if(CPM_SOURCE_CACHE) + set(CPM_DOWNLOAD_LOCATION "${CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +elseif(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_DOWNLOAD_LOCATION "$ENV{CPM_SOURCE_CACHE}/cpm/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +else() + set(CPM_DOWNLOAD_LOCATION "${CMAKE_BINARY_DIR}/cmake/CPM_${CPM_DOWNLOAD_VERSION}.cmake") +endif() + +# Expand relative path. This is important if the provided path contains a tilde (~) +get_filename_component(CPM_DOWNLOAD_LOCATION ${CPM_DOWNLOAD_LOCATION} ABSOLUTE) + +file(DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v${CPM_DOWNLOAD_VERSION}/CPM.cmake + ${CPM_DOWNLOAD_LOCATION} EXPECTED_HASH SHA256=${CPM_HASH_SUM} +) + +include(${CPM_DOWNLOAD_LOCATION}) diff --git a/cmake/RapidCheck.cmake b/cmake/RapidCheck.cmake new file mode 100644 index 0000000..727e87b --- /dev/null +++ b/cmake/RapidCheck.cmake @@ -0,0 +1,19 @@ +CPMAddPackage( + NAME RapidCheck + GITHUB_REPOSITORY emil-e/rapidcheck + GIT_TAG ff6af6fc683159deb51c543b065eba14dfcf329b +) + +add_subdirectory(${RapidCheck_SOURCE_DIR}/extras/catch ${CMAKE_BINARY_DIR}/rapidcheck_catch) + +# Treat rapidcheck headers as SYSTEM so their internal uses of deprecated APIs +# (e.g. std::aligned_storage in C++23) do not surface as warnings in our build. +foreach(_rc_target rapidcheck rapidcheck_catch) + if(TARGET ${_rc_target}) + get_target_property(_rc_includes ${_rc_target} INTERFACE_INCLUDE_DIRECTORIES) + if(_rc_includes) + set_target_properties(${_rc_target} PROPERTIES + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${_rc_includes}") + endif() + endif() +endforeach() diff --git a/lib/mqtt_utils/CMakeLists.txt b/lib/mqtt_utils/CMakeLists.txt new file mode 100644 index 0000000..a482dfd --- /dev/null +++ b/lib/mqtt_utils/CMakeLists.txt @@ -0,0 +1,8 @@ +set(srcs mqtt_utils.c) + +add_library(mqtt_utils_lib ${srcs}) +target_include_directories(mqtt_utils_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) +target_include_directories(mqtt_utils_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include) +idf_component_get_property(log log COMPONENT_LIB) +target_link_libraries(mqtt_utils_lib PRIVATE ${log}) +add_library(idf::mqtt::utils ALIAS mqtt_utils_lib) diff --git a/lib/mqtt_utils/include/mqtt_utils.h b/lib/mqtt_utils/include/mqtt_utils.h new file mode 100644 index 0000000..7ac3274 --- /dev/null +++ b/lib/mqtt_utils/include/mqtt_utils.h @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif // #ifdef __cplusplus + +char *mqtt_create_string(const char *ptr, int len); +int esp_mqtt_decode_percent_encoded_string(char *uri); + +#ifdef __cplusplus +} +#endif // #ifdef __cplusplus diff --git a/lib/mqtt_utils/mqtt_utils.c b/lib/mqtt_utils/mqtt_utils.c new file mode 100644 index 0000000..30c262c --- /dev/null +++ b/lib/mqtt_utils/mqtt_utils.c @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "include/mqtt_utils.h" + +char *mqtt_create_string(const char *ptr, int len) +{ + if (len <= 0) { + return NULL; + } + + char *ret = calloc(1, len + 1); + + if (ret == NULL) { + return NULL; + } + + memcpy(ret, ptr, len); + return ret; +} + +int esp_mqtt_decode_percent_encoded_string(char *uri) +{ + if (uri == NULL) { + return -1; + } + + char *write_ptr = uri; + size_t uri_len = strlen(uri); + + for (intptr_t i = 0; i < uri_len; i++, write_ptr++) { + if (uri[i] == '%') { + if (!(isxdigit((unsigned char) uri[i + 1]) && isxdigit((unsigned char) uri[i + 2]))) { + // having non [0-9a-fA-F] characters after % is illegal in URI + return -1; + } + + char hexvalue[3] = {0, 0, 0}; + memcpy(hexvalue, uri + i + 1, 2); + *write_ptr = (char) strtol(hexvalue, NULL, 16); + i += 2; + } else { + *write_ptr = uri[i]; + } + } + + *write_ptr = '\0'; + return (int)(write_ptr - uri); +} diff --git a/mqtt_client.c b/mqtt_client.c index 4fe2859..4f8de82 100644 --- a/mqtt_client.c +++ b/mqtt_client.c @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: Apache-2.0 */ @@ -14,6 +14,7 @@ #include "mqtt_client_priv.h" #include "mqtt_msg.h" #include "mqtt_outbox.h" +#include "mqtt_utils.h" _Static_assert(sizeof(uint64_t) == sizeof(outbox_tick_t), "mqtt-client tick type size different from outbox tick type"); #ifdef ESP_EVENT_ANY_ID @@ -39,7 +40,6 @@ static esp_err_t esp_mqtt_dispatch_event_with_msgid(esp_mqtt_client_handle_t cli static esp_err_t esp_mqtt_connect(esp_mqtt_client_handle_t client, int timeout_ms); static void esp_mqtt_abort_connection(esp_mqtt_client_handle_t client); static esp_err_t esp_mqtt_client_ping(esp_mqtt_client_handle_t client); -static char *create_string(const char *ptr, int len); static int mqtt_message_receive(esp_mqtt_client_handle_t client, int read_poll_timeout_ms); static void esp_mqtt_client_dispatch_transport_error(esp_mqtt_client_handle_t client); static esp_err_t send_disconnect_msg(esp_mqtt_client_handle_t client); @@ -691,27 +691,27 @@ esp_err_t esp_mqtt_set_config(esp_mqtt_client_handle_t client, const esp_mqtt_cl client->config->scheme = NULL; if (config->broker.address.transport == MQTT_TRANSPORT_OVER_TCP) { - client->config->scheme = create_string(MQTT_OVER_TCP_SCHEME, strlen(MQTT_OVER_TCP_SCHEME)); + client->config->scheme = mqtt_create_string(MQTT_OVER_TCP_SCHEME, strlen(MQTT_OVER_TCP_SCHEME)); ESP_MEM_CHECK(TAG, client->config->scheme, goto _mqtt_set_config_failed); } #if MQTT_ENABLE_WS else if (config->broker.address.transport == MQTT_TRANSPORT_OVER_WS) { - client->config->scheme = create_string(MQTT_OVER_WS_SCHEME, strlen(MQTT_OVER_WS_SCHEME)); + client->config->scheme = mqtt_create_string(MQTT_OVER_WS_SCHEME, strlen(MQTT_OVER_WS_SCHEME)); ESP_MEM_CHECK(TAG, client->config->scheme, goto _mqtt_set_config_failed); } #endif #if MQTT_ENABLE_SSL else if (config->broker.address.transport == MQTT_TRANSPORT_OVER_SSL) { - client->config->scheme = create_string(MQTT_OVER_SSL_SCHEME, strlen(MQTT_OVER_SSL_SCHEME)); + client->config->scheme = mqtt_create_string(MQTT_OVER_SSL_SCHEME, strlen(MQTT_OVER_SSL_SCHEME)); ESP_MEM_CHECK(TAG, client->config->scheme, goto _mqtt_set_config_failed); } #endif #if MQTT_ENABLE_WSS else if (config->broker.address.transport == MQTT_TRANSPORT_OVER_WSS) { - client->config->scheme = create_string(MQTT_OVER_WSS_SCHEME, strlen(MQTT_OVER_WSS_SCHEME)); + client->config->scheme = mqtt_create_string(MQTT_OVER_WSS_SCHEME, strlen(MQTT_OVER_WSS_SCHEME)); ESP_MEM_CHECK(TAG, client->config->scheme, goto _mqtt_set_config_failed); } @@ -1064,20 +1064,6 @@ esp_err_t esp_mqtt_client_destroy(esp_mqtt_client_handle_t client) return ESP_OK; } -static char *create_string(const char *ptr, int len) -{ - char *ret; - - if (len <= 0) { - return NULL; - } - - ret = calloc(1, len + 1); - ESP_MEM_CHECK(TAG, ret, return NULL); - memcpy(ret, ptr, len); - return ret; -} - esp_err_t esp_mqtt_client_set_uri(esp_mqtt_client_handle_t client, const char *uri) { struct http_parser_url puri; @@ -1085,7 +1071,7 @@ esp_err_t esp_mqtt_client_set_uri(esp_mqtt_client_handle_t client, const char *u int parser_status = http_parser_parse_url(uri, strlen(uri), 0, &puri); if (parser_status != 0) { - ESP_LOGE(TAG, "Error parse uri = %s", uri); + ESP_LOGE(TAG, "Error parse uri (%d) = %s", parser_status, uri); return ESP_FAIL; } @@ -1103,8 +1089,8 @@ esp_err_t esp_mqtt_client_set_uri(esp_mqtt_client_handle_t client, const char *u free(client->config->scheme); free(client->config->host); free(client->config->path); - client->config->scheme = create_string(uri + puri.field_data[UF_SCHEMA].off, puri.field_data[UF_SCHEMA].len); - client->config->host = create_string(uri + puri.field_data[UF_HOST].off, puri.field_data[UF_HOST].len); + client->config->scheme = mqtt_create_string(uri + puri.field_data[UF_SCHEMA].off, puri.field_data[UF_SCHEMA].len); + client->config->host = mqtt_create_string(uri + puri.field_data[UF_HOST].off, puri.field_data[UF_HOST].len); client->config->path = NULL; #pragma GCC diagnostic pop @@ -1137,7 +1123,7 @@ esp_err_t esp_mqtt_client_set_uri(esp_mqtt_client_handle_t client, const char *u client->config->port = strtol((const char *)(uri + puri.field_data[UF_PORT].off), NULL, 10); } - char *user_info = create_string(uri + puri.field_data[UF_USERINFO].off, puri.field_data[UF_USERINFO].len); + char *user_info = mqtt_create_string(uri + puri.field_data[UF_USERINFO].off, puri.field_data[UF_USERINFO].len); if (user_info) { char *pass = strchr(user_info, ':'); @@ -1145,9 +1131,21 @@ esp_err_t esp_mqtt_client_set_uri(esp_mqtt_client_handle_t client, const char *u if (pass) { pass[0] = 0; //terminal username pass ++; + + // parse %-encoded symbols such as %40 = @ in the password + if (esp_mqtt_decode_percent_encoded_string(pass) < 0) { + ESP_LOGE(TAG, "Error parse uri (non-hexadecimal data after %%) = %s", uri); + return ESP_FAIL; + } + client->mqtt_state.connection.information.password = strdup(pass); } + if (esp_mqtt_decode_percent_encoded_string(user_info) < 0) { + ESP_LOGE(TAG, "Error parse uri (non-hexadecimal data after %%) = %s", uri); + return ESP_FAIL; + } + client->mqtt_state.connection.information.username = strdup(user_info); free(user_info); } diff --git a/test/host/CMakeLists.txt b/test/host/CMakeLists.txt index ecb657d..2630fed 100644 --- a/test/host/CMakeLists.txt +++ b/test/host/CMakeLists.txt @@ -2,6 +2,9 @@ # in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.16) +include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/CPM.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/RapidCheck.cmake) + include($ENV{IDF_PATH}/tools/cmake/project.cmake) idf_build_set_property(MINIMAL_BUILD ON) list(APPEND EXTRA_COMPONENT_DIRS diff --git a/test/host/main/CMakeLists.txt b/test/host/main/CMakeLists.txt index be82cf2..ef32740 100644 --- a/test/host/main/CMakeLists.txt +++ b/test/host/main/CMakeLists.txt @@ -4,7 +4,7 @@ idf_component_register(SRCS "test_mqtt_client.cpp" "test_log_intercept.cpp" "te target_compile_options(${COMPONENT_LIB} PUBLIC -fsanitize=address -Wno-missing-field-initializers) target_link_options(${COMPONENT_LIB} PUBLIC -fsanitize=address) -target_link_libraries(${COMPONENT_LIB} PUBLIC Catch2::Catch2WithMain) +target_link_libraries(${COMPONENT_LIB} PUBLIC Catch2::Catch2WithMain rapidcheck rapidcheck_catch) idf_component_get_property(mqtt mqtt COMPONENT_LIB) target_compile_definitions(${mqtt} PRIVATE SOC_WIFI_SUPPORTED=1) diff --git a/test/host/main/test_mqtt_client.cpp b/test/host/main/test_mqtt_client.cpp index f5ff253..49a5600 100644 --- a/test/host/main/test_mqtt_client.cpp +++ b/test/host/main/test_mqtt_client.cpp @@ -4,14 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ #include +#include #include #include #include +#include #include #include #include "esp_transport.h" #include #include +#include #include "mqtt_client.h" #include "test_log_intercept.hpp" @@ -31,7 +34,6 @@ extern "C" { #include "Mockidf_additions.h" #endif #include "Mockesp_timer.h" - /* * The following functions are not directly called but the generation of them * from cmock is broken, so we need to define them here. @@ -99,6 +101,40 @@ SCENARIO("MQTT Client Operation") REQUIRE(res == ESP_FAIL); } } + SECTION("Any well-formed URI is accepted") { + static constexpr std::array schemes = { + "mqtt", "mqtts", "ws", "wss" + }; + auto host_char = rc::gen::element('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', + 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'); + rc::check("esp_mqtt_client_set_uri accepts well-formed URIs", + [&] { + auto scheme = *rc::gen::elementOf(schemes); + auto host = *rc::gen::container( + *rc::gen::inRange(1, 33), host_char).as("host"); + auto path = *rc::gen::container( + *rc::gen::inRange(0, 17), host_char).as("path"); + auto port = *rc::gen::maybe(rc::gen::inRange(1, 65535)).as("port"); + std::string uri = scheme; + uri += "://"; + uri += host; + + if (port) { + uri += ":"; + uri += std::to_string(*port); + } + + if (!path.empty()) + { + uri += "/"; + uri += path; + } + + RC_ASSERT(esp_mqtt_client_set_uri(client.get(), uri.c_str()) == ESP_OK); + }); + } SECTION("User set interface to use") { struct ifreq if_name = {}; strncpy(if_name.ifr_name, "custom", IFNAMSIZ - 1); diff --git a/test/mqtt_utils_host_test/CMakeLists.txt b/test/mqtt_utils_host_test/CMakeLists.txt new file mode 100644 index 0000000..3d49967 --- /dev/null +++ b/test/mqtt_utils_host_test/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.16) + +include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/CPM.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/../../cmake/RapidCheck.cmake) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) + +project(mqtt_utils_host_test) diff --git a/test/mqtt_utils_host_test/README.md b/test/mqtt_utils_host_test/README.md new file mode 100644 index 0000000..d97faa5 --- /dev/null +++ b/test/mqtt_utils_host_test/README.md @@ -0,0 +1,38 @@ +# Host test for utility functions of MQTT client + +This is a host test which tests utility functions from `mqtt_utils` subcomponent. + +## Usage + +To run the test suite you will need to set target to `Linux` and build the application. You will also need to enable `(Top) → Compiler options → Enable C++ run-time type info (RTTI)` (`CONFIG_COMPILER_CXX_RTTI`) +The default sdkconfig should set those options automatically, so usually just the command below will be enough to run it. + +```bash +idf.py build monitor +``` + +## Example structure + +- [main/idf_component.yml](main/idf_component.yml) adds a dependency on `espressif/catch2` component. +- [main/CMakeLists.txt](main/CMakeLists.txt) specifies the source files and registers the `main` component with `WHOLE_ARCHIVE` option enabled. +- [CMakeLists.txt](CMakeLists.txt) includes CPM package manager and adds rapidcheck package +- [main/test_main.cpp](main/test_main.cpp) implements the application entry point which calls the test runner. +- [main/test_cases.cpp](main/test_cases.cpp) implements test cases. +- [sdkconfig.defaults](sdkconfig.defaults) sets the options required to run the example: enables C++ exceptions, increases the size of the `main` task stack, and enables C++ runtime type info. + +## Expected output + +``` +Randomness seeded to: 2196951535 +Using configuration: seed=951423191499285688 + +- Testing the decoding of random string +OK, passed 100 tests + +- Testing the decoding of random URI +OK, passed 100 tests +=============================================================================== +All tests passed (403 assertions in 1 test case) + +Test passed. +``` diff --git a/test/mqtt_utils_host_test/main/CMakeLists.txt b/test/mqtt_utils_host_test/main/CMakeLists.txt new file mode 100644 index 0000000..60ab4cd --- /dev/null +++ b/test/mqtt_utils_host_test/main/CMakeLists.txt @@ -0,0 +1,8 @@ +idf_component_register(SRCS "test_main.cpp" + "test_cases.cpp" + INCLUDE_DIRS "." + WHOLE_ARCHIVE) + +add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../../lib/mqtt_utils ${CMAKE_CURRENT_BINARY_DIR}/mqtt_utils) + +target_link_libraries(${COMPONENT_LIB} PUBLIC idf::mqtt::utils Catch2 rapidcheck) diff --git a/test/mqtt_utils_host_test/main/idf_component.yml b/test/mqtt_utils_host_test/main/idf_component.yml new file mode 100644 index 0000000..11341e2 --- /dev/null +++ b/test/mqtt_utils_host_test/main/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + espressif/catch2: + version: "*" diff --git a/test/mqtt_utils_host_test/main/test_cases.cpp b/test/mqtt_utils_host_test/main/test_cases.cpp new file mode 100644 index 0000000..e88b391 --- /dev/null +++ b/test/mqtt_utils_host_test/main/test_cases.cpp @@ -0,0 +1,146 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include +#include +#include +#include "rapidcheck.h" +#include "mqtt_utils.h" + +struct percent_encoded_string { + std::string value; +}; +struct uri_username { + std::string value; +}; +struct uri_password { + std::string value; +}; +struct uri_host { + std::string value; +}; + +struct URI { + uri_username username; + uri_password password; + uri_host host; +}; + +namespace rc +{ + +template +struct base { + static Gen arbitrary() + { + return gen::build(gen::set(&T::value)); + } +}; + +template<> +struct Arbitrary : base { }; + +template<> +struct Arbitrary : base { }; +template<> +struct Arbitrary : base { }; +template<> +struct Arbitrary : base { }; + +template<> +struct Arbitrary { + static Gen arbitrary() + { + return gen::build(gen::set(&URI::username), gen::set(&URI::password), gen::set(&URI::host)); + } +}; +} + +std::string percent_encode(std::string input, std::string filter = "`~!@#$%^&*(){}[]:\";'<>?,./\\-+=|") +{ + std::string encoded_str; + + for (char c : input) { + if (filter.find(c) == std::string::npos && std::isprint(c)) { + encoded_str.push_back(c); + } else { + encoded_str += std::format("%{:02x}", c); + } + } + + return encoded_str; +} + +TEST_CASE("Parse percent-encoded data") +{ + SECTION("Zero-length string as an input") { + char c_style_zero_length_string[1] = {0}; + REQUIRE(esp_mqtt_decode_percent_encoded_string(c_style_zero_length_string) == 0); + } + SECTION("Constant string as an input") { + std::string known_value_string = "p%40ssw0rd"; + std::vector known_value_tmp_vector{std::begin(known_value_string), std::end(known_value_string)}; + known_value_tmp_vector.push_back('\0'); + esp_mqtt_decode_percent_encoded_string(known_value_tmp_vector.data()); + std::string result{known_value_tmp_vector.data()}; + REQUIRE(result == "p@ssw0rd"); + } + SECTION("Decoding random string") { + rc::check("Testing the decoding of random string", + [](const std::string & str) { + RC_PRE(str.length() > 0); + std::string encoded_str = percent_encode(str); + RC_PRE([encoded_str]() -> bool { + std::string filter = "`~!@#$^&*(){}[]:\";'<>?,./\\-+=|"; + + for (char c : encoded_str) { + if (filter.find(c) != std::string::npos) { + return false; + } + } + return true; + }()); + char *buffer = (char *) malloc(encoded_str.length() + 1); + strcpy(buffer, encoded_str.c_str()); + std::vector encoded_str_tmp_vector{std::begin(encoded_str), std::end(encoded_str)}; + encoded_str_tmp_vector.push_back('\0'); + int len = esp_mqtt_decode_percent_encoded_string(encoded_str_tmp_vector.data()); + REQUIRE(len == str.length()); + std::string result{encoded_str_tmp_vector.data()}; + REQUIRE(result == str); + }); + } + SECTION("Decoding percent-encoding in URIs") { + rc::check("Testing the decoding of random URI", + [](const URI & uri) { + RC_PRE(uri.host.value.length() > 0); + RC_PRE(uri.username.value.length() > 0); + RC_PRE(uri.password.value.length() > 0); + std::string complete_uri_raw = uri.username.value + ":" + uri.password.value + "@" + uri.host.value; + std::string complete_uri_enc = percent_encode(uri.username.value) + ":" + percent_encode(uri.password.value) + "@" + percent_encode(uri.host.value); + // Verify that there are no prohibited characters in the encoded username + RC_PRE([complete_uri_enc]() -> bool { + // I have removed /, :, and @ as they are permitted symbols in URI + std::string filter = "`~!#$^&*(){}[]\";'<>?,.\\-+=|"; + + for (char c : complete_uri_enc) { + if (filter.find(c) != std::string::npos) { + std::cout << "Found '" << c << "' in \"" << complete_uri_enc << "\"" << std::endl; + return false; + } + } + return true; + }()); + std::vector complete_uri_tmp_vector{std::begin(complete_uri_enc), std::end(complete_uri_enc)}; + complete_uri_tmp_vector.push_back('\0'); + int len = esp_mqtt_decode_percent_encoded_string(complete_uri_tmp_vector.data()); + REQUIRE(len == complete_uri_raw.length()); + std::string result{complete_uri_tmp_vector.data()}; + REQUIRE(result == complete_uri_raw); + }); + } +} diff --git a/test/mqtt_utils_host_test/main/test_main.cpp b/test/mqtt_utils_host_test/main/test_main.cpp new file mode 100644 index 0000000..c0f5c62 --- /dev/null +++ b/test/mqtt_utils_host_test/main/test_main.cpp @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include + +extern "C" void app_main(void) +{ + int argc = 1; + const char *argv[2] = { + "target_test_main", + NULL + }; + auto result = Catch::Session().run(argc, argv); + + if (result != 0) { + printf("Test failed with result %d\n", result); + exit(1); + } else { + printf("Test passed.\n"); + exit(0); + } +} diff --git a/test/mqtt_utils_host_test/sdkconfig.defaults b/test/mqtt_utils_host_test/sdkconfig.defaults new file mode 100644 index 0000000..36bc0ad --- /dev/null +++ b/test/mqtt_utils_host_test/sdkconfig.defaults @@ -0,0 +1,5 @@ +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_ESP_MAIN_TASK_STACK_SIZE=10000 +CONFIG_IDF_TARGET="linux" +CONFIG_COMPILER_CXX_RTTI=y +CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS=y