/* 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 "double-conversion/double-conversion.h"
#include "mozilla/Assertions.h"
#include "mozilla/CheckedInt.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/Printf.h"
#include "mozilla/ResultExtensions.h"

#include <iterator>
#include "fmt/format.h"
#include "fmt/xchar.h"

#include "nsASCIIMask.h"
#include "nsCharTraits.h"
#include "nsISupports.h"
#include "nsString.h"
#include "nsTArray.h"

// It's not worthwhile to reallocate the buffer and memcpy the
// contents over when the size difference isn't large. With
// power-of-two allocation buckets and 64 as the typical inline
// capacity, considering that above 1000 there performance aspects
// of realloc and memcpy seem to be absorbed, relative to the old
// code, by the performance benefits of the new code being exact,
// we need to choose which transitions of 256 to 128, 512 to 256
// and 1024 to 512 to allow. As a guess, let's pick the middle
// one as the the largest potential transition that we forgo. So
// we'll shrink from 1024 bucket to 512 bucket but not from 512
// bucket to 256 bucket. We'll decide by comparing the difference
// of capacities. As bucket differences, the differences are 256
// and 512. Since the capacities have various overheads, we
// can't compare with 256 or 512 exactly but it's easier to
// compare to some number that's between the two, so it's
// far away from either to ignore the overheads.
const uint32_t kNsStringBufferShrinkingThreshold = 384;

using double_conversion::DoubleToStringConverter;

/**
 * helper function for down-casting a nsTSubstring to an nsTAutoString.
 */
template <typename T>
inline const nsTAutoString<T>* AsAutoString(const nsTSubstring<T>* aStr) {
  return static_cast<const nsTAutoString<T>*>(aStr);
}

template <typename T>
mozilla::Result<mozilla::BulkWriteHandle<T>, nsresult>
nsTSubstring<T>::BulkWrite(size_type aCapacity, size_type aPrefixToPreserve,
                           bool aAllowShrinking) {
  auto r = StartBulkWriteImpl(aCapacity, aPrefixToPreserve, aAllowShrinking);
  if (MOZ_UNLIKELY(r.isErr())) {
    return r.propagateErr();
  }
  return mozilla::BulkWriteHandle<T>(this, r.unwrap());
}

template <typename T>
auto nsTSubstring<T>::StartBulkWriteImpl(size_type aCapacity,
                                         size_type aPrefixToPreserve,
                                         bool aAllowShrinking,
                                         size_type aSuffixLength,
                                         size_type aOldSuffixStart,
                                         size_type aNewSuffixStart)
    -> mozilla::Result<size_type, nsresult> {
  // Note! Capacity does not include room for the terminating null char.

  MOZ_ASSERT(aPrefixToPreserve <= aCapacity,
             "Requested preservation of an overlong prefix.");
  MOZ_ASSERT(aNewSuffixStart + aSuffixLength <= aCapacity,
             "Requesed move of suffix to out-of-bounds location.");
  // Can't assert aOldSuffixStart, because mLength may not be valid anymore,
  // since this method allows itself to be called more than once.

  // If zero capacity is requested, set the string to the special empty
  // string.
  if (MOZ_UNLIKELY(!aCapacity)) {
    Finalize();
    SetToEmptyBuffer();
    return 0;
  }

  // Note! Capacity() returns 0 when the string is immutable.
  const size_type curCapacity = Capacity();

  bool shrinking = false;

  // We've established that aCapacity > 0.
  // |curCapacity == 0| means that the buffer is immutable or 0-sized, so we
  // need to allocate a new buffer. We cannot use the existing buffer even
  // though it might be large enough.

  if (aCapacity <= curCapacity) {
    if (aAllowShrinking) {
      shrinking = true;
    } else {
      char_traits::move(this->mData + aNewSuffixStart,
                        this->mData + aOldSuffixStart, aSuffixLength);
      if (aSuffixLength) {
        char_traits::uninitialize(this->mData + aPrefixToPreserve,
                                  XPCOM_MIN(aNewSuffixStart - aPrefixToPreserve,
                                            kNsStringBufferMaxPoison));
        char_traits::uninitialize(
            this->mData + aNewSuffixStart + aSuffixLength,
            XPCOM_MIN(curCapacity + 1 - aNewSuffixStart - aSuffixLength,
                      kNsStringBufferMaxPoison));
      } else {
        char_traits::uninitialize(this->mData + aPrefixToPreserve,
                                  XPCOM_MIN(curCapacity + 1 - aPrefixToPreserve,
                                            kNsStringBufferMaxPoison));
      }
      return curCapacity;
    }
  }

  char_type* oldData = this->mData;
  DataFlags oldFlags = this->mDataFlags;

  char_type* newData;
  DataFlags newDataFlags;
  size_type newCapacity;

  // If this is an nsTAutoStringN, it's possible that we can use the inline
  // buffer.
  if ((this->mClassFlags & ClassFlags::INLINE) &&
      (aCapacity <= AsAutoString(this)->mInlineCapacity)) {
    newCapacity = AsAutoString(this)->mInlineCapacity;
    newData = (char_type*)AsAutoString(this)->mStorage;
    newDataFlags = DataFlags::TERMINATED | DataFlags::INLINE;
  } else {
    // If |aCapacity > kMaxCapacity|, then our doubling algorithm may not be
    // able to allocate it.  Just bail out in cases like that.  We don't want
    // to be allocating 2GB+ strings anyway.
    static_assert((sizeof(mozilla::StringBuffer) & 0x1) == 0,
                  "bad size for mozilla::StringBuffer");
    if (MOZ_UNLIKELY(!this->CheckCapacity(aCapacity))) {
      return mozilla::Err(NS_ERROR_OUT_OF_MEMORY);
    }

    // We increase our capacity so that the allocated buffer grows
    // exponentially, which gives us amortized O(1) appending. Below the
    // threshold, we use powers-of-two. Above the threshold, we grow by at
    // least 1.125, rounding up to the nearest MiB.
    const size_type slowGrowthThreshold = 8 * 1024 * 1024;

    // mozilla::StringBuffer allocates sizeof(mozilla::StringBuffer) + passed
    // size, and storageSize below wants extra 1 * sizeof(char_type).
    const size_type neededExtraSpace =
        sizeof(mozilla::StringBuffer) / sizeof(char_type) + 1;

    size_type temp;
    if (aCapacity >= slowGrowthThreshold) {
      size_type minNewCapacity =
          curCapacity + (curCapacity >> 3);  // multiply by 1.125
      temp = XPCOM_MAX(aCapacity, minNewCapacity) + neededExtraSpace;

      // Round up to the next multiple of MiB, but ensure the expected
      // capacity doesn't include the extra space required by
      // mozilla::StringBuffer and null-termination.
      const size_t MiB = 1 << 20;
      temp = (MiB * ((temp + MiB - 1) / MiB)) - neededExtraSpace;
    } else {
      // Round up to the next power of two.
      temp =
          mozilla::RoundUpPow2(aCapacity + neededExtraSpace) - neededExtraSpace;
    }

    newCapacity = XPCOM_MIN(temp, base_string_type::kMaxCapacity);
    MOZ_ASSERT(newCapacity >= aCapacity,
               "should have hit the early return at the top");
    // Avoid shrinking if the new buffer size is close to the old. Note that
    // unsigned underflow is defined behavior.
    if ((curCapacity - newCapacity) <= kNsStringBufferShrinkingThreshold &&
        (oldFlags & DataFlags::OWNED)) {
      MOZ_ASSERT(aAllowShrinking, "How come we didn't return earlier?");
      // We're already close enough to the right size.
      newData = oldData;
      newCapacity = curCapacity;
      newDataFlags = oldFlags;
    } else {
      size_type storageSize = (newCapacity + 1) * sizeof(char_type);
      // Since we allocate only by powers of 2 we always fit into a full
      // mozjemalloc bucket, it's not useful to use realloc, which may spend
      // time uselessly copying too much.
      mozilla::StringBuffer* newHdr =
          mozilla::StringBuffer::Alloc(storageSize).take();
      if (newHdr) {
        newData = (char_type*)newHdr->Data();
        newDataFlags =
            DataFlags::TERMINATED | DataFlags::STRINGBUFFER | DataFlags::OWNED;
      } else if (shrinking) {
        // We're still in a consistent state.
        //
        // Since shrinking is just a memory footprint optimization, we
        // don't propagate OOM if we tried to shrink in order to avoid
        // OOM crashes from infallible callers. If we're lucky, soon enough
        // a fallible caller reaches OOM and is able to deal or we end up
        // disposing of this string before reaching OOM again.
        newData = oldData;
        newCapacity = curCapacity;
        newDataFlags = oldFlags;
      } else {
        return mozilla::Err(NS_ERROR_OUT_OF_MEMORY);
      }
    }
  }

  this->mData = newData;
  this->mDataFlags = newDataFlags;

  if (oldData == newData) {
    char_traits::move(newData + aNewSuffixStart, oldData + aOldSuffixStart,
                      aSuffixLength);
    if (aSuffixLength) {
      char_traits::uninitialize(this->mData + aPrefixToPreserve,
                                XPCOM_MIN(aNewSuffixStart - aPrefixToPreserve,
                                          kNsStringBufferMaxPoison));
      char_traits::uninitialize(
          this->mData + aNewSuffixStart + aSuffixLength,
          XPCOM_MIN(newCapacity + 1 - aNewSuffixStart - aSuffixLength,
                    kNsStringBufferMaxPoison));
    } else {
      char_traits::uninitialize(this->mData + aPrefixToPreserve,
                                XPCOM_MIN(newCapacity + 1 - aPrefixToPreserve,
                                          kNsStringBufferMaxPoison));
    }
  } else {
    char_traits::copy(newData, oldData, aPrefixToPreserve);
    char_traits::copy(newData + aNewSuffixStart, oldData + aOldSuffixStart,
                      aSuffixLength);
    ReleaseData(oldData, oldFlags);
  }

  return newCapacity;
}

template <typename T>
void nsTSubstring<T>::FinishBulkWriteImpl(size_type aLength) {
  if (aLength) {
    FinishBulkWriteImplImpl(aLength);
  } else {
    Finalize();
    SetToEmptyBuffer();
  }
  AssertValid();
}

template <typename T>
bool nsTSubstring<T>::ReplacePrep(index_type aCutStart, size_type aCutLength,
                                  size_type aNewLength) {
  aCutLength = XPCOM_MIN(aCutLength, this->mLength - aCutStart);

  mozilla::CheckedInt<size_type> newTotalLen = this->Length();
  newTotalLen += aNewLength;
  newTotalLen -= aCutLength;
  if (!newTotalLen.isValid()) {
    return false;
  }

  if (aCutStart == this->mLength && Capacity() > newTotalLen.value()) {
    this->mDataFlags &= ~DataFlags::VOIDED;
    this->mData[newTotalLen.value()] = char_type(0);
    this->mLength = newTotalLen.value();
    return true;
  }

  return ReplacePrepInternal(aCutStart, aCutLength, aNewLength,
                             newTotalLen.value());
}

template <typename T>
bool nsTSubstring<T>::ReplacePrepInternal(index_type aCutStart,
                                          size_type aCutLen, size_type aFragLen,
                                          size_type aNewLen) {
  size_type newSuffixStart = aCutStart + aFragLen;
  size_type oldSuffixStart = aCutStart + aCutLen;
  size_type suffixLength = this->mLength - oldSuffixStart;

  mozilla::Result<size_type, nsresult> r = StartBulkWriteImpl(
      aNewLen, aCutStart, false, suffixLength, oldSuffixStart, newSuffixStart);
  if (r.isErr()) {
    return false;
  }
  FinishBulkWriteImpl(aNewLen);
  return true;
}

template <typename T>
typename nsTSubstring<T>::size_type nsTSubstring<T>::Capacity() const {
  // return 0 to indicate an immutable or 0-sized buffer

  size_type capacity;
  const auto dataFlags = this->mDataFlags;
  if (dataFlags & DataFlags::STRINGBUFFER) {
    // if the string is readonly, then we pretend that it has no capacity.
    mozilla::StringBuffer* hdr = mozilla::StringBuffer::FromData(this->mData);
    if (!(dataFlags & DataFlags::OWNED) || hdr->IsReadonly()) {
      capacity = 0;
    } else {
      capacity = (size_t(hdr->StorageSize()) / sizeof(char_type)) - 1;
    }
  } else if (dataFlags & DataFlags::INLINE) {
    MOZ_ASSERT(this->mClassFlags & ClassFlags::INLINE);
    capacity = AsAutoString(this)->mInlineCapacity;
  } else if (dataFlags & DataFlags::OWNED) {
    // we don't store the capacity of an adopted buffer because that would
    // require an additional member field.  the best we can do is base the
    // capacity on our length.  remains to be seen if this is the right
    // trade-off.
    capacity = this->mLength;
  } else {
    capacity = 0;
  }

  return capacity;
}

template <typename T>
bool nsTSubstring<T>::EnsureMutable(size_type aNewLen) {
  if (aNewLen == size_type(-1) || aNewLen == this->mLength) {
    // If the buffer is owned or inline, and is not a shared string buffer, we
    // can immediately return.
    if (this->mDataFlags & (DataFlags::INLINE | DataFlags::OWNED) &&
        (!(this->mDataFlags & DataFlags::STRINGBUFFER) ||
         !mozilla::StringBuffer::FromData(this->mData)->IsReadonly())) {
      return true;
    }
    aNewLen = this->mLength;
  }
  return SetLength(aNewLen, mozilla::fallible);
}

// ---------------------------------------------------------------------------

// This version of Assign is optimized for single-character assignment.
template <typename T>
void nsTSubstring<T>::Assign(char_type aChar) {
  if (MOZ_UNLIKELY(!Assign(aChar, mozilla::fallible))) {
    AllocFailed(1);
  }
}

template <typename T>
bool nsTSubstring<T>::Assign(char_type aChar, const fallible_t&) {
  auto r = StartBulkWriteImpl(1, 0, true);
  if (MOZ_UNLIKELY(r.isErr())) {
    return false;
  }
  *this->mData = aChar;
  FinishBulkWriteImpl(1);
  return true;
}

template <typename T>
void nsTSubstring<T>::Assign(const char_type* aData, size_type aLength) {
  if (MOZ_UNLIKELY(!Assign(aData, aLength, mozilla::fallible))) {
    AllocFailed(aLength == size_type(-1) ? char_traits::length(aData)
                                         : aLength);
  }
}

template <typename T>
bool nsTSubstring<T>::Assign(const char_type* aData,
                             const fallible_t& aFallible) {
  return Assign(aData, size_type(-1), aFallible);
}

template <typename T>
bool nsTSubstring<T>::Assign(const char_type* aData, size_type aLength,
                             const fallible_t& aFallible) {
  if (!aData || aLength == 0) {
    Truncate();
    return true;
  }

  if (MOZ_UNLIKELY(aLength == size_type(-1))) {
    aLength = char_traits::length(aData);
  }

  if (MOZ_UNLIKELY(this->IsDependentOn(aData, aData + aLength))) {
    return Assign(string_type(aData, aLength), aFallible);
  }

  auto r = StartBulkWriteImpl(aLength, 0, true);
  if (MOZ_UNLIKELY(r.isErr())) {
    return false;
  }
  char_traits::copy(this->mData, aData, aLength);
  FinishBulkWriteImpl(aLength);
  return true;
}

template <typename T>
void nsTSubstring<T>::AssignASCII(const char* aData, size_type aLength) {
  if (MOZ_UNLIKELY(!AssignASCII(aData, aLength, mozilla::fallible))) {
    AllocFailed(aLength);
  }
}

template <typename T>
void nsTSubstring<T>::AssignASCII(const nsLiteralCString& aData) {
  AssignASCII(aData.get(), aData.Length());
}

template <typename T>
bool nsTSubstring<T>::AssignASCII(const char* aData, size_type aLength,
                                  const fallible_t& aFallible) {
  MOZ_ASSERT(aLength != size_type(-1));

  // A Unicode string can't depend on an ASCII string buffer,
  // so this dependence check only applies to CStrings.
  if constexpr (std::is_same_v<T, char>) {
    if (this->IsDependentOn(aData, aData + aLength)) {
      return Assign(string_type(aData, aLength), aFallible);
    }
  }

  auto r = StartBulkWriteImpl(aLength, 0, true);
  if (MOZ_UNLIKELY(r.isErr())) {
    return false;
  }
  char_traits::copyASCII(this->mData, aData, aLength);
  FinishBulkWriteImpl(aLength);
  return true;
}

template <typename T>
void nsTSubstring<T>::AssignLiteral(const char_type* aData, size_type aLength) {
  Finalize();
  SetData(const_cast<char_type*>(aData), aLength,
          DataFlags::TERMINATED | DataFlags::LITERAL);
}

template <typename T>
void nsTSubstring<T>::Assign(const self_type& aStr) {
  if (!Assign(aStr, mozilla::fallible)) {
    AllocFailed(aStr.Length());
  }
}

template <typename T>
bool nsTSubstring<T>::Assign(const self_type& aStr,
                             const fallible_t& aFallible) {
  // |aStr| could be shareable. We need to check its flags to know how to
  // deal with it.

  if (&aStr == this) [[unlikely]] {
    return true;
  }

  if (!aStr.mLength) {
    Truncate();
    this->mDataFlags |= aStr.mDataFlags & DataFlags::VOIDED;
    return true;
  }

  if (aStr.mDataFlags & DataFlags::STRINGBUFFER) {
    // nice! we can avoid a string copy :-)

    // |aStr| should be null-terminated
    NS_ASSERTION(aStr.mDataFlags & DataFlags::TERMINATED,
                 "shared, but not terminated");

    Finalize();

    SetData(aStr.mData, aStr.mLength,
            DataFlags::TERMINATED | DataFlags::STRINGBUFFER | DataFlags::OWNED);

    // get an owning reference to the this->mData
    mozilla::StringBuffer::FromData(this->mData)->AddRef();
    return true;
  }
  if (aStr.mDataFlags & DataFlags::LITERAL) {
    MOZ_ASSERT(aStr.mDataFlags & DataFlags::TERMINATED, "Unterminated literal");

    AssignLiteral(aStr.mData, aStr.mLength);
    return true;
  }

  // else, treat this like an ordinary assignment.
  return Assign(aStr.Data(), aStr.Length(), aFallible);
}

template <typename T>
void nsTSubstring<T>::Assign(self_type&& aStr) {
  if (!Assign(std::move(aStr), mozilla::fallible)) {
    AllocFailed(aStr.Length());
  }
}

template <typename T>
void nsTSubstring<T>::AssignOwned(self_type&& aStr) {
  MOZ_ASSERT(aStr.mDataFlags & DataFlags::OWNED);
  // If they have an OWNED buffer, we can avoid a copy - so steal their buffer
  // and reset them to the empty string.

  // |aStr| should be null-terminated
  MOZ_ASSERT(aStr.mDataFlags & DataFlags::TERMINATED,
             "shared or owned, but not terminated");

  Finalize();

  SetData(aStr.mData, aStr.mLength, aStr.mDataFlags);
  aStr.SetToEmptyBuffer();
}

template <typename T>
bool nsTSubstring<T>::Assign(self_type&& aStr, const fallible_t& aFallible) {
  // We're moving |aStr| in this method, so we need to try to steal the data,
  // and in the fallback perform a copy-assignment followed by a truncation of
  // the original string.

  if (&aStr == this) {
    NS_WARNING("Move assigning a string to itself?");
    return true;
  }

  if (aStr.mDataFlags & DataFlags::OWNED) {
    AssignOwned(std::move(aStr));
    return true;
  }

  // Otherwise treat this as a normal assignment, and truncate the moved string.
  // We don't truncate the source string if the allocation failed.
  if (!Assign(aStr, aFallible)) {
    return false;
  }
  aStr.Truncate();
  return true;
}

template <typename T>
void nsTSubstring<T>::Assign(const substring_tuple_type& aTuple) {
  if (!Assign(aTuple, mozilla::fallible)) {
    AllocFailed(aTuple.Length());
  }
}

template <typename T>
bool nsTSubstring<T>::AssignNonDependent(const substring_tuple_type& aTuple,
                                         size_type aTupleLength,
                                         const mozilla::fallible_t& aFallible) {
  NS_ASSERTION(aTuple.Length() == aTupleLength, "wrong length passed");

  auto r = StartBulkWriteImpl(aTupleLength);
  if (r.isErr()) {
    return false;
  }

  aTuple.WriteTo(this->mData, aTupleLength);

  FinishBulkWriteImpl(aTupleLength);
  return true;
}

template <typename T>
bool nsTSubstring<T>::Assign(const substring_tuple_type& aTuple,
                             const fallible_t& aFallible) {
  const auto [isDependentOnThis, tupleLength] =
      aTuple.IsDependentOnWithLength(this->mData, this->mData + this->mLength);
  if (isDependentOnThis) {
    string_type temp;
    self_type& tempSubstring = temp;
    if (!tempSubstring.AssignNonDependent(aTuple, tupleLength, aFallible)) {
      return false;
    }
    AssignOwned(std::move(temp));
    return true;
  }

  return AssignNonDependent(aTuple, tupleLength, aFallible);
}

template <typename T>
void nsTSubstring<T>::Adopt(char_type* aData, size_type aLength) {
  if (aData) {
    Finalize();

    if (aLength == size_type(-1)) {
      aLength = char_traits::length(aData);
    }

    SetData(aData, aLength, DataFlags::TERMINATED | DataFlags::OWNED);

    // Treat this as construction of a "StringAdopt" object for leak
    // tracking purposes.
    MOZ_LOG_CTOR(this->mData, "StringAdopt", 1);
  } else {
    SetIsVoid(true);
  }
}

// This version of Replace is optimized for single-character replacement.
template <typename T>
void nsTSubstring<T>::Replace(index_type aCutStart, size_type aCutLength,
                              char_type aChar) {
  aCutStart = XPCOM_MIN(aCutStart, this->Length());

  if (ReplacePrep(aCutStart, aCutLength, 1)) {
    this->mData[aCutStart] = aChar;
  }
}

template <typename T>
bool nsTSubstring<T>::Replace(index_type aCutStart, size_type aCutLength,
                              char_type aChar, const fallible_t&) {
  aCutStart = XPCOM_MIN(aCutStart, this->Length());

  if (!ReplacePrep(aCutStart, aCutLength, 1)) {
    return false;
  }

  this->mData[aCutStart] = aChar;

  return true;
}

template <typename T>
void nsTSubstring<T>::Replace(index_type aCutStart, size_type aCutLength,
                              const char_type* aData, size_type aLength) {
  if (!Replace(aCutStart, aCutLength, aData, aLength, mozilla::fallible)) {
    AllocFailed(this->Length() - aCutLength + 1);
  }
}

template <typename T>
bool nsTSubstring<T>::Replace(index_type aCutStart, size_type aCutLength,
                              const char_type* aData, size_type aLength,
                              const fallible_t& aFallible) {
  // unfortunately, some callers pass null :-(
  if (!aData) {
    aLength = 0;
  } else {
    if (aLength == size_type(-1)) {
      aLength = char_traits::length(aData);
    }

    if (this->IsDependentOn(aData, aData + aLength)) {
      nsTAutoString<T> temp(aData, aLength);
      return Replace(aCutStart, aCutLength, temp, aFallible);
    }
  }

  aCutStart = XPCOM_MIN(aCutStart, this->Length());

  bool ok = ReplacePrep(aCutStart, aCutLength, aLength);
  if (!ok) {
    return false;
  }

  if (aLength > 0) {
    char_traits::copy(this->mData + aCutStart, aData, aLength);
  }

  return true;
}

template <typename T>
void nsTSubstring<T>::Replace(index_type aCutStart, size_type aCutLength,
                              const substring_tuple_type& aTuple) {
  if (!Replace(aCutStart, aCutLength, aTuple, mozilla::fallible)) {
    AllocFailed(this->Length() - aCutLength + aTuple.Length());
  }
}

template <typename T>
bool nsTSubstring<T>::Replace(index_type aCutStart, size_type aCutLength,
                              const substring_tuple_type& aTuple,
                              const fallible_t& aFallible) {
  const auto [isDependentOnThis, tupleLength] =
      aTuple.IsDependentOnWithLength(this->mData, this->mData + this->mLength);

  if (isDependentOnThis) {
    nsTAutoString<T> temp;
    if (!temp.AssignNonDependent(aTuple, tupleLength, mozilla::fallible)) {
      return false;
    }
    return Replace(aCutStart, aCutLength, temp, aFallible);
  }

  aCutStart = XPCOM_MIN(aCutStart, this->Length());

  if (!ReplacePrep(aCutStart, aCutLength, tupleLength)) {
    return false;
  }

  if (tupleLength > 0) {
    aTuple.WriteTo(this->mData + aCutStart, tupleLength);
  }
  return true;
}

template <typename T>
void nsTSubstring<T>::ReplaceLiteral(index_type aCutStart, size_type aCutLength,
                                     const char_type* aData,
                                     size_type aLength) {
  aCutStart = XPCOM_MIN(aCutStart, this->Length());

  if (!aCutStart && aCutLength == this->Length() &&
      !(this->mDataFlags & DataFlags::OWNED)) {
    // Check for OWNED to avoid undoing the effect of SetCapacity().
    AssignLiteral(aData, aLength);
  } else if (ReplacePrep(aCutStart, aCutLength, aLength) && aLength > 0) {
    char_traits::copy(this->mData + aCutStart, aData, aLength);
  }
}

template <typename T>
void nsTSubstring<T>::Append(char_type aChar) {
  if (MOZ_UNLIKELY(!Append(aChar, mozilla::fallible))) {
    AllocFailed(this->mLength + 1);
  }
}

template <typename T>
bool nsTSubstring<T>::Append(char_type aChar, const fallible_t& aFallible) {
  size_type oldLen = this->mLength;
  size_type newLen = oldLen + 1;  // Can't overflow
  auto r = StartBulkWriteImpl(newLen, oldLen, false);
  if (MOZ_UNLIKELY(r.isErr())) {
    return false;
  }
  this->mData[oldLen] = aChar;
  FinishBulkWriteImpl(newLen);
  return true;
}

template <typename T>
void nsTSubstring<T>::Append(const char_type* aData, size_type aLength) {
  if (MOZ_UNLIKELY(!Append(aData, aLength, mozilla::fallible))) {
    AllocFailed(this->mLength + (aLength == size_type(-1)
                                     ? char_traits::length(aData)
                                     : aLength));
  }
}

template <typename T>
bool nsTSubstring<T>::Append(const char_type* aData, size_type aLength,
                             const fallible_t& aFallible) {
  if (MOZ_UNLIKELY(aLength == size_type(-1))) {
    aLength = char_traits::length(aData);
  }

  if (MOZ_UNLIKELY(!aLength)) {
    // Avoid undoing the effect of SetCapacity() if both
    // mLength and aLength are zero.
    return true;
  }

  if (MOZ_UNLIKELY(this->IsDependentOn(aData, aData + aLength))) {
    return Append(string_type(aData, aLength), mozilla::fallible);
  }
  size_type oldLen = this->mLength;
  mozilla::CheckedInt<size_type> newLen(oldLen);
  newLen += aLength;
  if (MOZ_UNLIKELY(!newLen.isValid())) {
    return false;
  }
  auto r = StartBulkWriteImpl(newLen.value(), oldLen, false);
  if (MOZ_UNLIKELY(r.isErr())) {
    return false;
  }
  char_traits::copy(this->mData + oldLen, aData, aLength);
  FinishBulkWriteImpl(newLen.value());
  return true;
}

template <typename T>
void nsTSubstring<T>::AppendASCII(const char* aData, size_type aLength) {
  if (MOZ_UNLIKELY(!AppendASCII(aData, aLength, mozilla::fallible))) {
    AllocFailed(this->mLength +
                (aLength == size_type(-1) ? strlen(aData) : aLength));
  }
}

template <typename T>
void nsTSubstring<T>::AppendASCII(const nsLiteralCString& aData) {
  AppendASCII(aData.get(), aData.Length());
}

template <typename T>
bool nsTSubstring<T>::AppendASCII(const char* aData,
                                  const fallible_t& aFallible) {
  return AppendASCII(aData, size_type(-1), aFallible);
}

template <typename T>
bool nsTSubstring<T>::AppendASCII(const char* aData, size_type aLength,
                                  const fallible_t& aFallible) {
  if (MOZ_UNLIKELY(aLength == size_type(-1))) {
    aLength = strlen(aData);
  }

  if (MOZ_UNLIKELY(!aLength)) {
    // Avoid undoing the effect of SetCapacity() if both mLength and aLength are
    // zero.
    return true;
  }

  if constexpr (std::is_same_v<T, char>) {
    // 16-bit string can't depend on an 8-bit buffer
    if (MOZ_UNLIKELY(this->IsDependentOn(aData, aData + aLength))) {
      return Append(string_type(aData, aLength), mozilla::fallible);
    }
  }

  size_type oldLen = this->mLength;
  mozilla::CheckedInt<size_type> newLen(oldLen);
  newLen += aLength;
  if (MOZ_UNLIKELY(!newLen.isValid())) {
    return false;
  }
  auto r = StartBulkWriteImpl(newLen.value(), oldLen, false);
  if (MOZ_UNLIKELY(r.isErr())) {
    return false;
  }
  char_traits::copyASCII(this->mData + oldLen, aData, aLength);
  FinishBulkWriteImpl(newLen.value());
  return true;
}

template <typename T>
void nsTSubstring<T>::Append(const self_type& aStr) {
  if (MOZ_UNLIKELY(!Append(aStr, mozilla::fallible))) {
    AllocFailed(this->mLength + aStr.Length());
  }
}

template <typename T>
bool nsTSubstring<T>::Append(const self_type& aStr,
                             const fallible_t& aFallible) {
  // Check OWNED to avoid undoing the effects of SetCapacity().
  if (MOZ_UNLIKELY(!this->mLength && !(this->mDataFlags & DataFlags::OWNED))) {
    return Assign(aStr, mozilla::fallible);
  }
  return Append(aStr.BeginReading(), aStr.Length(), mozilla::fallible);
}

template <typename T>
void nsTSubstring<T>::Append(const substring_tuple_type& aTuple) {
  if (MOZ_UNLIKELY(!Append(aTuple, mozilla::fallible))) {
    AllocFailed(this->mLength + aTuple.Length());
  }
}

template <typename T>
bool nsTSubstring<T>::Append(const substring_tuple_type& aTuple,
                             const fallible_t& aFallible) {
  const auto [isDependentOnThis, tupleLength] =
      aTuple.IsDependentOnWithLength(this->mData, this->mData + this->mLength);

  if (MOZ_UNLIKELY(!tupleLength)) {
    // Avoid undoing the effect of SetCapacity() if both
    // mLength and tupleLength are zero.
    return true;
  }

  if (MOZ_UNLIKELY(isDependentOnThis)) {
    return Append(string_type(aTuple), aFallible);
  }

  size_type oldLen = this->mLength;
  mozilla::CheckedInt<size_type> newLen(oldLen);
  newLen += tupleLength;
  if (MOZ_UNLIKELY(!newLen.isValid())) {
    return false;
  }
  auto r = StartBulkWriteImpl(newLen.value(), oldLen, false);
  if (MOZ_UNLIKELY(r.isErr())) {
    return false;
  }
  aTuple.WriteTo(this->mData + oldLen, tupleLength);
  FinishBulkWriteImpl(newLen.value());
  return true;
}

template <typename T>
void nsTSubstring<T>::SetCapacity(size_type aCapacity) {
  if (!SetCapacity(aCapacity, mozilla::fallible)) {
    AllocFailed(aCapacity);
  }
}

template <typename T>
bool nsTSubstring<T>::SetCapacity(size_type aCapacity, const fallible_t&) {
  size_type length = this->mLength;
  // This method can no longer be used to shorten the
  // logical length.
  size_type capacity = XPCOM_MAX(aCapacity, length);

  auto r = StartBulkWriteImpl(capacity, length, true);
  if (r.isErr()) {
    return false;
  }

  if (MOZ_UNLIKELY(!capacity)) {
    // Zero capacity was requested on a zero-length
    // string. In this special case, we are pointing
    // to the special empty buffer, which is already
    // zero-terminated and not writable, so we must
    // not attempt to zero-terminate it.
    AssertValid();
    return true;
  }

  // FinishBulkWriteImpl with argument zero releases
  // the heap-allocated buffer. However, SetCapacity()
  // is a special case that allows mLength to be zero
  // while a heap-allocated buffer exists.
  // By calling FinishBulkWriteImplImpl, we skip the
  // zero case handling that's inappropriate in the
  // SetCapacity() case.
  FinishBulkWriteImplImpl(length);
  return true;
}

template <typename T>
void nsTSubstring<T>::SetLength(size_type aLength) {
  if (!SetLength(aLength, mozilla::fallible)) {
    AllocFailed(aLength);
  }
}

template <typename T>
bool nsTSubstring<T>::SetLength(size_type aLength,
                                const fallible_t& aFallible) {
  size_type preserve = XPCOM_MIN(aLength, this->Length());
  auto r = StartBulkWriteImpl(aLength, preserve, true);
  if (r.isErr()) {
    return false;
  }

  FinishBulkWriteImpl(aLength);

  return true;
}

template <typename T>
void nsTSubstring<T>::Truncate() {
  Finalize();
  SetToEmptyBuffer();
  AssertValid();
}

template <typename T>
void nsTSubstring<T>::SetIsVoid(bool aVal) {
  if (aVal) {
    Truncate();
    this->mDataFlags |= DataFlags::VOIDED;
  } else {
    this->mDataFlags &= ~DataFlags::VOIDED;
  }
}

template <typename T>
void nsTSubstring<T>::StripChar(char_type aChar) {
  if (this->mLength == 0) {
    return;
  }

  if (!EnsureMutable()) {  // XXX do this lazily?
    AllocFailed(this->mLength);
  }

  // XXX(darin): this code should defer writing until necessary.

  char_type* to = this->mData;
  char_type* from = this->mData;
  char_type* end = this->mData + this->mLength;

  while (from < end) {
    char_type theChar = *from++;
    if (aChar != theChar) {
      *to++ = theChar;
    }
  }
  *to = char_type(0);  // add the null
  this->mLength = to - this->mData;
}

template <typename T>
void nsTSubstring<T>::StripChars(const char_type* aChars) {
  if (this->mLength == 0) {
    return;
  }

  if (!EnsureMutable()) {  // XXX do this lazily?
    AllocFailed(this->mLength);
  }

  // XXX(darin): this code should defer writing until necessary.

  char_type* to = this->mData;
  char_type* from = this->mData;
  char_type* end = this->mData + this->mLength;

  while (from < end) {
    char_type theChar = *from++;
    const char_type* test = aChars;

    for (; *test && *test != theChar; ++test);

    if (!*test) {
      // Not stripped, copy this char.
      *to++ = theChar;
    }
  }
  *to = char_type(0);  // add the null
  this->mLength = to - this->mData;
}

template <typename T>
void nsTSubstring<T>::StripTaggedASCII(const ASCIIMaskArray& aToStrip) {
  if (this->mLength == 0) {
    return;
  }

  size_t untaggedPrefixLength = 0;
  for (; untaggedPrefixLength < this->mLength; ++untaggedPrefixLength) {
    uint32_t theChar = (uint32_t)this->mData[untaggedPrefixLength];
    if (mozilla::ASCIIMask::IsMasked(aToStrip, theChar)) {
      break;
    }
  }

  if (untaggedPrefixLength == this->mLength) {
    return;
  }

  if (!EnsureMutable()) {
    AllocFailed(this->mLength);
  }

  char_type* to = this->mData + untaggedPrefixLength;
  char_type* from = to;
  char_type* end = this->mData + this->mLength;

  while (from < end) {
    uint32_t theChar = (uint32_t)*from++;
    // Replacing this with a call to ASCIIMask::IsMasked
    // regresses performance somewhat, so leaving it inlined.
    if (!mozilla::ASCIIMask::IsMasked(aToStrip, theChar)) {
      // Not stripped, copy this char.
      *to++ = (char_type)theChar;
    }
  }
  *to = char_type(0);  // add the null
  this->mLength = to - this->mData;
}

template <typename T>
void nsTSubstring<T>::StripCRLF() {
  // Expanding this call to copy the code from StripTaggedASCII
  // instead of just calling it does somewhat help with performance
  // but it is not worth it given the duplicated code.
  StripTaggedASCII(mozilla::ASCIIMask::MaskCRLF());
}

// An adapter type for a `nsTSubstring<T>` to provide a std::string-like
// interface for `{fmt}`.
template <typename T>
class nsTSubstringStdCollectionAdapter {
 public:
  using value_type = T;

  explicit nsTSubstringStdCollectionAdapter(nsTSubstring<T>& aString)
      : mSize(aString.Length()), mHandle(InfallibleBulkWrite(aString)) {}

  ~nsTSubstringStdCollectionAdapter() { mHandle.Finish(mSize, false); }

  size_t size() const { return mSize; }
  void resize(size_t aNewSize) {
    EnsureCapacity(aNewSize);
    mSize = aNewSize;
    // XXX: technically resize should zero-initialize any new bytes to match
    // std::string.
  }

  void push_back(T aChar) {
    if (MOZ_UNLIKELY(mSize == mHandle.Length())) {
      EnsureCapacity(mSize + 1);
    }
    mHandle.Elements()[mSize++] = aChar;
  }

  T& operator[](size_t i) {
    MOZ_RELEASE_ASSERT(i < mSize);
    return mHandle.Elements()[i];
  }
  const T& operator[](size_t i) const {
    MOZ_RELEASE_ASSERT(i < mSize);
    return mHandle.Elements()[i];
  }

 private:
  void EnsureCapacity(size_t aNewCapacity) {
    if (aNewCapacity > mHandle.Length()) {
      auto result = mHandle.RestartBulkWrite(aNewCapacity, mSize, false);
      if (result.isErr()) {
        ::NS_ABORT_OOM(aNewCapacity * sizeof(value_type));
      }
    }
  }
  static mozilla::BulkWriteHandle<T> InfallibleBulkWrite(
      nsTSubstring<T>& aString) {
    size_t length = aString.Length();
    auto res = aString.BulkWrite(length, length, false);
    if (res.isErr()) {
      ::NS_ABORT_OOM(length * sizeof(value_type));
    }
    return res.unwrap();
  }

  size_t mSize;
  mozilla::BulkWriteHandle<T> mHandle;
};

// Mark the collection as contiguous for {fmt} so that the buffer will be used
// directly.
namespace fmt {
template <typename T>
struct is_contiguous<nsTSubstringStdCollectionAdapter<T>> : std::true_type {};
}  // namespace fmt

template <typename T>
void nsTSubstring<T>::AppendVfmt(
    fmt::basic_string_view<char_type> aFormatStr,
    fmt::basic_format_args<fmt::buffered_context<char_type>> aArgs) {
  nsTSubstringStdCollectionAdapter<char_type> adapter{*this};
  fmt::vformat_to(std::back_inserter(adapter), aFormatStr, aArgs);
}

template <typename T>
struct MOZ_STACK_CLASS PrintfAppend : public mozilla::PrintfTarget {
  explicit PrintfAppend(nsTSubstring<T>* aString) : mString(aString) {}

  bool append(const char* aStr, size_t aLen) override {
    if (aLen == 0) {
      return true;
    }

    mString->AppendASCII(aStr, aLen);
    return true;
  }

 private:
  nsTSubstring<T>* mString;
};

template <typename T>
void nsTSubstring<T>::AppendPrintf(const char* aFormat, ...) {
  PrintfAppend<T> appender(this);
  va_list ap;
  va_start(ap, aFormat);
  bool r = appender.vprint(aFormat, ap);
  if (!r) {
    MOZ_CRASH("Allocation or other failure in PrintfTarget::print");
  }
  va_end(ap);
}

template <typename T>
void nsTSubstring<T>::AppendVprintf(const char* aFormat, va_list aAp) {
  PrintfAppend<T> appender(this);
  bool r = appender.vprint(aFormat, aAp);
  if (!r) {
    MOZ_CRASH("Allocation or other failure in PrintfTarget::print");
  }
}

template <typename T>
void nsTSubstring<T>::AppendIntDec(int32_t aInteger) {
  PrintfAppend<T> appender(this);
  bool r = appender.appendIntDec(aInteger);
  if (MOZ_UNLIKELY(!r)) {
    MOZ_CRASH("Allocation or other failure while appending integers");
  }
}

template <typename T>
void nsTSubstring<T>::AppendIntDec(uint32_t aInteger) {
  PrintfAppend<T> appender(this);
  bool r = appender.appendIntDec(aInteger);
  if (MOZ_UNLIKELY(!r)) {
    MOZ_CRASH("Allocation or other failure while appending integers");
  }
}

template <typename T>
void nsTSubstring<T>::AppendIntOct(uint32_t aInteger) {
  PrintfAppend<T> appender(this);
  bool r = appender.appendIntOct(aInteger);
  if (MOZ_UNLIKELY(!r)) {
    MOZ_CRASH("Allocation or other failure while appending integers");
  }
}

template <typename T>
void nsTSubstring<T>::AppendIntHex(uint32_t aInteger) {
  PrintfAppend<T> appender(this);
  bool r = appender.appendIntHex(aInteger);
  if (MOZ_UNLIKELY(!r)) {
    MOZ_CRASH("Allocation or other failure while appending integers");
  }
}

template <typename T>
void nsTSubstring<T>::AppendIntDec(int64_t aInteger) {
  PrintfAppend<T> appender(this);
  bool r = appender.appendIntDec(aInteger);
  if (MOZ_UNLIKELY(!r)) {
    MOZ_CRASH("Allocation or other failure while appending integers");
  }
}

template <typename T>
void nsTSubstring<T>::AppendIntDec(uint64_t aInteger) {
  PrintfAppend<T> appender(this);
  bool r = appender.appendIntDec(aInteger);
  if (MOZ_UNLIKELY(!r)) {
    MOZ_CRASH("Allocation or other failure while appending integers");
  }
}

template <typename T>
void nsTSubstring<T>::AppendIntOct(uint64_t aInteger) {
  PrintfAppend<T> appender(this);
  bool r = appender.appendIntOct(aInteger);
  if (MOZ_UNLIKELY(!r)) {
    MOZ_CRASH("Allocation or other failure while appending integers");
  }
}

template <typename T>
void nsTSubstring<T>::AppendIntHex(uint64_t aInteger) {
  PrintfAppend<T> appender(this);
  bool r = appender.appendIntHex(aInteger);
  if (MOZ_UNLIKELY(!r)) {
    MOZ_CRASH("Allocation or other failure while appending integers");
  }
}

// Returns the length of the formatted aDouble in aBuf.
static int FormatWithoutTrailingZeros(char (&aBuf)[40], double aDouble,
                                      int aPrecision) {
  static const DoubleToStringConverter converter(
      DoubleToStringConverter::UNIQUE_ZERO |
          DoubleToStringConverter::NO_TRAILING_ZERO |
          DoubleToStringConverter::EMIT_POSITIVE_EXPONENT_SIGN,
      "Infinity", "NaN", 'e', -6, 21, 6, 1);
  double_conversion::StringBuilder builder(aBuf, sizeof(aBuf));
  converter.ToPrecision(aDouble, aPrecision, &builder);
  int length = builder.position();
  builder.Finalize();
  return length;
}

template <typename T>
void nsTSubstring<T>::AppendFloat(float aFloat) {
  char buf[40];
  int length = FormatWithoutTrailingZeros(buf, aFloat, 6);
  AppendASCII(buf, length);
}

template <typename T>
void nsTSubstring<T>::AppendFloat(double aFloat) {
  char buf[40];
  int length = FormatWithoutTrailingZeros(buf, aFloat, 15);
  AppendASCII(buf, length);
}

template <typename T>
size_t nsTSubstring<T>::SizeOfExcludingThisIfUnshared(
    mozilla::MallocSizeOf aMallocSizeOf) const {
  if (this->mDataFlags & DataFlags::OWNED) {
    if (this->mDataFlags & DataFlags::STRINGBUFFER) {
      return mozilla::StringBuffer::FromData(this->mData)
          ->SizeOfIncludingThisIfUnshared(aMallocSizeOf);
    }
    return aMallocSizeOf(this->mData);
  }

  // If we reach here, exactly one of the following must be true:
  // - DataFlags::VOIDED is set, and this->mData points to sEmptyBuffer;
  // - DataFlags::INLINE is set, and this->mData points to a buffer within a
  //   string object (e.g. nsAutoString);
  // - Neither of DataFlags::OWNED or DataFlags::INLINE is
  //   set, and this->mData points to a buffer owned by something else.
  //
  // In all three cases, we don't measure it.
  return 0;
}

template <typename T>
size_t nsTSubstring<T>::SizeOfExcludingThisEvenIfShared(
    mozilla::MallocSizeOf aMallocSizeOf) const {
  // This is identical to SizeOfExcludingThisIfUnshared except for the
  // DataFlags::STRINGBUFFER case.
  if (this->mDataFlags & DataFlags::OWNED) {
    if (this->mDataFlags & DataFlags::STRINGBUFFER) {
      return mozilla::StringBuffer::FromData(this->mData)
          ->SizeOfIncludingThisEvenIfShared(aMallocSizeOf);
    }
    return aMallocSizeOf(this->mData);
  }
  return 0;
}

template <typename T>
size_t nsTSubstring<T>::SizeOfIncludingThisIfUnshared(
    mozilla::MallocSizeOf aMallocSizeOf) const {
  return aMallocSizeOf(this) + SizeOfExcludingThisIfUnshared(aMallocSizeOf);
}

template <typename T>
size_t nsTSubstring<T>::SizeOfIncludingThisEvenIfShared(
    mozilla::MallocSizeOf aMallocSizeOf) const {
  return aMallocSizeOf(this) + SizeOfExcludingThisEvenIfShared(aMallocSizeOf);
}

template <typename T>
nsTSubstringSplitter<T> nsTSubstring<T>::Split(const char_type aChar) const {
  return nsTSubstringSplitter<T>(
      nsTCharSeparatedTokenizerTemplate<
          NS_TokenizerIgnoreNothing, T,
          nsTokenizerFlags::IncludeEmptyTokenAtEnd>(*this, aChar));
}

// Common logic for nsTSubstring<T>::ToInteger and nsTSubstring<T>::ToInteger64.
template <typename T, typename int_type>
int_type ToIntegerCommon(const nsTSubstring<T>& aSrc, nsresult* aErrorCode,
                         uint32_t aRadix) {
  MOZ_ASSERT(aRadix == 10 || aRadix == 16);

  // Initial value, override if we find an integer.
  *aErrorCode = NS_ERROR_ILLEGAL_VALUE;

  // Begin by skipping over leading chars that shouldn't be part of the number.
  auto cp = aSrc.BeginReading();
  auto endcp = aSrc.EndReading();
  bool negate = false;
  bool done = false;

  // NB: For backwards compatibility I'm not going to change this logic but
  //     it seems really odd. Previously there was logic to auto-detect the
  //     radix if kAutoDetect was passed in. In practice this value was never
  //     used, so it pretended to auto detect and skipped some preceding
  //     letters (excluding valid hex digits) but never used the result.
  //
  //     For example if you pass in "Get the number: 10", aRadix = 10 we'd
  //     skip the 'G', and then fail to parse "et the number: 10". If aRadix =
  //     16 we'd skip the 'G', and parse just 'e' returning 14.
  while ((cp < endcp) && (!done)) {
    switch (*cp++) {
        // clang-format off
      case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
      case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
      case '0': case '1': case '2': case '3': case '4':
      case '5': case '6': case '7': case '8': case '9':
        done = true;
        break;
      // clang-format on
      case '-':
        if constexpr (!std::is_signed_v<int_type>) {
          return 0;
        }
        negate = true;
        break;
      default:
        break;
    }
  }

  if (!done) {
    // No base 16 or base 10 digits were found.
    return 0;
  }

  // Step back.
  cp--;

  mozilla::CheckedInt<int_type> result;

  // Now iterate the numeric chars and build our result.
  while (cp < endcp) {
    auto theChar = *cp++;
    if (('0' <= theChar) && (theChar <= '9')) {
      result = (aRadix * result) + (theChar - '0');
    } else if ((theChar >= 'A') && (theChar <= 'F')) {
      if (10 == aRadix) {
        // Invalid base 10 digit, error out.
        return 0;
      }
      result = (aRadix * result) + ((theChar - 'A') + 10);
    } else if ((theChar >= 'a') && (theChar <= 'f')) {
      if (10 == aRadix) {
        // Invalid base 10 digit, error out.
        return 0;
      }
      result = (aRadix * result) + ((theChar - 'a') + 10);
    } else if ((('X' == theChar) || ('x' == theChar)) && result == 0) {
      // For some reason we support a leading 'x' regardless of radix. For
      // example: "000000x500", aRadix = 10 would be parsed as 500 rather
      // than 0.
      continue;
    } else {
      // We've encountered a char that's not a legal number or sign and we can
      // terminate processing.
      break;
    }

    if (!result.isValid()) {
      // Overflow!
      return 0;
    }
  }

  // Integer found.
  *aErrorCode = NS_OK;

  if (negate) {
    result = -result;
  }

  return result.value();
}

template <typename T>
int32_t nsTSubstring<T>::ToInteger(nsresult* aErrorCode,
                                   uint32_t aRadix) const {
  return ToIntegerCommon<T, int32_t>(*this, aErrorCode, aRadix);
}

template <typename T>
uint32_t nsTSubstring<T>::ToUnsignedInteger(nsresult* aErrorCode,
                                            uint32_t aRadix) const {
  return ToIntegerCommon<T, uint32_t>(*this, aErrorCode, aRadix);
}

/**
 * nsTSubstring::ToInteger64
 */
template <typename T>
int64_t nsTSubstring<T>::ToInteger64(nsresult* aErrorCode,
                                     uint32_t aRadix) const {
  return ToIntegerCommon<T, int64_t>(*this, aErrorCode, aRadix);
}

/**
 * nsTSubstring::ToUnsignedInteger64
 */
template <typename T>
uint64_t nsTSubstring<T>::ToUnsignedInteger64(nsresult* aErrorCode,
                                              uint32_t aRadix) const {
  return ToIntegerCommon<T, uint64_t>(*this, aErrorCode, aRadix);
}

/**
 * nsTSubstring::Mid
 */
template <typename T>
typename nsTSubstring<T>::size_type nsTSubstring<T>::Mid(
    self_type& aResult, index_type aStartPos, size_type aLengthToCopy) const {
  if (aStartPos == 0 && aLengthToCopy >= this->mLength) {
    aResult = *this;
  } else {
    aResult = Substring(*this, aStartPos, aLengthToCopy);
  }

  return aResult.mLength;
}

/**
 * nsTSubstring::StripWhitespace
 */

template <typename T>
void nsTSubstring<T>::StripWhitespace() {
  if (!StripWhitespace(mozilla::fallible)) {
    this->AllocFailed(this->mLength);
  }
}

template <typename T>
bool nsTSubstring<T>::StripWhitespace(const fallible_t&) {
  if (!this->EnsureMutable()) {
    return false;
  }

  this->StripTaggedASCII(mozilla::ASCIIMask::MaskWhitespace());
  return true;
}

/**
 * nsTSubstring::ReplaceChar,ReplaceSubstring
 */

template <typename T>
void nsTSubstring<T>::ReplaceChar(char_type aOldChar, char_type aNewChar) {
  int32_t i = this->FindChar(aOldChar);
  if (i == kNotFound) {
    return;
  }

  if (!this->EnsureMutable()) {
    this->AllocFailed(this->mLength);
  }
  for (; i != kNotFound; i = this->FindChar(aOldChar, i + 1)) {
    this->mData[i] = aNewChar;
  }
}

template <typename T>
void nsTSubstring<T>::ReplaceChar(const string_view& aSet, char_type aNewChar) {
  int32_t i = this->FindCharInSet(aSet);
  if (i == kNotFound) {
    return;
  }

  if (!this->EnsureMutable()) {
    this->AllocFailed(this->mLength);
  }
  for (; i != kNotFound; i = this->FindCharInSet(aSet, i + 1)) {
    this->mData[i] = aNewChar;
  }
}

template <typename T>
void nsTSubstring<T>::ReplaceSubstring(const char_type* aTarget,
                                       const char_type* aNewValue) {
  ReplaceSubstring(nsTDependentString<T>(aTarget),
                   nsTDependentString<T>(aNewValue));
}

template <typename T>
bool nsTSubstring<T>::ReplaceSubstring(const char_type* aTarget,
                                       const char_type* aNewValue,
                                       const fallible_t& aFallible) {
  return ReplaceSubstring(nsTDependentString<T>(aTarget),
                          nsTDependentString<T>(aNewValue), aFallible);
}

template <typename T>
void nsTSubstring<T>::ReplaceSubstring(const self_type& aTarget,
                                       const self_type& aNewValue) {
  if (!ReplaceSubstring(aTarget, aNewValue, mozilla::fallible)) {
    // Note that this may wildly underestimate the allocation that failed, as
    // we could have been replacing multiple copies of aTarget.
    this->AllocFailed(this->mLength + (aNewValue.Length() - aTarget.Length()));
  }
}

template <typename T>
bool nsTSubstring<T>::ReplaceSubstring(const self_type& aTarget,
                                       const self_type& aNewValue,
                                       const fallible_t&) {
  struct Segment {
    uint32_t mBegin, mLength;
    Segment(uint32_t aBegin, uint32_t aLength)
        : mBegin(aBegin), mLength(aLength) {}
  };

  if (aTarget.Length() == 0) {
    return true;
  }

  // Remember all of the non-matching parts.
  AutoTArray<Segment, 16> nonMatching;
  uint32_t i = 0;
  mozilla::CheckedUint32 newLength;
  while (true) {
    int32_t r = this->Find(aTarget, i);
    int32_t until = (r == kNotFound) ? this->Length() - i : r - i;
    nonMatching.AppendElement(Segment(i, until));
    newLength += until;
    if (r == kNotFound) {
      break;
    }

    newLength += aNewValue.Length();
    i = r + aTarget.Length();
    if (i >= this->Length()) {
      // Add an auxiliary entry at the end of the list to help as an edge case
      // for the algorithms below.
      nonMatching.AppendElement(Segment(this->Length(), 0));
      break;
    }
  }

  if (!newLength.isValid()) {
    return false;
  }

  // If there's only one non-matching segment, then the target string was not
  // found, and there's nothing to do.
  if (nonMatching.Length() == 1) {
    MOZ_ASSERT(
        nonMatching[0].mBegin == 0 && nonMatching[0].mLength == this->Length(),
        "We should have the correct non-matching segment.");
    return true;
  }

  // Make sure that we can mutate our buffer.
  // Note that we always allocate at least an this->mLength sized buffer,
  // because the rest of the algorithm relies on having access to all of the
  // original string.  In other words, we over-allocate in the shrinking case.
  uint32_t oldLen = this->Length();
  auto r =
      this->StartBulkWriteImpl(XPCOM_MAX(oldLen, newLength.value()), oldLen);
  if (r.isErr()) {
    return false;
  }

  if (aTarget.Length() >= aNewValue.Length()) {
    // In the shrinking case, start filling the buffer from the beginning.
    const uint32_t delta = (aTarget.Length() - aNewValue.Length());
    for (i = 1; i < nonMatching.Length(); ++i) {
      // When we move the i'th non-matching segment into position, we need to
      // account for the characters deleted by the previous |i| replacements by
      // subtracting |i * delta|.
      const char_type* sourceSegmentPtr = this->mData + nonMatching[i].mBegin;
      char_type* destinationSegmentPtr =
          this->mData + nonMatching[i].mBegin - i * delta;
      // Write the i'th replacement immediately before the new i'th non-matching
      // segment.
      char_traits::copy(destinationSegmentPtr - aNewValue.Length(),
                        aNewValue.Data(), aNewValue.Length());
      char_traits::move(destinationSegmentPtr, sourceSegmentPtr,
                        nonMatching[i].mLength);
    }
  } else {
    // In the growing case, start filling the buffer from the end.
    const uint32_t delta = (aNewValue.Length() - aTarget.Length());
    for (i = nonMatching.Length() - 1; i > 0; --i) {
      // When we move the i'th non-matching segment into position, we need to
      // account for the characters added by the previous |i| replacements by
      // adding |i * delta|.
      const char_type* sourceSegmentPtr = this->mData + nonMatching[i].mBegin;
      char_type* destinationSegmentPtr =
          this->mData + nonMatching[i].mBegin + i * delta;
      char_traits::move(destinationSegmentPtr, sourceSegmentPtr,
                        nonMatching[i].mLength);
      // Write the i'th replacement immediately before the new i'th non-matching
      // segment.
      char_traits::copy(destinationSegmentPtr - aNewValue.Length(),
                        aNewValue.Data(), aNewValue.Length());
    }
  }

  // Adjust the length and make sure the string is null terminated.
  this->FinishBulkWriteImpl(newLength.value());

  return true;
}

/**
 * nsTSubstring::Trim
 */

template <typename T>
void nsTSubstring<T>::Trim(const std::string_view& aSet, bool aTrimLeading,
                           bool aTrimTrailing, bool aIgnoreQuotes) {
  char_type* start = this->mData;
  char_type* end = this->mData + this->mLength;

  // skip over quotes if requested
  if (aIgnoreQuotes && this->mLength > 2 &&
      this->mData[0] == this->mData[this->mLength - 1] &&
      (this->mData[0] == '\'' || this->mData[0] == '"')) {
    ++start;
    --end;
  }

  if (aTrimLeading) {
    uint32_t cutStart = start - this->mData;
    uint32_t cutLength = 0;

    // walk forward from start to end
    for (; start != end; ++start, ++cutLength) {
      if ((*start & ~0x7F) ||  // non-ascii
          aSet.find(char(*start)) == std::string_view::npos) {
        break;
      }
    }

    if (cutLength) {
      this->Cut(cutStart, cutLength);

      // reset iterators
      start = this->mData + cutStart;
      end = this->mData + this->mLength - cutStart;
    }
  }

  if (aTrimTrailing) {
    uint32_t cutEnd = end - this->mData;
    uint32_t cutLength = 0;

    // walk backward from end to start
    --end;
    for (; end >= start; --end, ++cutLength) {
      if ((*end & ~0x7F) ||  // non-ascii
          aSet.find(char(*end)) == std::string_view::npos) {
        break;
      }
    }

    if (cutLength) {
      this->Cut(cutEnd - cutLength, cutLength);
    }
  }
}

/**
 * nsTSubstring::CompressWhitespace.
 */

template <typename T>
void nsTSubstring<T>::CompressWhitespace(bool aTrimLeading,
                                         bool aTrimTrailing) {
  // Quick exit
  if (this->mLength == 0) {
    return;
  }

  if (!this->EnsureMutable()) {
    this->AllocFailed(this->mLength);
  }

  const ASCIIMaskArray& mask = mozilla::ASCIIMask::MaskWhitespace();

  char_type* to = this->mData;
  char_type* from = this->mData;
  char_type* end = this->mData + this->mLength;

  // Compresses runs of whitespace down to a normal space ' ' and convert
  // any whitespace to a normal space.  This assumes that whitespace is
  // all standard 7-bit ASCII.
  bool skipWS = aTrimLeading;
  while (from < end) {
    uint32_t theChar = *from++;
    if (mozilla::ASCIIMask::IsMasked(mask, theChar)) {
      if (!skipWS) {
        *to++ = ' ';
        skipWS = true;
      }
    } else {
      *to++ = theChar;
      skipWS = false;
    }
  }

  // If we need to trim the trailing whitespace, back up one character.
  if (aTrimTrailing && skipWS && to > this->mData) {
    to--;
  }

  *to = char_type(0);  // add the null
  this->mLength = to - this->mData;
}

template class nsTSubstring<char>;
template class nsTSubstring<char16_t>;
