Upa URL C++ library
A WHATWG URL Standard implementation
Loading...
Searching...
No Matches
url_percent_encode.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// This file contains portions of modified code from:
6// https://cs.chromium.org/chromium/src/url/url_canon_internal.h
7// Copyright 2013 The Chromium Authors. All rights reserved.
8//
9
10#ifndef UPA_URL_PERCENT_ENCODE_H
11#define UPA_URL_PERCENT_ENCODE_H
12
13#include "config.h" // IWYU pragma: export
14#include "str_arg.h"
15#include "url_utf.h"
16#include "util.h"
17
18#ifndef UPA_MODULE
19# include <array>
20# include <cstdint> // uint8_t
21# include <initializer_list>
22# include <string>
23# include <type_traits>
24# include <utility>
25#endif // UPA_MODULE
26
27
28namespace upa {
29
30UPA_EXPORT_BEGIN
31
38public:
45 constexpr explicit code_point_set(void (*fun)(code_point_set& self)) {
46 fun(*this);
47 }
48
51 constexpr void copy(const code_point_set& other) {
52 arr_ = other.arr_;
53 }
54
57 constexpr void exclude(std::uint8_t c) {
58 arr_[c >> 3] &= ~(1u << (c & 0x07));
59 }
60
63 constexpr void include(std::uint8_t c) {
64 arr_[c >> 3] |= (1u << (c & 0x07));
65 }
66
69 constexpr void exclude(std::initializer_list<std::uint8_t> clist) {
70 for (auto c : clist)
71 exclude(c);
72 }
73
76 constexpr void include(std::initializer_list<std::uint8_t> clist) {
77 for (auto c : clist)
78 include(c);
79 }
80
83 constexpr void include(std::uint8_t from, std::uint8_t to) {
84 for (auto c = from; c <= to; ++c)
85 include(c);
86 }
87
90 template <typename CharT>
91 [[nodiscard]] constexpr bool operator[](CharT c) const {
92 const auto uc = util::to_unsigned(c);
93 return is_8bit(uc) && (arr_[uc >> 3] & (1u << (uc & 0x07))) != 0;
94 }
95
96private:
97 // Check code point value is 8 bit (<=0xFF)
98 static constexpr bool is_8bit(unsigned char) noexcept {
99 return true;
100 }
101
102 template <typename CharT>
103 static constexpr bool is_8bit(CharT c) noexcept {
104 return c <= 0xFF;
105 }
106
107 // Data
108 std::array<std::uint8_t, 32> arr_{};
109};
110
111
112// Percent encode sets
113
114// fragment percent-encode set
115// https://url.spec.whatwg.org/#fragment-percent-encode-set
116inline constexpr code_point_set fragment_no_encode_set{ [](code_point_set& self) constexpr {
117 self.include(0x20, 0x7E); // C0 control percent-encode set
118 self.exclude({ 0x20, 0x22, 0x3C, 0x3E, 0x60 });
119 } };
120
121// query percent-encode set
122// https://url.spec.whatwg.org/#query-percent-encode-set
123inline constexpr code_point_set query_no_encode_set{ [](code_point_set& self) constexpr {
124 self.include(0x20, 0x7E); // C0 control percent-encode set
125 self.exclude({ 0x20, 0x22, 0x23, 0x3C, 0x3E });
126 } };
127
128// special query percent-encode set
129// https://url.spec.whatwg.org/#special-query-percent-encode-set
130inline constexpr code_point_set special_query_no_encode_set{ [](code_point_set& self) constexpr {
131 self.copy(query_no_encode_set);
132 self.exclude(0x27);
133 } };
134
135// path percent-encode set
136// https://url.spec.whatwg.org/#path-percent-encode-set
137inline constexpr code_point_set path_no_encode_set{ [](code_point_set& self) constexpr {
138 self.copy(query_no_encode_set);
139 self.exclude({ 0x3F, 0x5E, 0x60, 0x7B, 0x7D });
140 } };
141
142// path percent-encode set with '%' (0x25)
143inline constexpr code_point_set raw_path_no_encode_set{ [](code_point_set& self) constexpr {
144 self.copy(path_no_encode_set);
145 self.exclude(0x25);
146 } };
147
148// POSIX path percent-encode set
149// Additionally encode ':', '|' and '\' to prevent windows drive letter detection and interpretation of the
150// '\' as directory separator in POSIX paths (for example "/c:\end" will be encoded to "/c%3A%5Cend").
151inline constexpr code_point_set posix_path_no_encode_set{ [](code_point_set& self) constexpr {
152 self.copy(raw_path_no_encode_set);
153 self.exclude({ 0x3A, 0x5C, 0x7C }); // ':' (0x3A), '\' (0x5C), '|' (0x7C)
154 } };
155
156// userinfo percent-encode set
157// https://url.spec.whatwg.org/#userinfo-percent-encode-set
158inline constexpr code_point_set userinfo_no_encode_set{ [](code_point_set& self) constexpr {
159 self.copy(path_no_encode_set);
160 self.exclude({ 0x2F, 0x3A, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x7C });
161 } };
162
163// component percent-encode set
164// https://url.spec.whatwg.org/#component-percent-encode-set
165inline constexpr code_point_set component_no_encode_set{ [](code_point_set& self) constexpr {
166 self.copy(userinfo_no_encode_set);
167 self.exclude({ 0x24, 0x25, 0x26, 0x2B, 0x2C });
168 } };
169
170UPA_EXPORT_END
171
172namespace detail {
173
174// Code point sets in one bytes array
175
176enum CP_SET : std::uint8_t {
177 ASCII_DOMAIN_SET = 0x01,
178 DOMAIN_FORBIDDEN_SET = 0x02,
179 HOST_FORBIDDEN_SET = 0x04,
180 HEX_DIGIT_SET = 0x08,
181 IPV4_CHAR_SET = 0x10,
182 SCHEME_SET = 0x20,
183};
184
185class code_points_multiset {
186public:
187 constexpr code_points_multiset() {
188 // Forbidden host code points: U+0000 NULL, U+0009 TAB, U+000A LF, U+000D CR,
189 // U+0020 SPACE, U+0023 (#), U+002F (/), U+003A (:), U+003C (<), U+003E (>),
190 // U+003F (?), U+0040 (@), U+005B ([), U+005C (\‍), U+005D (]), U+005E (^) and
191 // U+007C (|).
192 // https://url.spec.whatwg.org/#forbidden-host-code-point
193 include(static_cast<CP_SET>(HOST_FORBIDDEN_SET | DOMAIN_FORBIDDEN_SET), {
194 0x00, 0x09, 0x0A, 0x0D, 0x20, 0x23, 0x2F, 0x3A, 0x3C, 0x3E, 0x3F, 0x40, 0x5B,
195 0x5C, 0x5D, 0x5E, 0x7C });
196
197 // Forbidden domain code points: forbidden host code points, C0 controls, U+0025 (%)
198 // and U+007F DELETE.
199 // https://url.spec.whatwg.org/#forbidden-domain-code-point
200 include(DOMAIN_FORBIDDEN_SET, 0x00, 0x1F); // C0 controls
201 include(DOMAIN_FORBIDDEN_SET, { 0x25, 0x7F });
202
203 // ASCII domain code points
204
205 // All ASCII excluding C0 controls (forbidden in domains)
206 include(ASCII_DOMAIN_SET, 0x20, 0x7F);
207 // exclude forbidden host code points
208 exclude(ASCII_DOMAIN_SET, {
209 0x00, 0x09, 0x0A, 0x0D, 0x20, 0x23, 0x2F, 0x3A, 0x3C, 0x3E, 0x3F, 0x40, 0x5B,
210 0x5C, 0x5D, 0x5E, 0x7C });
211 // exclude forbidden domain code points
212 exclude(ASCII_DOMAIN_SET, { 0x25, 0x7F });
213
214 // Hex digits
215 include(static_cast<CP_SET>(HEX_DIGIT_SET | IPV4_CHAR_SET), '0', '9');
216 include(static_cast<CP_SET>(HEX_DIGIT_SET | IPV4_CHAR_SET), 'A', 'F');
217 include(static_cast<CP_SET>(HEX_DIGIT_SET | IPV4_CHAR_SET), 'a', 'f');
218
219 // Characters allowed in IPv4
220 include(IPV4_CHAR_SET, { '.', 'X', 'x' });
221
222 // Scheme code points
223 // ASCII alphanumeric, U+002B (+), U+002D (-), or U+002E (.)
224 // https://url.spec.whatwg.org/#scheme-state
225 include(SCHEME_SET, '0', '9');
226 include(SCHEME_SET, 'A', 'Z');
227 include(SCHEME_SET, 'a', 'z');
228 include(SCHEME_SET, { 0x2B, 0x2D, 0x2E });
229 }
230
234 template <typename CharT>
235 [[nodiscard]] constexpr bool char_in_set(CharT c, CP_SET cps) const {
236 const auto uc = util::to_unsigned(c);
237 return is_8bit(uc) && (arr_[uc] & cps);
238 }
239
240private:
244 constexpr void include(CP_SET cpsbits, std::uint8_t c) {
245 arr_[c] |= cpsbits;
246 }
247
251 constexpr void include(CP_SET cpsbits, std::initializer_list<std::uint8_t> clist) {
252 for (auto c : clist)
253 include(cpsbits, c);
254 }
255
259 constexpr void include(CP_SET cpsbits, std::uint8_t from, std::uint8_t to) {
260 for (auto c = from; c <= to; ++c)
261 include(cpsbits, c);
262 }
263
267 constexpr void exclude(CP_SET cpsbits, std::uint8_t c) {
268 arr_[c] &= ~cpsbits;
269 }
270
274 constexpr void exclude(CP_SET cpsbits, std::initializer_list<std::uint8_t> clist) {
275 for (auto c : clist)
276 exclude(cpsbits, c);
277 }
278
279 // Check code point value is 8 bit (<=0xFF)
280 static constexpr bool is_8bit(unsigned char) noexcept {
281 return true;
282 }
283
284 template <typename CharT>
285 static constexpr bool is_8bit(CharT c) noexcept {
286 return c <= 0xFF;
287 }
288
289 // Data
290 std::array<std::uint8_t, 256> arr_{};
291};
292
293inline constexpr code_points_multiset code_points;
294
295// ----------------------------------------------------------------------------
296// Check char is in predefined set
297
298template <typename CharT>
299constexpr bool is_char_in_set(CharT c, const code_point_set& cpset) {
300 return cpset[c];
301}
302
303template <typename CharT>
304constexpr bool is_ipv4_char(CharT c) {
305 return code_points.char_in_set(c, IPV4_CHAR_SET);
306}
307
308template <typename CharT>
309constexpr bool is_hex_char(CharT c) {
310 return code_points.char_in_set(c, HEX_DIGIT_SET);
311}
312
313template <typename CharT>
314constexpr bool is_scheme_char(CharT c) {
315 return code_points.char_in_set(c, SCHEME_SET);
316}
317
318template <typename CharT>
319constexpr bool is_forbidden_domain_char(CharT c) {
320 return code_points.char_in_set(c, DOMAIN_FORBIDDEN_SET);
321}
322
323template <typename CharT>
324constexpr bool is_forbidden_host_char(CharT c) {
325 return code_points.char_in_set(c, HOST_FORBIDDEN_SET);
326}
327
328template <typename CharT>
329constexpr bool is_ascii_domain_char(CharT c) {
330 return code_points.char_in_set(c, ASCII_DOMAIN_SET);
331}
332
333// Char classification
334
335template <typename CharT>
336constexpr bool is_ascii_digit(CharT ch) noexcept {
337 return ch <= '9' && ch >= '0';
338}
339
340template <typename CharT>
341constexpr bool is_ascii_alpha(CharT ch) noexcept {
342 return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
343}
344
345// ----------------------------------------------------------------------------
346// Hex digit conversion tables and functions
347
348// Maps the hex numerical values 0x0 to 0xf to the corresponding ASCII digit
349// that will be used to represent it.
350inline constexpr char kHexCharLookup[0x10] = {
351 '0', '1', '2', '3', '4', '5', '6', '7',
352 '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
353};
354
355// This lookup table allows fast conversion between ASCII hex letters and their
356// corresponding numerical value. The 8-bit range is divided up into 8
357// regions of 0x20 characters each. Each of the three character types (numbers,
358// uppercase, lowercase) falls into different regions of this range. The table
359// contains the amount to subtract from characters in that range to get at
360// the corresponding numerical value.
361//
362// See hex_char_to_num for the lookup.
363inline constexpr char kCharToHexLookup[8] = {
364 0, // 0x00 - 0x1f
365 '0', // 0x20 - 0x3f: digits 0 - 9 are 0x30 - 0x39
366 'A' - 10, // 0x40 - 0x5f: letters A - F are 0x41 - 0x46
367 'a' - 10, // 0x60 - 0x7f: letters a - f are 0x61 - 0x66
368 0, // 0x80 - 0x9F
369 0, // 0xA0 - 0xBF
370 0, // 0xC0 - 0xDF
371 0, // 0xE0 - 0xFF
372};
373
374// Assumes the input is a valid hex digit! Call is_hex_char before using this.
375constexpr unsigned char hex_char_to_num(unsigned char c) noexcept {
376 return c - kCharToHexLookup[c / 0x20];
377}
378
379// ----------------------------------------------------------------------------
380// Percent decode
381
382// Given a character after '%' at |*first| in the string, this will decode
383// the escaped value and put it into |*unescaped_value| on success (returns
384// true). On failure, this will return false, and will not write into
385// |*unescaped_value|.
386//
387// |*first| will be updated to point after the last character of the escape
388// sequence. On failure, |*first| will be unchanged.
389
390template <typename CharT>
391constexpr bool decode_hex_to_byte(const CharT*& first, const CharT* last, unsigned char& unescaped_value) {
392 if (last - first < 2 ||
393 !is_hex_char(first[0]) || !is_hex_char(first[1])) {
394 // not enough or invalid hex digits
395 return false;
396 }
397
398 // Valid escape sequence.
399 const auto uc1 = static_cast<unsigned char>(first[0]);
400 const auto uc2 = static_cast<unsigned char>(first[1]);
401 unescaped_value = (hex_char_to_num(uc1) << 4) + hex_char_to_num(uc2);
402 first += 2;
403 return true;
404}
405
406// ----------------------------------------------------------------------------
407// Percent encode
408
409// Percent-encodes byte and appends to string
410// See: https://url.spec.whatwg.org/#percent-encode
411
412UPA_CONSTEXPR_20 void append_percent_encoded_byte(unsigned char uc, std::string& output) {
413 output.push_back('%');
414 output.push_back(kHexCharLookup[uc >> 4]);
415 output.push_back(kHexCharLookup[uc & 0xf]);
416}
417
418// Reads one character from string (first, last), converts to UTF-8, then
419// percent-encodes, and appends to `output`. Replaces invalid UTF-8, UTF-16 or UTF-32
420// sequences in input with Unicode replacement characters (U+FFFD) if present.
421
422template <typename CharT>
423UPA_CONSTEXPR_20 bool append_utf8_percent_encoded_char(const CharT*& first, const CharT* last, std::string& output) {
424 // url_util::read_utf_char(..) will handle invalid characters for us and give
425 // us the kUnicodeReplacementCharacter, so we don't have to do special
426 // checking after failure, just pass through the failure to the caller.
427 const auto cp_res = url_utf::read_utf_char(first, last);
428 // convert cp_res.value code point to UTF-8, then percent encode and append to `output`
429 url_utf::append_utf8<std::string, append_percent_encoded_byte>(cp_res.value, output);
430 return cp_res.result;
431}
432
433// Converts input string (first, last) to UTF-8, then percent encodes bytes not
434// in `cpset`, and appends to `output`. Replaces invalid UTF-8, UTF-16 or UTF-32
435// sequences in input with Unicode replacement characters (U+FFFD) if present.
436
437template<typename CharT>
438UPA_CONSTEXPR_20 void append_utf8_percent_encoded(const CharT* first, const CharT* last, const code_point_set& cpset, std::string& output) {
439 using UCharT = std::make_unsigned_t<CharT>;
440
441 for (auto it = first; it < last; ) {
442 const auto uch = static_cast<UCharT>(*it);
443 if (uch >= 0x80) {
444 // invalid utf-8/16/32 sequences will be replaced with kUnicodeReplacementCharacter
445 append_utf8_percent_encoded_char(it, last, output);
446 } else {
447 // Just append the 7-bit character, possibly percent encoding it
448 const auto uc = static_cast<unsigned char>(uch);
449 if (is_char_in_set(uc, cpset)) {
450 output.push_back(uc);
451 } else {
452 // other characters are percent encoded
453 append_percent_encoded_byte(uc, output);
454 }
455 ++it;
456 }
457 }
458}
459
469template <class StrT, enable_if_str_arg_t<StrT> = 0>
470inline void append_percent_decoded(StrT&& str, std::string& output) {
471 const auto inp = make_str_arg(std::forward<StrT>(str));
472 const auto* first = inp.begin();
473 const auto* last = inp.end();
474
475 for (auto it = first; it != last;) {
476 const auto uch = util::to_unsigned(*it); ++it;
477 if (uch < 0x80) {
478 if (uch != '%') {
479 output.push_back(static_cast<char>(uch));
480 continue;
481 }
482 // uch == '%'
483 unsigned char uc8; // NOLINT(cppcoreguidelines-init-variables)
484 if (decode_hex_to_byte(it, last, uc8)) {
485 if (uc8 < 0x80) {
486 output.push_back(static_cast<char>(uc8));
487 continue;
488 }
489 // percent encoded utf-8 sequence
490 std::string buff_utf8;
491 buff_utf8.push_back(static_cast<char>(uc8));
492 while (it != last && *it == '%') {
493 ++it; // skip '%'
494 if (!decode_hex_to_byte(it, last, uc8))
495 uc8 = '%';
496 buff_utf8.push_back(static_cast<char>(uc8));
497 }
498 url_utf::check_fix_utf8(buff_utf8);
499 output += buff_utf8;
500 continue;
501 }
502 // detected invalid percent encoding
503 output.push_back('%');
504 } else { // uch >= 0x80
505 --it;
506 url_utf::read_char_append_utf8(it, last, output);
507 }
508 }
509}
510
511
512} // namespace detail
513
514UPA_EXPORT_BEGIN
515
525template <class StrT, enable_if_str_arg_t<StrT> = 0>
526[[nodiscard]] inline std::string percent_decode(StrT&& str) {
527 std::string out;
528 detail::append_percent_decoded(std::forward<StrT>(str), out);
529 return out;
530}
531
543template <class StrT, enable_if_str_arg_t<StrT> = 0>
544[[nodiscard]] UPA_CONSTEXPR_20 std::string percent_encode(StrT&& str, const code_point_set& no_encode_set) {
545 const auto inp = make_str_arg(std::forward<StrT>(str));
546
547 std::string out;
548 detail::append_utf8_percent_encoded(inp.begin(), inp.end(), no_encode_set, out);
549 return out;
550}
551
562template <class StrT, enable_if_str_arg_t<StrT> = 0>
563[[nodiscard]] UPA_CONSTEXPR_20 std::string encode_url_component(StrT&& str) {
564 return percent_encode(std::forward<StrT>(str), component_no_encode_set);
565}
566
567UPA_EXPORT_END
568
569} // namespace upa
570
571#endif // UPA_URL_PERCENT_ENCODE_H
Represents code point set.
constexpr void include(std::uint8_t c)
include c code point to set
constexpr void exclude(std::initializer_list< std::uint8_t > clist)
exclude list of code points from set
constexpr code_point_set(void(*fun)(code_point_set &self))
constructor for code point set initialization
constexpr void include(std::initializer_list< std::uint8_t > clist)
include code points from list
constexpr void include(std::uint8_t from, std::uint8_t to)
include range of code points to set
constexpr bool operator[](CharT c) const
test code point set contains code point c
constexpr void copy(const code_point_set &other)
copy code points from other set
constexpr void exclude(std::uint8_t c)
exclude c code point from set
Definition url.h:50
constexpr code_point_set query_no_encode_set
std::string encode_url_component(StrT &&str)
UTF-8 percent encode input string using component percent encode set.
std::string percent_decode(StrT &&str)
Percent decode input string.
std::string percent_encode(StrT &&str, const code_point_set &no_encode_set)
UTF-8 percent encode input string using specified percent encode set.
constexpr code_point_set component_no_encode_set
constexpr code_point_set special_query_no_encode_set
constexpr code_point_set raw_path_no_encode_set
constexpr code_point_set path_no_encode_set
constexpr code_point_set fragment_no_encode_set
constexpr code_point_set userinfo_no_encode_set
constexpr code_point_set posix_path_no_encode_set