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