Moderate firefox Logic Error 🔧 Commit mapped

Overview

Moderate
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionIncorrect boundary conditions in the DOM: Editor component
ComponentDOM
Bug ClassLogic Error
Tracker2068443
Fix commit64bc200bd6f3 (firefox) +435/-153
CISA KEVNot listed
CreditedMozilla
Disclosed2026-10-13

Changed Functions

FunctionChangeNotes
if
dom/base/CharacterDataBuffer.cpp
modified
if
editor/libeditor/CompositionTransaction.cpp
modified
MOZ_STACK_CLASS
editor/libeditor/EditorBase.cpp
modified
if
editor/libeditor/EditorBase.cpp
modified

Files Changed

  • dom/base/CharacterDataBuffer.cpp
  • dom/base/CharacterDataBuffer.h
  • dom/base/nsINode.h
  • dom/base/test/gtest/TestCharacterDataBuffer.cpp
  • dom/script/ScriptElement.cpp
  • editor/libeditor/CompositionTransaction.cpp
  • editor/libeditor/DeleteTextTransaction.cpp
  • editor/libeditor/EditorBase.cpp
  • editor/libeditor/EditorBase.h
  • editor/libeditor/HTMLEditor.cpp
  • editor/libeditor/InsertTextTransaction.cpp
  • editor/libeditor/ReplaceTextTransaction.cpp
diff --git a/dom/base/CharacterDataBuffer.cpp b/dom/base/CharacterDataBuffer.cpp
index fa1764d6c1a..bad1b5f1fd1 100644
--- a/dom/base/CharacterDataBuffer.cpp
+++ b/dom/base/CharacterDataBuffer.cpp
@@ -542,3 +542,33 @@ bool CharacterDataBuffer::BufferEquals(
 
   return true;
 }
+
+bool CharacterDataBuffer::Equals(const nsAString& aString) const {
+  const uint32_t length = GetLength();
+  if (!length) {
+    return aString.IsEmpty();
+  }
+  if (length != aString.Length()) {
+    return false;
+  }
+  if (Is2b()) {
+    return aString.Equals(nsDependentSubstring(Get2b(), length));
+  }
+  return aString.Equals(
+      NS_ConvertASCIItoUTF16(nsDependentCSubstring(Get1b(), length)));
+}
+
+bool CharacterDataBuffer::Equals(const nsACString& aString) const {
+  const uint32_t length = GetLength();
+  if (!length) {
+    return aString.IsEmpty();
+  }
+  if (length != aString.Length()) {
+    return false;
+  }
+  if (!Is2b()) {
+    return aString.Equals(nsDependentCSubstring(Get1b(), length));
+  }
+  NS_ConvertASCIItoUTF16 string(aString);
+  return string.Equals(nsDependentSubstring(Get2b(), length));
+}
diff --git a/dom/base/CharacterDataBuffer.h b/dom/base/CharacterDataBuffer.h
index b05bda5799c..86cde4110a5 100644
--- a/dom/base/CharacterDataBuffer.h
+++ b/dom/base/CharacterDataBuffer.h
@@ -334,6 +334,16 @@ class CharacterDataBuffer final {
    */
   [[nodiscard]] bool BufferEquals(const CharacterDataBuffer& aOther) const;
 
+  /**
+   * Return true if aString matches with the data.
+   */
+  [[nodiscard]] bool Equals(const nsAString& aString) const;
+
+  /**
+   * Return true if aString matches with the data.
+   */
+  [[nodiscard]] bool Equals(const nsACString& aString) const;
+
   // FYI: FragmentBits::mLength is only 29 bits.  Therefore, UINT32_MAX won't
   // be valid offset in the data.
   constexpr static uint32_t kNotFound = UINT32_MAX;
diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h
index 271c98870e2..c0ecbaadff4 100644
--- a/dom/base/nsINode.h
+++ b/dom/base/nsINode.h
@@ -667,6 +667,13 @@ class nsINode : public mozilla::dom::EventTarget {
     return false;
   }
 
+  /**
+   * Return true if the concrete class inherits ScriptElement.
+   */
+  [[nodiscard]] bool IsScriptElement() const {
+    return IsHTMLElement(nsGkAtoms::script) || IsSVGElement(nsGkAtoms::script);
+  }
+
   // Returns non-null if this element subclasses `LinkStyle`.
   virtual const mozilla::dom::LinkStyle* AsLinkStyle() const { return nullptr; }
   mozilla::dom::LinkStyle* AsLinkStyle() {
diff --git a/dom/script/ScriptElement.cpp b/dom/script/ScriptElement.cpp
index 5598ccc23d8..febd3836528 100644
--- a/dom/script/ScriptElement.cpp
+++ b/dom/script/ScriptElement.cpp
@@ -164,6 +164,9 @@ void ScriptElement::ContentWillBeRemoved(nsIContent* aChild,
 
 bool ScriptElement::MaybeProcessScript(nsCOMPtr<nsIParser> aParser) {
   nsIContent* cont = GetAsContent();
+  MOZ_ASSERT(cont->IsScriptElement(),
+             "nsINode::IsScriptElement should return true if the node inherits "
+             "ScriptElement");
 
   NS_ASSERTION(cont->DebugGetSlots()->mMutationObservers.contains(this),
                "You forgot to add self as observer");
diff --git a/editor/libeditor/CompositionTransaction.cpp b/editor/libeditor/CompositionTransaction.cpp
index a5952101802..8fe06766590 100644
--- a/editor/libeditor/CompositionTransaction.cpp
+++ b/editor/libeditor/CompositionTransaction.cpp
@@ -123,11 +123,10 @@ NS_IMETHODIMP CompositionTransaction::DoTransaction() {
 
   // Advance caret: This requires the presentation shell to get the selection.
   if (mReplaceLength == 0) {
-    IgnoredErrorResult error;
-    editorBase->DoInsertText(*textNode, mOffset, mStringToInsert, error);
-    if (error.Failed()) {
+    nsresult rv = editorBase->DoInsertText(*textNode, mOffset, mStringToInsert);
+    if (NS_FAILED(rv)) [[unlikely]] {
       NS_WARNING("EditorBase::DoInsertText() failed");
-      return error.StealNSResult();
+      return rv;
     }
     editorBase->RangeUpdaterRef().SelAdjInsertText(*textNode, mOffset,
                                                    mStringToInsert.Length());
@@ -182,15 +181,14 @@ NS_IMETHODIMP CompositionTransaction::DoTransaction() {
       return {offsetInTextNode,
               replaceEndOffsetInCompositionStartTextNode - offsetInTextNode};
     }();
-    IgnoredErrorResult error;
     // FIXME: If mStringToInsert is empty string and mOffset is 0 in HTMLEditor,
     // we should delete the `Text` instead. See bug 2019186.
-    editorBase->DoReplaceText(*textNode, replaceStartInFirstText,
-                              replaceableLengthInFirstText, mStringToInsert,
-                              error);
-    if (error.Failed()) [[unlikely]] {
+    nsresult rv = editorBase->DoReplaceText(*textNode, replaceStartInFirstText,
+                                            replaceableLengthInFirstText,
+                                            mStringToInsert);
+    if (NS_FAILED(rv)) [[unlikely]] {
       NS_WARNING("EditorBase::DoReplaceText() failed");
-      return error.StealNSResult();
+      return rv;
     }
 
     // Don't use RangeUpdaterRef().SelAdjReplaceText() here because undoing
@@ -210,7 +208,6 @@ NS_IMETHODIMP CompositionTransaction::DoTransaction() {
       //     non-empty text nodes which are inserted by JS.  Instead, we
       //     should remove all text in the ranges of IME selections.
       uint32_t remainingLength = mReplaceLength - replaceableLengthInFirstText;
-      IgnoredErrorResult ignoredError;
       for (RefPtr<Text> text = Text::FromNodeOrNull(textNode->GetNextSibling());
            text && remainingLength;
            text = Text::FromNodeOrNull(text->GetNextSibling())) {
@@ -218,10 +215,11 @@ NS_IMETHODIMP CompositionTransaction::DoTransaction() {
             std::min(text->TextDataLength(), remainingLength);
         // FIXME: We should delete the Text when all of its data is deleted
         // now and we're working for HTMLEditor, see bug 2019186.
-        editorBase->DoDeleteText(*text, 0, deletableLengthInText, ignoredError);
-        NS_WARNING_ASSERTION(!ignoredError.Failed(),
-                             "EditorBase::DoDeleteText() failed, but ignored");
-        ignoredError.SuppressException();
+        nsresult rv = editorBase->DoDeleteText(*text, 0, deletableLengthInText);
+        if (NS_FAILED(rv)) [[unlikely]] {
+          NS_WARNING("EditorBase::DoDeleteText() failed, but ignored");
+          return rv;
+        }
         editorBase->RangeUpdaterRef().SelAdjDeleteText(*text, 0,
                                                        deletableLengthInText);
         remainingLength -= deletableLengthInText;
@@ -261,17 +259,17 @@ NS_IMETHODIMP CompositionTransaction::UndoTransaction() {
   }
 
   const OwningNonNull<EditorBase> editorBase = *mEditorBase;
-  IgnoredErrorResult error;
-  editorBase->DoDeleteText(*textNode, mOffset, mStringToInsert.Length(), error);
-  if (MOZ_UNLIKELY(error.Failed())) {
+  nsresult rv =
+      editorBase->DoDeleteText(*textNode, mOffset, mStringToInsert.Length());
+  if (NS_FAILED(rv)) [[unlikely]] {
     NS_WARNING("EditorBase::DoDeleteText() failed");
-    return error.StealNSResult();
+    return rv;
   }
 
   // set the selection to the insertion point where the string was removed
-  editorBase->CollapseSelectionTo(EditorRawDOMPoint(textNode, mOffset), error);
-  NS_ASSERTION(!error.Failed(), "EditorBase::CollapseSelectionTo() failed");
-  return error.StealNSResult();
+  rv = editorBase->CollapseSelectionTo(EditorRawDOMPoint(textNode, mOffset));
+  NS_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CollapseSelectionTo() failed");
+  return rv;
 }
 
 NS_IMETHODIMP CompositionTransaction::RedoTransaction() {
diff --git a/editor/libeditor/DeleteTextTransaction.cpp b/editor/libeditor/DeleteTextTransaction.cpp
index 6bfc0d6ca6b..4ecc428d5d3 100644
--- a/editor/libeditor/DeleteTextTransaction.cpp
+++ b/editor/libeditor/DeleteTextTransaction.cpp
@@ -146,10 +146,10 @@ NS_IMETHODIMP DeleteTextTransaction::DoTransaction() {
   }
 
   const OwningNonNull<EditorBase> editorBase = *mEditorBase;
-  editorBase->DoDeleteText(*textNode, mOffset, mLengthToDelete, error);
-  if (MOZ_UNLIKELY(error.Failed())) {
+  nsresult rv = editorBase->DoDeleteText(*textNode, mOffset, mLengthToDelete);
+  if (NS_FAILED(rv)) [[unlikely]] {
     NS_WARNING("EditorBase::DoDeleteText() failed");
-    return error.StealNSResult();
+    return rv;
   }
 
   editorBase->RangeUpdaterRef().SelAdjDeleteText(*textNode, mOffset,
@@ -195,10 +195,12 @@ NS_IMETHODIMP DeleteTextTransaction::UndoTransaction() {
     return NS_ERROR_NOT_AVAILABLE;
   }
   const OwningNonNull<EditorBase> editorBase = *mEditorBase;
-  IgnoredErrorResult error;
-  editorBase->DoInsertText(*textNode, mOffset, mDeletedText, error);
-  NS_WARNING_ASSERTION(!error.Failed(), "EditorBase::DoInsertText() failed");
-  return error.StealNSResult();
+  nsresult rv = editorBase->DoInsertText(*textNode, mOffset, mDeletedText);
+  if (NS_FAILED(rv)) [[unlikely]] {
+    return rv;
+  }
+  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::DoInsertText() failed");
+  return rv;
 }
 
 NS_IMETHODIMP DeleteTextTransaction::RedoTransaction() {
diff --git a/editor/libeditor/EditorBase.cpp b/editor/libeditor/EditorBase.cpp
index 07a22bd5d06..cc66a578948 100644
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -116,6 +116,7 @@
 #include "nsIInlineSpellChecker.h"     // for nsIInlineSpellChecker, etc.
 #include "nsNameSpaceManager.h"        // for kNameSpaceID_None, etc.
 #include "nsINode.h"                   // for nsINode, etc.
+#include "nsIScriptElement.h"          // for nsIScriptElement
 #include "nsISelectionController.h"    // for nsISelectionController, etc.
 #include "nsISelectionDisplay.h"       // for nsISelectionDisplay, etc.
 #include "nsISupports.h"               // for nsISupports
@@ -152,6 +153,95 @@ static LazyLogModule gHTMLEditorEditActionStartLog("HTMLEditorEditActionStart");
 
 LazyLogModule gTextInputLog("EditorTextInput");
 
+/******************************************************************************
+ * mozilla::EditorBase::AutoTextEditVerifier
+ *****************************************************************************/
+class MOZ_STACK_CLASS EditorBase::AutoTextEditVerifier {
+ public:
+  MOZ_CAN_RUN_SCRIPT explicit AutoTextEditVerifier(EditorBase& aEditorBase,
+                                                   const Text& aText)
+      : mEditorBase(aEditorBase), mText(aText), mDoCheck(false) {
+    if (mEditorBase.IsHTMLEditor() && mEditorBase.GetEditActionEditContext()) {
+      return;
+    }
+    // The most expensive cost of this check must be that we need to duplicate
+    // the data as a string. Fortunately, known unsafe situation is only when
+    // the `Text` is a descendant of a <script>. For avoiding the bad affect to
+    // usual web apps, we should check whether we need to check it strictly only
+    // when the known unsafe case.
+    for (Element* const element : mText.AncestorsOfType<Element>()) {
+      if (element->IsScriptElement()) {
+        mDoCheck = true;
+        break;
+      }
+    }
+    if (!mDoCheck) [[likely]] {
+      return;
+    }
+    mText.AppendTextTo(mExpectedData);
+    mParentNode = mText.GetParentNode();
+  }
+
+  [[nodiscard]] bool IsExpectedData() const {
+    if (!mDoCheck) [[likely]] {
+      return true;
+    }
+    if (mText.TextDataLength() != mExpectedData.Length() ||
+        mParentNode != mText.GetParentNode()) [[unlikely]] {
+      return false;
+    }
+    return mText.DataBuffer().Equals(mExpectedData);
+  }
+
+  void WillInsertText(uint32_t aOffset, const nsAString& aStringToInsert) {
+    if (!mDoCheck) {
+      return;
+    }
+    // FYI: CharacterData::InsertData() returns error if aOffset is greater than
+    // its data length, but in that case, IsExpectedData() won't be used. For
+    // unexpected behavior change of CharacterData, we should make mExpectedData
+    // even in the case.
+    mExpectedData.Insert(aStringToInsert,
+                         std::min<uint32_t>(aOffset, mExpectedData.Length()));
+  }
+
+  void WillDeleteText(uint32_t aOffset, uint32_t aCount) {
+    if (!mDoCheck) {
+      return;
+    }
+    // FYI: CharacterData::DeleteData() returns error if aOffset is greater than
+    // its data length, but in that case, IsExpectedData() won't be used. For
+    // unexpected behavior change of CharacterData, we should make mExpectedData
+    // even in the case.
+    const uint32_t offset = std::min<uint32_t>(aOffset, mExpectedData.Length());
+    const uint32_t count = aCount > mExpectedData.Length() - offset
+                               ? mExpectedData.Length() - offset
+                               : aCount;
+    if (!count) {
+      return;
+    }
+    mExpectedData.Cut(offset, count);
+  }
+
+  void WillReplaceText(uint32_t aOffset, uint32_t aCount,
+                       const nsAString& aStringToInsert) {
+    WillDeleteText(aOffset, aCount);
+    WillInsertText(aOffset, aStringToInsert);
+  }
+
+  void WillSetText(const nsAString& aStringToSet) {
+    WillDeleteText(0, mText.TextDataLength());
+    WillInsertText(0, aStringToSet);
+  }
+
+ private:
+  MOZ_KNOWN_LIVE EditorBase& mEditorBase;
+  MOZ_KNOWN_LIVE const Text& mText;
+  nsCOMPtr<nsINode> mParentNode;
+  nsAutoString mExpectedData;
+  bool mDoCheck;
+};
+
 /*****************************************************************************
  * mozilla::EditorBase
  *****************************************************************************/
@@ -3205,68 +3295,94 @@ nsresult EditorBase::OnEndHandlingTopLevelEditSubAction() {
   return NS_OK;
 }
 
-void EditorBase::DoInsertText(Text& aText, uint32_t aOffset,
-                              const nsAString& aStringToInsert,
-                              ErrorResult& aRv) {
+nsresult EditorBase::DoInsertText(Text& aText, uint32_t aOffset,
+                                  const nsAString& aStringToInsert) {
+  AutoTextEditVerifier verify(*this, aText);
+  verify.WillInsertText(aOffset, aStringToInsert);
   {
     AutoCharacterDataAPIWrapper charDataWrapper(*this, aText);
-    aRv = charDataWrapper.InsertData(aOffset, aStringToInsert);
-    if (MOZ_UNLIKELY(aRv.Failed())) {
+    nsresult rv = charDataWrapper.InsertData(aOffset, aStringToInsert);
+    if (NS_WARN_IF(Destroyed())) [[unlikely]] {
+      return NS_ERROR_EDITOR_DESTROYED;
+    }
+    if (NS_FAILED(rv)) [[unlikely]] {
       NS_WARNING("AutoCharacterDataAPIWrapper::InsertData() failed");
-      return;
+      return rv;
     }
-    NS_WARNING_ASSERTION(charDataWrapper.IsExpectedResult(aStringToInsert),
+    NS_WARNING_ASSERTION(!verify.IsExpectedData() ||
+                             charDataWrapper.IsExpectedResult(aStringToInsert),
                          "Inserting data caused other mutations, but ignored");
   }
   if (IsTextEditor() && !aStringToInsert.IsEmpty()) {
-    aRv = MOZ_KnownLive(AsTextEditor())
-              ->DidInsertText(aText.TextLength(), aOffset,
-                              aStringToInsert.Length());
-    NS_WARNING_ASSERTION(!aRv.Failed(), "TextEditor::DidInsertText() failed");
+    nsresult rv = MOZ_KnownLive(AsTextEditor())
+                      ->DidInsertText(aText.TextLength(), aOffset,
+                                      aStringToInsert.Length());
+    if (NS_FAILED(rv)) [[unlikely]] {
+      NS_WARNING("TextEditor::DidInsertText() failed");
+      return rv;
+    }
   }
+  return verify.IsExpectedData() ? NS_OK : NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
 }
 
-void EditorBase::DoDeleteText(Text& aText, uint32_t aOffset, uint32_t aCount,
-                              ErrorResult& aRv) {
+nsresult EditorBase::DoDeleteText(Text& aText, uint32_t aOffset,
+                                  uint32_t aCount) {
+  AutoTextEditVerifier verify(*this, aText);
+  verify.WillDeleteText(aOffset, aCount);
   if (IsTextEditor() && aCount > 0) {
     AsTextEditor()->WillDeleteText(aText.TextLength(), aOffset, aCount);
   }
-  AutoCharacterDataAPIWrapper charDataWrapper(*this, aText);
-  aRv = charDataWrapper.DeleteData(aOffset, aCount);
-  if (MOZ_UNLIKELY(aRv.Failed())) {
-    NS_WARNING("AutoCharacterDataAPIWrapper::DeleteData() failed");
-    return;
+  {
+    AutoCharacterDataAPIWrapper charDataWrapper(*this, aText);
+    nsresult rv = charDataWrapper.DeleteData(aOffset, aCount);
+    if (NS_WARN_IF(Destroyed())) {
+      return NS_ERROR_EDITOR_DESTROYED;
+    }
+    if (NS_FAILED(rv)) [[unlikely]] {
+      NS_WARNING("AutoCharacterDataAPIWrapper::DeleteData() failed");
+      return rv;
+    }
+    NS_WARNING_ASSERTION(!verify.IsExpectedData() ||
+                             charDataWrapper.IsExpectedResult(EmptyString()),
+                         "Deleting data caused other mutations, but ignored");
   }
-  NS_WARNING_ASSERTION(charDataWrapper.IsExpectedResult(EmptyString()),
-                       "Deleting data caused other mutations, but ignored");
+  return verify.IsExpectedData() ? NS_OK : NS_ERROR_EDITOR_UNEXPECTED_DOM_TREE;
 }
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/dom/base/test/gtest/TestCharacterDataBuffer.cpp b/dom/base/test/gtest/TestCharacterDataBuffer.cpp
index c41467dce09..6f4a9d7dd31 100644
--- a/dom/base/test/gtest/TestCharacterDataBuffer.cpp
+++ b/dom/base/test/gtest/TestCharacterDataBuffer.cpp
@@ -536,4 +536,108 @@ TEST(CharacterDataBufferTest, SafeAPIsChar1b)
   EXPECT_EQ(characterDataBuffer.SafeCharAt(100), u'\0');
 }
 
+TEST(CharacterDataBufferTest, EqualsString1b)
+{
+  const RefPtr<Document> doc = CreateHTMLDoc();
+  const RefPtr<nsTextNode> textNode = doc->CreateTextNode(EmptyString());
+  MOZ_RELEASE_ASSERT(textNode);
+  CharacterDataBuffer& characterDataBuffer =
+      const_cast<CharacterDataBuffer&>(textNode->DataBuffer());
+
+  MOZ_RELEASE_ASSERT(!characterDataBuffer.Is2b());
+  EXPECT_EQ(characterDataBuffer.Equals(EmptyString()), true);
+  EXPECT_EQ(characterDataBuffer.Equals(u"a"_ns), false);
+
+  characterDataBuffer.SetTo(u"abc"_ns, /* aUpdateBidi = */ false,
+                            /* aForce2b = */ false);
+  MOZ_RELEASE_ASSERT(!characterDataBuffer.Is2b());
+  EXPECT_EQ(characterDataBuffer.Equals(u"abc"_ns), true);
+  EXPECT_EQ(characterDataBuffer.Equals(u"abcd"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals(u"ab"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals(EmptyString()), false);
+
+  characterDataBuffer.SetTo(u"\xC0"_ns + u"bc"_ns, /* aUpdateBidi = */ false,
+                            /* aForce2b = */ false);
+  MOZ_RELEASE_ASSERT(!characterDataBuffer.Is2b());
+  EXPECT_EQ(characterDataBuffer.Equals(u"\xC0"_ns + u"bc"_ns), true);
+  EXPECT_EQ(characterDataBuffer.Equals(u"abc"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals(EmptyString()), false);
+}
+
+TEST(CharacterDataBufferTest, EqualsString2b)
+{
+  const RefPtr<Document> doc = CreateHTMLDoc();
+  const RefPtr<nsTextNode> textNode = doc->CreateTextNode(EmptyString());
+  MOZ_RELEASE_ASSERT(textNode);
+  CharacterDataBuffer& characterDataBuffer =
+      const_cast<CharacterDataBuffer&>(textNode->DataBuffer());
+
+  characterDataBuffer.SetTo(u"abc"_ns, /* aUpdateBidi = */ false,
+                            /* aForce2b = */ true);
+  MOZ_RELEASE_ASSERT(characterDataBuffer.Is2b());
+  EXPECT_EQ(characterDataBuffer.Equals(u"abc"_ns), true);
+  EXPECT_EQ(characterDataBuffer.Equals(u"abcd"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals(u"ab"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals(EmptyString()), false);
+
+  characterDataBuffer.SetTo(u"\xC0"_ns + u"bc"_ns, /* aUpdateBidi = */ false,
+                            /* aForce2b = */ true);
+  MOZ_RELEASE_ASSERT(characterDataBuffer.Is2b());
+  EXPECT_EQ(characterDataBuffer.Equals(u"\xC0"_ns + u"bc"_ns), true);
+  EXPECT_EQ(characterDataBuffer.Equals(u"abc"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals(EmptyString()), false);
+}
+
+TEST(CharacterDataBufferTest, EqualsCString1b)
+{
+  const RefPtr<Document> doc = CreateHTMLDoc();
+  const RefPtr<nsTextNode> textNode = doc->CreateTextNode(EmptyString());
+  MOZ_RELEASE_ASSERT(textNode);
+  CharacterDataBuffer& characterDataBuffer =
+      const_cast<CharacterDataBuffer&>(textNode->DataBuffer());
+
+  MOZ_RELEASE_ASSERT(!characterDataBuffer.Is2b());
+  EXPECT_EQ(characterDataBuffer.Equals(EmptyCString()), true);
+  EXPECT_EQ(characterDataBuffer.Equals("a"_ns), false);
+
+  characterDataBuffer.SetTo(u"abc"_ns, /* aUpdateBidi = */ false,
+                            /* aForce2b = */ false);
+  MOZ_RELEASE_ASSERT(!characterDataBuffer.Is2b());
+  EXPECT_EQ(characterDataBuffer.Equals("abc"_ns), true);
+  EXPECT_EQ(characterDataBuffer.Equals("abcd"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals("ab"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals(EmptyCString()), false);
+
+  characterDataBuffer.SetTo(u"\xC0"_ns + u"bc"_ns, /* aUpdateBidi = */ false,
+                            /* aForce2b = */ false);
+  MOZ_RELEASE_ASSERT(!characterDataBuffer.Is2b());
+  EXPECT_EQ(characterDataBuffer.Equals("\xC0"_ns + "bc"_ns), true);
+  EXPECT_EQ(characterDataBuffer.Equals("abc"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals(EmptyCString()), false);
+}
+
+TEST(CharacterDataBufferTest, EqualsCString2b)
+{
+  const RefPtr<Document> doc = CreateHTMLDoc();
+  const RefPtr<nsTextNode> textNode = doc->CreateTextNode(EmptyString());
+  MOZ_RELEASE_ASSERT(textNode);
+  CharacterDataBuffer& characterDataBuffer =
+      const_cast<CharacterDataBuffer&>(textNode->DataBuffer());
+
+  characterDataBuffer.SetTo(u"abc"_ns, /* aUpdateBidi = */ false,
+                            /* aForce2b = */ true);
+  MOZ_RELEASE_ASSERT(characterDataBuffer.Is2b());
+  EXPECT_EQ(characterDataBuffer.Equals("abc"_ns), true);
+  EXPECT_EQ(characterDataBuffer.Equals("abcd"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals("ab"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals(EmptyCString()), false);
+
+  characterDataBuffer.SetTo(u"\xC0"_ns + u"bc"_ns, /* aUpdateBidi = */ false,
+                            /* aForce2b = */ true);
+  MOZ_RELEASE_ASSERT(characterDataBuffer.Is2b());
+  EXPECT_EQ(characterDataBuffer.Equals("\xC0"_ns + "bc"_ns), true);
+  EXPECT_EQ(characterDataBuffer.Equals("abc"_ns), false);
+  EXPECT_EQ(characterDataBuffer.Equals(EmptyCString()), false);
+}
+
 };  // namespace mozilla::dom
Loading diff…