/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "IPv4Parser.h"
#include "mozilla/EndianUtils.h"
#include "nsPrintfCString.h"
#include "nsTArray.h"

namespace mozilla::net::IPv4Parser {

// https://url.spec.whatwg.org/#ends-in-a-number-checker
bool EndsInANumber(const nsCString& input) {
  // 1. Let parts be the result of strictly splitting input on U+002E (.).
  nsTArray<nsDependentCSubstring> parts;
  for (const nsDependentCSubstring& part : input.Split('.')) {
    parts.AppendElement(part);
  }

  if (parts.Length() == 0) {
    return false;
  }

  // 2.If the last item in parts is the empty string, then:
  //    1. If parts’s size is 1, then return false.
  //    2. Remove the last item from parts.
  if (parts.LastElement().IsEmpty()) {
    if (parts.Length() == 1) {
      return false;
    }
    (void)parts.PopLastElement();
  }

  // 3. Let last be the last item in parts.
  const nsDependentCSubstring& last = parts.LastElement();

  // 4. If last is non-empty and contains only ASCII digits, then return true.
  // The erroneous input "09" will be caught by the IPv4 parser at a later
  // stage.
  if (!last.IsEmpty()) {
    if (ContainsOnlyAsciiDigits(last)) {
      return true;
    }
  }

  // 5. If parsing last as an IPv4 number does not return failure, then return
  // true. This is equivalent to checking that last is "0X" or "0x", followed by
  // zero or more ASCII hex digits.
  if (StringBeginsWith(last, "0x"_ns) || StringBeginsWith(last, "0X"_ns)) {
    if (ContainsOnlyAsciiHexDigits(Substring(last, 2))) {
      return true;
    }
  }

  return false;
}

nsresult ParseIPv4Number10(const nsACString& input, uint32_t& number,
                           uint32_t maxNumber) {
  uint64_t value = 0;
  const char* current = input.BeginReading();
  const char* end = input.EndReading();
  for (; current < end; ++current) {
    char c = *current;
    MOZ_ASSERT(c >= '0' && c <= '9');
    value *= 10;
    value += c - '0';
    // Any IPv4 number must fit in 32 bits. Bail out as soon as we exceed
    // that to avoid silently wrapping the uint64_t accumulator.
    if (value > 0xffffffffu) {
      number = 0;
      return NS_ERROR_FAILURE;
    }
  }
  if (value <= maxNumber) {
    number = value;
    return NS_OK;
  }

  // The error case
  number = 0;
  return NS_ERROR_FAILURE;
}

nsresult ParseIPv4Number(const nsACString& input, int32_t base,
                         uint32_t& number, uint32_t maxNumber) {
  // Accumulate in the 64-bit value
  uint64_t value = 0;
  const char* current = input.BeginReading();
  const char* end = input.EndReading();
  switch (base) {
    case 16:
      ++current;
      [[fallthrough]];
    case 8:
      ++current;
      break;
    case 10:
    default:
      break;
  }
  for (; current < end; ++current) {
    value *= base;
    char c = *current;
    MOZ_ASSERT((base == 10 && IsAsciiDigit(c)) ||
               (base == 8 && c >= '0' && c <= '7') ||
               (base == 16 && IsAsciiHexDigit(c)));
    if (IsAsciiDigit(c)) {
      value += c - '0';
    } else if (c >= 'a' && c <= 'f') {
      value += c - 'a' + 10;
    } else if (c >= 'A' && c <= 'F') {
      value += c - 'A' + 10;
    }
    // Any IPv4 number must fit in 32 bits. Bail out as soon as we exceed
    // that to avoid silently wrapping the uint64_t accumulator.
    if (value > 0xffffffffu) {
      number = 0;
      return NS_ERROR_FAILURE;
    }
  }

  if (value <= maxNumber) {
    number = value;
    return NS_OK;
  }

  // The error case
  number = 0;
  return NS_ERROR_FAILURE;
}

// IPv4 parser spec: https://url.spec.whatwg.org/#concept-ipv4-parser
nsresult NormalizeIPv4(const nsACString& host, nsCString& result) {
  int32_t bases[4] = {10, 10, 10, 10};
  bool onlyBase10 = true;  // Track this as a special case
  int32_t dotIndex[3];     // The positions of the dots in the string

  // Use "length" rather than host.Length() after call to
  // ValidateIPv4Number because of potential trailing period.
  nsDependentCSubstring filteredHost;
  bool trailingDot = false;
  if (host.Length() > 0 && host.Last() == '.') {
    trailingDot = true;
    filteredHost.Rebind(host.BeginReading(), host.Length() - 1);
  } else {
    filteredHost.Rebind(host.BeginReading(), host.Length());
  }

  int32_t length = static_cast<int32_t>(filteredHost.Length());
  int32_t dotCount = ValidateIPv4Number(filteredHost, bases, dotIndex,
                                        onlyBase10, length, trailingDot);
  if (dotCount < 0 || length <= 0) {
    return NS_ERROR_FAILURE;
  }

  // Max values specified by the spec
  static const uint32_t upperBounds[] = {0xffffffffu, 0xffffffu, 0xffffu,
                                         0xffu};
  uint32_t ipv4;
  int32_t start = (dotCount > 0 ? dotIndex[dotCount - 1] + 1 : 0);

  // parse the last part first
  nsresult res;
  // Doing a special case for all items being base 10 gives ~35% speedup
  res = (onlyBase10
             ? ParseIPv4Number10(Substring(host, start, length - start), ipv4,
                                 upperBounds[dotCount])
             : ParseIPv4Number(Substring(host, start, length - start),
                               bases[dotCount], ipv4, upperBounds[dotCount]));
  if (NS_FAILED(res)) {
    return NS_ERROR_FAILURE;
  }

  // parse remaining parts starting from first part
  int32_t lastUsed = -1;
  for (int32_t i = 0; i < dotCount; i++) {
    uint32_t number;
    start = lastUsed + 1;
    lastUsed = dotIndex[i];
    res =
        (onlyBase10 ? ParseIPv4Number10(
                          Substring(host, start, lastUsed - start), number, 255)
                    : ParseIPv4Number(Substring(host, start, lastUsed - start),
                                      bases[i], number, 255));
    if (NS_FAILED(res)) {
      return NS_ERROR_FAILURE;
    }
    ipv4 += number << (8 * (3 - i));
  }

  // A special case for ipv4 URL like "127." should have the same result as
  // "127".
  if (dotCount == 1 && dotIndex[0] == length - 1) {
    ipv4 = (ipv4 & 0xff000000) >> 24;
  }

  uint8_t ipSegments[4];
  NetworkEndian::writeUint32(ipSegments, ipv4);
  result = nsPrintfCString("%d.%d.%d.%d", ipSegments[0], ipSegments[1],
                           ipSegments[2], ipSegments[3]);
  return NS_OK;
}

// Return the number of "dots" in the string, or -1 if invalid.  Note that the
// number of relevant entries in the bases/starts/ends arrays is number of
// dots + 1.
//
// length is assumed to be <= host.Length(); the caller is responsible for that
//
// Note that the value returned is guaranteed to be in [-1, 3] range.
int32_t ValidateIPv4Number(const nsACString& host, int32_t bases[4],
                           int32_t dotIndex[3], bool& onlyBase10,
                           int32_t length, bool trailingDot) {
  MOZ_ASSERT(length <= (int32_t)host.Length());
  if (length <= 0) {
    return -1;
  }

  bool lastWasNumber = false;  // We count on this being false for i == 0
  int32_t dotCount = 0;
  onlyBase10 = true;

  for (int32_t i = 0; i < length; i++) {
    char current = host[i];
    if (current == '.') {
      // A dot should not follow a dot, or be first - it can follow an x though.
      if (!(lastWasNumber ||
            (i >= 2 && (host[i - 1] == 'X' || host[i - 1] == 'x') &&
             host[i - 2] == '0')) ||
          (i == (length - 1) && trailingDot)) {
        return -1;
      }

      if (dotCount > 2) {
        return -1;
      }
      lastWasNumber = false;
      dotIndex[dotCount] = i;
      dotCount++;
    } else if (current == 'X' || current == 'x') {
      if (!lastWasNumber ||  // An X should not follow an X or a dot or be first
          i == (length - 1) ||  // No trailing Xs allowed
          (dotCount == 0 &&
           i != 1) ||            // If we had no dots, an X should be second
          host[i - 1] != '0' ||  // X should always follow a 0.  Guaranteed i >
                                 // 0 as lastWasNumber is true
          (dotCount > 0 &&
           host[i - 2] != '.')) {  // And that zero follows a dot if it exists
        return -1;
      }
      lastWasNumber = false;
      bases[dotCount] = 16;
      onlyBase10 = false;

    } else if (current == '0') {
      if (i < length - 1 &&      // Trailing zero doesn't signal octal
          host[i + 1] != '.' &&  // Lone zero is not octal
          (i == 0 || host[i - 1] == '.')) {  // Zero at start or following a dot
                                             // is a candidate for octal
        bases[dotCount] = 8;  // This will turn to 16 above if X shows up
        onlyBase10 = false;
      }
      lastWasNumber = true;

    } else if (current >= '1' && current <= '7') {
      lastWasNumber = true;

    } else if (current >= '8' && current <= '9') {
      if (bases[dotCount] == 8) {
        return -1;
      }
      lastWasNumber = true;

    } else if ((current >= 'a' && current <= 'f') ||
               (current >= 'A' && current <= 'F')) {
      if (bases[dotCount] != 16) {
        return -1;
      }
      lastWasNumber = true;

    } else {
      return -1;
    }
  }

  return dotCount;
}

bool ContainsOnlyAsciiDigits(const nsDependentCSubstring& input) {
  for (const auto* c = input.BeginReading(); c < input.EndReading(); c++) {
    if (!IsAsciiDigit(*c)) {
      return false;
    }
  }

  return true;
}

bool ContainsOnlyAsciiHexDigits(const nsDependentCSubstring& input) {
  for (const auto* c = input.BeginReading(); c < input.EndReading(); c++) {
    if (!IsAsciiHexDigit(*c)) {
      return false;
    }
  }
  return true;
}

}  // namespace mozilla::net::IPv4Parser
