Upa URL C++ library
A WHATWG URL Standard implementation
Loading...
Searching...
No Matches
url_host.h
Go to the documentation of this file.
1// Copyright 2016-2026 Rimas Misevičius
2// Distributed under the BSD-style license that can be
3// found in the LICENSE file.
4//
5
6#ifndef UPA_URL_HOST_H
7#define UPA_URL_HOST_H
8
9#include "buffer.h"
10#include "config.h"
11#include "idna.h"
12#include "str_arg.h"
13#include "url_ip.h"
14#include "url_percent_encode.h"
15#include "url_result.h"
16#include "url_utf.h"
17#include "util.h"
18
19#ifndef UPA_MODULE
20# include <algorithm> // any_of
21# include <cassert>
22# include <cstdint> // uint16_t, uint32_t
23# include <string>
24# include <string_view>
25# include <type_traits>
26#endif // UPA_MODULE
27
28namespace upa {
29
33UPA_EXPORT enum class HostType {
34 Empty = 0,
40};
41
42
43class UPA_SO_VISIBLE host_output {
44protected:
45 host_output() = default;
46 inline host_output(bool need_save)
47 : need_save_{ need_save } {}
48public:
49 host_output(const host_output&) = delete;
50 host_output& operator=(const host_output&) = delete;
51 virtual ~host_output() = default;
52
53 virtual std::string& hostStart() = 0;
54 virtual void hostDone(HostType /*ht*/) = 0;
55 inline bool need_save() const noexcept { return need_save_; }
56private:
57 bool need_save_ = true;
58};
59
60class host_parser {
61public:
62 template <typename CharT>
63 static validation_errc parse_host(const CharT* first, const CharT* last, bool is_opaque, host_output& dest);
64
65 template <typename CharT>
66 static validation_errc parse_opaque_host(const CharT* first, const CharT* last, host_output& dest);
67
68 template <typename CharT>
69 static validation_errc parse_ipv4(const CharT* first, const CharT* last, host_output& dest);
70
71 template <typename CharT>
72 static validation_errc parse_ipv6(const CharT* first, const CharT* last, host_output& dest);
73};
74
75
76// url_host class
77// https://github.com/whatwg/url/pull/288
78// https://whatpr.org/url/288.html#urlhost-class
79
80UPA_EXPORT class url_host {
81public:
82 url_host() = delete;
83 url_host(const url_host&) = default;
84 url_host(url_host&&) noexcept = default;
85 url_host& operator=(const url_host&) = default;
86 url_host& operator=(url_host&&) noexcept = default;
87
93 template <class StrT, enable_if_str_arg_t<StrT> = 0>
94 inline explicit url_host(const StrT& str) {
95 host_out out(*this);
96
97 const auto inp = make_str_arg(str);
98 const auto res = host_parser::parse_host(inp.begin(), inp.end(), false, out);
99 if (res != validation_errc::ok)
100 throw url_error(res, "Host parse error");
101 }
102
104 ~url_host() = default;
105
109 [[nodiscard]] inline HostType type() const {
110 return type_;
111 }
112
116 [[nodiscard]] inline std::string_view name() const UPA_LIFETIMEBOUND {
117 return host_str_;
118 }
119
123 [[nodiscard]] inline std::string to_string() const {
124 return host_str_;
125 }
126
127private:
128 class UPA_SO_VISIBLE host_out : public host_output {
129 public:
130 inline explicit host_out(url_host& host)
131 : host_(host)
132 {}
133 inline std::string& hostStart() override {
134 return host_.host_str_;
135 }
136 inline void hostDone(HostType ht) override {
137 host_.type_ = ht;
138 }
139 private:
140 url_host& host_; // NOLINT(cppcoreguidelines-avoid-const-or-ref-data-members)
141 };
142
143 // members
144 std::string host_str_;
146};
147
148
149// Helper functions
150
151namespace detail {
152
153template <typename CharT>
154UPA_CONSTEXPR_20 bool contains_forbidden_domain_char(const CharT* first, const CharT* last) {
155 return std::any_of(first, last, detail::is_forbidden_domain_char<CharT>);
156}
157
158template <typename CharT>
159UPA_CONSTEXPR_20 bool contains_forbidden_host_char(const CharT* first, const CharT* last) {
160 return std::any_of(first, last, detail::is_forbidden_host_char<CharT>);
161}
162
163// https://url.spec.whatwg.org/#domain-parser-toascii
164template <typename CharT>
165inline bool domain_parser_to_ascii(std::string& domain, const CharT* input, const CharT* input_end,
166 bool be_strict, bool is_input_ascii = false)
167{
168 return idna::to_ascii(domain, input, input_end,
169 idna::domain_options(be_strict, is_input_ascii));
170}
171
172} // namespace detail
173
174
175// IDNA
176// https://url.spec.whatwg.org/#idna
177
188template <class StrT, enable_if_str_arg_t<StrT> = 0>
189inline validation_errc domain_parser(std::string& output, const StrT& input, bool be_strict)
190{
191 const auto inp = make_str_arg(input);
192
193 const bool is_ascii = util::is_ascii(inp.begin(), inp.end());
194 if (be_strict) {
195 return detail::domain_parser_to_ascii(output, inp.begin(), inp.end(), true, is_ascii)
197 }
198 if (is_ascii) {
199 if (inp.empty() || detail::contains_forbidden_domain_char(inp.begin(), inp.end()))
201 output.clear();
202 util::append_ascii_lowercase(output, inp.begin(), inp.end());
203 return validation_errc::ok;
204 }
205
206 // be_strict == false & non-ASCII input
207 if (detail::domain_parser_to_ascii(output, inp.begin(), inp.end(), false, false) &&
208 !output.empty() &&
209 !detail::contains_forbidden_domain_char(output.data(), output.data() + output.size()))
210 return validation_errc::ok;
212}
213
227UPA_EXPORT template <class CharT, class StrT, enable_if_str_arg_t<StrT> = 0>
228inline bool domain_to_unicode(std::basic_string<CharT>& output, const StrT& input,
229 bool be_strict = false, bool is_input_ascii = false)
230{
231 static constexpr auto to_unicode =
232 [](std::u32string& domain, const auto* input, const auto* input_end,
233 bool be_strict, bool is_input_ascii) {
234 return idna::to_unicode(domain, input, input_end, idna::domain_options(
235 be_strict, is_input_ascii) | idna::Option::FailFast);
236 };
237
238 const auto inp = make_str_arg(input);
239
240 if constexpr (std::is_same_v<CharT, char32_t>) {
241 const auto len = output.length();
242 if (to_unicode(output, inp.begin(), inp.end(), be_strict, is_input_ascii))
243 return true;
244 output.resize(len);
245 } else {
246 std::u32string domain;
247 if (to_unicode(domain, inp.begin(), inp.end(), be_strict, is_input_ascii)) {
248 if constexpr (sizeof(CharT) == sizeof(char)) {
249 // CharT is char8_t, or char
250 for (auto cp : domain)
251 url_utf::append_utf8<std::basic_string<CharT>, detail::append_to_string>(cp, output);
252 } else if constexpr (sizeof(CharT) == sizeof(char16_t)) {
253 // CharT is char16_t, or wchar_t (Windows)
254 for (auto cp : domain)
255 url_utf::append_utf16(cp, output);
256 } else if constexpr (sizeof(CharT) == sizeof(char32_t)) {
257 // CharT is wchar_t (non Windows)
258 util::append(output, domain);
259 } else {
260 static_assert(util::false_v<CharT>, "unsupported output character type");
261 }
262 return true;
263 }
264 }
265
266 // If an error was recorded, then return input
267 using InpCharT = typename decltype(inp)::value_type;
268 if constexpr (sizeof(CharT) == sizeof(InpCharT)) {
269 // The character encodings are the same. Append the input to the output.
270 util::append(output, inp);
271 } else {
272 // The character encodings differ, so convert and append the input to the output.
273 const auto* last = inp.end();
274 for (const auto* ptr = inp.begin(); ptr != last;) {
275 const auto cp = url_utf::read_utf_char(ptr, last).value;
276 if constexpr (sizeof(CharT) == sizeof(char)) {
277 // CharT is char8_t, or char
278 url_utf::append_utf8<std::basic_string<CharT>, detail::append_to_string>(cp, output);
279 } else if constexpr (sizeof(CharT) == sizeof(char16_t)) {
280 // CharT is char16_t, or wchar_t (Windows)
281 url_utf::append_utf16(cp, output);
282 } else if constexpr (sizeof(CharT) == sizeof(char32_t)) {
283 // CharT is char32_t, or wchar_t (non Windows)
284 output.push_back(static_cast<CharT>(cp));
285 } else {
286 static_assert(util::false_v<CharT>, "unsupported output character type");
287 }
288 }
289 }
290 return false;
291}
292
293// The host parser
294// https://url.spec.whatwg.org/#concept-host-parser
295
296template <typename CharT>
297inline validation_errc host_parser::parse_host(const CharT* first, const CharT* last, bool is_opaque, host_output& dest) {
298 using UCharT = std::make_unsigned_t<CharT>;
299
300 static constexpr auto to_ascii_non_empty =
301 [](std::string& domain, const auto* input, const auto* input_end) {
302 return idna::to_ascii(domain, input, input_end, idna::domain_options(false, false)) &&
303 !domain.empty();
304 };
305
306 // 1. Non-"file" special URL's cannot have an empty host.
307 // 2. For "file" URL's empty host is set in the file_host_state 1.2
308 // https://url.spec.whatwg.org/#file-host-state
309 // 3. Non-special URLs can have empty host and it will be set here.
310 if (first == last) {
311 // https://github.com/whatwg/url/issues/79
312 // https://github.com/whatwg/url/pull/189
313 // set empty host
314 dest.hostStart();
315 dest.hostDone(HostType::Empty);
317 }
318 assert(first < last);
319
320 if (*first == '[') {
321 if (*(last - 1) == ']') {
322 return parse_ipv6(first + 1, last - 1, dest);
323 }
324 // 1.1. If input does not end with U+005D (]), IPv6-unclosed
325 // validation error, return failure.
327 }
328
329 if (is_opaque)
330 return parse_opaque_host(first, last, dest);
331
332 // Is ASCII domain?
333 const auto ptr = std::find_if_not(first, last, detail::is_ascii_domain_char<CharT>);
334 if (ptr == last) {
335 // Fast path for ASCII domain
336
337 // If asciiDomain ends in a number, return the result of IPv4 parsing asciiDomain
338 if (hostname_ends_in_a_number(first, last))
339 return parse_ipv4(first, last, dest);
340
341 if (dest.need_save()) {
342 // Return asciiDomain lower cased
343 std::string& str_host = dest.hostStart();
344 util::append_ascii_lowercase(str_host, first, last);
345 dest.hostDone(HostType::Domain);
346 }
347 return validation_errc::ok;
348 }
349 if (static_cast<UCharT>(*ptr) < 0x80 && *ptr != '%') {
350 // NFC normalizes U+003C (<), U+003D (=), U+003E (>) characters if they precede
351 // U+0338. Therefore, no errors are reported here for forbidden < and > characters
352 // if there is a possibility to normalize them.
353 if (!(*ptr >= 0x3C && *ptr <= 0x3E && ptr + 1 < last && static_cast<UCharT>(ptr[1]) >= 0x80))
354 // domain parser: 8. If result contains a forbidden domain code point, then
355 // return failure
357 }
358
359 std::string buff_ascii;
360
361 const auto pes = std::find(ptr, last, '%');
362 if (pes == last) {
363 // Input (first, last) contains non-ASCII characters
364 if (!to_ascii_non_empty(buff_ascii, first, last))
366 } else {
367 // Buffer for domain_to_ascii's input
368 simple_buffer<char32_t> buff_uc;
369
370 // copy ASCII chars
371 for (auto it = first; it != ptr; ++it) {
372 const auto uch = static_cast<UCharT>(*it);
373 buff_uc.push_back(static_cast<char32_t>(uch));
374 }
375
376 // Let buff_uc be the result of running UTF-8 decode (to UTF-32) without BOM
377 // on the percent decoding of UTF-8 encode on input
378 bool is_ascii = true;
379 for (auto it = ptr; it != last;) {
380 const auto uch = static_cast<UCharT>(*it++);
381 if (uch < 0x80) {
382 if (uch != '%') {
383 buff_uc.push_back(static_cast<char32_t>(uch));
384 continue;
385 }
386 // uch == '%'
387 unsigned char uc8; // NOLINT(cppcoreguidelines-init-variables)
388 if (detail::decode_hex_to_byte(it, last, uc8)) {
389 // TODO-WARN:
390 // If input contains a percent-encoded byte, domain-percent-encoded
391 // validation error
392 if (uc8 < 0x80) {
393 buff_uc.push_back(static_cast<char32_t>(uc8));
394 continue;
395 }
396 is_ascii = false;
397 // percent encoded utf-8 sequence
398 // TODO: Perhaps process one code point at a time. Then, a buffer
399 // for a single character encoded in UTF-8 would suffice.
400 simple_buffer<char> buff_utf8;
401 buff_utf8.push_back(static_cast<char>(uc8));
402 while (it != last && *it == '%') {
403 ++it; // skip '%'
404 if (!detail::decode_hex_to_byte(it, last, uc8))
405 uc8 = '%';
406 buff_utf8.push_back(static_cast<char>(uc8));
407 }
408 // utf-8 to utf-32
409 const auto* last_utf8 = buff_utf8.data() + buff_utf8.size();
410 for (const auto* it_utf8 = buff_utf8.data(); it_utf8 < last_utf8;)
411 buff_uc.push_back(url_utf::read_utf_char(it_utf8, last_utf8).value);
412 //buff_utf8.clear();
413 continue;
414 }
415 // Detected an invalid percent-encoding sequence
416 // see 2. step in https://url.spec.whatwg.org/#percent-decode
417 buff_uc.push_back('%');
418 } else { // uch >= 0x80
419 is_ascii = false;
420 --it;
421 buff_uc.push_back(url_utf::read_utf_char(it, last).value);
422 }
423 }
424 if (is_ascii)
425 util::append_ascii_lowercase(buff_ascii, buff_uc.begin(), buff_uc.end());
426 else if (!to_ascii_non_empty(buff_ascii, buff_uc.begin(), buff_uc.end()))
428 }
429
430 if (detail::contains_forbidden_domain_char(buff_ascii.data(), buff_ascii.data() + buff_ascii.size())) {
431 // domain parser: 8. If result contains a forbidden domain code point, then return failure
433 }
434
435 // If asciiDomain ends in a number:
436 // TODO-WARN: 1. If domain is not an ASCII string, IPv4-non-ASCII-input validation error.
437 // 2. Return the result of IPv4 parsing asciiDomain.
438 if (hostname_ends_in_a_number(buff_ascii.data(), buff_ascii.data() + buff_ascii.size()))
439 return parse_ipv4(buff_ascii.data(), buff_ascii.data() + buff_ascii.size(), dest);
440
441 if (dest.need_save()) {
442 // Return asciiDomain
443 std::string& str_host = dest.hostStart();
444 str_host.append(buff_ascii);
445 dest.hostDone(HostType::Domain);
446 }
447 return validation_errc::ok;
448}
449
450// The opaque-host parser
451// https://url.spec.whatwg.org/#concept-opaque-host-parser
452
453template <typename CharT>
454inline validation_errc host_parser::parse_opaque_host(const CharT* first, const CharT* last, host_output& dest) {
455 // 1. If input contains a forbidden host code point, host-invalid-code-point
456 // validation error, return failure.
457 if (detail::contains_forbidden_host_char(first, last))
459
460 // TODO-WARN:
461 // 2. If input contains a code point that is not a URL code point and not U+0025 (%),
462 // invalid-URL-unit validation error.
463 // 3. If input contains a U+0025 (%) and the two code points following it are not ASCII hex digits,
464 // invalid-URL-unit validation error.
465
466 if (dest.need_save()) {
467 std::string& str_host = dest.hostStart();
468
469 //TODO: UTF-8 percent encode it using the C0 control percent-encode set
470 //detail::append_utf8_percent_encoded(first, last, detail::CHAR_C0_CTRL, str_host);
471 using UCharT = std::make_unsigned_t<CharT>;
472
473 const CharT* pointer = first;
474 while (pointer < last) {
475 // UTF-8 percent encode c using the C0 control percent-encode set (U+0000 ... U+001F and >U+007E)
476 const auto uch = static_cast<UCharT>(*pointer);
477 if (uch >= 0x7f) {
478 // invalid utf-8/16/32 sequences will be replaced with 0xfffd
479 detail::append_utf8_percent_encoded_char(pointer, last, str_host);
480 } else {
481 // Just append the 7-bit character, percent encoding C0 control chars
482 const auto uc = static_cast<unsigned char>(uch);
483 if (uc <= 0x1f)
484 detail::append_percent_encoded_byte(uc, str_host);
485 else
486 str_host.push_back(uc);
487 ++pointer;
488 }
489 }
490
491 dest.hostDone(str_host.empty() ? HostType::Empty : HostType::Opaque);
492 }
493 return validation_errc::ok;
494}
495
496template <typename CharT>
497inline validation_errc host_parser::parse_ipv4(const CharT* first, const CharT* last, host_output& dest) {
498 std::uint32_t ipv4; // NOLINT(cppcoreguidelines-init-variables)
499
500 const auto res = ipv4_parse(first, last, ipv4);
501 if (res == validation_errc::ok && dest.need_save()) {
502 std::string& str_ipv4 = dest.hostStart();
503 ipv4_serialize(ipv4, str_ipv4);
504 dest.hostDone(HostType::IPv4);
505 }
506 return res;
507}
508
509template <typename CharT>
510inline validation_errc host_parser::parse_ipv6(const CharT* first, const CharT* last, host_output& dest) {
511 std::uint16_t ipv6addr[8];
512
513 const auto res = ipv6_parse(first, last, ipv6addr);
514 if (res == validation_errc::ok && dest.need_save()) {
515 std::string& str_ipv6 = dest.hostStart();
516 str_ipv6.push_back('[');
517 ipv6_serialize(ipv6addr, str_ipv6);
518 str_ipv6.push_back(']');
519 dest.hostDone(HostType::IPv6);
520 }
521 return res;
522}
523
524
525} // namespace upa
526
527#endif // UPA_URL_HOST_H
URL exception class.
Definition url_result.h:103
url_host()=delete
HostType type() const
Definition url_host.h:109
url_host(const url_host &)=default
~url_host()=default
destructor
std::string to_string() const
Definition url_host.h:123
std::string_view name() const
Definition url_host.h:116
url_host(url_host &&) noexcept=default
Definition url.h:50
HostType
Host representation.
Definition url_host.h:33
@ IPv4
host is an IPv4 address
Definition url_host.h:38
@ Empty
empty host is the empty string
Definition url_host.h:34
@ Opaque
opaque host is a non-empty ASCII string used in a not special URL
Definition url_host.h:35
@ IPv6
host is an IPv6 address
Definition url_host.h:39
validation_errc
URL validation and other error codes.
Definition url_result.h:22
@ host_invalid_code_point
An opaque host contains a forbidden host code point.
Definition url_result.h:55
@ host_missing
The input has a special scheme, but does not contain a host.
Definition url_result.h:76
@ domain_to_ascii
Unicode ToASCII records an error or returns the empty string.
Definition url_result.h:52
@ ipv6_unclosed
An IPv6 address is missing the closing U+005D (]).
Definition url_result.h:58
validation_errc domain_parser(std::string &output, const StrT &input, bool be_strict)
Implements the domain parser algorithm.
Definition url_host.h:189
bool domain_to_unicode(std::basic_string< CharT > &output, const StrT &input, bool be_strict=false, bool is_input_ascii=false)
Implements the domain to Unicode algorithm.
Definition url_host.h:228