/
auroraos
/
mirror_ada
Обзор
Документация
Войти
/
auroraos
/
mirror_ada
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
src/url_pattern_helpers.cpp
1 254 строки
47 KB
Abdul Rawoof Khan
keep braces around a prefixed part in generate_pattern_string (#1191)
27 июл 2026, 19:24
Не верифицирован
27 июл 2026, 19:24
95608e5
Код
Авторство
О чём код?
#if ADA_INCLUDE_URL_PATTERN #include "ada/url_pattern_helpers-inl.h" #include <algorithm> #include <array> #include <charconv> #include <optional> #include <ranges> #include <string> #include "ada/character_sets.h" #include "ada/checkers.h" #include "ada/helpers.h" #include "ada/scheme.h" #include "ada/unicode.h" namespace ada::url_pattern_helpers { std::tuple<std::string, std::vector<std::string>> generate_regular_expression_and_name_list( const std::vector<url_pattern_part>& part_list, url_pattern_compile_component_options options) { // Let result be "^" std::string result = "^"; // Reserve capacity to reduce reallocations result.reserve(part_list.size() * 16); // Let name list be a new list std::vector<std::string> name_list{}; name_list.reserve(part_list.size()); // Pre-generate segment wildcard regexp if needed (avoids repeated generation) std::string segment_wildcard_regexp; // For each part of part list: for (const url_pattern_part& part : part_list) { // If part's type is "fixed-text": if (part.type == url_pattern_part_type::FIXED_TEXT) { // If part's modifier is "none" if (part.modifier == url_pattern_part_modifier::none) { result.append(escape_regexp_string(part.value)); } else { // (?:<fixed text>)<modifier> result.append("(?:"); result.append(escape_regexp_string(part.value)); result.push_back(')'); result.append(convert_modifier_to_string(part.modifier)); } continue; } // Assert: part's name is not the empty string ADA_ASSERT_TRUE(!part.name.empty()); name_list.push_back(part.name); // Use string_view to avoid copies where possible std::string_view regexp_value = part.value; if (part.type == url_pattern_part_type::SEGMENT_WILDCARD) { // Lazy generate segment wildcard regexp if (segment_wildcard_regexp.empty()) { segment_wildcard_regexp = generate_segment_wildcard_regexp(options); } regexp_value = segment_wildcard_regexp; } else if (part.type == url_pattern_part_type::FULL_WILDCARD) { regexp_value = ".*"; } // If part's prefix is the empty string and part's suffix is the empty // string if (part.prefix.empty() && part.suffix.empty()) { // If part's modifier is "none" or "optional" if (part.modifier == url_pattern_part_modifier::none || part.modifier == url_pattern_part_modifier::optional) { // (<regexp value>)<modifier> result.push_back('('); result.append(regexp_value); result.push_back(')'); result.append(convert_modifier_to_string(part.modifier)); } else { // ((?:<regexp value>)<modifier>) result.append("((?:"); result.append(regexp_value); result.push_back(')'); result.append(convert_modifier_to_string(part.modifier)); result.push_back(')'); } continue; } // If part's modifier is "none" or "optional" if (part.modifier == url_pattern_part_modifier::none || part.modifier == url_pattern_part_modifier::optional) { // (?:<prefix>(<regexp value>)<suffix>)<modifier> result.append("(?:"); result.append(escape_regexp_string(part.prefix)); result.push_back('('); result.append(regexp_value); result.push_back(')'); result.append(escape_regexp_string(part.suffix)); result.push_back(')'); result.append(convert_modifier_to_string(part.modifier)); continue; } // Assert: part's modifier is "zero-or-more" or "one-or-more" ADA_ASSERT_TRUE(part.modifier == url_pattern_part_modifier::zero_or_more || part.modifier == url_pattern_part_modifier::one_or_more); // Assert: part's prefix is not the empty string or part's suffix is not the // empty string ADA_ASSERT_TRUE(!part.prefix.empty() || !part.suffix.empty()); // (?:<prefix>((?:<regexp value>)(?:<suffix><prefix>(?:<regexp // value>))*)<suffix>)? // Append "(?:" to the end of result. result.append("(?:"); // Append the result of running escape a regexp string given part's prefix // to the end of result. result.append(escape_regexp_string(part.prefix)); // Append "((?:" to the end of result. result.append("((?:"); // Append regexp value to the end of result. result.append(regexp_value); // Append ")(?:" to the end of result. result.append(")(?:"); // Append the result of running escape a regexp string given part's suffix // to the end of result. result.append(escape_regexp_string(part.suffix)); // Append the result of running escape a regexp string given part's prefix // to the end of result. result.append(escape_regexp_string(part.prefix)); // Append "(?:" to the end of result. result.append("(?:"); // Append regexp value to the end of result. result.append(regexp_value); // Append "))*)" to the end of result. result.append("))*)"); // Append the result of running escape a regexp string given part's suffix // to the end of result. result.append(escape_regexp_string(part.suffix)); // Append ")" to the end of result. result.append(")"); // If part's modifier is "zero-or-more" then append "?" to the end of result if (part.modifier == url_pattern_part_modifier::zero_or_more) { result += "?"; } } // Append "$" to the end of result result += "$"; // Return (result, name list) return {std::move(result), std::move(name_list)}; } bool is_ipv6_address(std::string_view input) noexcept { // If input's code point length is less than 2, then return false. if (input.size() < 2) return false; // Let input code points be input interpreted as a list of code points. // If input code points[0] is U+005B ([), then return true. if (input.front() == '[') return true; // If input code points[0] is U+007B ({) and input code points[1] is U+005B // ([), then return true. if (input.starts_with("{[")) return true; // If input code points[0] is U+005C (\) and input code points[1] is U+005B // ([), then return true. return input.starts_with("\\["); } std::string_view convert_modifier_to_string( url_pattern_part_modifier modifier) { switch (modifier) { // If modifier is "zero-or-more", then return "*". case url_pattern_part_modifier::zero_or_more: return "*"; // If modifier is "optional", then return "?". case url_pattern_part_modifier::optional: return "?"; // If modifier is "one-or-more", then return "+". case url_pattern_part_modifier::one_or_more: return "+"; // Return the empty string. default: return ""; } } std::string generate_segment_wildcard_regexp( url_pattern_compile_component_options options) { // Let result be "[^". std::string result = "[^"; // Append the result of running escape a regexp string given options's // delimiter code point to the end of result. result.append(escape_regexp_string(options.get_delimiter())); // Append "]+?" to the end of result. result.append("]+?"); // Return result. ada_log("generate_segment_wildcard_regexp result: ", result); return result; } namespace { // Unified lookup table for URL pattern character classification // Bit flags for different character types constexpr uint8_t CHAR_SCHEME = 1; // valid in scheme (a-z, A-Z, 0-9, +, -, .) constexpr uint8_t CHAR_UPPER = 2; // uppercase letter (needs lowercasing) constexpr uint8_t CHAR_SIMPLE_HOSTNAME = 4; // simple hostname (a-z, 0-9, -, .) constexpr uint8_t CHAR_SIMPLE_PATHNAME = 8; // simple pathname (a-z, A-Z, 0-9, /, -, _, ~) constexpr std::array<uint8_t, 256> char_class_table = []() consteval { std::array<uint8_t, 256> table{}; for (int c = 'a'; c <= 'z'; c++) table[c] = CHAR_SCHEME | CHAR_SIMPLE_HOSTNAME | CHAR_SIMPLE_PATHNAME; for (int c = 'A'; c <= 'Z'; c++) table[c] = CHAR_SCHEME | CHAR_UPPER | CHAR_SIMPLE_PATHNAME; for (int c = '0'; c <= '9'; c++) table[c] = CHAR_SCHEME | CHAR_SIMPLE_HOSTNAME | CHAR_SIMPLE_PATHNAME; table['+'] = CHAR_SCHEME; table['-'] = CHAR_SCHEME | CHAR_SIMPLE_HOSTNAME | CHAR_SIMPLE_PATHNAME; table['.'] = CHAR_SCHEME | CHAR_SIMPLE_HOSTNAME; // not pathname (needs normalization) table['/'] = CHAR_SIMPLE_PATHNAME; table['_'] = CHAR_SIMPLE_PATHNAME; table['~'] = CHAR_SIMPLE_PATHNAME; return table; }(); } // namespace tl::expected<std::string, errors> canonicalize_protocol( std::string_view input) { ada_log("canonicalize_protocol called with input=", input); if (input.empty()) [[unlikely]] { return ""; } if (input.ends_with(":")) { input.remove_suffix(1); } // Fast path: special schemes are already canonical if (scheme::is_special(input)) { return std::string(input); } // Fast path: validate scheme chars and check for uppercase // First char must be alpha (not +, -, ., or digit) uint8_t first_flags = char_class_table[static_cast<uint8_t>(input[0])]; if (!(first_flags & CHAR_SCHEME) || input[0] == '+' || input[0] == '-' || input[0] == '.' || unicode::is_ascii_digit(input[0])) { return tl::unexpected(errors::type_error); } uint8_t needs_lowercase = first_flags & CHAR_UPPER; for (size_t i = 1; i < input.size(); i++) { uint8_t flags = char_class_table[static_cast<uint8_t>(input[i])]; if (!(flags & CHAR_SCHEME)) { return tl::unexpected(errors::type_error); } needs_lowercase |= flags & CHAR_UPPER; } if (needs_lowercase == 0) { return std::string(input); } std::string result(input); unicode::to_lower_ascii(result.data(), result.size()); return result; } tl::expected<std::string, errors> canonicalize_username( std::string_view input) { // If value is the empty string, return value. if (input.empty()) [[unlikely]] { return ""; } // Percent-encode the input using the userinfo percent-encode set. size_t idx = ada::unicode::percent_encode_index( input, character_sets::USERINFO_PERCENT_ENCODE); if (idx == input.size()) { // No encoding needed, return input as-is return std::string(input); } // Percent-encode from the first character that needs encoding return ada::unicode::percent_encode( input, character_sets::USERINFO_PERCENT_ENCODE, idx); } tl::expected<std::string, errors> canonicalize_password( std::string_view input) { // If value is the empty string, return value. if (input.empty()) [[unlikely]] { return ""; } // Percent-encode the input using the userinfo percent-encode set. size_t idx = ada::unicode::percent_encode_index( input, character_sets::USERINFO_PERCENT_ENCODE); if (idx == input.size()) { // No encoding needed, return input as-is return std::string(input); } // Percent-encode from the first character that needs encoding return ada::unicode::percent_encode( input, character_sets::USERINFO_PERCENT_ENCODE, idx); } tl::expected<std::string, errors> canonicalize_hostname( std::string_view input) { ada_log("canonicalize_hostname input=", input); if (input.empty()) [[unlikely]] { return ""; } // Fast path: simple hostnames (lowercase ASCII, digits, -, .) need no IDNA. // The simple character set also covers IPv4-shaped hosts, which the slow // path rewrites ("0" -> "0.0.0.0", "0x7f.1" -> "127.0.0.1"). Exclude those // so the fast path can't return a non-canonical hostname. // // Pure-ASCII "xn--" labels are intentionally left on the fast path: with // beStrict=false, domain-to-ASCII returns the lowercased ASCII input even // when Unicode ToASCII would fail (e.g. invalid ACE "xn--a"). See // https://url.spec.whatwg.org/#concept-domain-to-ascii bool needs_processing = false; for (char c : input) { needs_processing |= !(char_class_table[static_cast<uint8_t>(c)] & CHAR_SIMPLE_HOSTNAME); } if (!needs_processing && !checkers::is_ipv4(input)) { return std::string(input); } // Let dummyURL be a new URL record. // Let parseResult be the result of running the basic URL parser given value // with dummyURL as url and hostname state as state override. // IMPORTANT: The protocol needs to be a special protocol, otherwise the // hostname will not be converted using IDNA. auto url = ada::parse<url_aggregator>("https://dummy.test", nullptr); ADA_ASSERT_TRUE(url); // if (!isValidHostnameInput(hostname)) return kj::none; if (!url->set_hostname(input)) { // If parseResult is failure, then throw a TypeError. return tl::unexpected(errors::type_error); } // Return dummyURL's host, serialized, or empty string if it is null. return std::string(url->get_hostname()); } tl::expected<std::string, errors> canonicalize_ipv6_hostname( std::string_view input) { ada_log("canonicalize_ipv6_hostname input=", input); // TODO: Optimization opportunity: Use lookup table to speed up checking if (std::ranges::any_of(input, [](char c) { return c != '[' && c != ']' && c != ':' && !unicode::is_ascii_hex_digit(c); })) { return tl::unexpected(errors::type_error); } // Append the result of running ASCII lowercase given code point to the end of // result. auto hostname = std::string(input); unicode::to_lower_ascii(hostname.data(), hostname.size()); return hostname; } tl::expected<std::string, errors> canonicalize_port( std::string_view port_value) { // If portValue is the empty string, return portValue. if (port_value.empty()) [[unlikely]] { return ""; } // Remove ASCII tab or newline characters std::string trimmed(port_value); helpers::remove_ascii_tab_or_newline(trimmed); if (trimmed.empty()) { return ""; } // Input should start with a digit character if (!unicode::is_ascii_digit(trimmed.front())) { return tl::unexpected(errors::type_error); } // Find the first non-digit character auto first_non_digit = std::ranges::find_if_not(trimmed, unicode::is_ascii_digit); std::string_view digits_to_parse = std::string_view(trimmed.data(), first_non_digit - trimmed.begin()); // Here we have a range of ASCII digit characters identified by // digits_to_parse. It is non-empty. The port state of the URL parser reads // it as an integer, so leading zeros are part of the number: "0080" is 80 // and "065535" is 65535, not a six-digit overflow. Only the significant // digits count toward the 5-digit maximum, matching the fast path in // implementation.cpp. size_t first_significant = digits_to_parse.find_first_not_of('0'); if (first_significant == std::string_view::npos) { // The value is all zeros, i.e. port 0. return "0"; } digits_to_parse.remove_prefix(first_significant); // We want to determine whether it is a valid port number (0-65535). // Clearly, if the length is greater than 5, it is invalid. // If the length is 5, we need to compare lexicographically to "65535". // Otherwise it is valid. if (digits_to_parse.size() == 5) { if (digits_to_parse > "65535") { return tl::unexpected(errors::type_error); } } else if (digits_to_parse.size() > 5) { return tl::unexpected(errors::type_error); } // It is valid! Most times, we do not need to parse it into an integer. return std::string(digits_to_parse); } tl::expected<std::string, errors> canonicalize_port_with_protocol( std::string_view port_value, std::string_view protocol) { // If portValue is the empty string, return portValue. if (port_value.empty()) [[unlikely]] { return ""; } // Handle empty or trailing colon in protocol if (protocol.empty()) { protocol = "fake"; } else if (protocol.ends_with(":")) { protocol.remove_suffix(1); } // Remove ASCII tab or newline characters std::string trimmed(port_value); helpers::remove_ascii_tab_or_newline(trimmed); if (trimmed.empty()) { return ""; } // Input should start with a digit character if (!unicode::is_ascii_digit(trimmed.front())) { return tl::unexpected(errors::type_error); } // Find the first non-digit character auto first_non_digit = std::ranges::find_if_not(trimmed, unicode::is_ascii_digit); std::string_view digits_to_parse = std::string_view(trimmed.data(), first_non_digit - trimmed.begin()); // Parse the port number uint16_t parsed_port{}; // NOLINTNEXTLINE(bugprone-suspicious-stringview-data-usage) auto result = std::from_chars(digits_to_parse.data(), digits_to_parse.data() + digits_to_parse.size(), parsed_port); if (result.ec == std::errc::result_out_of_range) { return tl::unexpected(errors::type_error); } if (result.ec == std::errc()) { // Check if this is the default port for the scheme uint16_t default_port = scheme::get_special_port(protocol); // If it's the default port for a special scheme, return empty string if (default_port != 0 && default_port == parsed_port) { return ""; } // Successfully parsed, return as string return std::to_string(parsed_port); } return tl::unexpected(errors::type_error); } tl::expected<std::string, errors> canonicalize_pathname( std::string_view input) { if (input.empty()) [[unlikely]] { return ""; } // Fast path: simple pathnames (no . which needs normalization) can be // returned as-is bool needs_processing = false; for (char c : input) { needs_processing |= !(char_class_table[static_cast<uint8_t>(c)] & CHAR_SIMPLE_PATHNAME); } if (!needs_processing) { return std::string(input); } // Let leading slash be true if the first code point in value is U+002F (/) // and otherwise false. const bool leading_slash = input.starts_with("/"); // Let modified value be "/-" if leading slash is false and otherwise the // empty string. const auto modified_value = (leading_slash ? std::string() : std::string("/-")) + std::string(input); // The spec runs the basic URL parser with the path in state override, where // '?' and '#' are ordinary path code points (percent-encoded) and leading or // trailing C0 control or space is kept. Building a full URL string and // parsing it (no state override) instead let '?' and '#' start the // query/fragment, so the pathname was silently truncated at the first literal // '?' or '#'. set_pathname runs the parser in path state override, matching // how canonicalize_hostname uses set_hostname. auto url = ada::parse<url_aggregator>("fake://fake-url", nullptr); ADA_ASSERT_TRUE(url); if (!url->set_pathname(modified_value)) { // If parseResult is failure, then throw a TypeError. return tl::unexpected(errors::type_error); } const auto pathname = url->get_pathname(); // If leading slash is false, then set result to the code point substring // from 2 to the end of the string within result. if (!leading_slash) { // pathname should start with "/-" but path traversal (e.g. "../../") // can reduce it to just "/" which is shorter than 2 characters. if (pathname.size() < 2) { return tl::unexpected(errors::type_error); } return std::string(pathname.substr(2)); } return std::string(pathname); } tl::expected<std::string, errors> canonicalize_opaque_pathname( std::string_view input) { // If value is the empty string, return value. if (input.empty()) [[unlikely]] { return ""; } // Let dummyURL be a new URL record. // Set dummyURL's path to the empty string. // Let parseResult be the result of running URL parsing given value with // dummyURL as url and opaque path state as state override. if (auto url = ada::parse<url_aggregator>("fake:" + std::string(input), nullptr)) { // Return the result of URL path serializing dummyURL. return std::string(url->get_pathname()); } // If parseResult is failure, then throw a TypeError. return tl::unexpected(errors::type_error); } tl::expected<std::string, errors> canonicalize_search(std::string_view input) { // If value is the empty string, return value. if (input.empty()) [[unlikely]] { return ""; } // A leading '?' is a valid query code point here: it is the caller (process a // search) that removes a single delimiter, not this step, which mirrors the // basic URL parser's query state. std::string new_value(input); // Remove ASCII tab or newline characters helpers::remove_ascii_tab_or_newline(new_value); if (new_value.empty()) { return ""; } // Percent-encode using QUERY_PERCENT_ENCODE (for non-special URLs) // Note: "fake://dummy.test" is not a special URL, so we use // QUERY_PERCENT_ENCODE size_t idx = ada::unicode::percent_encode_index( new_value, character_sets::QUERY_PERCENT_ENCODE); if (idx == new_value.size()) { // No encoding needed return new_value; } // Percent-encode from the first character that needs encoding return ada::unicode::percent_encode( new_value, character_sets::QUERY_PERCENT_ENCODE, idx); } tl::expected<std::string, errors> canonicalize_hash(std::string_view input) { // If value is the empty string, return value. if (input.empty()) [[unlikely]] { return ""; } // A leading '#' is a valid fragment code point here: it is the caller // (process a hash) that removes a single delimiter, not this step, which // mirrors the basic URL parser's fragment state. std::string new_value(input); // Remove ASCII tab or newline characters helpers::remove_ascii_tab_or_newline(new_value); if (new_value.empty()) { return ""; } // Percent-encode using FRAGMENT_PERCENT_ENCODE size_t idx = ada::unicode::percent_encode_index( new_value, character_sets::FRAGMENT_PERCENT_ENCODE); if (idx == new_value.size()) { // No encoding needed return new_value; } // Percent-encode from the first character that needs encoding return ada::unicode::percent_encode( new_value, character_sets::FRAGMENT_PERCENT_ENCODE, idx); } tl::expected<std::vector<token>, errors> tokenize(std::string_view input, token_policy policy) { ada_log("tokenize input: ", input); // Let tokenizer be a new tokenizer. // Set tokenizer's input to input. // Set tokenizer's policy to policy. auto tokenizer = Tokenizer(input, policy); // While tokenizer's index is less than tokenizer's input's code point length: while (tokenizer.index < tokenizer.input.size()) { // Run seek and get the next code point given tokenizer and tokenizer's // index. tokenizer.seek_and_get_next_code_point(tokenizer.index); // Malformed UTF-8 must not stall tokenization: report an invalid-char // token (lenient) or fail fast (strict), while always making progress. if (tokenizer.had_invalid_code_point()) { if (auto error = tokenizer.process_tokenizing_error(tokenizer.next_index, tokenizer.index)) { return tl::unexpected(*error); } continue; } // If tokenizer's code point is U+002A (*): if (tokenizer.code_point == '*') { // Run add a token with default position and length given tokenizer and // "asterisk". tokenizer.add_token_with_defaults(token_type::ASTERISK); ada_log("add ASTERISK token"); // Continue. continue; } // If tokenizer's code point is U+002B (+) or U+003F (?): if (tokenizer.code_point == '+' || tokenizer.code_point == '?') { // Run add a token with default position and length given tokenizer and // "other-modifier". tokenizer.add_token_with_defaults(token_type::OTHER_MODIFIER); // Continue. continue; } // If tokenizer's code point is U+005C (\): if (tokenizer.code_point == '\\') { // If tokenizer's index is equal to tokenizer's input's code point length // - 1: if (tokenizer.index == tokenizer.input.size() - 1) { // Run process a tokenizing error given tokenizer, tokenizer's next // index, and tokenizer's index. if (auto error = tokenizer.process_tokenizing_error( tokenizer.next_index, tokenizer.index)) { ada_log("process_tokenizing_error failed"); return tl::unexpected(*error); } continue; } // Let escaped index be tokenizer's next index. auto escaped_index = tokenizer.next_index; // Run get the next code point given tokenizer. tokenizer.get_next_code_point(); if (tokenizer.had_invalid_code_point()) { if (auto error = tokenizer.process_tokenizing_error( tokenizer.next_index, escaped_index)) { return tl::unexpected(*error); } continue; } // Run add a token with default length given tokenizer, "escaped-char", // tokenizer's next index, and escaped index. tokenizer.add_token_with_default_length( token_type::ESCAPED_CHAR, tokenizer.next_index, escaped_index); ada_log("add ESCAPED_CHAR token on next_index ", tokenizer.next_index, " with escaped index ", escaped_index); // Continue. continue; } // If tokenizer's code point is U+007B ({): if (tokenizer.code_point == '{') { // Run add a token with default position and length given tokenizer and // "open". tokenizer.add_token_with_defaults(token_type::OPEN); ada_log("add OPEN token"); continue; } // If tokenizer's code point is U+007D (}): if (tokenizer.code_point == '}') { // Run add a token with default position and length given tokenizer and // "close". tokenizer.add_token_with_defaults(token_type::CLOSE); ada_log("add CLOSE token"); continue; } // If tokenizer's code point is U+003A (:): if (tokenizer.code_point == ':') { // Let name position be tokenizer's next index. auto name_position = tokenizer.next_index; // Let name start be name position. auto name_start = name_position; bool invalid_name = false; // While name position is less than tokenizer's input's code point length: while (name_position < tokenizer.input.size()) { // Run seek and get the next code point given tokenizer and name // position. tokenizer.seek_and_get_next_code_point(name_position); if (tokenizer.had_invalid_code_point()) { if (auto error = tokenizer.process_tokenizing_error( tokenizer.next_index, name_position)) { return tl::unexpected(*error); } invalid_name = true; break; } // Let first code point be true if name position equals name start and // false otherwise. bool first_code_point = name_position == name_start; // Let valid code point be the result of running is a valid name code // point given tokenizer's code point and first code point. auto valid_code_point = idna::valid_name_code_point(tokenizer.code_point, first_code_point); ada_log("tokenizer.code_point=", uint32_t(tokenizer.code_point), " first_code_point=", first_code_point, " valid_code_point=", valid_code_point); // If valid code point is false break. if (!valid_code_point) break; // Set name position to tokenizer's next index. name_position = tokenizer.next_index; } if (invalid_name) { continue; } // If name position is less than or equal to name start: if (name_position <= name_start) { // Run process a tokenizing error given tokenizer, name start, and // tokenizer's index. if (auto error = tokenizer.process_tokenizing_error(name_start, tokenizer.index)) { ada_log("process_tokenizing_error failed"); return tl::unexpected(*error); } // Continue continue; } // Run add a token with default length given tokenizer, "name", name // position, and name start. tokenizer.add_token_with_default_length(token_type::NAME, name_position, name_start); continue; } // If tokenizer's code point is U+0028 ((): if (tokenizer.code_point == '(') { // Let depth be 1. size_t depth = 1; // Let regexp position be tokenizer's next index. auto regexp_position = tokenizer.next_index; // Let regexp start be regexp position. auto regexp_start = regexp_position; // Let error be false. bool error = false; // While regexp position is less than tokenizer's input's code point // length: while (regexp_position < tokenizer.input.size()) { // Run seek and get the next code point given tokenizer and regexp // position. tokenizer.seek_and_get_next_code_point(regexp_position); if (tokenizer.had_invalid_code_point()) { if (auto process_error = tokenizer.process_tokenizing_error( tokenizer.next_index, regexp_position)) { return tl::unexpected(*process_error); } error = true; break; } // TODO: Optimization opportunity: The next 2 if statements can be // merged. If the result of running is ASCII given tokenizer's code // point is false: if (!unicode::is_ascii(tokenizer.code_point)) { // Run process a tokenizing error given tokenizer, regexp start, and // tokenizer's index. if (auto process_error = tokenizer.process_tokenizing_error( regexp_start, tokenizer.index)) { return tl::unexpected(*process_error); } // Set error to true. error = true; break; } // If regexp position equals regexp start and tokenizer's code point is // U+003F (?): if (regexp_position == regexp_start && tokenizer.code_point == '?') { // Run process a tokenizing error given tokenizer, regexp start, and // tokenizer's index. if (auto process_error = tokenizer.process_tokenizing_error( regexp_start, tokenizer.index)) { return tl::unexpected(*process_error); } // Set error to true; error = true; break; } // If tokenizer's code point is U+005C (\): if (tokenizer.code_point == '\\') { // If regexp position equals tokenizer's input's code point length - 1 if (regexp_position == tokenizer.input.size() - 1) { // Run process a tokenizing error given tokenizer, regexp start, and // tokenizer's index. if (auto process_error = tokenizer.process_tokenizing_error( regexp_start, tokenizer.index)) { return tl::unexpected(*process_error); } // Set error to true. error = true; break; } // Run get the next code point given tokenizer. auto escaped_index = tokenizer.next_index; tokenizer.get_next_code_point(); if (tokenizer.had_invalid_code_point()) { if (auto process_error = tokenizer.process_tokenizing_error( tokenizer.next_index, escaped_index)) { return tl::unexpected(*process_error); } error = true; break; } // If the result of running is ASCII given tokenizer's code point is // false: if (!unicode::is_ascii(tokenizer.code_point)) { // Run process a tokenizing error given tokenizer, regexp start, and // tokenizer's index. if (auto process_error = tokenizer.process_tokenizing_error( regexp_start, tokenizer.index); process_error.has_value()) { return tl::unexpected(*process_error); } // Set error to true. error = true; break; } // Set regexp position to tokenizer's next index. regexp_position = tokenizer.next_index; continue; } // If tokenizer's code point is U+0029 ()): if (tokenizer.code_point == ')') { // Decrement depth by 1. depth--; // If depth is 0: if (depth == 0) { // Set regexp position to tokenizer's next index. regexp_position = tokenizer.next_index; // Break. break; } } else if (tokenizer.code_point == '(') { // Otherwise if tokenizer's code point is U+0028 ((): // Increment depth by 1. depth++; // If regexp position equals tokenizer's input's code point length - // 1: if (regexp_position == tokenizer.input.size() - 1) { // Run process a tokenizing error given tokenizer, regexp start, and // tokenizer's index. if (auto process_error = tokenizer.process_tokenizing_error( regexp_start, tokenizer.index)) { return tl::unexpected(*process_error); } // Set error to true. error = true; break; } // Let temporary position be tokenizer's next index. auto temporary_position = tokenizer.next_index; // Run get the next code point given tokenizer. tokenizer.get_next_code_point(); if (tokenizer.had_invalid_code_point()) { if (auto process_error = tokenizer.process_tokenizing_error( tokenizer.next_index, temporary_position)) { return tl::unexpected(*process_error); } error = true; break; } // If tokenizer's code point is not U+003F (?): if (tokenizer.code_point != '?') { // Run process a tokenizing error given tokenizer, regexp start, and // tokenizer's index. if (auto process_error = tokenizer.process_tokenizing_error( regexp_start, tokenizer.index)) { return tl::unexpected(*process_error); } // Set error to true. error = true; break; } // Set tokenizer's next index to temporary position. tokenizer.next_index = temporary_position; } // Set regexp position to tokenizer's next index. regexp_position = tokenizer.next_index; } // If error is true continue. if (error) continue; // If depth is not zero: if (depth != 0) { // Run process a tokenizing error given tokenizer, regexp start, and // tokenizer's index. if (auto process_error = tokenizer.process_tokenizing_error( regexp_start, tokenizer.index)) { return tl::unexpected(*process_error); } continue; } // Let regexp length be regexp position - regexp start - 1. auto regexp_length = regexp_position - regexp_start - 1; // If regexp length is zero: if (regexp_length == 0) { // Run process a tokenizing error given tokenizer, regexp start, and // tokenizer's index. if (auto process_error = tokenizer.process_tokenizing_error( regexp_start, tokenizer.index)) { ada_log("process_tokenizing_error failed"); return tl::unexpected(*process_error); } continue; } // Run add a token given tokenizer, "regexp", regexp position, regexp // start, and regexp length. tokenizer.add_token(token_type::REGEXP, regexp_position, regexp_start, regexp_length); continue; } // Run add a token with default position and length given tokenizer and // "char". tokenizer.add_token_with_defaults(token_type::CHAR); } // Run add a token with default length given tokenizer, "end", tokenizer's // index, and tokenizer's index. tokenizer.add_token_with_default_length(token_type::END, tokenizer.index, tokenizer.index); ada_log("tokenizer.token_list size is: ", tokenizer.token_list.size()); // Return tokenizer's token list. return tokenizer.token_list; } namespace { constexpr std::array<uint8_t, 256> escape_pattern_table = []() consteval { std::array<uint8_t, 256> out{}; for (auto& c : {'+', '*', '?', ':', '{', '}', '(', ')', '\\'}) { out[c] = 1; } return out; }(); constexpr bool should_escape_pattern_char(char c) { return escape_pattern_table[static_cast<uint8_t>(c)]; } } // namespace std::string escape_pattern_string(std::string_view input) { ada_log("escape_pattern_string called with input=", input); if (input.empty()) [[unlikely]] { return ""; } // Assert: input is an ASCII string. ADA_ASSERT_TRUE(ada::idna::is_ascii(input)); // Let result be the empty string. std::string result{}; // Reserve extra space for potential escapes result.reserve(input.size() * 2); // While index is less than input's length: for (const char c : input) { if (should_escape_pattern_char(c)) { // Append U+005C (\) to the end of result. result.push_back('\\'); } // Append c to the end of result. result.push_back(c); } // Return result. return result; } namespace { constexpr std::array<uint8_t, 256> escape_regexp_table = []() consteval { std::array<uint8_t, 256> out{}; for (auto& c : {'.', '+', '*', '?', '^', '$', '{', '}', '(', ')', '[', ']', '|', '/', '\\'}) { out[c] = 1; } return out; }(); constexpr bool should_escape_regexp_char(char c) { return escape_regexp_table[(uint8_t)c]; } } // namespace std::string escape_regexp_string(std::string_view input) { // Assert: input is an ASCII string. ADA_ASSERT_TRUE(idna::is_ascii(input)); // Let result be the empty string. std::string result{}; // Reserve extra space for potential escapes (worst case: all chars escaped) result.reserve(input.size() * 2); for (const char c : input) { if (should_escape_regexp_char(c)) { // Avoid temporary string allocation - directly append characters result.push_back('\\'); result.push_back(c); } else { result.push_back(c); } } return result; } std::string process_base_url_string(std::string_view input, url_pattern_init::process_type type) { // If type is not "pattern" return input. if (type != url_pattern_init::process_type::pattern) { return std::string(input); } // Return the result of escaping a pattern string given input. return escape_pattern_string(input); } constexpr bool is_absolute_pathname( std::string_view input, url_pattern_init::process_type type) noexcept { // If input is the empty string, then return false. if (input.empty()) [[unlikely]] { return false; } // If input[0] is U+002F (/), then return true. if (input.starts_with("/")) return true; // If type is "url", then return false. if (type == url_pattern_init::process_type::url) return false; // If input's code point length is less than 2, then return false. if (input.size() < 2) return false; // If input[0] is U+005C (\) and input[1] is U+002F (/), then return true. // If input[0] is U+007B ({) and input[1] is U+002F (/), then return true. // Return false. return input[1] == '/' && (input[0] == '\\' || input[0] == '{'); } std::string generate_pattern_string( std::vector<url_pattern_part>& part_list, url_pattern_compile_component_options& options) { // Let result be the empty string. std::string result{}; // Let index list be the result of getting the indices for part list. // For each index of index list: for (size_t index = 0; index < part_list.size(); index++) { // Let part be part list[index]. // Use reference to avoid copy const auto& part = part_list[index]; // Let previous part be part list[index - 1] if index is greater than 0, // otherwise let it be null. // Use pointer to avoid copy const url_pattern_part* previous_part = index == 0 ? nullptr : &part_list[index - 1]; // Let next part be part list[index + 1] if index is less than index list's // size - 1, otherwise let it be null. const url_pattern_part* next_part = index < part_list.size() - 1 ? &part_list[index + 1] : nullptr; // If part's type is "fixed-text" then: if (part.type == url_pattern_part_type::FIXED_TEXT) { // If part's modifier is "none" then: if (part.modifier == url_pattern_part_modifier::none) { // Append the result of running escape a pattern string given part's // value to the end of result. result.append(escape_pattern_string(part.value)); continue; } // Append "{" to the end of result. result += "{"; // Append the result of running escape a pattern string given part's value // to the end of result. result.append(escape_pattern_string(part.value)); // Append "}" to the end of result. result += "}"; // Append the result of running convert a modifier to a string given // part's modifier to the end of result. result.append(convert_modifier_to_string(part.modifier)); continue; } // Let custom name be true if part's name[0] is not an ASCII digit; // otherwise false. bool custom_name = !unicode::is_ascii_digit(part.name[0]); // Let needs grouping be true if at least one of the following are true, // otherwise let it be false: // - part's suffix is not the empty string. // - part's prefix is not the empty string and is not options's prefix code // point. bool needs_grouping = !part.suffix.empty() || (!part.prefix.empty() && (options.get_prefix().empty() || part.prefix[0] != options.get_prefix()[0])); // If all of the following are true: // - needs grouping is false; and // - custom name is true; and // - part's type is "segment-wildcard"; and // - part's modifier is "none"; and // - next part is not null; and // - next part's prefix is the empty string; and // - next part's suffix is the empty string if (!needs_grouping && custom_name && part.type == url_pattern_part_type::SEGMENT_WILDCARD && part.modifier == url_pattern_part_modifier::none && next_part && next_part->prefix.empty() && next_part->suffix.empty()) { // If next part's type is "fixed-text": if (next_part->type == url_pattern_part_type::FIXED_TEXT) { // Set needs grouping to true if the result of running is a valid name // code point given next part's value's first code point and the boolean // false is true. if (idna::valid_name_code_point(next_part->value[0], false)) { needs_grouping = true; } } else { // Set needs grouping to true if next part's name[0] is an ASCII digit. needs_grouping = !next_part->name.empty() && unicode::is_ascii_digit(next_part->name[0]); } } // If all of the following are true: // - needs grouping is false; and // - part's prefix is the empty string; and // - previous part is not null; and // - previous part's type is "fixed-text"; and // - previous part's value's last code point is options's prefix code point. // then set needs grouping to true. if (!needs_grouping && part.prefix.empty() && previous_part && previous_part->type == url_pattern_part_type::FIXED_TEXT && !previous_part->value.empty() && !options.get_prefix().empty() && previous_part->value.back() == options.get_prefix()[0]) { needs_grouping = true; } // Assert: part's name is not the empty string or null. ADA_ASSERT_TRUE(!part.name.empty()); // If needs grouping is true, then append "{" to the end of result. if (needs_grouping) { result.append("{"); } // Append the result of running escape a pattern string given part's prefix // to the end of result. result.append(escape_pattern_string(part.prefix)); // If custom name is true: if (custom_name) { // Append ":" to the end of result. result.append(":"); // Append part's name to the end of result. result.append(part.name); } // If part's type is "regexp" then: if (part.type == url_pattern_part_type::REGEXP) { // Append "(" to the end of result. result.append("("); // Append part's value to the end of result. result.append(part.value); // Append ")" to the end of result. result.append(")"); } else if (part.type == url_pattern_part_type::SEGMENT_WILDCARD && !custom_name) { // Otherwise if part's type is "segment-wildcard" and custom name is // false: Append "(" to the end of result. result.append("("); // Append the result of running generate a segment wildcard regexp given // options to the end of result. result.append(generate_segment_wildcard_regexp(options)); // Append ")" to the end of result. result.append(")"); } else if (part.type == url_pattern_part_type::FULL_WILDCARD) { // Otherwise if part's type is "full-wildcard": // If custom name is false and one of the following is true: // - previous part is null; or // - previous part's type is "fixed-text"; or // - previous part's modifier is not "none"; or // - needs grouping is true; or // - part's prefix is not the empty string // - then append "*" to the end of result. if (!custom_name && (!previous_part || previous_part->type == url_pattern_part_type::FIXED_TEXT || previous_part->modifier != url_pattern_part_modifier::none || needs_grouping || !part.prefix.empty())) { result.append("*"); } else { // Append "(" to the end of result. // Append full wildcard regexp value to the end of result. // Append ")" to the end of result. result.append("(.*)"); } } // If all of the following are true: // - part's type is "segment-wildcard"; and // - custom name is true; and // - part's suffix is not the empty string; and // - The result of running is a valid name code point given part's suffix's // first code point and the boolean false is true then append U+005C (\) to // the end of result. if (part.type == url_pattern_part_type::SEGMENT_WILDCARD && custom_name && !part.suffix.empty() && idna::valid_name_code_point(part.suffix[0], false)) { result.append("\\"); } // Append the result of running escape a pattern string given part's suffix // to the end of result. result.append(escape_pattern_string(part.suffix)); // If needs grouping is true, then append "}" to the end of result. if (needs_grouping) result.append("}"); // Append the result of running convert a modifier to a string given part's // modifier to the end of result. result.append(convert_modifier_to_string(part.modifier)); } // Return result. return result; } } // namespace ada::url_pattern_helpers #endif // ADA_INCLUDE_URL_PATTERN