| // Copyright 2020 The Chromium Authors | |
| // Copyright 2014 Blake Embrey (hello@blakeembrey.com) | |
| // Use of this source code is governed by an MIT-style license that can be | |
| // found in the LICENSE file or at https://opensource.org/licenses/MIT. | |
| #include "third_party/liburlpattern/tokenize.h" | |
| #include <string_view> | |
| #include "base/compiler_specific.h" | |
| #include "third_party/abseil-cpp/absl/strings/str_format.h" | |
| #include "third_party/icu/source/common/unicode/uchar.h" | |
| #include "third_party/icu/source/common/unicode/utf8.h" | |
| #include "third_party/liburlpattern/utils.h" | |
| // The following code is a translation from the path-to-regexp typescript at: | |
| // | |
| // https://github.com/pillarjs/path-to-regexp/blob/125c43e6481f68cc771a5af22b914acdb8c5ba1f/src/index.ts#L4-L124 | |
| namespace liburlpattern { | |
| namespace { | |
| bool IsASCII(UChar32 c) { | |
| // Characters should be valid ASCII code points: | |
| // https://infra.spec.whatwg.org/#ascii-code-point | |
| return c >= 0x00 && c <= 0x7f; | |
| } | |
| class Tokenizer { | |
| public: | |
| Tokenizer(std::string_view pattern, TokenizePolicy policy) | |
| : pattern_(std::move(pattern)), policy_(policy) { | |
| token_list_.reserve(pattern_.size()); | |
| } | |
| base::expected<std::vector<Token>, absl::Status> Tokenize() { | |
| while (index_ < pattern_.size()) { | |
| if (!status_.ok()) | |
| return base::unexpected(std::move(status_)); | |
| if (!NextAt(index_)) { | |
| Error(absl::StrFormat("Invalid UTF-8 codepoint at index %d.", index_)); | |
| continue; | |
| } | |
| if (codepoint_ == '*') { | |
| AddToken(TokenType::kAsterisk); | |
| continue; | |
| } | |
| if (codepoint_ == '+' || codepoint_ == '?') { | |
| AddToken(TokenType::kOtherModifier); | |
| continue; | |
| } | |
| // Escape sequences always escape a single following character at the | |
| // level of the pattern. | |
| if (codepoint_ == '\\') { | |
| if (index_ == (pattern_.size() - 1)) { | |
| Error(absl::StrFormat("Trailing escape character at index %d.", | |
| index_)); | |
| continue; | |
| } | |
| size_t escaped_i = next_index_; | |
| if (!Next()) { | |
| Error(absl::StrFormat("Invalid UTF-8 codepoint at index %d.", | |
| next_index_)); | |
| continue; | |
| } | |
| AddToken(TokenType::kEscapedChar, next_index_, escaped_i); | |
| continue; | |
| } | |
| if (codepoint_ == '{') { | |
| AddToken(TokenType::kOpen); | |
| continue; | |
| } | |
| if (codepoint_ == '}') { | |
| AddToken(TokenType::kClose); | |
| continue; | |
| } | |
| if (codepoint_ == ':') { | |
| size_t pos = next_index_; | |
| size_t name_start = pos; | |
| // Iterate over codepoints until we find the first non-name codepoint. | |
| while (pos < pattern_.size()) { | |
| if (!status_.ok()) | |
| return base::unexpected(std::move(status_)); | |
| if (!NextAt(pos)) { | |
| Error(absl::StrFormat("Invalid UTF-8 codepoint at index %d.", pos)); | |
| continue; | |
| } | |
| if (!IsNameCodepoint(codepoint_, pos == name_start)) | |
| break; | |
| pos = next_index_; | |
| } | |
| if (pos <= name_start) { | |
| Error(absl::StrFormat("Missing parameter name at index %d.", index_), | |
| name_start, index_); | |
| continue; | |
| } | |
| AddToken(TokenType::kName, pos, name_start); | |
| continue; | |
| } | |
| if (codepoint_ == '(') { | |
| size_t paren_nesting = 1; | |
| size_t j = next_index_; | |
| const size_t regex_start = next_index_; | |
| bool error = false; | |
| while (j < pattern_.size()) { | |
| if (!NextAt(j)) { | |
| Error(absl::StrFormat("Invalid UTF-8 codepoint at index %d.", j)); | |
| error = true; | |
| break; | |
| } | |
| if (!IsASCII(codepoint_)) { | |
| Error(absl::StrFormat( | |
| "Invalid non-ASCII character 0x%02x at index %d.", | |
| codepoint_, j), | |
| regex_start, index_); | |
| error = true; | |
| break; | |
| } | |
| if (j == regex_start && codepoint_ == '?') { | |
| Error(absl::StrFormat("Regex cannot start with '?' at index %d", j), | |
| regex_start, index_); | |
| error = true; | |
| break; | |
| } | |
| // This escape processing only handles single character escapes since | |
| // we only need to understand escaped paren characters for our state | |
| // processing. The escape `\` character is propagated to the embedded | |
| // regex expression. This permits authors to write longer escape | |
| // sequences such as `\x22` since the later characters will be | |
| // propagated on subsequent loop iterations. | |
| if (codepoint_ == '\\') { | |
| if (j == (pattern_.size() - 1)) { | |
| Error( | |
| absl::StrFormat("Trailing escape character at index %d.", j), | |
| regex_start, index_); | |
| error = true; | |
| break; | |
| } | |
| size_t escaped_j = next_index_; | |
| if (!Next()) { | |
| Error(absl::StrFormat("Invalid UTF-8 codepoint at index %d.", | |
| next_index_)); | |
| error = true; | |
| break; | |
| } | |
| if (!IsASCII(codepoint_)) { | |
| Error(absl::StrFormat( | |
| "Invalid non-ASCII character 0x%02x at index %d.", | |
| codepoint_, escaped_j), | |
| regex_start, index_); | |
| error = true; | |
| break; | |
| } | |
| j = next_index_; | |
| continue; | |
| } | |
| if (codepoint_ == ')') { | |
| paren_nesting -= 1; | |
| if (paren_nesting == 0) { | |
| j = next_index_; | |
| break; | |
| } | |
| } else if (codepoint_ == '(') { | |
| paren_nesting += 1; | |
| if (j == (pattern_.size() - 1)) { | |
| Error(absl::StrFormat("Unbalanced regex at index %d.", j), | |
| regex_start, index_); | |
| error = true; | |
| break; | |
| } | |
| size_t tmp_j = next_index_; | |
| if (!Next()) { | |
| Error(absl::StrFormat("Invalid UTF-8 codepoint at index %d.", | |
| next_index_)); | |
| error = true; | |
| break; | |
| } | |
| // Require the the first character after an open paren is `?`. This | |
| // permits assertions, named capture groups, and non-capturing | |
| // groups. It blocks, however, unnamed capture groups. | |
| if (codepoint_ != '?') { | |
| Error(absl::StrFormat( | |
| "Unnamed capturing groups are not allowed at index %d.", | |
| tmp_j), | |
| regex_start, index_); | |
| error = true; | |
| break; | |
| } | |
| next_index_ = tmp_j; | |
| } | |
| j = next_index_; | |
| } | |
| if (error) | |
| continue; | |
| if (paren_nesting) { | |
| Error(absl::StrFormat("Unbalanced regex at index %d.", index_), | |
| regex_start, index_); | |
| continue; | |
| } | |
| const size_t regex_length = j - regex_start - 1; | |
| if (regex_length == 0) { | |
| Error(absl::StrFormat("Missing regex at index %d.", index_), | |
| regex_start, index_); | |
| continue; | |
| } | |
| AddToken(TokenType::kRegex, j, regex_start, regex_length); | |
| continue; | |
| } | |
| AddToken(TokenType::kChar); | |
| } | |
| if (!status_.ok()) | |
| return base::unexpected(std::move(status_)); | |
| AddToken(TokenType::kEnd, index_, index_); | |
| return std::move(token_list_); | |
| } | |
| private: | |
| // Read the codepoint at `next_index_` in `pattern_` and store it in | |
| // `codepoint_`. In addition, `next_index_` is updated to the codepoint to be | |
| // read next. Returns true iff the codepoint was read successfully. On | |
| // success, `codepoint_` is non-negative. | |
| [[nodiscard]] bool Next() { | |
| UNSAFE_TODO( | |
| U8_NEXT(pattern_.data(), next_index_, pattern_.size(), codepoint_)); | |
| return codepoint_ >= 0; | |
| } | |
| // Read the codepoint at the specified `index` in `pattern_` and store it in | |
| // `codepoint_`. In addition, `next_index_` is updated to the codepoint to be | |
| // read next. Returns true iff the codepoint was read successfully. On | |
| // success, `codepoint_` is non-negative. | |
| [[nodiscard]] bool NextAt(size_t index) { | |
| next_index_ = index; | |
| return Next(); | |
| } | |
| // Append a Token to our list of the given `type` and with a value consisting | |
| // of the codepoints in `pattern_` starting at `value_pos` with | |
| // `value_length`. Update `index_` to `next_pos` automatically. | |
| void AddToken(TokenType type, | |
| size_t next_pos, | |
| size_t value_pos, | |
| size_t value_length) { | |
| token_list_.emplace_back(type, index_, | |
| pattern_.substr(value_pos, value_length)); | |
| index_ = next_pos; | |
| } | |
| // Append a Token to our list of the given `type` and with a value consisting | |
| // of the codepoints in `pattern_` starting at `value_pos`. The value length | |
| // is automatically computed as the difference between `next_pos` and | |
| // `value_pos`. Update `index_` to `next_pos` automatically. | |
| void AddToken(TokenType type, size_t next_pos, size_t value_pos) { | |
| AddToken(type, next_pos, value_pos, next_pos - value_pos); | |
| } | |
| // Append a Token to our list of the given `type` and with a value consisting | |
| // of the codepoints in `pattern_` starting at `index_`. The value length | |
| // is automatically computed as the difference between `next_index_` and | |
| // `index_`. Update `index_` to `next_index_` automatically. | |
| void AddToken(TokenType type) { AddToken(type, next_index_, index_); } | |
| void Error(std::string_view message, size_t next_pos, size_t value_pos) { | |
| if (policy_ == TokenizePolicy::kLenient) | |
| AddToken(TokenType::kInvalidChar, next_pos, value_pos); | |
| else | |
| status_ = absl::InvalidArgumentError(message); | |
| } | |
| void Error(std::string_view message) { Error(message, next_index_, index_); } | |
| const std::string_view pattern_; | |
| const TokenizePolicy policy_; | |
| std::vector<Token> token_list_; | |
| absl::Status status_; | |
| // `index_` tracks our "current" byte index in the input string. Typically | |
| // this will be updated every time we commit a token to `token_list_`. It may | |
| // stay frozen in place if we have a sub-loop consuming a larger token like | |
| // a named group or regex group. | |
| size_t index_ = 0; | |
| // The `next_index_` member is used to find the next UTF8 codepoint in the | |
| // string. This is used as both an input and output from the U8_NEXT() | |
| // function. We keep this separate from `index_` because there are many | |
| // cases where we need to read ahead of the last token consumed. | |
| size_t next_index_ = 0; | |
| UChar32 codepoint_ = U_SENTINEL; | |
| }; | |
| } // namespace | |
| const char* TokenTypeToString(TokenType type) { | |
| switch (type) { | |
| case TokenType::kOpen: | |
| return "'{'"; | |
| case TokenType::kClose: | |
| return "'}'"; | |
| case TokenType::kRegex: | |
| return "regex group"; | |
| case TokenType::kName: | |
| return "named group"; | |
| case TokenType::kChar: | |
| return "character"; | |
| case TokenType::kEscapedChar: | |
| return "escaped character"; | |
| case TokenType::kOtherModifier: | |
| return "modifier"; | |
| case TokenType::kAsterisk: | |
| return "asterisk"; | |
| case TokenType::kEnd: | |
| return "end of pattern"; | |
| case TokenType::kInvalidChar: | |
| return "invalid character"; | |
| } | |
| } | |
| std::ostream& operator<<(std::ostream& o, Token token) { | |
| o << "{ type:" << static_cast<int>(token.type) << ", index:" << token.index | |
| << ", value:" << token.value << " }"; | |
| return o; | |
| } | |
| // Split the input pattern into a list of tokens. | |
| base::expected<std::vector<Token>, absl::Status> Tokenize( | |
| std::string_view pattern, | |
| TokenizePolicy policy) { | |
| Tokenizer tokenizer(std::move(pattern), policy); | |
| return tokenizer.Tokenize(); | |
| } | |
| } // namespace liburlpattern |