FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
url.hpp
Go to the documentation of this file.
1#pragma once
2// Minimal URL parsing / query building for venue endpoints. No heap allocation in QueryBuilder;
3// Url::parse works on string_views into the input.
4#include <array>
5#include <cstddef>
6#include <cstdint>
7#include <cstring>
8#include <optional>
9#include <span>
10#include <string_view>
11
12namespace fastmm::net {
13
14struct Url {
15 std::string_view scheme; // "wss", "https", ...
16 std::string_view host; // without brackets for IPv6 literals
17 std::uint16_t port = 0; // explicit or scheme default (80/443)
18 std::string_view path; // always at least "/"
19 std::string_view query; // without '?'
20 bool tls = false; // scheme is wss/https
22 // Path + "?" + query as sent in the request line; `buf` must hold both.
23 std::size_t request_target(std::span<char> buf) const noexcept {
24 if (buf.size() < path.size() + (query.empty() ? 0 : query.size() + 1)) return 0;
25 std::size_t n = 0;
26 for (char c : path) buf[n++] = c;
27 if (!query.empty()) {
28 buf[n++] = '?';
29 for (char c : query) buf[n++] = c;
30 }
31 return n;
32 }
33
34 // Accepts scheme://host[:port][/path][?query]. Fragment is not supported (venue URLs
35 // never carry one). Returns nullopt for a missing scheme/host or a bad port.
36 static std::optional<Url> parse(std::string_view s) noexcept {
37 Url u;
38 const std::size_t scheme_end = s.find("://");
39 if (scheme_end == std::string_view::npos || scheme_end == 0) return std::nullopt;
40 u.scheme = s.substr(0, scheme_end);
41 std::string_view rest = s.substr(scheme_end + 3);
42
43 std::size_t authority_end = rest.find_first_of("/?");
44 std::string_view authority =
45 authority_end == std::string_view::npos ? rest : rest.substr(0, authority_end);
46 std::string_view tail =
47 authority_end == std::string_view::npos ? "" : rest.substr(authority_end);
48 if (authority.empty()) return std::nullopt;
49
50 std::string_view port_str;
51 if (authority.front() == '[') { // IPv6 literal
52 const std::size_t close = authority.find(']');
53 if (close == std::string_view::npos) return std::nullopt;
54 u.host = authority.substr(1, close - 1);
55 std::string_view after = authority.substr(close + 1);
56 if (!after.empty()) {
57 if (after.front() != ':') return std::nullopt;
58 port_str = after.substr(1);
59 }
60 } else {
61 const std::size_t colon = authority.rfind(':');
62 if (colon == std::string_view::npos) {
63 u.host = authority;
64 } else {
65 u.host = authority.substr(0, colon);
66 port_str = authority.substr(colon + 1);
67 }
68 }
69 if (u.host.empty()) return std::nullopt;
70
71 u.tls = (u.scheme == "wss" || u.scheme == "https");
72 if (port_str.empty()) {
73 u.port = u.tls ? 443 : 80;
74 } else {
75 std::uint32_t p = 0;
76 for (char c : port_str) {
77 if (c < '0' || c > '9') return std::nullopt;
78 p = p * 10 + static_cast<std::uint32_t>(c - '0');
79 if (p > 65535) return std::nullopt;
80 }
81 if (p == 0) return std::nullopt;
82 u.port = static_cast<std::uint16_t>(p);
83 }
84
85 if (tail.empty() || tail.front() == '?') {
86 u.path = "/";
87 u.query = tail.empty() ? "" : tail.substr(1);
88 } else {
89 const std::size_t q = tail.find('?');
90 u.path = q == std::string_view::npos ? tail : tail.substr(0, q);
91 u.query = q == std::string_view::npos ? "" : tail.substr(q + 1);
92 }
93 return u;
94 }
95};
96
97// RFC 3986 unreserved characters pass through; everything else becomes %XX.
98constexpr bool is_url_unreserved(char c) noexcept {
99 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' ||
100 c == '_' || c == '.' || c == '~';
101}
102
103// Returns bytes written, or 0 if `out` is too small (worst case 3 * in.size()).
104inline std::size_t percent_encode(std::string_view in, std::span<char> out) noexcept {
105 static constexpr char kHex[] = "0123456789ABCDEF";
106 std::size_t n = 0;
107 for (char c : in) {
108 if (is_url_unreserved(c)) {
109 if (n + 1 > out.size()) return 0;
110 out[n++] = c;
111 } else {
112 if (n + 3 > out.size()) return 0;
113 const auto b = static_cast<std::uint8_t>(c);
114 out[n++] = '%';
115 out[n++] = kHex[b >> 4];
116 out[n++] = kHex[b & 0x0F];
117 }
118 }
119 return n;
120}
121
122// Builds "k1=v1&k2=v2" into a fixed buffer, percent-encoding values. Overflow is sticky:
123// `ok()` turns false and the view is truncated to the last complete pair, so a signed
124// request can never be built from a partially-encoded query.
125template <std::size_t Capacity>
126class QueryBuilder {
127 public:
128 QueryBuilder& add(std::string_view key, std::string_view value) noexcept {
129 if (!ok_) return *this;
130 const std::size_t start = len_;
131 if (len_ > 0 && !put('&')) return fail(start);
132 if (!put_all(key)) return fail(start);
133 if (!put('=')) return fail(start);
134 const std::size_t n =
135 percent_encode(value, std::span<char>(buf_.data() + len_, Capacity - len_));
136 if (n == 0 && !value.empty()) return fail(start);
137 len_ += n;
138 return *this;
139 }
140
141 QueryBuilder& add(std::string_view key, std::int64_t value) noexcept {
142 char tmp[21];
143 std::size_t n = 0;
144 std::uint64_t mag =
145 value < 0 ? 0 - static_cast<std::uint64_t>(value) : static_cast<std::uint64_t>(value);
146 char digits[20];
147 std::size_t d = 0;
148 do {
149 digits[d++] = static_cast<char>('0' + mag % 10);
150 mag /= 10;
151 } while (mag != 0);
152 if (value < 0) tmp[n++] = '-';
153 while (d > 0) tmp[n++] = digits[--d];
154 return add(key, std::string_view(tmp, n));
155 }
156
157 // Appends raw pre-encoded text (e.g. "&signature=...").
158 QueryBuilder& append_raw(std::string_view raw) noexcept {
159 if (!ok_) return *this;
160 const std::size_t start = len_;
161 if (!put_all(raw)) return fail(start);
162 return *this;
163 }
164
165 std::string_view view() const noexcept { return {buf_.data(), len_}; }
166 std::size_t size() const noexcept { return len_; }
167 bool ok() const noexcept { return ok_; }
168 bool empty() const noexcept { return len_ == 0; }
169 void clear() noexcept {
170 len_ = 0;
171 ok_ = true;
172 }
173
174 private:
175 bool put(char c) noexcept {
176 if (len_ >= Capacity) return false;
177 buf_[len_++] = c;
178 return true;
179 }
180 bool put_all(std::string_view s) noexcept {
181 if (s.size() > Capacity - len_) return false;
182 if (!s.empty()) std::memcpy(buf_.data() + len_, s.data(), s.size());
183 len_ += s.size();
184 return true;
185 }
186 QueryBuilder& fail(std::size_t rollback_to) noexcept {
187 len_ = rollback_to;
188 ok_ = false;
189 return *this;
190 }
191
192 // Not zeroed: only [0, len_) is ever read, and zeroing kilobytes per request showed up in
193 // the order encoders' profiles.
194 std::array<char, Capacity> buf_; // NOLINT(cppcoreguidelines-pro-type-member-init)
195 std::size_t len_ = 0;
196 bool ok_ = true;
197};
198
199} // namespace fastmm::net
Builds "k1=v1\&k2=v2" into a fixed buffer, percent-encoding values.
Definition url.hpp:128
std::string_view view() const noexcept
Definition url.hpp:167
QueryBuilder & append_raw(std::string_view raw) noexcept
Appends raw pre-encoded text (e.g. "\&signature=...").
Definition url.hpp:160
void clear() noexcept
Definition url.hpp:171
bool empty() const noexcept
Definition url.hpp:170
QueryBuilder & add(std::string_view key, std::string_view value) noexcept
Definition url.hpp:130
bool ok() const noexcept
Definition url.hpp:169
std::size_t size() const noexcept
Definition url.hpp:168
Definition reactor.hpp:37
std::size_t percent_encode(std::string_view in, std::span< char > out) noexcept
Returns bytes written, or 0 if out is too small (worst case 3 * in.size()).
Definition url.hpp:106
constexpr bool is_url_unreserved(char c) noexcept
RFC 3986 unreserved characters pass through; everything else becomes %XX.
Definition url.hpp:100
std::uint16_t port
explicit or scheme default (80/443)
Definition url.hpp:19
std::size_t request_target(std::span< char > buf) const noexcept
Path + "?" + query as sent in the request line; buf must hold both.
Definition url.hpp:25
bool tls
scheme is wss/https
Definition url.hpp:22
std::string_view scheme
"wss", "https", ...
Definition url.hpp:17
static std::optional< Url > parse(std::string_view s) noexcept
Accepts scheme://host[:port][/path][?query].
Definition url.hpp:38
std::string_view host
without brackets for IPv6 literals
Definition url.hpp:18
std::string_view query
without '?'
Definition url.hpp:21
std::string_view path
always at least "/"
Definition url.hpp:20