// Copyright 2024 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/url_pattern/url_pattern_util.h"

#include <ranges>
#include <string_view>

#include "base/compiler_specific.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "url/url_util.h"

namespace url_pattern {
namespace {

std::string StdStringFromCanonOutput(const url::CanonOutput& output,
                                     const url::Component& component) {
  return std::string(component.AsViewOn(output.view()));
}

}  // namespace

bool ContainsForbiddenHostnameCodePoint(std::string_view input,
                                        const bool allow_ipv6_delimiters) {
  // The full list of forbidden code points is defined at:
  //
  //  https://url.spec.whatwg.org/#forbidden-host-code-point
  //
  // We only check the code points the chromium URL parser incorrectly permits.
  // See: crbug.com/1065667#c18
  if (!allow_ipv6_delimiters) {
    return std::ranges::any_of(input, [](char c) {
      return c == ' ' || c == '#' || c == ':' || c == '<' || c == '>' ||
             c == '@' || c == '[' || c == ']' || c == '|';
    });
  } else {
    return std::ranges::any_of(input, [](char c) {
      return c == ' ' || c == '#' || c == '<' || c == '>' || c == '@' ||
             c == '|';
    });
  }
}

base::expected<std::string, absl::Status> ProtocolEncodeCallback(
    std::string_view input) {
  if (input.empty()) {
    return std::string();
  }

  url::RawCanonOutputT<char> canon_output;
  url::Component component;

  bool result = url::CanonicalizeScheme(input, &canon_output, &component);

  if (!result) {
    return base::unexpected(absl::InvalidArgumentError(
        base::StrCat({"Invalid protocol '", input, "'."})));
  }

  return StdStringFromCanonOutput(canon_output, component);
}

base::expected<std::string, absl::Status> UsernameEncodeCallback(
    std::string_view input) {
  if (input.empty()) {
    return std::string();
  }

  url::RawCanonOutputT<char> canon_output;
  url::Component username_component;
  url::Component password_component;

  bool result =
      url::CanonicalizeUserInfo(input, std::string_view(), &canon_output,
                                &username_component, &password_component);

  if (!result) {
    return base::unexpected(absl::InvalidArgumentError(
        base::StrCat({"Invalid username pattern '", input, "'."})));
  }

  return StdStringFromCanonOutput(canon_output, username_component);
}

base::expected<std::string, absl::Status> PasswordEncodeCallback(
    std::string_view input) {
  if (input.empty()) {
    return std::string();
  }

  url::RawCanonOutputT<char> canon_output;
  url::Component username_component;
  url::Component password_component;

  bool result =
      url::CanonicalizeUserInfo(std::string_view(), input, &canon_output,
                                &username_component, &password_component);

  if (!result) {
    return base::unexpected(absl::InvalidArgumentError(
        base::StrCat({"Invalid password pattern '", input, "'."})));
  }

  return StdStringFromCanonOutput(canon_output, password_component);
}

base::expected<std::string, absl::Status> IPv6HostnameEncodeCallback(
    std::string_view input) {
  std::string result;
  result.reserve(input.size());
  // This implements a light validation and canonicalization of IPv6 hostname
  // content.  Ideally we would use the URL parser's hostname canonicalizer
  // here, but that is too strict for the encoding callback.  The callback may
  // see only bits and pieces of the hostname pattern; e.g. for `[:address]` it
  // sees the `[` and `]` strings as separate calls.  Since the full URL
  // hostname parser wants to completely parse IPv6 hostnames, this will always
  // trigger an error.  Therefore, to allow pattern syntax within IPv6 brackets
  // we simply check for valid characters and lowercase any hex digits.
  for (size_t i = 0; i < input.size(); ++i) {
    char c = input[i];
    if (!base::IsHexDigit(c) && c != '[' && c != ']' && c != ':') {
      return base::unexpected(absl::InvalidArgumentError(
          base::StrCat({"Invalid IPv6 hostname character '",
                        std::string_view(&c, 1), "' in '", input, "'."})));
    }
    result += base::ToLowerASCII(c);
  }
  return result;
}

base::expected<std::string, absl::Status> HostnameEncodeCallback(
    std::string_view input) {
  if (input.empty()) {
    return std::string();
  }

  // Due to crbug.com/1065667 the url::CanonicalizeHost() call below will
  // permit and possibly encode some illegal code points.  Since we want
  // to ultimately fix that in the future we don't want to encourage more
  // use of these characters in URLPattern.  Therefore we apply an additional
  // restrictive check for these forbidden code points.
  //
  // TODO(crbug.com/40124263): Remove this check after the URL parser is fixed.
  if (ContainsForbiddenHostnameCodePoint(input)) {
    return base::unexpected(absl::InvalidArgumentError(
        base::StrCat({"Invalid hostname pattern '", input, "'."})));
  }

  url::RawCanonOutputT<char> canon_output;
  url::Component component;

  bool result = url::CanonicalizeHost(
      input, url::Component(0, base::checked_cast<int>(input.size())),
      &canon_output, &component);

  if (!result) {
    return base::unexpected(absl::InvalidArgumentError(
        base::StrCat({"Invalid hostname pattern '", input, "'."})));
  }

  return StdStringFromCanonOutput(canon_output, component);
}

base::expected<std::string, absl::Status> PortEncodeCallback(
    std::string_view input) {
  if (input.empty()) {
    return std::string();
  }

  url::RawCanonOutputT<char> canon_output;
  url::Component component;

  bool result = url::CanonicalizePort(input, url::PORT_UNSPECIFIED,
                                      &canon_output, &component);

  if (!result) {
    return base::unexpected(absl::InvalidArgumentError(
        base::StrCat({"Invalid port pattern '", input, "'."})));
  }

  return StdStringFromCanonOutput(canon_output, component);
}

base::expected<std::string, absl::Status> StandardURLPathnameEncodeCallback(
    std::string_view input) {
  if (input.empty()) {
    return std::string();
  }

  url::RawCanonOutputT<char> canon_output;
  url::Component component;

  bool result = url::CanonicalizePartialPath(input, &canon_output, &component);

  if (!result) {
    return base::unexpected(absl::InvalidArgumentError(
        base::StrCat({"Invalid pathname pattern '", input, "'."})));
  }

  return StdStringFromCanonOutput(canon_output, component);
}

base::expected<std::string, absl::Status> PathURLPathnameEncodeCallback(
    std::string_view input) {
  if (input.empty()) {
    return std::string();
  }

  url::RawCanonOutputT<char> canon_output;
  url::Component component;
  url::CanonicalizePathUrlPath(input, &canon_output, &component);

  return StdStringFromCanonOutput(canon_output, component);
}

base::expected<std::string, absl::Status> SearchEncodeCallback(
    std::string_view input) {
  if (input.empty()) {
    return std::string();
  }

  url::RawCanonOutputT<char> canon_output;
  url::Component component;

  url::CanonicalizeQuery(input, /*converter=*/nullptr, &canon_output,
                         &component);

  return StdStringFromCanonOutput(canon_output, component);
}

base::expected<std::string, absl::Status> HashEncodeCallback(
    std::string_view input) {
  if (input.empty()) {
    return std::string();
  }

  url::RawCanonOutputT<char> canon_output;
  url::Component component;

  url::CanonicalizeRef(input, &canon_output, &component);

  return StdStringFromCanonOutput(canon_output, component);
}

// Utility method to determine if a particular hostname pattern should be
// treated as an IPv6 hostname.  This implements a simple and fast heuristic
// looking for a leading `[`.  It is intended to catch the most common cases
// with minimum overhead.
bool TreatAsIPv6Hostname(std::string_view pattern_utf8) {
  // The `[` string cannot be a valid IPv6 hostname.  We need at least two
  // characters to represent `[*`.
  if (pattern_utf8.size() < 2) {
    return false;
  }

  if (pattern_utf8[0] == '[') {
    return true;
  }

  // We do a bit of extra work to detect brackets behind an escape and
  // within a grouping.
  if ((pattern_utf8[0] == '\\' || pattern_utf8[0] == '{') &&
      pattern_utf8[1] == '[') {
    return true;
  }

  return false;
}

}  // namespace url_pattern
