gear_backend_common/
utils.rs

1// This file is part of Gear.
2
3// Copyright (C) 2022-2023 Gear Technologies Inc.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19use alloc::{borrow::Cow, string::String};
20use scale_info::{
21    scale::{Decode, Encode},
22    TypeInfo,
23};
24
25#[macro_export]
26macro_rules! assert_ok {
27    ( $x:expr $(,)? ) => {
28        let is = $x;
29        match is {
30            Ok(_) => (),
31            _ => assert!(false, "Expected Ok(_). Got {:#?}", is),
32        }
33    };
34    ( $x:expr, $y:expr $(,)? ) => {
35        assert_eq!($x, Ok($y));
36    };
37}
38
39#[macro_export]
40macro_rules! assert_err {
41    ( $x:expr , $y:expr $(,)? ) => {
42        assert_eq!($x, Err($y.into()));
43    };
44}
45
46// Max amount of bytes allowed to be thrown as string explanation of the error.
47pub const TRIMMED_MAX_LEN: usize = 1024;
48
49fn smart_truncate(s: &mut String, max_bytes: usize) {
50    let mut last_byte = max_bytes;
51
52    if s.len() > last_byte {
53        while !s.is_char_boundary(last_byte) {
54            last_byte = last_byte.saturating_sub(1);
55        }
56
57        s.truncate(last_byte);
58    }
59}
60
61/// Wrapped string to fit `core_backend::TRIMMED_MAX_LEN` amount of bytes.
62///
63/// The `Cow` is used to avoid allocating a new `String` when the `LimitedStr` is
64/// created from a `&str`.
65///
66/// Plain `str` is not used because it can't be properly encoded/decoded via scale codec.
67#[derive(
68    TypeInfo, Encode, Decode, Debug, Clone, derive_more::Display, PartialEq, Eq, PartialOrd, Ord,
69)]
70pub struct LimitedStr<'a>(Cow<'a, str>);
71
72impl<'a> LimitedStr<'a> {
73    const INIT_ERROR_MSG: &str = concat!(
74        "String must be less than ",
75        stringify!(TRIMMED_MAX_LEN),
76        " bytes."
77    );
78
79    #[track_caller]
80    pub const fn from_small_str(s: &'a str) -> Self {
81        if s.len() > TRIMMED_MAX_LEN {
82            panic!("{}", Self::INIT_ERROR_MSG)
83        }
84
85        Self(Cow::Borrowed(s))
86    }
87
88    pub fn as_str(&self) -> &str {
89        self.0.as_ref()
90    }
91}
92
93#[derive(Clone, Debug, derive_more::Display)]
94#[display(fmt = "String must be less than {} bytes.", TRIMMED_MAX_LEN)]
95pub struct LimitedStrTryFromError;
96
97impl<'a> TryFrom<&'a str> for LimitedStr<'a> {
98    type Error = LimitedStrTryFromError;
99
100    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
101        if s.len() > TRIMMED_MAX_LEN {
102            return Err(LimitedStrTryFromError);
103        }
104
105        Ok(Self(Cow::from(s)))
106    }
107}
108
109impl<'a> From<String> for LimitedStr<'a> {
110    fn from(mut s: String) -> Self {
111        smart_truncate(&mut s, TRIMMED_MAX_LEN);
112        Self(Cow::from(s))
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use rand::{distributions::Standard, Rng};
120
121    fn assert_result(string: &'static str, max_bytes: usize, expectation: &'static str) {
122        let mut string = string.into();
123        smart_truncate(&mut string, max_bytes);
124        assert_eq!(string, expectation);
125    }
126
127    fn check_panicking(initial_string: &'static str, upper_boundary: usize) {
128        let initial_size = initial_string.len();
129
130        for max_bytes in 0..=upper_boundary {
131            let mut string = initial_string.into();
132            smart_truncate(&mut string, max_bytes);
133
134            // Extra check just for confidence.
135            if max_bytes >= initial_size {
136                assert_eq!(string, initial_string);
137            }
138        }
139    }
140
141    #[test]
142    fn truncate_test() {
143        // String for demonstration with UTF_8 encoding.
144        let utf_8 = "hello";
145        // Length in bytes.
146        assert_eq!(utf_8.len(), 5);
147        // Length in chars.
148        assert_eq!(utf_8.chars().count(), 5);
149
150        // Check that `smart_truncate` never panics.
151        //
152        // It calls the `smart_truncate` with `max_bytes` arg in 0..= len * 2.
153        check_panicking(utf_8, utf_8.len().saturating_mul(2));
154
155        // Asserting results.
156        assert_result(utf_8, 0, "");
157        assert_result(utf_8, 1, "h");
158        assert_result(utf_8, 2, "he");
159        assert_result(utf_8, 3, "hel");
160        assert_result(utf_8, 4, "hell");
161        assert_result(utf_8, 5, "hello");
162        assert_result(utf_8, 6, "hello");
163
164        // String for demonstration with CJK encoding.
165        let cjk = "你好吗";
166        // Length in bytes.
167        assert_eq!(cjk.len(), 9);
168        // Length in chars.
169        assert_eq!(cjk.chars().count(), 3);
170
171        // Check that `smart_truncate` never panics.
172        //
173        // It calls the `smart_truncate` with `max_bytes` arg in 0..= len * 2.
174        check_panicking(cjk, cjk.len().saturating_mul(2));
175
176        // Asserting results.
177        assert_result(cjk, 0, "");
178        assert_result(cjk, 1, "");
179        assert_result(cjk, 2, "");
180        assert_result(cjk, 3, "你");
181        assert_result(cjk, 4, "你");
182        assert_result(cjk, 5, "你");
183        assert_result(cjk, 6, "你好");
184        assert_result(cjk, 7, "你好");
185        assert_result(cjk, 8, "你好");
186        assert_result(cjk, 9, "你好吗");
187        assert_result(cjk, 10, "你好吗");
188
189        // String for demonstration with mixed CJK and UTF-8 encoding.
190        let mix = "你he好l吗lo"; // Chaotic sum of "hello" and "你好吗".
191                                 // Length in bytes.
192        assert_eq!(mix.len(), utf_8.len() + cjk.len());
193        assert_eq!(mix.len(), 14);
194        // Length in chars.
195        assert_eq!(
196            mix.chars().count(),
197            utf_8.chars().count() + cjk.chars().count()
198        );
199        assert_eq!(mix.chars().count(), 8);
200
201        // Check that `smart_truncate` never panics.
202        //
203        // It calls the `smart_truncate` with `max_bytes` arg in 0..= len * 2.
204        check_panicking(mix, mix.len().saturating_mul(2));
205
206        // Asserting results.
207        assert_result(mix, 0, "");
208        assert_result(mix, 1, "");
209        assert_result(mix, 2, "");
210        assert_result(mix, 3, "你");
211        assert_result(mix, 4, "你h");
212        assert_result(mix, 5, "你he");
213        assert_result(mix, 6, "你he");
214        assert_result(mix, 7, "你he");
215        assert_result(mix, 8, "你he好");
216        assert_result(mix, 9, "你he好l");
217        assert_result(mix, 10, "你he好l");
218        assert_result(mix, 11, "你he好l");
219        assert_result(mix, 12, "你he好l吗");
220        assert_result(mix, 13, "你he好l吗l");
221        assert_result(mix, 14, "你he好l吗lo");
222        assert_result(mix, 15, "你he好l吗lo");
223    }
224
225    #[test]
226    fn truncate_test_fuzz() {
227        for _ in 0..50 {
228            let mut thread_rng = rand::thread_rng();
229
230            let rand_len = thread_rng.gen_range(0..=100_000);
231            let max_bytes = thread_rng.gen_range(0..=rand_len);
232            let mut string = thread_rng
233                .sample_iter::<char, _>(Standard)
234                .take(rand_len)
235                .collect();
236
237            smart_truncate(&mut string, max_bytes);
238
239            if string.len() > max_bytes {
240                panic!("String '{}' input invalidated algorithms property", string);
241            }
242        }
243    }
244}