Files
weaseldb/src/api_url_parser.cpp
2025-09-04 20:47:58 -04:00

250 lines
7.3 KiB
C++

#include "api_url_parser.hpp"
#include <cassert>
#include <string_view>
namespace {
// RFC 3986 hex digit to value conversion
// Returns -1 for invalid hex digits
int hex_digit_to_value(char c) {
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
return -1;
}
// Decode percent-encoded sequence at src
// Returns decoded byte value, or -1 for malformed encoding
int decode_percent_sequence(const char *src) {
if (src[0] != '%')
return -1;
int high = hex_digit_to_value(src[1]);
int low = hex_digit_to_value(src[2]);
if (high == -1 || low == -1)
return -1;
return (high << 4) | low;
}
// Decode RFC 3986 percent-encoding in place (for path segments)
// Returns new length, or -1 for malformed encoding
int decode_path_segment(char *data, int length) {
char *write_pos = data;
const char *read_pos = data;
const char *end = data + length;
while (read_pos < end) {
if (*read_pos == '%') {
if (read_pos + 2 >= end)
return -1; // Incomplete sequence
int decoded = decode_percent_sequence(read_pos);
if (decoded == -1)
return -1; // Malformed sequence
*write_pos++ = static_cast<char>(decoded);
read_pos += 3;
} else {
*write_pos++ = *read_pos++;
}
}
return static_cast<int>(write_pos - data);
}
// Decode application/x-www-form-urlencoded in place (for query parameters)
// Handles + → space conversion, then percent-decoding
// Returns new length, or -1 for malformed encoding
int decode_query_value(char *data, int length) {
char *write_pos = data;
const char *read_pos = data;
const char *end = data + length;
while (read_pos < end) {
if (*read_pos == '+') {
*write_pos++ = ' ';
read_pos++;
} else if (*read_pos == '%') {
if (read_pos + 2 >= end)
return -1; // Incomplete sequence
int decoded = decode_percent_sequence(read_pos);
if (decoded == -1)
return -1; // Malformed sequence
*write_pos++ = static_cast<char>(decoded);
read_pos += 3;
} else {
*write_pos++ = *read_pos++;
}
}
return static_cast<int>(write_pos - data);
}
// A simplified helper to find a delimiter in a buffer
// Returns the position of the delimiter, or -1 if not found
int find_delimiter(const char *data, int length, char delimiter) {
for (int i = 0; i < length; ++i) {
if (data[i] == delimiter) {
return i;
}
}
return -1;
}
// Maps a string parameter key to its corresponding enum value.
// Unrecognized keys are ignored as per the design.
[[nodiscard]] std::optional<ApiParameterKey>
to_api_parameter_key(std::string_view key) {
if (key == "request_id")
return ApiParameterKey::RequestId;
if (key == "min_version")
return ApiParameterKey::MinVersion;
return std::nullopt;
}
// Parses the query string part of a URL (in-place decoding)
// Returns ParseResult::Success or ParseResult::MalformedEncoding
ParseResult parse_query_string(char *query_data, int query_length,
RouteMatch &match) {
int pos = 0;
while (pos < query_length) {
// Find end of current key=value pair
int pair_end = find_delimiter(query_data + pos, query_length - pos, '&');
if (pair_end == -1)
pair_end = query_length - pos;
// Find = separator within the pair
int eq_pos = find_delimiter(query_data + pos, pair_end, '=');
if (eq_pos == -1) {
// No value, skip this parameter
pos += pair_end + 1;
continue;
}
// Decode key and value in place
char *key_start = query_data + pos;
int key_length = eq_pos;
char *value_start = query_data + pos + eq_pos + 1;
int value_length = pair_end - eq_pos - 1;
// Decode value (query parameters use form encoding)
int decoded_value_length = decode_query_value(value_start, value_length);
if (decoded_value_length == -1) {
return ParseResult::MalformedEncoding;
}
// Check if this is a parameter we care about
std::string_view key_view(key_start, key_length);
if (auto key_enum = to_api_parameter_key(key_view)) {
match.params[static_cast<int>(*key_enum)] =
std::string_view(value_start, decoded_value_length);
}
pos += pair_end + 1;
}
return ParseResult::Success;
}
} // namespace
ParseResult ApiUrlParser::parse(std::string_view method, char *url_data,
int url_length, RouteMatch &result) {
assert(url_data != nullptr);
assert(url_length >= 0);
// Find query string separator
int query_start = find_delimiter(url_data, url_length, '?');
char *path_data = url_data;
int path_length = (query_start == -1) ? url_length : query_start;
char *query_data = (query_start == -1) ? nullptr : url_data + query_start + 1;
int query_length = (query_start == -1) ? 0 : url_length - query_start - 1;
// Parse and decode query string first
if (query_data && query_length > 0) {
ParseResult query_result =
parse_query_string(query_data, query_length, result);
if (query_result != ParseResult::Success) {
return query_result;
}
}
// Decode path segment (RFC 3986 percent-decoding)
int decoded_path_length = decode_path_segment(path_data, path_length);
if (decoded_path_length == -1) {
return ParseResult::MalformedEncoding;
}
std::string_view path(path_data, decoded_path_length);
// Route matching with decoded path
if (method == "GET") {
if (path == "/v1/version") {
result.route = HttpRoute::GetVersion;
return ParseResult::Success;
}
if (path == "/v1/subscribe") {
result.route = HttpRoute::GetSubscribe;
return ParseResult::Success;
}
if (path == "/v1/status") {
result.route = HttpRoute::GetStatus;
return ParseResult::Success;
}
if (path.starts_with("/v1/retention")) {
result.route = HttpRoute::GetRetention;
// Note: This matches both /v1/retention and /v1/retention/{id}
if (path.length() > 13) { // length of "/v1/retention"
std::string_view policy_id =
path.substr(14); // length of "/v1/retention/"
if (!policy_id.empty()) {
result.params[static_cast<int>(ApiParameterKey::PolicyId)] =
policy_id;
}
}
return ParseResult::Success;
}
if (path == "/metrics") {
result.route = HttpRoute::GetMetrics;
return ParseResult::Success;
}
if (path == "/ok") {
result.route = HttpRoute::GetOk;
return ParseResult::Success;
}
} else if (method == "POST") {
if (path == "/v1/commit") {
result.route = HttpRoute::PostCommit;
return ParseResult::Success;
}
} else if (method == "PUT") {
if (path.starts_with("/v1/retention/")) {
result.route = HttpRoute::PutRetention;
std::string_view policy_id = path.substr(14);
result.params[static_cast<int>(ApiParameterKey::PolicyId)] = policy_id;
return ParseResult::Success;
}
} else if (method == "DELETE") {
if (path.starts_with("/v1/retention/")) {
result.route = HttpRoute::DeleteRetention;
std::string_view policy_id = path.substr(14);
result.params[static_cast<int>(ApiParameterKey::PolicyId)] = policy_id;
return ParseResult::Success;
}
}
result.route = HttpRoute::NotFound;
return ParseResult::Success;
}