mirror of
https://github.com/espressif/esp-mqtt.git
synced 2026-06-05 21:04:46 +00:00
feat: Add support for percent-encoding in password and it's corresponding test
Add support of percent-encoded characters when passing the username and password in URI Closes https://github.com/espressif/esp-mqtt/issues/294
This commit is contained in:
+4
-1
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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})
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <stdlib.h>
|
||||
#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);
|
||||
}
|
||||
+22
-24
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <net/if.h>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include "esp_transport.h"
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers.hpp>
|
||||
#include <rapidcheck.h>
|
||||
|
||||
#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<const char *, 4> 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<std::string>(
|
||||
*rc::gen::inRange<int>(1, 33), host_char).as("host");
|
||||
auto path = *rc::gen::container<std::string>(
|
||||
*rc::gen::inRange<int>(0, 17), host_char).as("path");
|
||||
auto port = *rc::gen::maybe(rc::gen::inRange<uint16_t>(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);
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
```
|
||||
@@ -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)
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
espressif/catch2:
|
||||
version: "*"
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <format>
|
||||
#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<typename T>
|
||||
struct base {
|
||||
static Gen<T> arbitrary()
|
||||
{
|
||||
return gen::build<T>(gen::set(&T::value));
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct Arbitrary<percent_encoded_string> : base<percent_encoded_string> { };
|
||||
|
||||
template<>
|
||||
struct Arbitrary<uri_username> : base<uri_username> { };
|
||||
template<>
|
||||
struct Arbitrary<uri_password> : base<uri_password> { };
|
||||
template<>
|
||||
struct Arbitrary<uri_host> : base<uri_host> { };
|
||||
|
||||
template<>
|
||||
struct Arbitrary<URI> {
|
||||
static Gen<URI> arbitrary()
|
||||
{
|
||||
return gen::build<URI>(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<char> 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<char> 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<char> 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Unlicense OR CC0-1.0
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <catch2/catch_session.hpp>
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user