1use alloc::{borrow::Cow, string::String};
22use derive_more::{AsRef, Deref, Display, Into};
23use parity_scale_codec::{Decode, Encode};
24use scale_decode::DecodeAsType;
25use scale_encode::EncodeAsType;
26use scale_info::TypeInfo;
27
28#[derive(
36 Debug,
37 Display,
38 Clone,
39 Default,
40 PartialEq,
41 Eq,
42 PartialOrd,
43 Ord,
44 Decode,
45 DecodeAsType,
46 Encode,
47 EncodeAsType,
48 Hash,
49 TypeInfo,
50 AsRef,
51 Deref,
52 Into,
53)]
54#[as_ref(forward)]
55#[deref(forward)]
56pub struct LimitedStr<'a, const N: usize = 1024>(Cow<'a, str>);
57
58fn nearest_char_boundary(s: &str, pos: usize) -> usize {
61 (0..=pos.min(s.len()))
62 .rev()
63 .find(|&pos| s.is_char_boundary(pos))
64 .unwrap_or(0)
65}
66
67impl<'a, const N: usize> LimitedStr<'a, N> {
68 pub const MAX_LEN: usize = N;
70
71 pub fn try_new<S: Into<Cow<'a, str>>>(s: S) -> Result<Self, LimitedStrError> {
75 let s = s.into();
76
77 if s.len() > Self::MAX_LEN {
78 Err(LimitedStrError)
79 } else {
80 Ok(Self(s))
81 }
82 }
83
84 pub fn truncated<S: Into<Cow<'a, str>>>(s: S) -> Self {
87 let s = s.into();
88 let truncation_pos = nearest_char_boundary(&s, Self::MAX_LEN);
89
90 match s {
91 Cow::Borrowed(s) => Self(s[..truncation_pos].into()),
92 Cow::Owned(mut s) => {
93 s.truncate(truncation_pos);
94 Self(s.into())
95 }
96 }
97 }
98
99 #[track_caller]
111 pub const fn from_small_str(s: &'static str) -> Self {
112 if s.len() > Self::MAX_LEN {
113 panic!("{}", LimitedStrError::MESSAGE)
114 }
115
116 Self(Cow::Borrowed(s))
117 }
118
119 pub fn as_str(&self) -> &str {
121 self.as_ref()
122 }
123
124 pub fn into_inner(self) -> Cow<'a, str> {
126 self.0
127 }
128}
129
130impl<'a> TryFrom<&'a str> for LimitedStr<'a> {
131 type Error = LimitedStrError;
132
133 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
134 Self::try_new(value)
135 }
136}
137
138impl<'a> TryFrom<String> for LimitedStr<'a> {
139 type Error = LimitedStrError;
140
141 fn try_from(value: String) -> Result<Self, Self::Error> {
142 Self::try_new(value)
143 }
144}
145
146#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Display)]
148#[display("{}", Self::MESSAGE)]
149pub struct LimitedStrError;
150
151impl LimitedStrError {
152 pub const MESSAGE: &str = "string length limit is exceeded";
154
155 pub const fn as_str(&self) -> &'static str {
157 Self::MESSAGE
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use rand::{Rng, distributions::Standard};
165
166 fn assert_result(string: &'static str, max_bytes: usize, expectation: &'static str) {
167 let string = &string[..nearest_char_boundary(string, max_bytes)];
168 assert_eq!(string, expectation);
169 }
170
171 fn check_panicking(initial_string: &'static str, upper_boundary: usize) {
172 let initial_size = initial_string.len();
173
174 for max_bytes in 0..=upper_boundary {
175 let string = &initial_string[..nearest_char_boundary(initial_string, max_bytes)];
176
177 if max_bytes >= initial_size {
179 assert_eq!(string, initial_string);
180 }
181 }
182 }
183
184 #[test]
185 fn truncate_test() {
186 let utf_8 = "hello";
188 assert_eq!(utf_8.len(), 5);
190 assert_eq!(utf_8.chars().count(), 5);
192
193 check_panicking(utf_8, utf_8.len().saturating_mul(2));
197
198 assert_result(utf_8, 0, "");
200 assert_result(utf_8, 1, "h");
201 assert_result(utf_8, 2, "he");
202 assert_result(utf_8, 3, "hel");
203 assert_result(utf_8, 4, "hell");
204 assert_result(utf_8, 5, "hello");
205 assert_result(utf_8, 6, "hello");
206
207 let cjk = "你好吗";
209 assert_eq!(cjk.len(), 9);
211 assert_eq!(cjk.chars().count(), 3);
213 assert!(cjk.chars().all(|c| c.len_utf8() == 3));
215
216 check_panicking(cjk, cjk.len().saturating_mul(2));
220
221 assert_result(cjk, 0, "");
223 assert_result(cjk, 1, "");
224 assert_result(cjk, 2, "");
225 assert_result(cjk, 3, "你");
226 assert_result(cjk, 4, "你");
227 assert_result(cjk, 5, "你");
228 assert_result(cjk, 6, "你好");
229 assert_result(cjk, 7, "你好");
230 assert_result(cjk, 8, "你好");
231 assert_result(cjk, 9, "你好吗");
232 assert_result(cjk, 10, "你好吗");
233
234 let mix = "你he好l吗lo";
238 assert_eq!(mix.len(), utf_8.len() + cjk.len());
239 assert_eq!(mix.len(), 14);
240 assert_eq!(
242 mix.chars().count(),
243 utf_8.chars().count() + cjk.chars().count()
244 );
245 assert_eq!(mix.chars().count(), 8);
246
247 check_panicking(mix, mix.len().saturating_mul(2));
251
252 assert_result(mix, 0, "");
254 assert_result(mix, 1, "");
255 assert_result(mix, 2, "");
256 assert_result(mix, 3, "你");
257 assert_result(mix, 4, "你h");
258 assert_result(mix, 5, "你he");
259 assert_result(mix, 6, "你he");
260 assert_result(mix, 7, "你he");
261 assert_result(mix, 8, "你he好");
262 assert_result(mix, 9, "你he好l");
263 assert_result(mix, 10, "你he好l");
264 assert_result(mix, 11, "你he好l");
265 assert_result(mix, 12, "你he好l吗");
266 assert_result(mix, 13, "你he好l吗l");
267 assert_result(mix, 14, "你he好l吗lo");
268 assert_result(mix, 15, "你he好l吗lo");
269
270 assert_eq!(LimitedStr::<1>::truncated(String::from(mix)).as_str(), "");
271 assert_eq!(LimitedStr::<5>::truncated(mix).as_str(), "你he");
272 assert_eq!(
273 LimitedStr::<9>::truncated(String::from(mix)).as_str(),
274 "你he好l"
275 );
276 assert_eq!(LimitedStr::<13>::truncated(mix).as_str(), "你he好l吗l");
277 }
278
279 #[test]
280 fn truncate_test_fuzz() {
281 for _ in 0..50 {
282 let mut thread_rng = rand::thread_rng();
283
284 let rand_len = thread_rng.gen_range(0..=100_000);
285 let max_bytes = thread_rng.gen_range(0..=rand_len);
286 let mut string = thread_rng
287 .sample_iter::<char, _>(Standard)
288 .take(rand_len)
289 .collect::<String>();
290 string.truncate(nearest_char_boundary(&string, max_bytes));
291
292 if string.len() > max_bytes {
293 panic!("String '{}' input invalidated algorithms property", string);
294 }
295 }
296 }
297}