// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/omnibox/browser/autocomplete_input.h"

#include <optional>
#include <string_view>
#include <vector>

#include "base/i18n/case_conversion.h"
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/memory_usage_estimator.h"
#include "base/trace_event/typed_macros.h"
#include "build/build_config.h"
#include "components/omnibox/browser/autocomplete_scheme_classifier.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "components/search_engines/template_url_starter_pack_data.h"
#include "components/url_formatter/url_fixer.h"
#include "components/url_formatter/url_formatter.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/base/url_util.h"
#include "third_party/metrics_proto/omnibox_event.pb.h"
#include "third_party/re2/src/re2/re2.h"
#include "url/url_canon_ip.h"
#include "url/url_util.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chromeos/constants/url_constants.h"  // nogncheck
#endif                                         // BUILDFLAG(IS_CHROMEOS)

// static
const char16_t AutocompleteInput::kInvalidChars[] = {
    '\n',   '\r', '\t',
    0x2028,  // Line separator
    0x2029,  // Paragraph separator
    0};

namespace {

// Hardcode constant to avoid any dependencies on content/.
const char kViewSourceScheme[] = "view-source";

void AdjustCursorPositionIfNecessary(size_t num_leading_chars_removed,
                                     size_t* cursor_position) {
  if (*cursor_position == std::u16string::npos)
    return;
  if (num_leading_chars_removed < *cursor_position)
    *cursor_position -= num_leading_chars_removed;
  else
    *cursor_position = 0;
}

// Finds all terms in |text| that start with http:// or https:// plus at least
// one more character and puts the text after the prefix in
// |terms_prefixed_by_http_or_https|.
void PopulateTermsPrefixedByHttpOrHttps(
    std::u16string_view text,
    std::vector<std::u16string>* terms_prefixed_by_http_or_https) {
  // Split on whitespace rather than use ICU's word iterator because, for
  // example, ICU's iterator may break on punctuation (such as ://) or decide
  // to split a single term in a hostname (if it seems to think that the
  // hostname is multiple words).  Neither of these behaviors is desirable.
  std::string_view separator = url::kStandardSchemeSeparator;
  for (std::u16string_view term : base::SplitStringPiece(
           text, u" ", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) {
    const std::string term_utf8(base::UTF16ToUTF8(term));
    static const char* kSchemes[2] = {url::kHttpScheme, url::kHttpsScheme};
    for (const char* scheme : kSchemes) {
      const std::string prefix = base::StrCat({scheme, separator});
      // Doing an ASCII comparison is okay because prefix is ASCII.
      if (base::StartsWith(term_utf8, prefix,
                           base::CompareCase::INSENSITIVE_ASCII) &&
          (term_utf8.length() > prefix.length())) {
        terms_prefixed_by_http_or_https->emplace_back(
            term.substr(prefix.length()));
      }
    }
  }
}

// Offsets |parts| of a URL after the scheme by |offset| amount.
void OffsetComponentsExcludingScheme(url::Parsed* parts, int offset) {
  url::Component* components[] = {
      &parts->username, &parts->password, &parts->host, &parts->port,
      &parts->path,     &parts->query,    &parts->ref,
  };
  for (url::Component* component : components) {
    url_formatter::OffsetComponent(offset, component);
  }
}

bool HasScheme(const std::u16string& input, const char* scheme) {
  std::string utf8_input(base::UTF16ToUTF8(input));
  url::Component view_source_scheme;
  if (url::FindAndCompareScheme(utf8_input, kViewSourceScheme,
                                &view_source_scheme)) {
    utf8_input.erase(0, view_source_scheme.end() + 1);
  }
  return url::FindAndCompareScheme(utf8_input, scheme, nullptr);
}

}  // namespace

AutocompleteInput::AutocompleteInput()
    : cursor_position_(std::u16string::npos),
      current_page_classification_(metrics::OmniboxEventProto::INVALID_SPEC),
      type_(metrics::OmniboxInputType::EMPTY),
      prevent_inline_autocomplete_(false),
      allow_exact_keyword_match_(true),
      in_keyword_mode_(false),
      omit_asynchronous_matches_(false),
      should_use_https_as_default_scheme_(false),
      added_default_scheme_to_typed_url_(false),
      https_port_for_testing_(0),
      use_fake_https_for_https_upgrade_testing_(false) {}

AutocompleteInput::AutocompleteInput(
    const std::u16string& text,
    metrics::OmniboxEventProto::PageClassification current_page_classification,
    const AutocompleteSchemeClassifier& scheme_classifier,
    bool should_use_https_as_default_scheme,
    int https_port_for_testing,
    bool use_fake_https_for_https_upgrade_testing)
    : AutocompleteInput(text,
                        std::string::npos,
                        current_page_classification,
                        scheme_classifier,
                        should_use_https_as_default_scheme,
                        https_port_for_testing,
                        use_fake_https_for_https_upgrade_testing) {}

AutocompleteInput::AutocompleteInput(
    const std::u16string& text,
    size_t cursor_position,
    metrics::OmniboxEventProto::PageClassification current_page_classification,
    const AutocompleteSchemeClassifier& scheme_classifier,
    bool should_use_https_as_default_scheme,
    int https_port_for_testing,
    bool use_fake_https_for_https_upgrade_testing)
    : AutocompleteInput(text,
                        cursor_position,
                        "",
                        current_page_classification,
                        scheme_classifier,
                        should_use_https_as_default_scheme,
                        https_port_for_testing,
                        use_fake_https_for_https_upgrade_testing) {}

AutocompleteInput::AutocompleteInput(
    const std::u16string& text,
    size_t cursor_position,
    const std::string& desired_tld,
    metrics::OmniboxEventProto::PageClassification current_page_classification,
    const AutocompleteSchemeClassifier& scheme_classifier,
    bool should_use_https_as_default_scheme,
    int https_port_for_testing,
    bool use_fake_https_for_https_upgrade_testing)
    : AutocompleteInput() {
  cursor_position_ = cursor_position;
  current_page_classification_ = current_page_classification;
  desired_tld_ = desired_tld;
  should_use_https_as_default_scheme_ = should_use_https_as_default_scheme;
  https_port_for_testing_ = https_port_for_testing;
  use_fake_https_for_https_upgrade_testing_ =
      use_fake_https_for_https_upgrade_testing;
  Init(text, scheme_classifier);
}

void AutocompleteInput::Init(
    const std::u16string& text,
    const AutocompleteSchemeClassifier& scheme_classifier) {
  DCHECK(cursor_position_ <= text.length() ||
         cursor_position_ == std::u16string::npos)
      << "Text: '" << text << "', cp: " << cursor_position_;
  // None of the providers care about leading white space so we always trim it.
  // Providers that care about trailing white space handle trimming themselves.
  if ((base::TrimWhitespace(text, base::TRIM_LEADING, &text_) &
       base::TRIM_LEADING) != 0)
    AdjustCursorPositionIfNecessary(text.length() - text_.length(),
                                    &cursor_position_);

  GURL canonicalized_url;
  type_ = Parse(text_, desired_tld_, scheme_classifier, &parts_, &scheme_,
                &canonicalized_url);
  PopulateTermsPrefixedByHttpOrHttps(text_, &terms_prefixed_by_http_or_https_);

  DCHECK(!added_default_scheme_to_typed_url_);
  typed_url_had_http_scheme_ =
      base::StartsWith(text,
                       base::StrCat({url::kHttpScheme16,
                                     url::kStandardSchemeSeparator16}),
                       base::CompareCase::INSENSITIVE_ASCII) &&
      canonicalized_url.SchemeIs(url::kHttpScheme);
  GURL upgraded_url;
  if (should_use_https_as_default_scheme_ &&
      type_ == metrics::OmniboxInputType::URL &&
      ShouldUpgradeToHttps(text, canonicalized_url, https_port_for_testing_,
                           use_fake_https_for_https_upgrade_testing_,
                           &upgraded_url)) {
    DCHECK(upgraded_url.is_valid());
    added_default_scheme_to_typed_url_ = true;
    scheme_ = std::u16string(url::kHttpsScheme16);
    canonicalized_url = upgraded_url;
    // We changed the scheme from http to https. Offset remaining components
    // by one.
    OffsetComponentsExcludingScheme(&parts_, 1);
  }

  if (((type_ == metrics::OmniboxInputType::UNKNOWN) ||
       (type_ == metrics::OmniboxInputType::URL)) &&
      canonicalized_url.is_valid() &&
      (!canonicalized_url.IsStandard() || canonicalized_url.SchemeIsFile() ||
       canonicalized_url.SchemeIsFileSystem() ||
       !canonicalized_url.host().empty())) {
    canonicalized_url_ = canonicalized_url;
  }
}

AutocompleteInput::AutocompleteInput(const AutocompleteInput& other) = default;

AutocompleteInput::~AutocompleteInput() = default;

// static
std::string AutocompleteInput::TypeToString(metrics::OmniboxInputType type) {
  switch (type) {
    case metrics::OmniboxInputType::EMPTY:
      return "invalid";
    case metrics::OmniboxInputType::UNKNOWN:
      return "unknown";
    case metrics::OmniboxInputType::DEPRECATED_REQUESTED_URL:
      return "deprecated-requested-url";
    case metrics::OmniboxInputType::URL:
      return "url";
    case metrics::OmniboxInputType::QUERY:
      return "query";
    case metrics::OmniboxInputType::DEPRECATED_FORCED_QUERY:
      return "deprecated-forced-query";
  }
  return std::string();
}

// static
metrics::OmniboxInputType AutocompleteInput::Parse(
    std::u16string_view text,
    const std::string& desired_tld,
    const AutocompleteSchemeClassifier& scheme_classifier,
    url::Parsed* parts,
    std::u16string* scheme,
    GURL* canonicalized_url) {
  size_t first_non_white = text.find_first_not_of(base::kWhitespaceUTF16, 0);
  if (first_non_white == std::u16string::npos)
    return metrics::OmniboxInputType::EMPTY;  // All whitespace.

  // Ask our parsing back-end to help us understand what the user typed.  We
  // use the URLFixerUpper here because we want to be smart about what we
  // consider a scheme.  For example, we shouldn't consider www.google.com:80
  // to have a scheme.
  url::Parsed local_parts;
  if (!parts)
    parts = &local_parts;
  const std::u16string parsed_scheme(url_formatter::SegmentURL(text, parts));
  if (scheme)
    *scheme = parsed_scheme;
  const std::string parsed_scheme_utf8(base::UTF16ToUTF8(parsed_scheme));
  DCHECK(base::IsStringASCII(parsed_scheme_utf8));

  // If we can't canonicalize the user's input, the rest of the autocomplete
  // system isn't going to be able to produce a navigable URL match for it.
  // So we just return QUERY immediately in these cases.
  GURL placeholder_canonicalized_url;
  if (!canonicalized_url)
    canonicalized_url = &placeholder_canonicalized_url;
  *canonicalized_url =
      url_formatter::FixupURL(base::UTF16ToUTF8(text), desired_tld);
  if (!canonicalized_url->is_valid())
    return metrics::OmniboxInputType::QUERY;

#if BUILDFLAG(IS_CHROMEOS)
  if (base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8,
                                       chromeos::kAppInstallUriScheme)) {
    return metrics::OmniboxInputType::URL;
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  if (base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8, url::kFileScheme)) {
    // A user might or might not type a scheme when entering a file URL.  In
    // either case, |parsed_scheme_utf8| will tell us that this is a file URL,
    // but |parts->scheme| might be empty, e.g. if the user typed "C:\foo".

#if (BUILDFLAG(IS_IOS) || BUILDFLAG(IS_ANDROID))
    // On iOS and Android, which cannot display file:/// URLs, treat this case
    // like a query.
    return metrics::OmniboxInputType::QUERY;
#else
    return metrics::OmniboxInputType::URL;
#endif  // BUILDFLAG(IS_IOS)
  }

  // Treat javascript: scheme queries followed by things that are unlikely to
  // be code as UNKNOWN, rather than script to execute (URL).
  if (base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8,
                                       url::kJavaScriptScheme) &&
      RE2::FullMatch(base::UTF16ToUTF8(text), "(?i)javascript:([^;=().\"]*)")) {
    return metrics::OmniboxInputType::UNKNOWN;
  }

  // If the user typed a scheme, and it's HTTP or HTTPS, we know how to parse it
  // well enough that we can fall through to the heuristics below.  If it's
  // something else, we can just determine our action based on what we do with
  // any input of this scheme.  In theory we could do better with some schemes
  // (e.g. "ftp" or "view-source") but I'll wait to spend the effort on that
  // until I run into some cases that really need it.
  if (parts->scheme.is_nonempty() &&
      !base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8, url::kHttpScheme) &&
      !base::EqualsCaseInsensitiveASCII(parsed_scheme_utf8,
                                        url::kHttpsScheme)) {
    metrics::OmniboxInputType type =
        scheme_classifier.GetInputTypeForScheme(parsed_scheme_utf8);
    if (type != metrics::OmniboxInputType::EMPTY)
      return type;

    // We don't know about this scheme.  It might be that the user typed a
    // URL of the form "username:password@foo.com", or a custom query, such as
    // "site:socialmedia.com @tagname".
    const std::u16string http_scheme_prefix =
        base::StrCat({url::kHttpScheme16, url::kStandardSchemeSeparator16});
    const std::u16string tentative_url_candidate =
        base::StrCat({http_scheme_prefix, text});
    url::Parsed http_parts;
    std::u16string http_scheme;
    GURL http_canonicalized_url;
    metrics::OmniboxInputType http_type =
        Parse(tentative_url_candidate, desired_tld, scheme_classifier,
              &http_parts, &http_scheme, &http_canonicalized_url);
    DCHECK_EQ(std::string(url::kHttpScheme), base::UTF16ToUTF8(http_scheme));

    if ((http_type == metrics::OmniboxInputType::URL) &&
        http_parts.username.is_nonempty() &&
        http_parts.password.is_nonempty()) {
      // Recognize and re-classify queries like: `site:web.com @query`
      auto tentative_password_sv =
          http_parts.password.AsViewOn(tentative_url_candidate);
      if (tentative_password_sv.find(u' ') != tentative_password_sv.npos) {
        *canonicalized_url = GURL::EmptyGURL();
        return metrics::OmniboxInputType::QUERY;
      }

      // Manually re-jigger the parsed parts to match |text| (without the
      // http scheme added).
      http_parts.scheme.reset();
      OffsetComponentsExcludingScheme(
          &http_parts, -static_cast<int>(http_scheme_prefix.length()));

      *parts = http_parts;
      if (scheme)
        scheme->clear();
      *canonicalized_url = http_canonicalized_url;

      return metrics::OmniboxInputType::URL;
    }

    // We don't know about this scheme and it doesn't look like the user
    // typed a username and password.  It's likely to be a search operator
    // like "site:" or "link:".  We classify it as UNKNOWN so the user has
    // the option of treating it as a URL if we're wrong.
    // Note that SegmentURL() is smart so we aren't tricked by "c:\foo" or
    // "www.example.com:81" in this case.
    return metrics::OmniboxInputType::UNKNOWN;
  }

  // Either the user didn't type a scheme, in which case we need to distinguish
  // between an HTTP URL and a query, or the scheme is HTTP or HTTPS, in which
  // case we should reject invalid formulations.

  // Determine the host family.  We get this information by (re-)canonicalizing
  // the already-canonicalized host rather than using the user's original input,
  // in case fixup affected the result here (e.g. an input that looks like an
  // IPv4 address but with a non-empty desired TLD would return IPV4 before
  // fixup and NEUTRAL afterwards, and we want to treat it as NEUTRAL).
  url::CanonHostInfo host_info;
  net::CanonicalizeHost(canonicalized_url->GetHost(), &host_info);

  // Check if the canonicalized host has a known TLD, which we'll want to know
  // below.
  const size_t registry_length =
      net::registry_controlled_domains::GetCanonicalHostRegistry(
          canonicalized_url->GetHost(),
          net::registry_controlled_domains::EXCLUDE_UNKNOWN_REGISTRIES,
          net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES)
          .transform(&std::string_view::size)
          .value_or(std::string_view::npos);
  DCHECK_NE(std::string::npos, registry_length);
  const bool has_known_tld = registry_length != 0;

  // See if the hostname is valid.  While IE and GURL allow hostnames to contain
  // many other characters (perhaps for weird intranet machines), it's extremely
  // unlikely that a user would be trying to type those in for anything other
  // than a search query.
  //
  // Per https://tools.ietf.org/html/rfc6761, the .invalid TLD is considered
  // non-navigable and thus is treated like a non-compliant hostname. (Though
  // just the word "invalid" is not a hostname).
  const std::u16string original_host(
      text.substr(parts->host.begin, parts->host.len));
  if (text != u"invalid" && (host_info.family == url::CanonHostInfo::NEUTRAL) &&
      (!net::IsCanonicalizedHostCompliant(canonicalized_url->GetHost()) ||
       canonicalized_url->DomainIs("invalid"))) {
    // Invalid hostname.  There are several possible cases:
    // * The user is typing a multi-word query.  If we see a space anywhere in
    //   the input host we assume this is a search and return QUERY.  (We check
    //   the input string instead of canonicalized_url->host() in case fixup
    //   escaped the space.)
    // * The user is typing some garbage string.  Return QUERY.
    // * Our checker is too strict and the user is typing a real-world URL
    //   that's "invalid" but resolves.  To catch these, we return UNKNOWN when
    //   the user explicitly typed a scheme or when the hostname has a known
    //   TLD, so we'll still search by default but we'll show the accidental
    //   search infobar if necessary.
    //
    // This means we would block the following kinds of navigation attempts:
    // * Navigations to a hostname with spaces
    // * Navigations to a hostname with invalid characters and an unknown TLD
    // These might be possible in intranets, but we're not going to support them
    // without concrete evidence that doing so is necessary.
    return (parts->scheme.is_nonempty() ||
            (has_known_tld &&
             (original_host.find(' ') == std::u16string::npos)))
               ? metrics::OmniboxInputType::UNKNOWN
               : metrics::OmniboxInputType::QUERY;
  }

  // For hostnames that look like IP addresses, distinguish between IPv6
  // addresses, which are basically guaranteed to be navigations, and IPv4
  // addresses, which are much fuzzier.
  if (host_info.family == url::CanonHostInfo::IPV6)
    return metrics::OmniboxInputType::URL;
  if (host_info.family == url::CanonHostInfo::IPV4) {
    // The host may be a real IP address, or something that looks a bit like it
    // (e.g. "1.2" or "3232235521").  We check whether it was convertible to an
    // IP with a non-zero first octet; IPs with first octet zero are "source
    // IPs" and are almost never navigable as destination addresses.
    //
    // The one exception to this is 0.0.0.0; on many systems, attempting to
    // navigate to this IP actually navigates to localhost.  To support this
    // case, when the converted IP is 0.0.0.0, we go ahead and run the "did the
    // user actually type four components" test in the conditional below, so
    // that we'll allow explicit attempts to navigate to "0.0.0.0".  If the
    // input was anything else (e.g. "0"), we'll fall through to returning QUERY
    // afterwards.
    if ((host_info.address[0] != 0) ||
        ((host_info.address[1] == 0) && (host_info.address[2] == 0) &&
         (host_info.address[3] == 0))) {
      // This is theoretically a navigable IP.  We have four cases.  The first
      // three are:
      // * If the user typed four distinct components, this is an IP for sure.
      // * If the user typed two or three components, this is almost certainly a
      //   query, especially for two components (as in "13.5/7.25"), but we'll
      //   allow navigation for an explicit scheme or trailing slash below.
      // * If the user typed one component, this is likely a query, but could be
      //   a non-dotted-quad version of an IP address.
      // Unfortunately, since we called CanonicalizeHost() on the
      // already-canonicalized host, all of these cases will have been changed
      // to have four components (e.g. 13.2 -> 13.0.0.2), so we have to call
      // CanonicalizeHost() again, this time on the original input, so that we
      // can get the correct number of IP components.
      //
      // The fourth case is that the user typed something ambiguous like ".1.2"
      // that fixup converted to an IP address ("1.0.0.2").  In this case the
      // call to CanonicalizeHost() will return NEUTRAL here.  Since it's not
      // clear what the user intended, we fall back to our other heuristics.
      net::CanonicalizeHost(base::UTF16ToUTF8(original_host), &host_info);
      if ((host_info.family == url::CanonHostInfo::IPV4) &&
          (host_info.num_ipv4_components == 4))
        return metrics::OmniboxInputType::URL;
    }

    // By this point, if we have an "IP" with first octet zero, we know it
    // wasn't "0.0.0.0", so mark it as non-navigable.
    if (host_info.address[0] == 0)
      return metrics::OmniboxInputType::QUERY;
  }

  // Now that we've ruled out all schemes other than http or https and done a
  // little more sanity checking, the presence of a scheme means this is likely
  // a URL.
  if (parts->scheme.is_nonempty())
    return metrics::OmniboxInputType::URL;

  // Check to see if the username is set and, if so, whether it contains a
  // space.  Usernames usually do not contain a space.  If a username contains
  // a space, that's likely an indication of incorrectly parsing of the input.
  const bool username_has_space =
      parts->username.is_nonempty() &&
      (text.substr(parts->username.begin, parts->username.len)
           .find_first_of(base::kWhitespaceUTF16) != std::u16string::npos);

  // Generally, trailing slashes force the input to be treated as a URL.
  // However, if the username has a space, this may be input like
  // "dep missing: @test/", which should not be parsed as a URL (with the
  // username "dep missing: ").
  if (parts->path.is_nonempty() && !username_has_space) {
    char16_t c = text[parts->path.end() - 1];
    if ((c == '\\') || (c == '/'))
      return metrics::OmniboxInputType::URL;
  }

  // Handle the cases we detected in the IPv4 code above as "almost certainly a
  // query" now that we know the user hasn't tried to force navigation via a
  // scheme/trailing slash.
  if ((host_info.family == url::CanonHostInfo::IPV4) &&
      (host_info.num_ipv4_components > 1))
    return metrics::OmniboxInputType::QUERY;

  // The URL did not have an explicit scheme and has an unusual-looking
  // username (with a space).  It's not likely to be a URL.
  if (username_has_space)
    return metrics::OmniboxInputType::UNKNOWN;

  // If there is more than one recognized non-host component, this is likely to
  // be a URL, even if the TLD is unknown (in which case this is likely an
  // intranet URL).
  if (NumNonHostComponents(*parts) > 1)
    return metrics::OmniboxInputType::URL;

  // If we reach here with a username, our input looks something like
  // "user@host".  Unless there is a desired TLD, we think this is more likely
  // an email address than an HTTP auth attempt, so we search by default.  (When
  // there _is_ a desired TLD, the user hit ctrl-enter, and we assume that
  // implies an attempted navigation.)
  if (canonicalized_url->has_username() && desired_tld.empty())
    return metrics::OmniboxInputType::UNKNOWN;

  // If the host has a known TLD or a port, it's probably a URL. Just localhost
  // is considered a valid host name due to https://tools.ietf.org/html/rfc6761.
  if (has_known_tld || canonicalized_url->DomainIs("localhost") ||
      canonicalized_url->has_port())
    return metrics::OmniboxInputType::URL;

  // The .example and .test TLDs are special-cased as known TLDs due to
  // https://tools.ietf.org/html/rfc6761. Unlike localhost, these are not valid
  // host names, so they must have at least one subdomain to be a URL.
  // .local is used for Multicast DNS in https://www.rfc-editor.org/rfc/rfc6762.
  // .internal is reserved from delegation for private-use applications; see
  // https://www.icann.org/en/board-activities-and-meetings/materials/approved-resolutions-special-meeting-of-the-icann-board-29-07-2024-en
  for (const std::string_view domain :
       {"example", "test", "local", "internal"}) {
    // The +1 accounts for a possible trailing period.
    if (canonicalized_url->DomainIs(domain) &&
        (canonicalized_url->GetHost().length() > (domain.length() + 1))) {
      return metrics::OmniboxInputType::URL;
    }
  }

  // No scheme, username, port, and no known TLD on the host.
  // This could be:
  // * A single word "foo"; possibly an intranet site, but more likely a search.
  //   This is ideally an UNKNOWN, and we can let the Alternate Nav URL code
  //   catch our mistakes.
  // * A URL with a valid TLD we don't know about yet.  If e.g. a registrar adds
  //   "xxx" as a TLD, then until we add it to our data file, Chrome won't know
  //   "foo.xxx" is a real URL.  So ideally this is a URL, but we can't really
  //   distinguish this case from:
  // * A "URL-like" string that's not really a URL (like
  //   "browser.tabs.closeButtons" or "java.awt.event.*").  This is ideally a
  //   QUERY.  Since this is indistinguishable from the case above, and this
  //   case is much more likely, claim these are UNKNOWN, which should default
  //   to the right thing and let users correct us on a case-by-case basis.
  return metrics::OmniboxInputType::UNKNOWN;
}

// static
void AutocompleteInput::ParseForEmphasizeComponents(
    std::u16string_view text,
    const AutocompleteSchemeClassifier& scheme_classifier,
    url::Component* scheme,
    url::Component* host) {
  url::Parsed parts;
  std::u16string scheme_str;
  Parse(text, std::string(), scheme_classifier, &parts, &scheme_str, nullptr);

  *scheme = parts.scheme;
  *host = parts.host;

  int after_scheme_and_colon = parts.scheme.end() + 1;
  // For the view-source and blob schemes, we should emphasize the host of the
  // URL qualified by the view-source or blob prefix.
  if ((base::EqualsCaseInsensitiveASCII(scheme_str, kViewSourceScheme) ||
       base::EqualsCaseInsensitiveASCII(scheme_str, url::kBlobScheme)) &&
      (static_cast<int>(text.length()) > after_scheme_and_colon)) {
    // Obtain the URL prefixed by view-source or blob and parse it.
    std::u16string_view real_url = text.substr(after_scheme_and_colon);
    url::Parsed real_parts;
    AutocompleteInput::Parse(real_url, std::string(), scheme_classifier,
                             &real_parts, nullptr, nullptr);
    if (real_parts.scheme.is_nonempty() || real_parts.host.is_nonempty()) {
      if (real_parts.scheme.is_nonempty()) {
        *scheme =
            url::Component(after_scheme_and_colon + real_parts.scheme.begin,
                           real_parts.scheme.len);
      } else {
        scheme->reset();
      }
      if (real_parts.host.is_nonempty()) {
        *host = url::Component(after_scheme_and_colon + real_parts.host.begin,
                               real_parts.host.len);
      } else {
        host->reset();
      }
    }
  } else if (base::EqualsCaseInsensitiveASCII(scheme_str,
                                              url::kFileSystemScheme) &&
             parts.inner_parsed() && parts.inner_parsed()->scheme.is_valid()) {
    *host = parts.inner_parsed()->host;
  }
}

// static
bool AutocompleteInput::ShouldUpgradeToHttps(
    const std::u16string& text,
    const GURL& url,
    int https_port_for_testing,
    bool use_fake_https_for_https_upgrade_testing,
    GURL* upgraded_url) {
  if (url::HostIsIPAddress(url.host()) ||
      net::IsHostnameNonUnique(url.host())) {
#if !BUILDFLAG(IS_IOS)
    // Never upgrade IP addresses or non-unique hostnames on non-iOS builds.
    return false;
#else
    // On iOS, tests use a loopback IP address instead of hostnames due to
    // platform limitations. Only allow them when running tests.
    if (!https_port_for_testing || !url::HostIsIPAddress(url.host())) {
      return false;
    }
#endif
  }

  if (url.scheme() == url::kHttpScheme &&
      !base::StartsWith(text, url::kHttpScheme16,
                        base::CompareCase::INSENSITIVE_ASCII) &&
      (url.port().empty() || https_port_for_testing)) {
    // Use HTTPS as the default scheme for URLs that are typed without a scheme.
    // Inputs of type UNKNOWN can still be valid URLs, but these will be mainly
    // intranet hosts which we don't to upgrade to HTTPS so we only check the
    // URL type here.
    // In particular, we don't want to upgrade these types of inputs:
    // - Non-unique hostnames such as intranet hosts
    // - Single word hostnames (these are most likely non-unique).
    // - IP addresses
    // - URLs with a specified port. If it's a non-standard HTTP port, we can't
    //   simply change the scheme to HTTPS and assume that these will load over
    //   HTTPS. URLs with HTTP port 80 get their port dropped so they will be
    //   upgraded (e.g. example.com:80 will load https://example.com).
    DCHECK_EQ(url::kHttpScheme, url.scheme());
    GURL::Replacements replacements;
#if !BUILDFLAG(IS_IOS)
    // We sometimes use a fake HTTPS server on iOS as we can't serve good HTTPS
    // from a test server. On all other platforms, we never use fake HTTPS
    // server.
    DCHECK(!use_fake_https_for_https_upgrade_testing);
#else
    // On iOS, use_fake_https_for_https_upgrade_testing should only be true if
    // https_port_for_testing is also true.
    DCHECK(!use_fake_https_for_https_upgrade_testing || https_port_for_testing);
#endif

    if (!use_fake_https_for_https_upgrade_testing) {
      replacements.SetSchemeStr(url::kHttpsScheme);
    }
    // This needs to be in scope when ReplaceComponents() is called:
    const std::string port_str = base::NumberToString(https_port_for_testing);
    if (https_port_for_testing) {
      // We'll only get here in tests.
#if BUILDFLAG(IS_IOS)
      if (url.port().empty()) {
        // On iOS, if the URL doesn't have a port, this is probably an
        // incomplete URL that's still being typed. Ignore.
        return false;
      }
#else
      // On other platforms, tests should always have a non-default port on the
      // input text.
      DCHECK(!url.port().empty());
#endif
      replacements.SetPortStr(port_str);
    }
    *upgraded_url = url.ReplaceComponents(replacements);
    return true;
  }

  return false;
}

// static
std::u16string AutocompleteInput::FormattedStringWithEquivalentMeaning(
    const GURL& url,
    const std::u16string& formatted_url,
    const AutocompleteSchemeClassifier& scheme_classifier,
    size_t* offset) {
  if (!url_formatter::CanStripTrailingSlash(url))
    return formatted_url;
  const std::u16string url_with_path(formatted_url + u'/');
  if (AutocompleteInput::Parse(formatted_url, std::string(), scheme_classifier,
                               nullptr, nullptr, nullptr) ==
      AutocompleteInput::Parse(url_with_path, std::string(), scheme_classifier,
                               nullptr, nullptr, nullptr)) {
    return formatted_url;
  }
  // If offset is past the addition, shift it.
  if (offset && *offset == formatted_url.size())
    ++(*offset);
  return url_with_path;
}

// static
int AutocompleteInput::NumNonHostComponents(const url::Parsed& parts) {
  int num_nonhost_components = 0;
  if (parts.scheme.is_nonempty())
    ++num_nonhost_components;
  if (parts.username.is_nonempty())
    ++num_nonhost_components;
  if (parts.password.is_nonempty())
    ++num_nonhost_components;
  if (parts.port.is_nonempty())
    ++num_nonhost_components;
  if (parts.path.is_nonempty())
    ++num_nonhost_components;
  if (parts.query.is_nonempty())
    ++num_nonhost_components;
  if (parts.ref.is_nonempty())
    ++num_nonhost_components;
  return num_nonhost_components;
}

// static
bool AutocompleteInput::HasHTTPScheme(const std::u16string& input) {
  return HasScheme(input, url::kHttpScheme);
}

// static
bool AutocompleteInput::HasHTTPSScheme(const std::u16string& input) {
  return HasScheme(input, url::kHttpsScheme);
}

// static
AutocompleteInput::FeaturedKeywordMode
AutocompleteInput::GetFeaturedKeywordMode(std::u16string_view text) {
  if (text == u"@")
    return FeaturedKeywordMode::kExact;
  if (text.starts_with(u'@'))
    return FeaturedKeywordMode::kPrefix;
  return FeaturedKeywordMode::kFalse;
}

// static
const TemplateURL* AutocompleteInput::GetSubstitutingTemplateURLForInput(
    const TemplateURLService* model,
    AutocompleteInput* input) {
  if (!input->allow_exact_keyword_match()) {
    return nullptr;
  }

  DCHECK(model);
  std::u16string keyword, remaining_input;
  if (!ExtractKeywordFromInput(*input, model, &keyword, &remaining_input)) {
    return nullptr;
  }

  const TemplateURL* template_url = model->GetTemplateURLForKeyword(keyword);
  if (template_url &&
      template_url->SupportsReplacement(model->search_terms_data())) {
    // Adjust cursor position iff it was set before, otherwise leave it as is.
    size_t cursor_position = std::u16string::npos;
    // The adjustment assumes that the keyword was stripped from the beginning
    // of the original input.
    if (input->cursor_position() != std::u16string::npos &&
        !remaining_input.empty() &&
        base::EndsWith(input->text(), remaining_input,
                       base::CompareCase::SENSITIVE)) {
      int offset = input->text().length() - input->cursor_position();
      // The cursor should never be past the last character or before the
      // first character.
      DCHECK_GE(offset, 0);
      DCHECK_LE(offset, static_cast<int>(input->text().length()));
      if (offset <= 0) {
        // Normalize the cursor to be exactly after the last character.
        cursor_position = remaining_input.length();
      } else {
        // If somehow the cursor was before the remaining text, set it to 0,
        // otherwise adjust it relative to the remaining text.
        cursor_position = offset > static_cast<int>(remaining_input.length())
                              ? 0u
                              : remaining_input.length() - offset;
      }
    }
    input->UpdateText(remaining_input, cursor_position, input->parts());
    return template_url;
  }

  return nullptr;
}

// static
bool AutocompleteInput::ExtractKeywordFromInput(
    const AutocompleteInput& input,
    const TemplateURLService* template_url_service,
    std::u16string* keyword,
    std::u16string* remaining_input) {
  if ((input.type() == metrics::OmniboxInputType::EMPTY)) {
    return false;
  }

  DCHECK(template_url_service);
  *keyword = CleanUserInputKeyword(
      template_url_service,
      SplitKeywordFromInput(input.text(), true, remaining_input));
  return !keyword->empty();
}

// static
std::u16string AutocompleteInput::SplitReplacementStringFromInput(
    std::u16string_view input,
    bool trim_leading_whitespace) {
  // The input may contain leading whitespace, strip it.
  std::u16string_view trimmed_input =
      base::TrimWhitespace(input, base::TRIM_LEADING);

  // And extract the replacement string.
  std::u16string remaining_input;
  SplitKeywordFromInput(trimmed_input, trim_leading_whitespace,
                        &remaining_input);
  return remaining_input;
}

// static
std::u16string AutocompleteInput::CleanUserInputKeyword(
    const TemplateURLService* template_url_service,
    const std::u16string& keyword) {
  DCHECK(template_url_service);
  std::u16string result(base::i18n::ToLower(keyword));
  base::TrimWhitespace(result, base::TRIM_ALL, &result);
  // If this keyword is found with no additional cleaning of input, return it.
  if (template_url_service->GetTemplateURLForKeyword(result) != nullptr) {
    return result;
  }

  // If keyword is not found, try removing a "http" or "https" scheme if any.
  url::Component scheme_component;
  if (url::ExtractScheme(result, &scheme_component)) {
    const std::u16string_view scheme = std::u16string_view(result).substr(
        scheme_component.begin, scheme_component.len);
    if (scheme == url::kHttpScheme16 || scheme == url::kHttpsScheme16) {
      // Remove the scheme and the trailing ':'.
      result.erase(0, scheme_component.end() + 1);
      if (template_url_service->GetTemplateURLForKeyword(result) != nullptr) {
        return result;
      }
      // Many schemes usually have "//" after them, so strip it too.
      constexpr std::u16string_view kAfterScheme(u"//");
      if (base::StartsWith(result, kAfterScheme)) {
        result.erase(0, kAfterScheme.length());
      }
      if (template_url_service->GetTemplateURLForKeyword(result) != nullptr) {
        return result;
      }
    }
  }

  // Remove leading "www.", if any, and again try to find a matching keyword.
  // The 'www.' stripping is done directly here instead of calling
  // url_formatter::StripWWW because we're not assuming that the keyword is a
  // hostname.
  static constexpr std::u16string_view kWww(u"www.");
  if (base::StartsWith(result, kWww, base::CompareCase::SENSITIVE)) {
    result.erase(0, kWww.length());
  }
  if (template_url_service->GetTemplateURLForKeyword(result) != nullptr) {
    return result;
  }

  // Remove trailing "/", if any.
  if (!result.empty() && result.back() == '/') {
    result.pop_back();
  }
  return result;
}

// static
std::u16string AutocompleteInput::SplitKeywordFromInput(
    std::u16string_view input,
    bool trim_leading_whitespace,
    std::u16string* remaining_input) {
  // Find end of first token.  The AutocompleteController has trimmed leading
  // whitespace, so we need not skip over that.
  const size_t first_white(input.find_first_of(base::kWhitespaceUTF16));
  DCHECK_NE(0U, first_white);
  if (first_white == std::u16string::npos) {
    return std::u16string(input);  // Only one token provided.
  }

  // Set |remaining_input| to everything after the first token.
  if (remaining_input != nullptr) {
    const size_t remaining_start =
        trim_leading_whitespace
            ? input.find_first_not_of(base::kWhitespaceUTF16, first_white)
            : first_white + 1;

    if (remaining_start < input.length()) {
      remaining_input->assign(input.begin() + remaining_start, input.end());
    }
  }

  // Return first token as keyword.
  return std::u16string(input.substr(0, first_white));
}

// static
std::u16string AutocompleteInput::SanitizeString(std::u16string_view text,
                                                 bool trim_whitespace) {
  // NOTE: This logic is mirrored by `sanitizeString()` in
  // omnibox_custom_bindings.js.
  std::u16string result;
  if (trim_whitespace) {
    text = base::TrimWhitespace(text, base::TRIM_ALL);
  }
  base::RemoveChars(text, kInvalidChars, &result);
  return result;
}
void AutocompleteInput::UpdateText(const std::u16string& text,
                                   size_t cursor_position,
                                   const url::Parsed& parts) {
  DCHECK(cursor_position <= text.length() ||
         cursor_position == std::u16string::npos)
      << "Text: '" << text << "', cp: " << cursor_position;
  text_ = text;
  cursor_position_ = cursor_position;
  parts_ = parts;
}

void AutocompleteInput::set_current_title(const std::u16string& title) {
  current_title_ = SanitizeString(title);
}

void AutocompleteInput::Clear() {
  text_.clear();
  cursor_position_ = std::u16string::npos;
  current_url_ = GURL();
  current_title_.clear();
  current_page_classification_ = metrics::OmniboxEventProto::INVALID_SPEC;
  type_ = metrics::OmniboxInputType::EMPTY;
  parts_ = url::Parsed();
  scheme_.clear();
  canonicalized_url_ = GURL();
  prevent_inline_autocomplete_ = false;
  allow_exact_keyword_match_ = false;
  in_keyword_mode_ = false;
  omit_asynchronous_matches_ = false;
  focus_type_ = metrics::OmniboxFocusType::INTERACTION_DEFAULT;
  terms_prefixed_by_http_or_https_.clear();
  lens_overlay_suggest_inputs_.reset();
  input_state_ = omnibox::InputState();
  suggest_inventory_ = omnibox::SuggestInventory::SUGGEST_INVENTORY_DEFAULT;
  https_port_for_testing_ = 0;
  use_fake_https_for_https_upgrade_testing_ = false;
  context_tab_title_.clear();
  context_tab_url_ = GURL();
  previous_query_.clear();
  input_method_.reset();
  has_previous_submitted_thread_context_ = false;
  has_auto_suggested_tab_ = false;
}

size_t AutocompleteInput::EstimateMemoryUsage() const {
  size_t res = 0;

  res += base::trace_event::EstimateMemoryUsage(text_);
  res += base::trace_event::EstimateMemoryUsage(current_url_);
  res += base::trace_event::EstimateMemoryUsage(current_title_);
  res += base::trace_event::EstimateMemoryUsage(scheme_);
  res += base::trace_event::EstimateMemoryUsage(canonicalized_url_);
  res += base::trace_event::EstimateMemoryUsage(desired_tld_);
  res +=
      base::trace_event::EstimateMemoryUsage(terms_prefixed_by_http_or_https_);
  res += base::trace_event::EstimateMemoryUsage(input_state_);
  res += base::trace_event::EstimateMemoryUsage(context_tab_title_);
  res += base::trace_event::EstimateMemoryUsage(context_tab_url_);
  res += base::trace_event::EstimateMemoryUsage(previous_query_);

  return res;
}

void AutocompleteInput::WriteIntoTrace(perfetto::TracedValue context) const {
  auto dict = std::move(context).WriteDictionary();
  dict.Add("text", text_);
}

bool AutocompleteInput::IsZeroSuggest() const {
  return focus_type_ != metrics::OmniboxFocusType::INTERACTION_DEFAULT;
}

AutocompleteInput::FeaturedKeywordMode
AutocompleteInput::GetFeaturedKeywordMode() const {
  return GetFeaturedKeywordMode(text_);
}
