// Copyright 2016 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/autofill/core/common/signatures.h"

#include <string_view>

#include "base/containers/span.h"
#include "base/hash/sha1.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "components/autofill/core/common/autofill_util.h"
#include "components/autofill/core/common/form_data.h"
#include "components/autofill/core/common/form_field_data.h"
#include "url/gurl.h"

namespace autofill {

namespace {

// Returns a copy of |input| without >= 5 consecutive digits.
std::string StripDigitsIfRequired(std::string_view input) {
  static constexpr auto IsDigit = base::IsAsciiDigit<char>;
  std::string result;
  result.reserve(input.size());

  for (size_t i = 0; i < input.size();) {
    // If `input[i]` is not a digit, append it to `result` and move to the next
    // character.
    if (!IsDigit(input[i])) {
      result.push_back(input[i]);
      ++i;
      continue;
    }

    // If `input[i]` is a digit, find the range of consecutive digits starting
    // at `i`. If this range is shorter than 5 characters append it to `result`.
    auto end_it = std::ranges::find_if_not(input.substr(i), IsDigit);
    std::string_view digits = std::string_view(input.begin() + i, end_it);
    DCHECK(std::ranges::all_of(digits, IsDigit));
    if (digits.size() < 5)
      base::StrAppend(&result, {digits});
    i += digits.size();
  }

  return result;
}

std::string CalculateAlternativeFormSignatureBase(const FormData& form_data) {
  std::string_view scheme = form_data.action().scheme();
  std::string_view host = form_data.action().host();

  // If target host or scheme is empty, set scheme and host of source url.
  // This is done to match the Toolbar's behavior.
  if (scheme.empty() || host.empty()) {
    scheme = form_data.url().scheme();
    host = form_data.url().host();
  }

  std::string form_signature_field_types;
  for (const FormFieldData& field : form_data.fields()) {
    switch (field.form_control_type()) {
      case mojom::FormControlType::kInputDate:
        break;
      case mojom::FormControlType::kContentEditable:
      case mojom::FormControlType::kInputEmail:
      case mojom::FormControlType::kInputMonth:
      case mojom::FormControlType::kInputNumber:
      case mojom::FormControlType::kInputPassword:
      case mojom::FormControlType::kInputSearch:
      case mojom::FormControlType::kInputTelephone:
      case mojom::FormControlType::kInputText:
      case mojom::FormControlType::kInputUrl:
      case mojom::FormControlType::kSelectOne:
      case mojom::FormControlType::kTextArea:
        // We use the string representation of the FormControlType because
        // changing the signature algorithm is non-trivial. If and when the
        // sectioning algorithm changes, we could use the raw FormControlType
        // enum instead.
        base::StrAppend(
            &form_signature_field_types,
            {"&", FormControlTypeToString(field.form_control_type())});
    }
  }

  return base::StrCat({scheme, "://", host, form_signature_field_types});
}

template <size_t N>
uint64_t PackBytes(base::span<const uint8_t, N> bytes) {
  static_assert(N <= 8u,
                "Error: Can't pack more than 8 bytes into a uint64_t.");
  uint64_t result = 0;
  for (auto byte : bytes)
    result = (result << 8) | byte;
  return result;
}

}  // namespace

// If a form name was set by Chrome, we should ignore it when calculating
// the form signature.
std::string GetDOMFormName(const std::string& form_name) {
#if BUILDFLAG(IS_IOS)
  // In case of an empty form name, the synthetic name is created. Ignore it.
  return (StartsWith(form_name, "gChrome~form~", base::CompareCase::SENSITIVE)
              ? std::string()
              : form_name);
#else
  return form_name;
#endif
}

FormSignature CalculateFormSignature(const FormData& form_data) {
  const GURL& target_url = form_data.action();
  const GURL& source_url = form_data.url();
  std::string_view scheme = target_url.scheme();
  std::string_view host = target_url.host();

  // If target host or scheme is empty, set scheme and host of source url.
  // This is done to match the Toolbar's behavior.
  if (scheme.empty() || host.empty()) {
    scheme = source_url.scheme();
    host = source_url.host();
  }

  std::string form_signature_field_names;

  for (const FormFieldData& field : form_data.fields()) {
    switch (field.form_control_type()) {
      case mojom::FormControlType::kInputDate:
        break;
      case mojom::FormControlType::kContentEditable:
      case mojom::FormControlType::kInputEmail:
      case mojom::FormControlType::kInputMonth:
      case mojom::FormControlType::kInputNumber:
      case mojom::FormControlType::kInputPassword:
      case mojom::FormControlType::kInputSearch:
      case mojom::FormControlType::kInputTelephone:
      case mojom::FormControlType::kInputText:
      case mojom::FormControlType::kInputUrl:
      case mojom::FormControlType::kSelectOne:
      case mojom::FormControlType::kTextArea:
        base::StrAppend(
            &form_signature_field_names,
            {"&", StripDigitsIfRequired(base::UTF16ToUTF8(field.name()))});
    }
  }

  std::string form_name = StripDigitsIfRequired(
      GetDOMFormName(base::UTF16ToUTF8(form_data.name())));
  std::string form_string = base::StrCat(
      {scheme, "://", host, "&", form_name, form_signature_field_names});
  return FormSignature(StrToHash64Bit(form_string));
}

FormSignature CalculateStructuralFormSignature(const FormData& form_data) {
  return FormSignature(
      StrToHash64Bit(CalculateAlternativeFormSignatureBase(form_data)));
}

FormSignature CalculateAlternativeFormSignature(const FormData& form_data) {
  std::string form_string = CalculateAlternativeFormSignatureBase(form_data);

  // Add more non-empty elements (one of path, reference, or query ordered by
  // preference) for small forms with 1-2 fields in order to prevent signature
  // collisions.
  if (form_data.fields().size() <= 2) {
    // Path piece includes the slash "/", so a non-empty path must have length
    // longer than 1.
    if (form_data.url().path().length() > 1) {
      base::StrAppend(&form_string, {form_data.url().path()});
    } else if (form_data.url().has_ref()) {
      base::StrAppend(&form_string, {"#", form_data.url().ref()});
    } else if (form_data.url().has_query()) {
      base::StrAppend(&form_string, {"?", form_data.url().query()});
    }
  }

  return FormSignature(StrToHash64Bit(form_string));
}

FieldSignature CalculateFieldSignatureByNameAndType(
    std::u16string_view field_name,
    FormControlType field_type) {
  return FieldSignature(
      StrToHash32Bit(base::StrCat({base::UTF16ToUTF8(field_name), "&",
                                   FormControlTypeToString(field_type)})));
}

FieldSignature CalculateFieldSignatureForField(
    const FormFieldData& field_data) {
  return CalculateFieldSignatureByNameAndType(field_data.name(),
                                              field_data.form_control_type());
}

uint64_t StrToHash64Bit(std::string_view str) {
  auto bytes = base::as_byte_span(str);
  const base::SHA1Digest digest = base::SHA1Hash(bytes);
  return PackBytes(base::span(digest).first<8>());
}

uint32_t StrToHash32Bit(std::string_view str) {
  auto bytes = base::as_byte_span(str);
  const base::SHA1Digest digest = base::SHA1Hash(bytes);
  return PackBytes(base::span(digest).first<4>());
}

int32_t StrToHash3Bit(std::string_view str) {
  const base::SHA1Digest digest = base::SHA1Hash(base::as_byte_span(str));
  // Keep only the first 3 bits of the SHA1 hash.
  return static_cast<int32_t>((digest[0] >> 5) & 0x07);
}

int32_t StrToHash3Bit(std::u16string_view str) {
  return StrToHash3Bit(base::UTF16ToUTF8(str));
}

int64_t HashFormSignature(FormSignature form_signature) {
  return static_cast<uint64_t>(form_signature.value()) % 1021;
}

int64_t HashFieldSignature(FieldSignature field_signature) {
  return static_cast<uint64_t>(field_signature.value()) % 1021;
}

}  // namespace autofill
