1 /*
<lambda>null2 * Copyright 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.foundation.text
18
19 import androidx.annotation.VisibleForTesting
20 import androidx.compose.foundation.internal.checkPrecondition
21 import androidx.compose.ui.text.AnnotatedString
22 import androidx.compose.ui.text.input.OffsetMapping
23 import androidx.compose.ui.text.input.TransformedText
24 import androidx.compose.ui.text.input.VisualTransformation
25 import kotlin.math.min
26
27 internal val ValidatingEmptyOffsetMappingIdentity: OffsetMapping =
28 ValidatingOffsetMapping(
29 delegate = OffsetMapping.Identity,
30 originalLength = 0,
31 transformedLength = 0
32 )
33
34 internal fun VisualTransformation.filterWithValidation(text: AnnotatedString): TransformedText {
35 val delegate = filter(text)
36 // first throw if the transformation is faulty right away (limit 100)
37 delegate.throwIfNotValidTransform(text.length)
38 // we can't actually assume that transformations are pure, so add a runtime check to throw
39 // better error messages at every transformation as well
40 //
41 // we also don't pre-validate more than 100 indexes, so faults may occur at end
42 return TransformedText(
43 delegate.text,
44 ValidatingOffsetMapping(
45 delegate = delegate.offsetMapping,
46 originalLength = text.length,
47 transformedLength = delegate.text.length
48 )
49 )
50 }
51
52 /**
53 * Assuming TransformedText is a pure mapping this will validate:
54 * 1. The first limit characters map to a valid transformed offset
55 * 2. The first limit characters of transformed map to valid original offsets
56 * 3. The last position for both transformed and original (catching off by 1)
57 *
58 * @param limit how many offsets to check (default 100)
59 */
60 @VisibleForTesting
throwIfNotValidTransformnull61 internal fun TransformedText.throwIfNotValidTransform(originalLength: Int, limit: Int = 100) {
62 // validate originalToTransformed [0..limit] + last position
63 val transformedLength = text.length
64 for (offset in 0 until min(originalLength, limit)) {
65 val transformedOffset = offsetMapping.originalToTransformed(offset)
66 validateOriginalToTransformed(transformedOffset, transformedLength, offset)
67 }
68 val transformedOffset = offsetMapping.originalToTransformed(originalLength)
69 validateOriginalToTransformed(transformedOffset, transformedLength, originalLength)
70
71 // validate transformedToOriginal [0..limit] + last position
72 for (offset in 0 until min(transformedLength, limit)) {
73 val originalOffset = offsetMapping.transformedToOriginal(offset)
74 validateTransformedToOriginal(originalOffset, originalLength, offset)
75 }
76
77 val originalOffset = offsetMapping.transformedToOriginal(transformedLength)
78 validateTransformedToOriginal(originalOffset, originalLength, transformedLength)
79 }
80
81 private class ValidatingOffsetMapping(
82 private val delegate: OffsetMapping,
83 private val originalLength: Int,
84 private val transformedLength: Int
85 ) : OffsetMapping {
86
87 /**
88 * Calls [originalToTransformed][OffsetMapping.originalToTransformed] and throws a detailed
89 * exception if the returned value is outside the range of indices [0, [transformedLength]].
90 */
originalToTransformednull91 override fun originalToTransformed(offset: Int): Int {
92 return delegate.originalToTransformed(offset).also { transformedOffset ->
93 if (offset in 0..originalLength) {
94 // Only validate actually valid requests. The system is responsible for calling
95 // these functions correctly.
96 validateOriginalToTransformed(transformedOffset, transformedLength, offset)
97 }
98 }
99 }
100
101 /**
102 * Calls [transformedToOriginal][OffsetMapping.transformedToOriginal] and throws a detailed
103 * exception if the returned value is outside the range of indices [0, [originalLength]].
104 */
transformedToOriginalnull105 override fun transformedToOriginal(offset: Int): Int {
106 return delegate.transformedToOriginal(offset).also { originalOffset ->
107 if (offset in 0..transformedLength) {
108 // Only validate actually valid requests. The system is responsible for calling
109 // these functions correctly.
110 validateTransformedToOriginal(originalOffset, originalLength, offset)
111 }
112 }
113 }
114 }
115
validateTransformedToOriginalnull116 private fun validateTransformedToOriginal(originalOffset: Int, originalLength: Int, offset: Int) {
117 checkPrecondition(originalOffset in 0..originalLength) {
118 "OffsetMapping.transformedToOriginal returned invalid mapping: " +
119 "$offset -> $originalOffset is not in range of original text " +
120 "[0, $originalLength]"
121 }
122 }
123
validateOriginalToTransformednull124 private fun validateOriginalToTransformed(
125 transformedOffset: Int,
126 transformedLength: Int,
127 offset: Int
128 ) {
129 checkPrecondition(transformedOffset in 0..transformedLength) {
130 "OffsetMapping.originalToTransformed returned invalid mapping: " +
131 "$offset -> $transformedOffset is not in range of transformed text " +
132 "[0, $transformedLength]"
133 }
134 }
135