#include "api_url_parser.hpp" #include #include 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(decoded); read_pos += 3; } else { *write_pos++ = *read_pos++; } } return static_cast(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(decoded); read_pos += 3; } else { *write_pos++ = *read_pos++; } } return static_cast(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 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(*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(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(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(ApiParameterKey::PolicyId)] = policy_id; return ParseResult::Success; } } result.route = HttpRoute::NotFound; return ParseResult::Success; }