// Copyright 2021 The Chromium Authors
// 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/constructor_string_parser.h"

#include <string_view>
#include <vector>

#include "base/types/expected.h"
#include "third_party/abseil-cpp/absl/base/macros.h"

namespace liburlpattern {

ConstructorStringParser::ConstructorStringParser(
    std::string_view constructor_string)
    : input_(constructor_string) {}

absl::Status ConstructorStringParser::Parse(
    ProtocolCheckCallback protocol_matches_special_scheme) {
  ABSL_ASSERT(state_ == StringParseState::kInit);
  ABSL_ASSERT(token_index_ == 0u);

  auto tokenize_result = Tokenize(input_, TokenizePolicy::kLenient);
  if (!tokenize_result.has_value()) {
    // This should not happen with kLenient mode, but we handle it anyway.
    return tokenize_result.error();
  }

  token_list_ = std::move(tokenize_result.value());

  // When constructing a pattern using structured input like
  // `new URLPattern({ pathname: 'foo' })` any missing components will be
  // defaulted to wildcards.
  //
  // Components which ordinarily appear "later" than those specified are instead
  // treated as wildcards, which avoids the need to explicitly wildcard each of
  // them. As a result, these values are not initialized to be empty until a
  // "later" component is seen.

  // Iterate through the list of tokens and update our state machine as we go.
  for (; token_index_ < token_list_.size(); token_index_ += token_increment_) {
    // Reset back to our default `token_increment_` value.
    token_increment_ = 1;

    // All states must respect the end of the token list.  The liburlpattern
    // tokenizer guarantees that the last token will have the type `kEnd`.
    if (token_list_[token_index_].type == TokenType::kEnd) {
      // If we failed to find a protocol terminator then we are still in
      // relative mode.  We now need to determine the first component of the
      // relative URL.
      if (state_ == StringParseState::kInit) {
        // Reset back to the start of the input string.
        Rewind();

        // If the string begins with `?` then its a relative search component.
        // If it starts with `#` then its a relative hash component.  Otherwise
        // its a relative pathname.
        //
        // In each case we initialize any components following the initial
        // component to be empty string.
        if (IsHashPrefix()) {
          ChangeState(StringParseState::kHash, Skip(1));
        } else if (IsSearchPrefix()) {
          ChangeState(StringParseState::kSearch, Skip(1));
        } else {
          ChangeState(StringParseState::kPathname, Skip(0));
        }
        continue;
      }

      // If we failed to find an `@`, then there is no username and password.
      // We should rewind and process the data as a hostname.
      else if (state_ == StringParseState::kAuthority) {
        RewindAndSetState(StringParseState::kHostname);
        continue;
      }

      ChangeState(StringParseState::kDone, Skip(0));
      break;
    }

    // In addition, all states must handle pattern groups.  We do not permit
    // a component to end in the middle of a pattern group.  Therefore we skip
    // past any tokens that are within `{` and `}`.  Note, the tokenizer
    // handles grouping `(` and `)` and `:foo` groups for us automatically, so
    // we don't need special code for them here.
    if (IsGroupOpen()) {
      group_depth_ += 1;
      continue;
    }

    if (group_depth_ > 0) {
      if (IsGroupClose()) {
        group_depth_ -= 1;
      } else {
        continue;
      }
    }

    switch (state_) {
      case StringParseState::kInit:
        if (IsProtocolSuffix()) {
          // Update the state to expect the start of an absolute URL.
          RewindAndSetState(StringParseState::kProtocol);
        }
        break;

      case StringParseState::kProtocol:
        // If we find the end of the protocol component...
        if (IsProtocolSuffix()) {
          base::expected protocol_check_result =
              protocol_matches_special_scheme(MakeComponentString());
          if (!protocol_check_result.has_value()) {
            return protocol_check_result.error();
          }
          should_treat_as_standard_url_ = protocol_check_result.value();

          // By default we treat this as a "cannot-be-a-base-URL" or what chrome
          // calls a "path" URL.  In this case we go straight to the pathname
          // component.  The hostname and port are left with their default
          // empty string values.
          StringParseState next_state = StringParseState::kPathname;
          Skip skip = Skip(1);

          // If there are authority slashes, like `https://`, then
          // we must transition to the authority section of the URLPattern.
          if (NextIsAuthoritySlashes()) {
            next_state = StringParseState::kAuthority;
            skip = Skip(3);
          }

          // If there are no authority slashes, but the protocol is special
          // then we still go to the authority section as this is a "standard"
          // URL.  This differs from the above case since we don't need to skip
          // the extra slashes.
          else if (should_treat_as_standard_url_) {
            next_state = StringParseState::kAuthority;
          }

          ChangeState(next_state, skip);
        }
        break;

      case StringParseState::kAuthority:
        // Before going to the hostname state we must see if there is an
        // identity of the form:
        //
        //  <username>:<password>@<hostname>
        //
        // We check for this by looking for the `@` character.  The username
        // and password are themselves each optional, so the `:` may not be
        // present.  If we see the `@` we just go to the username state
        // and let it proceed until it hits either the password separator
        // or the `@` terminator.
        if (IsIdentityTerminator()) {
          RewindAndSetState(StringParseState::kUsername);
        }

        // Stop searching for the `@` character if we see the beginning
        // of the pathname, search, or hash components.
        else if (IsPathnameStart() || IsSearchPrefix() || IsHashPrefix()) {
          RewindAndSetState(StringParseState::kHostname);
        }
        break;

      case StringParseState::kUsername:
        // If we find a `:` then transition to the password component state.
        if (IsPasswordPrefix()) {
          ChangeState(StringParseState::kPassword, Skip(1));
        }

        // If we find a `@` then transition to the hostname component state.
        else if (IsIdentityTerminator()) {
          ChangeState(StringParseState::kHostname, Skip(1));
        }
        break;

      case StringParseState::kPassword:
        // If we find a `@` then transition to the hostname component state.
        if (IsIdentityTerminator()) {
          ChangeState(StringParseState::kHostname, Skip(1));
        }
        break;

      case StringParseState::kHostname:
        // Track whether we are inside ipv6 address brackets.
        if (IsIPv6Open()) {
          hostname_ipv6_bracket_depth_ += 1;
        } else if (IsIPv6Close()) {
          hostname_ipv6_bracket_depth_ -= 1;
        }

        // If we find a `:` then we transition to the port component state.
        // However, we ignore `:` when parsing an ipv6 address.
        else if (IsPortPrefix() && !hostname_ipv6_bracket_depth_) {
          ChangeState(StringParseState::kPort, Skip(1));
        }

        // If we find a `/` then we transition to the pathname component state.
        else if (IsPathnameStart()) {
          ChangeState(StringParseState::kPathname, Skip(0));
        }

        // If we find a `?` then we transition to the search component state.
        else if (IsSearchPrefix()) {
          ChangeState(StringParseState::kSearch, Skip(1));
        }

        // If we find a `#` then we transition to the hash component state.
        else if (IsHashPrefix()) {
          ChangeState(StringParseState::kHash, Skip(1));
        }
        break;

      case StringParseState::kPort:
        // If we find a `/` then we transition to the pathname component state.
        if (IsPathnameStart()) {
          ChangeState(StringParseState::kPathname, Skip(0));
        }
        // If we find a `?` then we transition to the search component state.
        else if (IsSearchPrefix()) {
          ChangeState(StringParseState::kSearch, Skip(1));
        }
        // If we find a `#` then we transition to the hash component state.
        else if (IsHashPrefix()) {
          ChangeState(StringParseState::kHash, Skip(1));
        }
        break;
      case StringParseState::kPathname:
        // If we find a `?` then we transition to the search component state.
        if (IsSearchPrefix()) {
          ChangeState(StringParseState::kSearch, Skip(1));
        }
        // If we find a `#` then we transition to the hash component state.
        else if (IsHashPrefix()) {
          ChangeState(StringParseState::kHash, Skip(1));
        }
        break;
      case StringParseState::kSearch:
        // If we find a `#` then we transition to the hash component state.
        if (IsHashPrefix()) {
          ChangeState(StringParseState::kHash, Skip(1));
        }
        break;
      case StringParseState::kHash:
        // Nothing to do here as we are just looking for the end.
        break;
      case StringParseState::kDone:
        ABSL_ASSERT(false);
        break;
    };
  }

  // Special case: if you specify a hostname, it is assumed that you want the
  // default port, if you didn't specify. This is ensures that
  // https://example.com/* does not match https://example.com:8443/, which is
  // another origin entirely.
  if (result_.hostname && !result_.port) {
    result_.port = "";
  }
  return absl::OkStatus();
}

void ConstructorStringParser::ChangeState(StringParseState new_state,
                                          Skip skip) {
  // First we convert the tokens between `component_start_` and `token_index_`
  // a component pattern string.  This is stored in the appropriate result
  // property based on the current `state_`.
  switch (state_) {
    case StringParseState::kInit:
      // No component to set when transitioning from this state.
      break;
    case StringParseState::kProtocol:
      result_.protocol = MakeComponentString();
      break;
    case StringParseState::kAuthority:
      // No component to set when transitioning from this state.
      break;
    case StringParseState::kUsername:
      result_.username = MakeComponentString();
      break;
    case StringParseState::kPassword:
      result_.password = MakeComponentString();
      break;
    case StringParseState::kHostname:
      result_.hostname = MakeComponentString();
      break;
    case StringParseState::kPort:
      result_.port = MakeComponentString();
      break;
    case StringParseState::kPathname:
      result_.pathname = MakeComponentString();
      break;
    case StringParseState::kSearch:
      result_.search = MakeComponentString();
      break;
    case StringParseState::kHash:
      result_.hash = MakeComponentString();
      break;
    case StringParseState::kDone:
      ABSL_ASSERT(false);
      break;
  }

  if (state_ != StringParseState::kInit &&
      new_state != StringParseState::kDone) {
    // If a component was skipped but a later component is present, it gets its
    // default value, explicitly.
    //
    // This relies on the ordering of the states, which does correspond to the
    // order of components (aside from authority/username/password, which are
    // special).
    static_assert(StringParseState::kHostname < StringParseState::kPort);
    static_assert(StringParseState::kPort < StringParseState::kPathname);
    static_assert(StringParseState::kPathname < StringParseState::kSearch);
    static_assert(StringParseState::kSearch < StringParseState::kHash);
    if (state_ < StringParseState::kHostname &&
        new_state > StringParseState::kHostname && !result_.hostname) {
      result_.hostname = "";
    }
    if (state_ < StringParseState::kPort &&
        new_state > StringParseState::kPort && !result_.port) {
      result_.port = "";
    }
    if (state_ < StringParseState::kPathname &&
        new_state > StringParseState::kPathname && !result_.pathname) {
      result_.pathname = should_treat_as_standard_url_ ? "/" : "";
    }
    if (state_ < StringParseState::kSearch &&
        new_state > StringParseState::kSearch && !result_.search) {
      result_.search = "";
    }
  }

  ChangeStateWithoutSettingComponent(new_state, skip);
}

void ConstructorStringParser::ChangeStateWithoutSettingComponent(
    StringParseState new_state,
    Skip skip) {
  state_ = new_state;

  // Now update `component_start_` to point to the new component.  The `skip`
  // argument tells us how many tokens to ignore to get to the next start.
  component_start_ = token_index_ + skip;

  // Next, move the `token_index_` so that the top of the loop will begin
  // parsing the new component.  We adjust the `token_increment_` down to
  // zero as the skip value already takes into account moving to the start
  // of the next component.
  token_index_ += skip;
  token_increment_ = 0;
}

void ConstructorStringParser::Rewind() {
  token_index_ = component_start_;
  token_increment_ = 0;
}

void ConstructorStringParser::RewindAndSetState(StringParseState new_state) {
  Rewind();
  state_ = new_state;
}

const Token& ConstructorStringParser::SafeToken(size_t index) const {
  if (index < token_list_.size()) {
    return token_list_[index];
  }
  ABSL_ASSERT(!token_list_.empty());
  ABSL_ASSERT(token_list_.back().type == TokenType::kEnd);
  return token_list_.back();
}

bool ConstructorStringParser::IsNonSpecialPatternChar(size_t index,
                                                      const char* value) const {
  const Token& token = SafeToken(index);
  return token.value == value && (token.type == TokenType::kChar ||
                                  token.type == TokenType::kEscapedChar ||
                                  token.type == TokenType::kInvalidChar);
}

bool ConstructorStringParser::IsProtocolSuffix() const {
  return IsNonSpecialPatternChar(token_index_, ":");
}

bool ConstructorStringParser::NextIsAuthoritySlashes() const {
  return IsNonSpecialPatternChar(token_index_ + 1, "/") &&
         IsNonSpecialPatternChar(token_index_ + 2, "/");
}

bool ConstructorStringParser::IsIdentityTerminator() const {
  return IsNonSpecialPatternChar(token_index_, "@");
}

bool ConstructorStringParser::IsPasswordPrefix() const {
  return IsNonSpecialPatternChar(token_index_, ":");
}

bool ConstructorStringParser::IsPortPrefix() const {
  return IsNonSpecialPatternChar(token_index_, ":");
}

bool ConstructorStringParser::IsPathnameStart() const {
  return IsNonSpecialPatternChar(token_index_, "/");
}

bool ConstructorStringParser::IsSearchPrefix() const {
  if (IsNonSpecialPatternChar(token_index_, "?")) {
    return true;
  }

  if (token_list_[token_index_].value != "?") {
    return false;
  }

  // If we have a "?" that is not a normal character, then it must be an
  // optional group modifier.
  ABSL_ASSERT(SafeToken(token_index_).type == TokenType::kOtherModifier);

  // We have a `?` tokenized as a modifier.  We only want to treat this as
  // the search prefix if it would not normally be valid in a liburlpattern
  // string.  A modifier must follow a matching group.  Therefore we inspect
  // the preceding token to see if the `?` is immediately following a group
  // construct.
  //
  // So if the string is:
  //
  //  https://example.com/foo?bar
  //
  // Then we return true because the previous token is a `o` with type kChar.
  // For the string:
  //
  //  https://example.com/:name?bar
  //
  // Then we return false because the previous token is `:name` with type
  // kName.  If the developer intended this to be a search prefix then they
  // would need to escape like question mark like `:name\\?bar`.
  //
  // Note, if `token_index_` is zero the index will wrap around and
  // `SafeToken()` will return the kEnd token.  This will correctly return true
  // from this method as a pattern cannot normally begin with an unescaped `?`.
  const auto& previous_token = SafeToken(token_index_ - 1);
  return previous_token.type != TokenType::kName &&
         previous_token.type != TokenType::kRegex &&
         previous_token.type != TokenType::kClose &&
         previous_token.type != TokenType::kAsterisk;
}

bool ConstructorStringParser::IsHashPrefix() const {
  return IsNonSpecialPatternChar(token_index_, "#");
}

bool ConstructorStringParser::IsGroupOpen() const {
  return token_list_[token_index_].type == TokenType::kOpen;
}

bool ConstructorStringParser::IsGroupClose() const {
  return token_list_[token_index_].type == TokenType::kClose;
}

bool ConstructorStringParser::IsIPv6Open() const {
  return IsNonSpecialPatternChar(token_index_, "[");
}

bool ConstructorStringParser::IsIPv6Close() const {
  return IsNonSpecialPatternChar(token_index_, "]");
}

std::string_view ConstructorStringParser::MakeComponentString() const {
  ABSL_ASSERT(token_index_ < token_list_.size());
  const auto& token = token_list_[token_index_];

  size_t component_char_start = SafeToken(component_start_).index;

  ABSL_ASSERT(component_char_start <= input_.size());
  ABSL_ASSERT(token.index >= component_char_start);
  ABSL_ASSERT(token.index < input_.size() ||
              (token.index == input_.size() && token.type == TokenType::kEnd));
  return input_.substr(component_char_start,
                       token.index - component_char_start);
}

}  // namespace liburlpattern
