From a7fe572d63c42ec1e6984b100a85a5119b8bbf74 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Sat, 21 Jun 2025 22:47:50 +0200 Subject: [PATCH] Implement url query parsing --- Tactility/Include/Tactility/network/Url.h | 19 +++ .../Tactility/app/development/Development.h | 4 + .../service/development/DevelopmentService.h | 2 +- .../Source/app/development/Development.cpp | 6 +- Tactility/Source/network/Url.cpp | 82 +++++++++++ .../development/DevelopmentService.cpp | 14 +- Tests/Tactility/UrlTest.cpp | 137 +++++++----------- 7 files changed, 173 insertions(+), 91 deletions(-) create mode 100644 Tactility/Include/Tactility/network/Url.h create mode 100644 Tactility/Source/network/Url.cpp diff --git a/Tactility/Include/Tactility/network/Url.h b/Tactility/Include/Tactility/network/Url.h new file mode 100644 index 00000000..6fb6c236 --- /dev/null +++ b/Tactility/Include/Tactility/network/Url.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace tt::network { + +/** + * Parse a query from a URL + * @param[in] query + * @return a map with key-values + */ +std::map parseUrlQuery(std::string query); + +std::string urlEncode(const std::string& input); + +std::string urlDecode(const std::string& input); + +} // namespace \ No newline at end of file diff --git a/Tactility/Private/Tactility/app/development/Development.h b/Tactility/Private/Tactility/app/development/Development.h index 77ec9363..ca93c52d 100644 --- a/Tactility/Private/Tactility/app/development/Development.h +++ b/Tactility/Private/Tactility/app/development/Development.h @@ -1,7 +1,11 @@ #pragma once +#ifdef ESP_PLATFORM + namespace tt::app::development { void start(); } + +#endif // ESP_PLATFORM \ No newline at end of file diff --git a/Tactility/Private/Tactility/service/development/DevelopmentService.h b/Tactility/Private/Tactility/service/development/DevelopmentService.h index 6e28e2b9..7fb0d9d4 100644 --- a/Tactility/Private/Tactility/service/development/DevelopmentService.h +++ b/Tactility/Private/Tactility/service/development/DevelopmentService.h @@ -36,7 +36,7 @@ class DevelopmentService final : public Service { httpd_uri_t appInstallEndpoint = { .uri = "/app/install", - .method = HTTP_POST, + .method = HTTP_PUT, .handler = handleAppInstall, .user_ctx = this }; diff --git a/Tactility/Source/app/development/Development.cpp b/Tactility/Source/app/development/Development.cpp index cc6ee31c..5c9dbd89 100644 --- a/Tactility/Source/app/development/Development.cpp +++ b/Tactility/Source/app/development/Development.cpp @@ -1,3 +1,5 @@ +#ifdef ESP_PLATFORM + #include "Tactility/app/AppManifest.h" #include "Tactility/lvgl/Style.h" #include "Tactility/lvgl/Toolbar.h" @@ -153,4 +155,6 @@ void start() { app::start(manifest.id); } -} // namespace \ No newline at end of file +} // namespace + +#endif // ESP_PLATFORM \ No newline at end of file diff --git a/Tactility/Source/network/Url.cpp b/Tactility/Source/network/Url.cpp new file mode 100644 index 00000000..7cbdca85 --- /dev/null +++ b/Tactility/Source/network/Url.cpp @@ -0,0 +1,82 @@ +#include "Tactility/network/Url.h" + +#include + +namespace tt::network { + +std::map parseUrlQuery(std::string query) { + std::map result; + + if (query.empty()) { + return result; + } + + size_t current_index = query[0] == '?' ? 1U : 0U; + auto equals_index = query.find_first_of('=', current_index); + while (equals_index != std::string::npos) { + auto index_boundary = query.find_first_of('&', equals_index + 1); + if (index_boundary == std::string::npos) { + index_boundary = query.size(); + } + auto key = query.substr(current_index, (equals_index - current_index)); + auto decodedKey = urlDecode(key); + auto value = query.substr(equals_index + 1, (index_boundary - equals_index - 1)); + auto decodedValue = urlDecode(value); + + result[decodedKey] = decodedValue; + + // Find next token + current_index = index_boundary + 1; + equals_index = query.find_first_of('=', current_index); + } + + return result; +} + +// Adapted from https://stackoverflow.com/a/29962178/3848666 +std::string urlEncode(const std::string& input) { + std::string result = ""; + const char* characters = input.c_str(); + char hex_buffer[10]; + size_t input_length = input.length(); + + for (size_t i = 0;i < input_length;i++) { + unsigned char c = characters[i]; + // uncomment this if you want to encode spaces with + + if (c==' ') { + result += '+'; + } else if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { + result += c; + } else { + sprintf(hex_buffer, "%%%02X", c); //%% means '%' literal, %02X means at least two digits, paddable with a leading zero + result += hex_buffer; + } + } + + return result; +} + +// Adapted from https://stackoverflow.com/a/29962178/3848666 +std::string urlDecode(const std::string& input) { + std::string result; + size_t conversion_buffer, input_length = input.length(); + + for (size_t i = 0; i < input_length; i++) { + if (input[i] != '%') { + if (input[i] == '+') { + result += ' '; + } else { + result += input[i]; + } + } else { + sscanf(input.substr(i + 1, 2).c_str(), "%x", &conversion_buffer); + char c = static_cast(conversion_buffer); + result += c; + i = i + 2; + } + } + + return result; +} + +} // namespace diff --git a/Tactility/Source/service/development/DevelopmentService.cpp b/Tactility/Source/service/development/DevelopmentService.cpp index f163c9b0..731835d4 100644 --- a/Tactility/Source/service/development/DevelopmentService.cpp +++ b/Tactility/Source/service/development/DevelopmentService.cpp @@ -2,6 +2,7 @@ #include "Tactility/service/development/DevelopmentService.h" +#include "Tactility/network/Url.h" #include "Tactility/TactilityHeadless.h" #include "Tactility/service/ServiceManifest.h" #include "Tactility/service/ServiceRegistry.h" @@ -200,8 +201,17 @@ esp_err_t DevelopmentService::handleGetInfo(httpd_req_t* request) { esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) { httpd_resp_send(request, nullptr, 0); - TT_LOG_I(TAG, "[200] /app/run"); - return ESP_OK; + size_t buffer_length = httpd_req_get_url_query_len(request) + 1; + auto buffer = std::make_unique(buffer_length); + if (buffer.get() != nullptr && httpd_req_get_url_query_str(request, buffer.get(), buffer_length) == ESP_OK) { + auto key_values = network::parseUrlQuery(std::string(buffer.get())); + TT_LOG_I(TAG, "[200] /app/run %s", buffer.get()); + return ESP_OK; + } else { + TT_LOG_I(TAG, "[500] /app/run failed to get query"); + httpd_resp_send_500(request); + return ESP_FAIL; + } } esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) { diff --git a/Tests/Tactility/UrlTest.cpp b/Tests/Tactility/UrlTest.cpp index 70243947..f2c2fe99 100644 --- a/Tests/Tactility/UrlTest.cpp +++ b/Tests/Tactility/UrlTest.cpp @@ -1,98 +1,61 @@ #include "doctest.h" -#include - -#include +#include using namespace tt; -class TestDevice final : public hal::Device { - -private: - - hal::Device::Type type; - std::string name; - std::string description; - -public: - - TestDevice(hal::Device::Type type, std::string name, std::string description) : - type(type), - name(std::move(name)), - description(std::move(description)) - {} - - TestDevice() : TestDevice(hal::Device::Type::Power, "PowerMock", "PowerMock description") {} - - ~TestDevice() final = default; - - Type getType() const final { return type; } - std::string getName() const final { return name; } - std::string getDescription() const final { return description; } -}; - -class DeviceAutoRegistration { - - std::shared_ptr device; - -public: - - explicit DeviceAutoRegistration(std::shared_ptr inDevice) : device(std::move(inDevice)) { - hal::registerDevice(device); - } - - ~DeviceAutoRegistration() { - hal::deregisterDevice(device); - } -}; - -/** We add 3 tests into 1 to ensure cleanup happens */ -TEST_CASE("registering and deregistering a device works") { - auto device = std::make_shared(); - - // Pre-registration - CHECK_EQ(hal::findDevice(device->getId()), nullptr); - - // Registration - hal::registerDevice(device); - auto found_device = hal::findDevice(device->getId()); - CHECK_NE(found_device, nullptr); - CHECK_EQ(found_device->getId(), device->getId()); - - // Deregistration - hal::deregisterDevice(device); - CHECK_EQ(hal::findDevice(device->getId()), nullptr); - found_device = nullptr; // to decrease use count - CHECK_EQ(device.use_count(), 1); +TEST_CASE("parseUrlQuery can handle a single key-value pair") { + auto map = network::parseUrlQuery("?key=value"); + CHECK_EQ(map.size(), 1); + CHECK_EQ(map["key"], "value"); } -TEST_CASE("find device by id") { - auto device = std::make_shared(); - DeviceAutoRegistration auto_registration(device); - - auto found_device = hal::findDevice(device->getId()); - CHECK_NE(found_device, nullptr); - CHECK_EQ(found_device->getId(), device->getId()); +TEST_CASE("parseUrlQuery can handle empty value in the middle") { + auto map = network::parseUrlQuery("?a=1&b=&c=3"); + CHECK_EQ(map.size(), 3); + CHECK_EQ(map["a"], "1"); + CHECK_EQ(map["b"], ""); + CHECK_EQ(map["c"], "3"); } -TEST_CASE("find device by name") { - auto device = std::make_shared(); - DeviceAutoRegistration auto_registration(device); - - auto found_device = hal::findDevice(device->getName()); - CHECK_NE(found_device, nullptr); - CHECK_EQ(found_device->getId(), device->getId()); +TEST_CASE("parseUrlQuery can handle empty value at the end") { + auto map = network::parseUrlQuery("?a=1&b="); + CHECK_EQ(map.size(), 2); + CHECK_EQ(map["a"], "1"); + CHECK_EQ(map["b"], ""); } -TEST_CASE("find device by type") { - // Headless mode shouldn't have a display, so we want to create one to find only our own display as unique device - // We first verify the initial assumption that there is no display: - auto unexpected_display = hal::findFirstDevice(hal::Device::Type::Display); - CHECK_EQ(unexpected_display, nullptr); - - auto device = std::make_shared(hal::Device::Type::Display, "DisplayMock", ""); - DeviceAutoRegistration auto_registration(device); - - auto found_device = hal::findFirstDevice(hal::Device::Type::Display); - CHECK_NE(found_device, nullptr); - CHECK_EQ(found_device->getId(), device->getId()); +TEST_CASE("parseUrlQuery returns empty map when query s questionmark with a key without a value") { + auto map = network::parseUrlQuery("?a"); + CHECK_EQ(map.size(), 0); } + +TEST_CASE("parseUrlQuery returns empty map when query is a questionmark") { + auto map = network::parseUrlQuery("?"); + CHECK_EQ(map.size(), 0); +} + +TEST_CASE("parseUrlQuery should url-decode the value") { + auto map = network::parseUrlQuery("?key=Test%21Test"); + CHECK_EQ(map.size(), 1); + CHECK_EQ(map["key"], "Test!Test"); +} + +TEST_CASE("parseUrlQuery should url-decode the key") { + auto map = network::parseUrlQuery("?Test%21Test=value"); + CHECK_EQ(map.size(), 1); + CHECK_EQ(map["Test!Test"], "value"); +} + +TEST_CASE("urlDecode") { + auto input = std::string("prefix!*'();:@&=+$,/?#[]<>%-.^_`{}|~ \\"); + auto expected = std::string("prefix%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%23%5B%5D%3C%3E%25-.%5E_%60%7B%7D%7C~+%5C"); + auto encoded = network::urlEncode(input); + CHECK_EQ(encoded, expected); +} + +TEST_CASE("urlDecode") { + auto input = std::string("prefix%21%2A%27%28%29%3B%3A%40%26%3D%2B%24%2C%2F%3F%23%5B%5D%3C%3E%25-.%5E_%60%7B%7D%7C~+%5C"); + auto expected = std::string("prefix!*'();:@&=+$,/?#[]<>%-.^_`{}|~ \\"); + auto decoded = network::urlDecode(input); + CHECK_EQ(decoded, expected); +} \ No newline at end of file