Handle Int.MAX in Delete commands
Some software keyboards would send Int.MAX_VALUE for
before/after cursor values to the delete commands.
This CL sanitizes those values so that edit command can continue
processing the command.
Test: Added tests for Delete and DeleteInCodePoints
Test: ./gradlew compose:ui:ui:test
Bug: 256543967
Change-Id: I244a07d69d15bab58e7d3965b4dee17c04fc6a3e
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt
index eeb8ba6..1b03ee1 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt
@@ -263,15 +263,15 @@
}
override fun applyTo(buffer: EditingBuffer) {
- buffer.delete(
- buffer.selectionEnd,
- minOf(buffer.selectionEnd + lengthAfterCursor, buffer.length)
- )
+ // calculate the end with safe addition since lengthAfterCursor can be set to e.g. Int.MAX
+ // by the input
+ val end = buffer.selectionEnd.addExactOrElse(lengthAfterCursor) { buffer.length }
+ buffer.delete(buffer.selectionEnd, minOf(end, buffer.length))
- buffer.delete(
- maxOf(0, buffer.selectionStart - lengthBeforeCursor),
- buffer.selectionStart
- )
+ // calculate the start with safe subtraction since lengthBeforeCursor can be set to e.g.
+ // Int.MAX by the input
+ val start = buffer.selectionStart.subtractExactOrElse(lengthBeforeCursor) { 0 }
+ buffer.delete(maxOf(0, start), buffer.selectionStart)
}
override fun equals(other: Any?): Boolean {
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/MathUtils.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/MathUtils.kt
new file mode 100644
index 0000000..68dbd15
--- /dev/null
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/MathUtils.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.input
+
+/**
+ * Adds [this] and [right], and if an overflow occurs returns result of [defaultValue].
+ */
+internal inline fun Int.addExactOrElse(right: Int, defaultValue: () -> Int): Int {
+ val result = this + right
+ // HD 2-12 Overflow iff both arguments have the opposite sign of the result
+ return if (this xor result and (right xor result) < 0) defaultValue() else result
+}
+
+/**
+ * Subtracts [right] from [this], and if an overflow occurs returns result of [defaultValue].
+ */
+internal fun Int.subtractExactOrElse(right: Int, defaultValue: () -> Int): Int {
+ val result = this - right
+ // HD 2-12 Overflow iff the arguments have different signs and
+ // the sign of the result is different from the sign of x
+ return if (this xor right and (this xor result) < 0) defaultValue() else result
+}
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextCommandTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextCommandTest.kt
index 713c016..fe3eea8 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextCommandTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextCommandTest.kt
@@ -235,4 +235,66 @@
}
assertThat(error).hasMessageThat().contains("-42")
}
+
+ @Test
+ fun deletes_whenLengthAfterCursorOverflows_withMaxValue() {
+ val text = "abcde"
+ val textAfterDelete = "abcd"
+ val selection = TextRange(textAfterDelete.length)
+ val eb = EditingBuffer(text, selection)
+
+ DeleteSurroundingTextCommand(
+ lengthBeforeCursor = 0,
+ lengthAfterCursor = Int.MAX_VALUE
+ ).applyTo(eb)
+
+ assertThat(eb.toString()).isEqualTo(textAfterDelete)
+ assertThat(eb.cursor).isEqualTo(textAfterDelete.length)
+ }
+
+ @Test
+ fun deletes_whenLengthBeforeCursorOverflows_withMaxValue() {
+ val text = "abcde"
+ val selection = TextRange(1)
+ val eb = EditingBuffer(text, selection)
+
+ DeleteSurroundingTextCommand(
+ lengthBeforeCursor = Int.MAX_VALUE,
+ lengthAfterCursor = 0
+ ).applyTo(eb)
+
+ assertThat(eb.toString()).isEqualTo("bcde")
+ assertThat(eb.cursor).isEqualTo(0)
+ }
+
+ @Test
+ fun deletes_whenLengthAfterCursorOverflows() {
+ val text = "abcde"
+ val textAfterDelete = "abcd"
+ val selection = TextRange(textAfterDelete.length)
+ val eb = EditingBuffer(text, selection)
+
+ DeleteSurroundingTextCommand(
+ lengthBeforeCursor = 0,
+ lengthAfterCursor = Int.MAX_VALUE - 1
+ ).applyTo(eb)
+
+ assertThat(eb.toString()).isEqualTo(textAfterDelete)
+ assertThat(eb.cursor).isEqualTo(textAfterDelete.length)
+ }
+
+ @Test
+ fun deletes_whenLengthBeforeCursorOverflows() {
+ val text = "abcde"
+ val selection = TextRange(1)
+ val eb = EditingBuffer(text, selection)
+
+ DeleteSurroundingTextCommand(
+ lengthBeforeCursor = Int.MAX_VALUE - 1,
+ lengthAfterCursor = 0
+ ).applyTo(eb)
+
+ assertThat(eb.toString()).isEqualTo("bcde")
+ assertThat(eb.cursor).isEqualTo(0)
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextInCodePointsCommandTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextInCodePointsCommandTest.kt
index 0f4d16a..72d1ccf4 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextInCodePointsCommandTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/input/DeleteSurroundingTextInCodePointsCommandTest.kt
@@ -246,4 +246,66 @@
}
assertThat(error).hasMessageThat().contains("-42")
}
+
+ @Test
+ fun deletes_whenLengthAfterCursorOverflows_withMaxValue() {
+ val text = "abcde"
+ val textAfterDelete = "abcd"
+ val selection = TextRange(textAfterDelete.length)
+ val eb = EditingBuffer(text, selection)
+
+ DeleteSurroundingTextInCodePointsCommand(
+ lengthBeforeCursor = 0,
+ lengthAfterCursor = Int.MAX_VALUE
+ ).applyTo(eb)
+
+ assertThat(eb.toString()).isEqualTo(textAfterDelete)
+ assertThat(eb.cursor).isEqualTo(textAfterDelete.length)
+ }
+
+ @Test
+ fun deletes_whenLengthBeforeCursorOverflows_withMaxValue() {
+ val text = "abcde"
+ val selection = TextRange(1)
+ val eb = EditingBuffer(text, selection)
+
+ DeleteSurroundingTextInCodePointsCommand(
+ lengthBeforeCursor = Int.MAX_VALUE,
+ lengthAfterCursor = 0
+ ).applyTo(eb)
+
+ assertThat(eb.toString()).isEqualTo("bcde")
+ assertThat(eb.cursor).isEqualTo(0)
+ }
+
+ @Test
+ fun deletes_whenLengthAfterCursorOverflows() {
+ val text = "abcde"
+ val textAfterDelete = "abcd"
+ val selection = TextRange(textAfterDelete.length)
+ val eb = EditingBuffer(text, selection)
+
+ DeleteSurroundingTextInCodePointsCommand(
+ lengthBeforeCursor = 0,
+ lengthAfterCursor = Int.MAX_VALUE - 1
+ ).applyTo(eb)
+
+ assertThat(eb.toString()).isEqualTo(textAfterDelete)
+ assertThat(eb.cursor).isEqualTo(textAfterDelete.length)
+ }
+
+ @Test
+ fun deletes_whenLengthBeforeCursorOverflows() {
+ val text = "abcde"
+ val selection = TextRange(1)
+ val eb = EditingBuffer(text, selection)
+
+ DeleteSurroundingTextInCodePointsCommand(
+ lengthBeforeCursor = Int.MAX_VALUE - 1,
+ lengthAfterCursor = 0
+ ).applyTo(eb)
+
+ assertThat(eb.toString()).isEqualTo("bcde")
+ assertThat(eb.cursor).isEqualTo(0)
+ }
}
\ No newline at end of file