Title: [294955] trunk
Revision
294955
Author
drou...@apple.com
Date
2022-05-27 14:50:26 -0700 (Fri, 27 May 2022)

Log Message

CrashTracer: com.apple.WebKit.WebContent at com.apple.WebKit: WebKit::TextCheckingControllerProxy::annotatedSubstringBetweenPositions
https://bugs.webkit.org/show_bug.cgi?id=239909
<rdar://problem/87885717>

Reviewed by Wenson Hsieh.

This exception happens when trying to add attributes for text that contains collapsed whitespace and
has also been wrapped due to the size of its parent container. The exception specifically is about
trying to add attributes beyond the current length of a `NSAttributedString`.

* Source/WebCore/editing/TextIteratorBehavior.h:
* Source/WebCore/editing/TextIterator.cpp:
(WebCore::TextIterator::handleTextRun):
In the case that `m_lastTextNodeEndedWithCollapsedSpace`, we only want to add the remaining text if
we're still within the desired portion of the `m_textRun`. Otherwise, we'll iterate over too much of
the text and result in a string that's longer than what would be the case if one manually calculated
it from the given `offset` and `offsetEnd`. Add a new `TextIteratorBehavior::IgnoresWhiteSpaceAtEndOfRun`
to not include the trailing whitespace.

* Source/WebKit/WebProcess/WebPage/Cocoa/TextCheckingControllerProxy.mm:
(WebKit::TextCheckingControllerProxy::annotatedSubstringBetweenPositions):
Use the new `WebCore::TextIteratorBehavior::IgnoresWhiteSpaceAtEndOfRun` to not include the trailing
whitespace.
Also add some defensive checks just in case.

* Source/WebCore/testing/Internals.idl:
* Source/WebCore/testing/Internals.h:
* Source/WebCore/testing/Internals.cpp:
(WebCore::toTextIteratorBehaviors): Added.
(WebCore::Internals::locationFromRange):
(WebCore::Internals::lengthFromRange):
(WebCore::Internals::statesOfTextIterator): Added.
Add a way to provide `TextIteratorBehaviors` to methods that use `TextIterator`.
Add a method that gets the `text` and `range` of a `TextIterator` after every `advance`.

* LayoutTests/editing/text-iterator/sequential-collapsed-ranges.html: Added.
* LayoutTests/editing/text-iterator/sequential-collapsed-ranges-expected.txt: Added.
* LayoutTests/editing/text-iterator/subrange-with-trailing-collapsed-whitespace.html: Added.
* LayoutTests/editing/text-iterator/subrange-with-trailing-collapsed-whitespace-expected.txt: Added.

Canonical link: https://commits.webkit.org/251061@main

Modified Paths

Added Paths

Diff

Added: trunk/LayoutTests/editing/text-iterator/sequential-collapsed-ranges-expected.txt (0 => 294955)


--- trunk/LayoutTests/editing/text-iterator/sequential-collapsed-ranges-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/text-iterator/sequential-collapsed-ranges-expected.txt	2022-05-27 21:50:26 UTC (rev 294955)
@@ -0,0 +1,53 @@
+a b
+
+Testing first letter with narrow width...
+  Default behavior
+    'a' (location 0 length 1)
+  IgnoresWhiteSpaceAtEndOfRun
+    'a' (location 0 length 1)
+
+Testing first letter with wide width...
+  Default behavior
+    'a' (location 0 length 1)
+  IgnoresWhiteSpaceAtEndOfRun
+    'a' (location 0 length 1)
+
+Testing everything except last letter with narrow width...
+  Default behavior
+    'a' (location 0 length 1)
+    ' ' (location 1 length 0)
+  IgnoresWhiteSpaceAtEndOfRun
+    'a' (location 0 length 1)
+
+Testing everything except last letter with wide width...
+  Default behavior
+    'a ' (location 0 length 2)
+  IgnoresWhiteSpaceAtEndOfRun
+    'a ' (location 0 length 2)
+
+Testing everything with narrow width...
+  Default behavior
+    'a' (location 0 length 1)
+    ' ' (location 1 length 0)
+    'b' (location 2 length 1)
+    '\n' (location 3 length 0)
+  IgnoresWhiteSpaceAtEndOfRun
+    'a' (location 0 length 1)
+    ' ' (location 1 length 0)
+    'b' (location 1 length 1)
+    '\n' (location 3 length 0)
+
+Testing everything with wide width...
+  Default behavior
+    'a ' (location 0 length 2)
+    'b' (location 2 length 1)
+    '\n' (location 3 length 0)
+  IgnoresWhiteSpaceAtEndOfRun
+    'a ' (location 0 length 2)
+    'b' (location 2 length 1)
+    '\n' (location 3 length 0)
+
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/editing/text-iterator/sequential-collapsed-ranges.html (0 => 294955)


--- trunk/LayoutTests/editing/text-iterator/sequential-collapsed-ranges.html	                        (rev 0)
+++ trunk/LayoutTests/editing/text-iterator/sequential-collapsed-ranges.html	2022-05-27 21:50:26 UTC (rev 294955)
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+</head>
+<body>
+<div id="test">a    <span>    </span>    <span>    </span>    b</div>
+<div id="console"></div>
+<script>
+
+if (!window.internals)
+    testFailed("This test requires internals object");
+
+debug("");
+
+let container = document.getElementById("test");
+
+function test(description, rangeToSelect) {
+    let selection = window.getSelection();
+    selection.removeAllRanges();
+    selection.addRange(rangeToSelect);
+
+    debug(description);
+
+    [
+        [ ],
+        [ "IgnoresWhiteSpaceAtEndOfRun" ],
+    ].forEach((behaviors) => {
+        debug("  " + (behaviors.join("|") || "Default behavior"));
+        for (let {text, range} of internals.statesOfTextIterator(rangeToSelect, behaviors))
+            debug(`    '${text.replace("\n", "\\n")}' (location ${internals.locationFromRange(container, range, behaviors)} length ${internals.lengthFromRange(container, range, behaviors)})`);
+    });
+
+    debug("");
+}
+
+let rangeOfFirstLetter = document.createRange();
+rangeOfFirstLetter.setStart(container.firstChild, 0);
+rangeOfFirstLetter.setEnd(container.firstChild, 1);
+
+container.style.width = "1ch";
+test("Testing first letter with narrow width...", rangeOfFirstLetter);
+
+container.style.width = "";
+test("Testing first letter with wide width...", rangeOfFirstLetter);
+
+let rangeUntilLastLetter = document.createRange();
+rangeUntilLastLetter.setStart(container.firstChild, 0);
+rangeUntilLastLetter.setEnd(container.lastChild, 4);
+
+container.style.width = "1ch";
+test("Testing everything except last letter with narrow width...", rangeUntilLastLetter);
+
+container.style.width = "";
+test("Testing everything except last letter with wide width...", rangeUntilLastLetter);
+
+let rangeOfNode = document.createRange();
+rangeOfNode.selectNode(container);
+
+container.style.width = "1ch";
+test("Testing everything with narrow width...", rangeOfNode);
+
+container.style.width = "";
+test("Testing everything with wide width...", rangeOfNode);
+
+</script>
+</body>
+</html>

Added: trunk/LayoutTests/editing/text-iterator/subrange-with-trailing-collapsed-whitespace-expected.txt (0 => 294955)


--- trunk/LayoutTests/editing/text-iterator/subrange-with-trailing-collapsed-whitespace-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/editing/text-iterator/subrange-with-trailing-collapsed-whitespace-expected.txt	2022-05-27 21:50:26 UTC (rev 294955)
@@ -0,0 +1,33 @@
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+
+Testing first line...
+  Default behavior
+    'Lorem ipsum' (location 0 length 12)
+    ' ' (location 12 length 0)
+  IgnoresWhiteSpaceAtEndOfRun
+    'Lorem ipsum' (location 0 length 11)
+
+Testing second line...
+  Default behavior
+    'dolor sit amet,' (location 12 length 16)
+    ' ' (location 28 length 0)
+  IgnoresWhiteSpaceAtEndOfRun
+    'dolor sit amet,' (location 11 length 15)
+
+Testing third line...
+  Default behavior
+    'consectetur' (location 28 length 12)
+    ' ' (location 40 length 0)
+  IgnoresWhiteSpaceAtEndOfRun
+    'consectetur' (location 27 length 11)
+
+Testing fourth line...
+  Default behavior
+    'adipiscing elit.' (location 40 length 16)
+  IgnoresWhiteSpaceAtEndOfRun
+    'adipiscing elit.' (location 39 length 16)
+
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/editing/text-iterator/subrange-with-trailing-collapsed-whitespace.html (0 => 294955)


--- trunk/LayoutTests/editing/text-iterator/subrange-with-trailing-collapsed-whitespace.html	                        (rev 0)
+++ trunk/LayoutTests/editing/text-iterator/subrange-with-trailing-collapsed-whitespace.html	2022-05-27 21:50:26 UTC (rev 294955)
@@ -0,0 +1,59 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+</head>
+<body>
+<div id="test" style="width: 100px">Lorem ipsum dolor sit amet, consectetur adipiscing elit.</div>
+<div id="console"></div>
+<script>
+
+if (!window.internals)
+    testFailed("This test requires internals object");
+
+debug("");
+
+let container = document.getElementById("test");
+
+function test(description, rangeToSelect) {
+    let selection = window.getSelection();
+    selection.removeAllRanges();
+    selection.addRange(rangeToSelect);
+
+    debug(description);
+
+    [
+        [ ],
+        [ "IgnoresWhiteSpaceAtEndOfRun" ],
+    ].forEach((behaviors) => {
+        debug("  " + (behaviors.join("|") || "Default behavior"));
+        for (let {text, range} of internals.statesOfTextIterator(rangeToSelect, behaviors))
+            debug(`    '${text}' (location ${internals.locationFromRange(container, range, behaviors)} length ${internals.lengthFromRange(container, range, behaviors)})`);
+    });
+
+    debug("");
+}
+
+let rangeOfFirstLine = document.createRange();
+rangeOfFirstLine.setStart(container.firstChild, 0);
+rangeOfFirstLine.setEnd(container.firstChild, 12);
+test("Testing first line...", rangeOfFirstLine);
+
+let rangeOfSecondLine = document.createRange();
+rangeOfSecondLine.setStart(container.firstChild, 12);
+rangeOfSecondLine.setEnd(container.firstChild, 28);
+test("Testing second line...", rangeOfSecondLine);
+
+let rangeOfThirdLine = document.createRange();
+rangeOfThirdLine.setStart(container.firstChild, 28);
+rangeOfThirdLine.setEnd(container.firstChild, 40);
+test("Testing third line...", rangeOfThirdLine);
+
+let rangeOfFourthLine = document.createRange();
+rangeOfFourthLine.setStart(container.firstChild, 40);
+rangeOfFourthLine.setEnd(container.firstChild, 56);
+test("Testing fourth line...", rangeOfFourthLine);
+
+</script>
+</body>
+</html>

Modified: trunk/Source/WebCore/editing/TextIterator.cpp (294954 => 294955)


--- trunk/Source/WebCore/editing/TextIterator.cpp	2022-05-27 21:37:00 UTC (rev 294954)
+++ trunk/Source/WebCore/editing/TextIterator.cpp	2022-05-27 21:50:26 UTC (rev 294955)
@@ -623,9 +623,20 @@
         unsigned textRunStart = m_textRun->start();
         unsigned runStart = std::max(textRunStart, start);
 
+        unsigned textRunEnd = textRunStart + m_textRun->length();
+        unsigned runEnd = std::min(textRunEnd, end);
+
+        // Determine what the next text run will be, but don't advance yet
+        auto nextTextRun = InlineIterator::nextTextBoxInLogicalOrder(m_textRun, m_textRunLogicalOrderCache);
+
         // Check for collapsed space at the start of this run.
         bool needSpace = m_lastTextNodeEndedWithCollapsedSpace || (m_textRun == firstTextRun && textRunStart == runStart && runStart);
         if (needSpace && !renderer.style().isCollapsibleWhiteSpace(m_lastCharacter) && m_lastCharacter) {
+            if (runStart >= runEnd && m_behaviors.contains(TextIteratorBehavior::IgnoresWhiteSpaceAtEndOfRun)) {
+                m_textRun = nextTextRun;
+                continue;
+            }
+
             if (m_lastTextNode == &textNode && runStart && rendererText[runStart - 1] == ' ') {
                 unsigned spaceRunStart = runStart - 1;
                 while (spaceRunStart && rendererText[spaceRunStart - 1] == ' ')
@@ -635,11 +646,6 @@
                 emitCharacter(' ', textNode, nullptr, runStart, runStart);
             return;
         }
-        unsigned textRunEnd = textRunStart + m_textRun->length();
-        unsigned runEnd = std::min(textRunEnd, end);
-        
-        // Determine what the next text run will be, but don't advance yet
-        auto nextTextRun = InlineIterator::nextTextBoxInLogicalOrder(m_textRun, m_textRunLogicalOrderCache);
 
         if (runStart < runEnd) {
             auto isNewlineOrTab = [&](UChar character) {

Modified: trunk/Source/WebCore/editing/TextIteratorBehavior.h (294954 => 294955)


--- trunk/Source/WebCore/editing/TextIteratorBehavior.h	2022-05-27 21:37:00 UTC (rev 294954)
+++ trunk/Source/WebCore/editing/TextIteratorBehavior.h	2022-05-27 21:50:26 UTC (rev 294955)
@@ -61,6 +61,8 @@
     TraversesFlatTree = 1 << 9,
 
     EntersImageOverlays = 1 << 10,
+
+    IgnoresWhiteSpaceAtEndOfRun = 1 << 11,
 };
 
 using TextIteratorBehaviors = OptionSet<TextIteratorBehavior>;

Modified: trunk/Source/WebCore/testing/Internals.cpp (294954 => 294955)


--- trunk/Source/WebCore/testing/Internals.cpp	2022-05-27 21:37:00 UTC (rev 294954)
+++ trunk/Source/WebCore/testing/Internals.cpp	2022-05-27 21:50:26 UTC (rev 294955)
@@ -2184,19 +2184,29 @@
     return { };
 }
 
+static TextIteratorBehaviors toTextIteratorBehaviors(const Vector<String>& stringBehaviors)
+{
+    TextIteratorBehaviors behaviors;
+    for (const auto& stringBehavior : stringBehaviors) {
+        if (stringBehavior == "IgnoresWhiteSpaceAtEndOfRun"_s)
+            behaviors.add(TextIteratorBehavior::IgnoresWhiteSpaceAtEndOfRun);
+    }
+    return behaviors;
+}
+
 RefPtr<Range> Internals::rangeFromLocationAndLength(Element& scope, unsigned rangeLocation, unsigned rangeLength)
 {
     return createLiveRange(resolveCharacterRange(makeRangeSelectingNodeContents(scope), { rangeLocation, rangeLength }));
 }
 
-unsigned Internals::locationFromRange(Element& scope, const Range& range)
+unsigned Internals::locationFromRange(Element& scope, const Range& range, const Vector<String>& stringBehaviors)
 {
-    return clampTo<unsigned>(characterRange(makeBoundaryPointBeforeNodeContents(scope), makeSimpleRange(range)).location);
+    return clampTo<unsigned>(characterRange(makeBoundaryPointBeforeNodeContents(scope), makeSimpleRange(range), toTextIteratorBehaviors(stringBehaviors)).location);
 }
 
-unsigned Internals::lengthFromRange(Element& scope, const Range& range)
+unsigned Internals::lengthFromRange(Element& scope, const Range& range, const Vector<String>& stringBehaviors)
 {
-    return clampTo<unsigned>(characterRange(makeBoundaryPointBeforeNodeContents(scope), makeSimpleRange(range)).length);
+    return clampTo<unsigned>(characterRange(makeBoundaryPointBeforeNodeContents(scope), makeSimpleRange(range), toTextIteratorBehaviors(stringBehaviors)).length);
 }
 
 String Internals::rangeAsText(const Range& liveRange)
@@ -2240,6 +2250,17 @@
     return createLiveRange(findClosestPlainText(range, text, { }, targetOffset));
 }
 
+Vector<Internals::TextIteratorState> Internals::statesOfTextIterator(const Range& liveRange, const Vector<String>& stringBehaviors)
+{
+    auto simpleRange = makeSimpleRange(liveRange);
+    simpleRange.start.document().updateLayout();
+
+    Vector<TextIteratorState> states;
+    for (TextIterator it(simpleRange, toTextIteratorBehaviors(stringBehaviors)); !it.atEnd(); it.advance())
+        states.append({ it.text().toString(), createLiveRange(it.range()) });
+    return states;
+}
+
 #if !PLATFORM(MAC)
 ExceptionOr<RefPtr<Range>> Internals::rangeForDictionaryLookupAtLocation(int, int)
 {

Modified: trunk/Source/WebCore/testing/Internals.h (294954 => 294955)


--- trunk/Source/WebCore/testing/Internals.h	2022-05-27 21:37:00 UTC (rev 294954)
+++ trunk/Source/WebCore/testing/Internals.h	2022-05-27 21:50:26 UTC (rev 294955)
@@ -352,8 +352,8 @@
     ExceptionOr<void> invalidateControlTints();
 
     RefPtr<Range> rangeFromLocationAndLength(Element& scope, unsigned rangeLocation, unsigned rangeLength);
-    unsigned locationFromRange(Element& scope, const Range&);
-    unsigned lengthFromRange(Element& scope, const Range&);
+    unsigned locationFromRange(Element& scope, const Range&, const Vector<String>& behaviors = { });
+    unsigned lengthFromRange(Element& scope, const Range&, const Vector<String>& behaviors = { });
     String rangeAsText(const Range&);
     String rangeAsTextUsingBackwardsTextIterator(const Range&);
     Ref<Range> subrange(Range&, unsigned rangeLocation, unsigned rangeLength);
@@ -360,6 +360,12 @@
     ExceptionOr<RefPtr<Range>> rangeForDictionaryLookupAtLocation(int x, int y);
     RefPtr<Range> rangeOfStringNearLocation(const Range&, const String&, unsigned);
 
+    struct TextIteratorState {
+        String text;
+        RefPtr<Range> range;
+    };
+    Vector<TextIteratorState> statesOfTextIterator(const Range&, const Vector<String>& behaviors = { });
+
     ExceptionOr<void> setDelegatesScrolling(bool enabled);
 
     ExceptionOr<uint64_t> lastSpellCheckRequestSequence();

Modified: trunk/Source/WebCore/testing/Internals.idl (294954 => 294955)


--- trunk/Source/WebCore/testing/Internals.idl	2022-05-27 21:37:00 UTC (rev 294954)
+++ trunk/Source/WebCore/testing/Internals.idl	2022-05-27 21:50:26 UTC (rev 294955)
@@ -291,6 +291,14 @@
 [
     ExportMacro=WEBCORE_TESTSUPPORT_EXPORT,
     JSGenerateToJSObject,
+] dictionary TextIteratorState {
+    DOMString text;
+    Range range;
+};
+
+[
+    ExportMacro=WEBCORE_TESTSUPPORT_EXPORT,
+    JSGenerateToJSObject,
 ] dictionary ImageOverlayText {
     required DOMString text;
     required DOMPointReadOnly topLeft;
@@ -479,13 +487,14 @@
     undefined scrollElementToRect(Element element, long x, long y, long w, long h);
 
     Range? rangeFromLocationAndLength(Element scope, unsigned long rangeLocation, unsigned long rangeLength);
-    unsigned long locationFromRange(Element scope, Range range);
-    unsigned long lengthFromRange(Element scope, Range range);
+    unsigned long locationFromRange(Element scope, Range range, optional sequence<DOMString> behaviors = []);
+    unsigned long lengthFromRange(Element scope, Range range, optional sequence<DOMString> behaviors = []);
     DOMString rangeAsText(Range range);
     DOMString rangeAsTextUsingBackwardsTextIterator(Range range);
     Range subrange(Range range, unsigned long rangeLocation, unsigned long rangeLength);
     Range? rangeForDictionaryLookupAtLocation(long x, long y);
     Range? rangeOfStringNearLocation(Range range, DOMString text, long targetOffset);
+    sequence<TextIteratorState> statesOfTextIterator(Range range, optional sequence<DOMString> behaviors = []);
 
     undefined setDelegatesScrolling(boolean enabled);
 

Modified: trunk/Source/WebKit/WebProcess/WebPage/Cocoa/TextCheckingControllerProxy.mm (294954 => 294955)


--- trunk/Source/WebKit/WebProcess/WebPage/Cocoa/TextCheckingControllerProxy.mm	2022-05-27 21:37:00 UTC (rev 294954)
+++ trunk/Source/WebKit/WebProcess/WebPage/Cocoa/TextCheckingControllerProxy.mm	2022-05-27 21:50:26 UTC (rev 294955)
@@ -171,7 +171,9 @@
 
     auto string = adoptNS([[NSMutableAttributedString alloc] init]);
 
-    for (TextIterator it(*entireRange); !it.atEnd(); it.advance()) {
+    constexpr TextIteratorBehaviors behaviors { TextIteratorBehavior::IgnoresWhiteSpaceAtEndOfRun };
+
+    for (TextIterator it(*entireRange, behaviors); !it.atEnd(); it.advance()) {
         if (!it.text().length())
             continue;
         [string appendAttributedString:adoptNS([[NSAttributedString alloc] initWithString:it.text().createNSStringWithoutCopying().get()]).get()];
@@ -178,8 +180,11 @@
         auto range = it.range();
         for (auto* marker : range.start.document().markers().markersInRange(range, DocumentMarker::PlatformTextChecking)) {
             auto& data = ""
-            auto subrange = resolveCharacterRange(range, { marker->startOffset(), marker->endOffset() - marker->startOffset() });
-            [string addAttribute:data.key value:data.value range:characterRange(*entireRange, subrange)];
+            auto subrange = resolveCharacterRange(range, { marker->startOffset(), marker->endOffset() - marker->startOffset() }, behaviors);
+            auto attributeRange = characterRange(*entireRange, subrange, behaviors);
+            ASSERT(attributeRange.location + attributeRange.length <= [string length]);
+            if (attributeRange.location + attributeRange.length <= [string length])
+                [string addAttribute:data.key value:data.value range:WTFMove(attributeRange)];
         }
     }
 
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to