Source
stdlib/parsers/url/url.hpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
#pragma once5
// cheatah::parsers::url — a from-scratch parser for the http(s) URL subset the requests module6
// speaks: `scheme://host[:port][/path][?query]`. No allocation beyond the component strings, no7
// regex, no dependencies. Userinfo (`user@`) and fragments (`#...`) are not supported — the first8
// is an obsolete security hazard in http URLs, the second is never sent to the server anyway.9
//10
// Laid out as a cheatah stdlib module: from .purr this is `import parsers.url.Parser as Parser`,11
// mirroring `import parsers.json.Parser as Parser` — each parsers submodule exposes a Parser.13
#include <string>14
#include <string_view>16
namespace cheatah::parsers::url {18
/**19
* @brief One parsed http(s) URL. @c target is the HTTP request-target — the path plus the original20
* query, always beginning with '/' (an empty path becomes "/").21
*/22
struct Url {23
std::string scheme; ///< the lowercased scheme: "http" or "https".24
std::string host; ///< the host (name or IP); never empty on success.25
long long port = 0; ///< the explicit port, or the scheme default (80 for http, 443 for https).26
std::string target; ///< the HTTP request-target "/path?query" (always begins with '/').27
};29
/**30
* @brief The URL parser. Stateless and reusable; a class (not a free function) so the module31
* surface is symmetric with parsers::json::Parser and imports the same way from cheatah.32
*/33
class Parser {34
public:35
/**36
* Parse @p text into @p out. Accepts `scheme://host[:port][/path][?query]` with scheme http or37
* https (case-insensitive). Rejects empty hosts, non-numeric or out-of-range ports, userinfo,38
* and fragments. On failure @p out is left unspecified.39
*40
* @param text the URL text to parse.41
* @param out receives the parsed components on success.42
* @return true iff @p text is a valid accepted URL.43
* @complexity O(|text|)44
* @alloc the component strings in @p out45
* @test UrlParser.Components46
*/47
[[nodiscard]] bool parse(std::string_view text, Url& out) const {48
const std::size_t scheme_end = text.find("://");49
if (scheme_end == std::string_view::npos || scheme_end == 0) {50
return false;51
}52
out.scheme.clear();53
for (const char ch : text.substr(0, scheme_end)) { // lowercase the scheme as we copy54
out.scheme.push_back(ch >= 'A' && ch <= 'Z' ? static_cast<char>(ch - 'A' + 'a') : ch);55
}56
if (out.scheme != "http" && out.scheme != "https") {57
return false;58
}60
std::string_view rest = text.substr(scheme_end + 3);61
const std::size_t path_start = rest.find('/');62
const std::size_t query_start = rest.find('?');63
const std::size_t authority_end = std::min(path_start, query_start);64
const std::string_view authority = rest.substr(0, authority_end);65
if (authority.empty() || authority.find('@') != std::string_view::npos ||66
rest.find('#') != std::string_view::npos) {67
return false; // empty host, userinfo, and fragments are all rejected68
}70
const std::size_t colon = authority.rfind(':');71
if (colon == std::string_view::npos) {72
out.host = std::string(authority);73
out.port = (out.scheme == "https") ? 443 : 80;74
} else {75
out.host = std::string(authority.substr(0, colon));76
const std::string_view digits = authority.substr(colon + 1);77
if (out.host.empty() || digits.empty() || digits.size() > 5) {78
return false;79
}80
long long port = 0;81
for (const char ch : digits) {82
if (ch < '0' || ch > '9') {83
return false;84
}85
port = port * 10 + (ch - '0');86
}87
if (port < 1 || port > 65535) {88
return false;89
}90
out.port = port;91
}93
// The request-target: everything from the first '/' on (or "/" when the path is absent,94
// including the bare-query form "host?x=1" -> "/?x=1").95
if (path_start != std::string_view::npos) {96
out.target = std::string(rest.substr(path_start));97
} else if (query_start != std::string_view::npos) {98
out.target = "/" + std::string(rest.substr(query_start));99
} else {100
out.target = "/";101
}102
return true;103
}104
};106
} // namespace cheatah::parsers::url