cheatah
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 once
5// cheatah::parsers::url — a from-scratch parser for the http(s) URL subset the requests module
6// speaks: `scheme://host[:port][/path][?query]`. No allocation beyond the component strings, no
7// regex, no dependencies. Userinfo (`user@`) and fragments (`#...`) are not supported — the first
8// 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>
16namespace cheatah::parsers::url {
18/**
19 * @brief One parsed http(s) URL. @c target is the HTTP request-target — the path plus the original
20 * query, always beginning with '/' (an empty path becomes "/").
21 */
22struct 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 module
31 * surface is symmetric with parsers::json::Parser and imports the same way from cheatah.
32 */
33class Parser {
34public:
35 /**
36 * Parse @p text into @p out. Accepts `scheme://host[:port][/path][?query]` with scheme http or
37 * 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 out
45 * @test UrlParser.Components
46 */
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 copy
54 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 rejected
68 }
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