mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-02-18 10:53:17 +00:00
Implement url query parsing
This commit is contained in:
parent
00fae61b14
commit
a7fe572d63
19
Tactility/Include/Tactility/network/Url.h
Normal file
19
Tactility/Include/Tactility/network/Url.h
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace tt::network {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a query from a URL
|
||||||
|
* @param[in] query
|
||||||
|
* @return a map with key-values
|
||||||
|
*/
|
||||||
|
std::map<std::string, std::string> parseUrlQuery(std::string query);
|
||||||
|
|
||||||
|
std::string urlEncode(const std::string& input);
|
||||||
|
|
||||||
|
std::string urlDecode(const std::string& input);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@ -1,7 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
|
||||||
namespace tt::app::development {
|
namespace tt::app::development {
|
||||||
|
|
||||||
void start();
|
void start();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif // ESP_PLATFORM
|
||||||
@ -36,7 +36,7 @@ class DevelopmentService final : public Service {
|
|||||||
|
|
||||||
httpd_uri_t appInstallEndpoint = {
|
httpd_uri_t appInstallEndpoint = {
|
||||||
.uri = "/app/install",
|
.uri = "/app/install",
|
||||||
.method = HTTP_POST,
|
.method = HTTP_PUT,
|
||||||
.handler = handleAppInstall,
|
.handler = handleAppInstall,
|
||||||
.user_ctx = this
|
.user_ctx = this
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
#ifdef ESP_PLATFORM
|
||||||
|
|
||||||
#include "Tactility/app/AppManifest.h"
|
#include "Tactility/app/AppManifest.h"
|
||||||
#include "Tactility/lvgl/Style.h"
|
#include "Tactility/lvgl/Style.h"
|
||||||
#include "Tactility/lvgl/Toolbar.h"
|
#include "Tactility/lvgl/Toolbar.h"
|
||||||
@ -154,3 +156,5 @@ void start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
#endif // ESP_PLATFORM
|
||||||
82
Tactility/Source/network/Url.cpp
Normal file
82
Tactility/Source/network/Url.cpp
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#include "Tactility/network/Url.h"
|
||||||
|
|
||||||
|
#include <Tactility/Log.h>
|
||||||
|
|
||||||
|
namespace tt::network {
|
||||||
|
|
||||||
|
std::map<std::string, std::string> parseUrlQuery(std::string query) {
|
||||||
|
std::map<std::string, std::string> 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<char>(conversion_buffer);
|
||||||
|
result += c;
|
||||||
|
i = i + 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "Tactility/service/development/DevelopmentService.h"
|
#include "Tactility/service/development/DevelopmentService.h"
|
||||||
|
|
||||||
|
#include "Tactility/network/Url.h"
|
||||||
#include "Tactility/TactilityHeadless.h"
|
#include "Tactility/TactilityHeadless.h"
|
||||||
#include "Tactility/service/ServiceManifest.h"
|
#include "Tactility/service/ServiceManifest.h"
|
||||||
#include "Tactility/service/ServiceRegistry.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) {
|
esp_err_t DevelopmentService::handleAppRun(httpd_req_t* request) {
|
||||||
httpd_resp_send(request, nullptr, 0);
|
httpd_resp_send(request, nullptr, 0);
|
||||||
TT_LOG_I(TAG, "[200] /app/run");
|
size_t buffer_length = httpd_req_get_url_query_len(request) + 1;
|
||||||
return ESP_OK;
|
auto buffer = std::make_unique<char[]>(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) {
|
esp_err_t DevelopmentService::handleAppInstall(httpd_req_t* request) {
|
||||||
|
|||||||
@ -1,98 +1,61 @@
|
|||||||
#include "doctest.h"
|
#include "doctest.h"
|
||||||
#include <Tactility/hal/Device.h>
|
#include <Tactility/network/Url.h>
|
||||||
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
using namespace tt;
|
using namespace tt;
|
||||||
|
|
||||||
class TestDevice final : public hal::Device {
|
TEST_CASE("parseUrlQuery can handle a single key-value pair") {
|
||||||
|
auto map = network::parseUrlQuery("?key=value");
|
||||||
private:
|
CHECK_EQ(map.size(), 1);
|
||||||
|
CHECK_EQ(map["key"], "value");
|
||||||
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<hal::Device> device;
|
|
||||||
|
|
||||||
public:
|
|
||||||
|
|
||||||
explicit DeviceAutoRegistration(std::shared_ptr<hal::Device> 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<TestDevice>();
|
|
||||||
|
|
||||||
// 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("find device by id") {
|
TEST_CASE("parseUrlQuery can handle empty value in the middle") {
|
||||||
auto device = std::make_shared<TestDevice>();
|
auto map = network::parseUrlQuery("?a=1&b=&c=3");
|
||||||
DeviceAutoRegistration auto_registration(device);
|
CHECK_EQ(map.size(), 3);
|
||||||
|
CHECK_EQ(map["a"], "1");
|
||||||
auto found_device = hal::findDevice(device->getId());
|
CHECK_EQ(map["b"], "");
|
||||||
CHECK_NE(found_device, nullptr);
|
CHECK_EQ(map["c"], "3");
|
||||||
CHECK_EQ(found_device->getId(), device->getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("find device by name") {
|
TEST_CASE("parseUrlQuery can handle empty value at the end") {
|
||||||
auto device = std::make_shared<TestDevice>();
|
auto map = network::parseUrlQuery("?a=1&b=");
|
||||||
DeviceAutoRegistration auto_registration(device);
|
CHECK_EQ(map.size(), 2);
|
||||||
|
CHECK_EQ(map["a"], "1");
|
||||||
auto found_device = hal::findDevice(device->getName());
|
CHECK_EQ(map["b"], "");
|
||||||
CHECK_NE(found_device, nullptr);
|
|
||||||
CHECK_EQ(found_device->getId(), device->getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("find device by type") {
|
TEST_CASE("parseUrlQuery returns empty map when query s questionmark with a key without a value") {
|
||||||
// Headless mode shouldn't have a display, so we want to create one to find only our own display as unique device
|
auto map = network::parseUrlQuery("?a");
|
||||||
// We first verify the initial assumption that there is no display:
|
CHECK_EQ(map.size(), 0);
|
||||||
auto unexpected_display = hal::findFirstDevice<TestDevice>(hal::Device::Type::Display);
|
}
|
||||||
CHECK_EQ(unexpected_display, nullptr);
|
|
||||||
|
TEST_CASE("parseUrlQuery returns empty map when query is a questionmark") {
|
||||||
auto device = std::make_shared<TestDevice>(hal::Device::Type::Display, "DisplayMock", "");
|
auto map = network::parseUrlQuery("?");
|
||||||
DeviceAutoRegistration auto_registration(device);
|
CHECK_EQ(map.size(), 0);
|
||||||
|
}
|
||||||
auto found_device = hal::findFirstDevice<TestDevice>(hal::Device::Type::Display);
|
|
||||||
CHECK_NE(found_device, nullptr);
|
TEST_CASE("parseUrlQuery should url-decode the value") {
|
||||||
CHECK_EQ(found_device->getId(), device->getId());
|
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);
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user