Commit cc8ec3cb authored by Victor Fei's avatar Victor Fei Committed by Commit Bot

Fix AXPosition::AtEndOfTextSpan to not rely on CreateParentPosition

AXPosition::AtEndOfTextSpan relies on CreateParentPosition to
determine if an inline text box marks the end of text span. Since
CreateParentPosition calls into AtStartOfLine which calls
AtEndOfTextSpan so we end up with an infinite loop and stack overflow.

https://crrev.com/c/2245611 fixed most cases with this infinite loop
of AtStartOfLine/CreateParentPosition/AtEndOfTextSpan by checking for
AtEndOfAnchor first before calling AtEndOfTextSpan.

However consider the following example:
<style>
  .required-label::after {
    content: " *";
  }
</style>
<label class="required-label">Required </label>

labelText #1
++staticText name='Required ' nextOnLineId=inlineTextBox  #2
++++inlineTextBox name='Required ' nextOnLineId=inlineTextBox #3
++staticText name=' *' previousOnLineId=inlineTextBox #4
++++inlineTextBox name='*' previousOnLineId=inlineTextBox #5

AtEndOfAnchor check would not prevent this infinite loop, if
AtStartOfLine is called on staticText #4 name=' *' as the max text
offsets differ between staticText #4 and its inlineText #5.

This CL fixes the above issue by making AtEndOfTextSpan not to call
CreateParentPosition. Instead it determines end of text span by
directly looking at the current position's parent role and its index
among its siblings.

AX-Relnotes: Fixes hangs in certain accessibility scenarios.

Bug: 1120636
Change-Id: Ic99c27b069f220f26da736b3946560ef23d6b622
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2369681
Commit-Queue: Victor Fei <vicfei@microsoft.com>
Reviewed-by: default avatarNektarios Paisios <nektar@chromium.org>
Cr-Commit-Position: refs/heads/master@{#804531}
parent c8254d64
...@@ -104,6 +104,13 @@ int BrowserAccessibilityPosition::AnchorIndexInParent() const { ...@@ -104,6 +104,13 @@ int BrowserAccessibilityPosition::AnchorIndexInParent() const {
: AXPosition::INVALID_INDEX; : AXPosition::INVALID_INDEX;
} }
int BrowserAccessibilityPosition::AnchorSiblingCount() const {
BrowserAccessibility* parent = GetAnchor()->PlatformGetParent();
if (parent)
return static_cast<int>(parent->InternalChildCount());
return 0;
}
base::stack<BrowserAccessibility*> base::stack<BrowserAccessibility*>
BrowserAccessibilityPosition::GetAncestorAnchors() const { BrowserAccessibilityPosition::GetAncestorAnchors() const {
base::stack<BrowserAccessibility*> anchors; base::stack<BrowserAccessibility*> anchors;
...@@ -186,11 +193,16 @@ bool BrowserAccessibilityPosition::IsInLineBreakingObject() const { ...@@ -186,11 +193,16 @@ bool BrowserAccessibilityPosition::IsInLineBreakingObject() const {
!GetAnchor()->IsInListMarker(); !GetAnchor()->IsInListMarker();
} }
ax::mojom::Role BrowserAccessibilityPosition::GetRole() const { ax::mojom::Role BrowserAccessibilityPosition::GetAnchorRole() const {
if (IsNullPosition()) if (IsNullPosition())
return ax::mojom::Role::kNone; return ax::mojom::Role::kNone;
DCHECK(GetAnchor()); DCHECK(GetAnchor());
return GetAnchor()->GetRole(); return GetRole(GetAnchor());
}
ax::mojom::Role BrowserAccessibilityPosition::GetRole(
BrowserAccessibility* node) const {
return node->GetRole();
} }
ui::AXNodeTextStyles BrowserAccessibilityPosition::GetTextStyles() const { ui::AXNodeTextStyles BrowserAccessibilityPosition::GetTextStyles() const {
......
...@@ -43,6 +43,7 @@ class CONTENT_EXPORT BrowserAccessibilityPosition ...@@ -43,6 +43,7 @@ class CONTENT_EXPORT BrowserAccessibilityPosition
int AnchorChildCount() const override; int AnchorChildCount() const override;
int AnchorUnignoredChildCount() const override; int AnchorUnignoredChildCount() const override;
int AnchorIndexInParent() const override; int AnchorIndexInParent() const override;
int AnchorSiblingCount() const override;
base::stack<BrowserAccessibility*> GetAncestorAnchors() const override; base::stack<BrowserAccessibility*> GetAncestorAnchors() const override;
BrowserAccessibility* GetLowestUnignoredAncestor() const override; BrowserAccessibility* GetLowestUnignoredAncestor() const override;
void AnchorParent(AXTreeID* tree_id, void AnchorParent(AXTreeID* tree_id,
...@@ -55,7 +56,8 @@ class CONTENT_EXPORT BrowserAccessibilityPosition ...@@ -55,7 +56,8 @@ class CONTENT_EXPORT BrowserAccessibilityPosition
bool IsEmbeddedObjectInParent() const override; bool IsEmbeddedObjectInParent() const override;
bool IsInLineBreakingObject() const override; bool IsInLineBreakingObject() const override;
ax::mojom::Role GetRole() const override; ax::mojom::Role GetAnchorRole() const override;
ax::mojom::Role GetRole(BrowserAccessibility* node) const override;
ui::AXNodeTextStyles GetTextStyles() const override; ui::AXNodeTextStyles GetTextStyles() const override;
std::vector<int32_t> GetWordStartOffsets() const override; std::vector<int32_t> GetWordStartOffsets() const override;
std::vector<int32_t> GetWordEndOffsets() const override; std::vector<int32_t> GetWordEndOffsets() const override;
......
...@@ -99,6 +99,14 @@ int AXNodePosition::AnchorIndexInParent() const { ...@@ -99,6 +99,14 @@ int AXNodePosition::AnchorIndexInParent() const {
return GetAnchor() ? int{GetAnchor()->index_in_parent()} : INVALID_INDEX; return GetAnchor() ? int{GetAnchor()->index_in_parent()} : INVALID_INDEX;
} }
int AXNodePosition::AnchorSiblingCount() const {
AXNode* parent = GetAnchor()->GetUnignoredParent();
if (parent)
return static_cast<int>(parent->GetUnignoredChildCount());
return 0;
}
base::stack<AXNode*> AXNodePosition::GetAncestorAnchors() const { base::stack<AXNode*> AXNodePosition::GetAncestorAnchors() const {
base::stack<AXNode*> anchors; base::stack<AXNode*> anchors;
AXNode* current_anchor = GetAnchor(); AXNode* current_anchor = GetAnchor();
...@@ -283,11 +291,15 @@ bool AXNodePosition::IsInLineBreakingObject() const { ...@@ -283,11 +291,15 @@ bool AXNodePosition::IsInLineBreakingObject() const {
!GetAnchor()->IsInListMarker(); !GetAnchor()->IsInListMarker();
} }
ax::mojom::Role AXNodePosition::GetRole() const { ax::mojom::Role AXNodePosition::GetAnchorRole() const {
if (IsNullPosition()) if (IsNullPosition())
return ax::mojom::Role::kNone; return ax::mojom::Role::kNone;
DCHECK(GetAnchor()); DCHECK(GetAnchor());
return GetAnchor()->data().role; return GetRole(GetAnchor());
}
ax::mojom::Role AXNodePosition::GetRole(AXNode* node) const {
return node->data().role;
} }
AXNodeTextStyles AXNodePosition::GetTextStyles() const { AXNodeTextStyles AXNodePosition::GetTextStyles() const {
......
...@@ -49,6 +49,7 @@ class AX_EXPORT AXNodePosition : public AXPosition<AXNodePosition, AXNode> { ...@@ -49,6 +49,7 @@ class AX_EXPORT AXNodePosition : public AXPosition<AXNodePosition, AXNode> {
int AnchorChildCount() const override; int AnchorChildCount() const override;
int AnchorUnignoredChildCount() const override; int AnchorUnignoredChildCount() const override;
int AnchorIndexInParent() const override; int AnchorIndexInParent() const override;
int AnchorSiblingCount() const override;
base::stack<AXNode*> GetAncestorAnchors() const override; base::stack<AXNode*> GetAncestorAnchors() const override;
AXNode* GetLowestUnignoredAncestor() const override; AXNode* GetLowestUnignoredAncestor() const override;
void AnchorParent(AXTreeID* tree_id, AXNode::AXID* parent_id) const override; void AnchorParent(AXTreeID* tree_id, AXNode::AXID* parent_id) const override;
...@@ -58,7 +59,8 @@ class AX_EXPORT AXNodePosition : public AXPosition<AXNodePosition, AXNode> { ...@@ -58,7 +59,8 @@ class AX_EXPORT AXNodePosition : public AXPosition<AXNodePosition, AXNode> {
bool IsEmbeddedObjectInParent() const override; bool IsEmbeddedObjectInParent() const override;
bool IsInLineBreakingObject() const override; bool IsInLineBreakingObject() const override;
ax::mojom::Role GetRole() const override; ax::mojom::Role GetAnchorRole() const override;
ax::mojom::Role GetRole(AXNode* node) const override;
AXNodeTextStyles GetTextStyles() const override; AXNodeTextStyles GetTextStyles() const override;
std::vector<int32_t> GetWordStartOffsets() const override; std::vector<int32_t> GetWordStartOffsets() const override;
std::vector<int32_t> GetWordEndOffsets() const override; std::vector<int32_t> GetWordEndOffsets() const override;
......
...@@ -1260,6 +1260,59 @@ TEST_F(AXPositionTest, AtStartOfLineWithTextPosition) { ...@@ -1260,6 +1260,59 @@ TEST_F(AXPositionTest, AtStartOfLineWithTextPosition) {
EXPECT_FALSE(text_position->AtStartOfLine()); EXPECT_FALSE(text_position->AtStartOfLine());
} }
TEST_F(AXPositionTest, AtStartOfLineStaticTextExtraPrecedingSpace) {
// Consider the following web content:
// <style>
// .required-label::after {
// content: " *";
// }
// </style>
// <label class="required-label">Required </label>
//
// Which has the following AXTree, where the static text (#3)
// contains an extra preceding space compared to its inline text (#4).
// ++1 kRootWebArea
// ++++2 kLabelText
// ++++++3 kStaticText name=" *"
// ++++++++4 kInlineTextBox name="*"
// This test ensures that this difference between static text and its inline
// text box does not cause a hang when AtStartOfLine is called on static text
// with text position " <*>".
AXNodeData root;
root.id = 1;
root.role = ax::mojom::Role::kRootWebArea;
// "kIsLineBreakingObject" is not strictly necessary but is added for
// completeness.
root.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject, true);
AXNodeData label_text;
label_text.id = 2;
label_text.role = ax::mojom::Role::kLabelText;
AXNodeData static_text1;
static_text1.id = 3;
static_text1.role = ax::mojom::Role::kStaticText;
static_text1.SetName(" *");
AXNodeData inline_text1;
inline_text1.id = 4;
inline_text1.role = ax::mojom::Role::kInlineTextBox;
inline_text1.SetName("*");
static_text1.child_ids = {inline_text1.id};
root.child_ids = {static_text1.id};
SetTree(CreateAXTree({root, static_text1, inline_text1}));
// Calling AtStartOfLine on |static_text1| with position " <*>",
// text_offset_=1, should not get into an infinite loop; it should be
// guaranteed to terminate.
TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), static_text1.id, 1 /* child_index */,
ax::mojom::TextAffinity::kDownstream);
ASSERT_FALSE(text_position->AtStartOfLine());
}
TEST_F(AXPositionTest, AtEndOfLineWithTextPosition) { TEST_F(AXPositionTest, AtEndOfLineWithTextPosition) {
TestPositionType text_position = AXNodePosition::CreateTextPosition( TestPositionType text_position = AXNodePosition::CreateTextPosition(
GetTreeID(), inline_box1_.id, 5 /* text_offset */, GetTreeID(), inline_box1_.id, 5 /* text_offset */,
......
...@@ -968,7 +968,7 @@ class AXPosition { ...@@ -968,7 +968,7 @@ class AXPosition {
bool AtStartOfDocument() const { bool AtStartOfDocument() const {
if (IsNullPosition()) if (IsNullPosition())
return false; return false;
return IsDocument(GetRole()) && AtStartOfAnchor(); return IsDocument(GetAnchorRole()) && AtStartOfAnchor();
} }
bool AtEndOfDocument() const { bool AtEndOfDocument() const {
...@@ -3148,8 +3148,8 @@ class AXPosition { ...@@ -3148,8 +3148,8 @@ class AXPosition {
// infinite loop. However, GetAnchor()->IsIgnored() is sufficient here // infinite loop. However, GetAnchor()->IsIgnored() is sufficient here
// because we know that the anchor at this position doesn't have an // because we know that the anchor at this position doesn't have an
// unignored child, making this a leaf tree or text position. // unignored child, making this a leaf tree or text position.
return !GetAnchor()->IsIgnored() && !IsDocument(GetRole()) && return !GetAnchor()->IsIgnored() && !IsDocument(GetAnchorRole()) &&
!IsInTextObject() && !IsIframe(GetRole()); !IsInTextObject() && !IsIframe(GetAnchorRole());
} }
bool IsInDescendantOfEmptyObject() const { bool IsInDescendantOfEmptyObject() const {
...@@ -3343,6 +3343,7 @@ class AXPosition { ...@@ -3343,6 +3343,7 @@ class AXPosition {
// When we call the following method on TextField, it would return 1. // When we call the following method on TextField, it would return 1.
virtual int AnchorUnignoredChildCount() const = 0; virtual int AnchorUnignoredChildCount() const = 0;
virtual int AnchorIndexInParent() const = 0; virtual int AnchorIndexInParent() const = 0;
virtual int AnchorSiblingCount() const = 0;
virtual base::stack<AXNodeType*> GetAncestorAnchors() const = 0; virtual base::stack<AXNodeType*> GetAncestorAnchors() const = 0;
virtual AXNodeType* GetLowestUnignoredAncestor() const = 0; virtual AXNodeType* GetLowestUnignoredAncestor() const = 0;
virtual void AnchorParent(AXTreeID* tree_id, int32_t* parent_id) const = 0; virtual void AnchorParent(AXTreeID* tree_id, int32_t* parent_id) const = 0;
...@@ -3366,7 +3367,8 @@ class AXPosition { ...@@ -3366,7 +3367,8 @@ class AXPosition {
// break in the text representation, e.g. a block level element or a <br>. // break in the text representation, e.g. a block level element or a <br>.
virtual bool IsInLineBreakingObject() const = 0; virtual bool IsInLineBreakingObject() const = 0;
virtual ax::mojom::Role GetRole() const = 0; virtual ax::mojom::Role GetAnchorRole() const = 0;
virtual ax::mojom::Role GetRole(AXNodeType* node) const = 0;
virtual AXNodeTextStyles GetTextStyles() const = 0; virtual AXNodeTextStyles GetTextStyles() const = 0;
virtual std::vector<int32_t> GetWordStartOffsets() const = 0; virtual std::vector<int32_t> GetWordStartOffsets() const = 0;
virtual std::vector<int32_t> GetWordEndOffsets() const = 0; virtual std::vector<int32_t> GetWordEndOffsets() const = 0;
...@@ -3400,12 +3402,17 @@ class AXPosition { ...@@ -3400,12 +3402,17 @@ class AXPosition {
// A text span is defined by a series of inline text boxes that make up a // A text span is defined by a series of inline text boxes that make up a
// single static text object. // single static text object.
bool AtEndOfTextSpan() const { bool AtEndOfTextSpan() const {
if (GetRole() != ax::mojom::Role::kInlineTextBox || !AtEndOfAnchor()) if (GetAnchorRole() != ax::mojom::Role::kInlineTextBox || !AtEndOfAnchor())
return false; return false;
AXPositionInstance parent_position = CreateParentPosition(); // We are at the end of text span if |this| position has
return parent_position->GetRole() == ax::mojom::Role::kStaticText && // role::kInlineTextBox, the parent of |this| has role::kStaticText, and the
parent_position->AtEndOfAnchor(); // anchor node of |this| is the last child of parent's children.
const bool is_last_child =
AnchorIndexInParent() == (AnchorSiblingCount() - 1);
return is_last_child && GetRole(GetLowestUnignoredAncestor()) ==
ax::mojom::Role::kStaticText;
} }
// Uses depth-first pre-order traversal. // Uses depth-first pre-order traversal.
...@@ -3650,8 +3657,8 @@ class AXPosition { ...@@ -3650,8 +3657,8 @@ class AXPosition {
} }
// Treat moving into or out of nodes with certain roles as a format break. // Treat moving into or out of nodes with certain roles as a format break.
ax::mojom::Role from_role = move_from.GetRole(); ax::mojom::Role from_role = move_from.GetAnchorRole();
ax::mojom::Role to_role = move_to.GetRole(); ax::mojom::Role to_role = move_to.GetAnchorRole();
if (from_role != to_role) { if (from_role != to_role) {
if (IsFormatBoundary(from_role) || IsFormatBoundary(to_role)) if (IsFormatBoundary(from_role) || IsFormatBoundary(to_role))
return true; return true;
...@@ -3833,7 +3840,7 @@ class AXPosition { ...@@ -3833,7 +3840,7 @@ class AXPosition {
AXPositionInstance CreateDocumentAncestorPosition() const { AXPositionInstance CreateDocumentAncestorPosition() const {
AXPositionInstance iterator = Clone(); AXPositionInstance iterator = Clone();
while (!iterator->IsNullPosition()) { while (!iterator->IsNullPosition()) {
if (IsDocument(iterator->GetRole()) && if (IsDocument(iterator->GetAnchorRole()) &&
iterator->CreateParentPosition()->IsNullPosition()) { iterator->CreateParentPosition()->IsNullPosition()) {
break; break;
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment