1use 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
46pub 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#[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 if max_bytes >= initial_size {
136 assert_eq!(string, initial_string);
137 }
138 }
139 }
140
141 #[test]
142 fn truncate_test() {
143 let utf_8 = "hello";
145 assert_eq!(utf_8.len(), 5);
147 assert_eq!(utf_8.chars().count(), 5);
149
150 check_panicking(utf_8, utf_8.len().saturating_mul(2));
154
155 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 let cjk = "你好吗";
166 assert_eq!(cjk.len(), 9);
168 assert_eq!(cjk.chars().count(), 3);
170
171 check_panicking(cjk, cjk.len().saturating_mul(2));
175
176 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 let mix = "你he好l吗lo"; assert_eq!(mix.len(), utf_8.len() + cjk.len());
193 assert_eq!(mix.len(), 14);
194 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_panicking(mix, mix.len().saturating_mul(2));
205
206 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}