[EditContext] Fix indexing in FirstRectForCharacterRange

The `location` parameter to EditContext::FirstRectForCharacterRange is
relative to the entire contents of the editable region, but
character_bounds_ (if HasValidCompositionBounds() is true) is indexed
only relative to the composition range within the editable region.
So, when indexing into character_bounds_ we must adjust `location` by
the start offset of the editable region.

The `length == 0` cases in FirstRectForCharacterRange were failing
to do this adjustment when indexing into character_bounds_.

Fix the array indexing and add a test.

Bug: 1507725
Change-Id: Ie742cfe0c56fb016d70b32b0f13c4d53e94db2d6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5219458
Commit-Queue: Dan Clark <daniec@microsoft.com>
Reviewed-by: Anupam Snigdha <snianu@microsoft.com>
Reviewed-by: Koji Ishii <kojii@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1251542}
diff --git a/third_party/blink/renderer/core/editing/ime/edit_context.cc b/third_party/blink/renderer/core/editing/ime/edit_context.cc
index b5d06fe..79053d4 100644
--- a/third_party/blink/renderer/core/editing/ime/edit_context.cc
+++ b/third_party/blink/renderer/core/editing/ime/edit_context.cc
@@ -739,29 +739,32 @@
                                              gfx::Rect& rect_in_viewport) {
   if (HasValidCompositionBounds()) {
     WebRange range = this->CompositionRange();
+    CHECK_GE(range.StartOffset(), 0);
+    CHECK_GE(range.EndOffset(), 0);
 
     // If the requested range is within the current composition range,
     // we'll use that to provide the result.
     if (base::saturated_cast<int>(location) >= range.StartOffset() &&
         base::saturated_cast<int>(location + length) <= range.EndOffset()) {
+      const size_t start_in_composition = location - range.StartOffset();
+      const size_t end_in_composition = location + length - range.StartOffset();
       if (length == 0) {
-        if (location == character_bounds_.size()) {
+        if (start_in_composition == character_bounds_.size()) {
           // Zero-width rect after the last character in the composition range
           rect_in_viewport =
-              gfx::Rect(character_bounds_[location - 1].right(),
-                        character_bounds_[location - 1].y(), 0,
-                        character_bounds_[location - 1].height());
+              gfx::Rect(character_bounds_[start_in_composition - 1].right(),
+                        character_bounds_[start_in_composition - 1].y(), 0,
+                        character_bounds_[start_in_composition - 1].height());
         } else {
           // Zero-width rect before the next character in the composition range
-          rect_in_viewport = gfx::Rect(character_bounds_[location].x(),
-                                       character_bounds_[location].y(), 0,
-                                       character_bounds_[location].height());
+          rect_in_viewport =
+              gfx::Rect(character_bounds_[start_in_composition].x(),
+                        character_bounds_[start_in_composition].y(), 0,
+                        character_bounds_[start_in_composition].height());
         }
       } else {
-        const int start_in_composition = location - range.StartOffset();
-        const int end_in_composition = location + length - range.StartOffset();
         gfx::Rect rect = character_bounds_[start_in_composition];
-        for (int i = start_in_composition + 1; i < end_in_composition; ++i) {
+        for (size_t i = start_in_composition + 1; i < end_in_composition; ++i) {
           rect.Union(character_bounds_[i]);
         }
 
diff --git a/third_party/blink/web_tests/editing/input/edit-context.html b/third_party/blink/web_tests/editing/input/edit-context.html
index 4d68cd1..feeacd0 100644
--- a/third_party/blink/web_tests/editing/input/edit-context.html
+++ b/third_party/blink/web_tests/editing/input/edit-context.html
@@ -540,6 +540,36 @@
   textInputController.setComposition("bar");
 
 }, 'Testing EditContext GC');
+
+test(function() {
+  const editContext = new EditContext();
+  assert_not_equals(editContext, null);
+  const test = document.createElement("div");
+  document.body.appendChild(test);
+  test.editContext = editContext;
+  test.focus();
+  textInputController.insertText("abc")
+  textInputController.setComposition("def");
+  const rect1 = new DOMRect(0, 2, 4, 8);
+  const rect2 = new DOMRect(4, 2, 4, 8);
+  const rect3 = new DOMRect(8, 2, 4, 8);
+  editContext.updateCharacterBounds(3, [rect1, rect2, rect3]);
+  assert_array_equals(textInputController.firstRectForCharacterRange(2, 0), [], "0-width call at index 2 should get empty array");
+  assert_array_equals(textInputController.firstRectForCharacterRange(3, 0), [0, 2, 0, 8], "0-width call at index 3 should get caret position before first character");
+  assert_array_equals(textInputController.firstRectForCharacterRange(4, 0), [4, 2, 0, 8], "0-width call at index 4 should get caret position before second character");
+  assert_array_equals(textInputController.firstRectForCharacterRange(5, 0), [8, 2, 0, 8], "0-width call at index 5 should get caret position before third character");
+  assert_array_equals(textInputController.firstRectForCharacterRange(6, 0), [12, 2, 0, 8], "0-width call at index 6 should get caret position after third character");
+  assert_array_equals(textInputController.firstRectForCharacterRange(7, 0), [], "0-width call at index 7 should get empty array");
+
+  assert_array_equals(textInputController.firstRectForCharacterRange(3, 1), [0, 2, 4, 8], "1-width call at index 3 should get first character rect");
+  assert_array_equals(textInputController.firstRectForCharacterRange(4, 1), [4, 2, 4, 8], "1-width call at index 4 should get second character rect");
+  assert_array_equals(textInputController.firstRectForCharacterRange(5, 1), [8, 2, 4, 8], "1-width call at index 5 should get third character rect");
+  assert_array_equals(textInputController.firstRectForCharacterRange(6, 1), [], "1-width call at index 6 should get empty array");
+
+  assert_array_equals(textInputController.firstRectForCharacterRange(3, 3), [0, 2, 12, 8], "3-width call should join rects for characters 3-5");
+
+  test.remove();
+}, 'Testing firstRectForCharacterRange');
 </script>
 </body>
 </html>