1#[inline(always)]
11pub fn truncate_bytes(s: &mut String, max_bytes: usize) {
12 if s.len() <= max_bytes {
13 return;
14 }
15 truncate_bytes_cold(s, max_bytes)
16}
17
18#[inline(always)]
25pub fn truncate_chars(s: &mut String, max_chars: usize) {
26 if s.len() <= max_chars {
27 return;
28 }
29 truncate_chars_cold(s, max_chars)
30}
31
32#[inline(never)]
33#[cold]
34fn truncate_bytes_cold(s: &mut String, max_bytes: usize) {
35 for idx in (max_bytes.saturating_sub(3)..=max_bytes).rev() {
38 if s.is_char_boundary(idx) {
39 s.truncate(idx);
40 break;
41 }
42 }
43}
44
45#[inline(never)]
46#[cold]
47fn truncate_chars_cold(s: &mut String, max_chars: usize) {
48 const HIGH_BITS: u64 = 0x8080_8080_8080_8080;
49
50 let bytes = s.as_bytes();
51 let len = bytes.len();
52 let (chunks, _) = bytes.as_chunks::<8>();
53
54 let mut idx = 0usize;
55 let mut chars_seen = 0usize;
56
57 for chunk in chunks {
58 let word = u64::from_ne_bytes(*chunk);
59
60 let continuation_mask = (word & HIGH_BITS) & !((word << 1) & HIGH_BITS);
62 let continuation_count = continuation_mask.count_ones() as usize;
63 let chunk_chars = 8usize - continuation_count;
64
65 chars_seen += chunk_chars;
68 if chars_seen > max_chars {
69 chars_seen -= chunk_chars;
70 break;
71 }
72
73 idx += 8;
74 }
75
76 while idx < len {
77 if (bytes[idx] & 0b1100_0000) != 0b1000_0000 {
78 chars_seen += 1;
79 if chars_seen > max_chars {
80 s.truncate(idx);
83 return;
84 }
85 }
86 idx += 1;
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 #[cfg(not(target_env = "sgx"))]
93 use proptest::{prop_assert, prop_assert_eq, proptest};
94
95 use super::*;
96
97 fn tb(s: &str, max_bytes: usize) -> String {
99 let mut s = s.to_owned();
100 truncate_bytes(&mut s, max_bytes);
101 s
102 }
103
104 fn tc(s: &str, max_chars: usize) -> String {
106 let mut s = s.to_owned();
107 truncate_chars(&mut s, max_chars);
108 s
109 }
110
111 #[test]
112 fn test_truncate_bytes() {
113 assert_eq!(tb("", 10), "");
115 assert_eq!(tb("hello", 10), "hello");
116 assert_eq!(tb("hello", 5), "hello");
117
118 assert_eq!(tb("hello world", 5), "hello");
120
121 assert_eq!(tb("a\u{1F600}b", 3), "a");
124
125 assert_eq!(tb("日本語", 7), "日本"); assert_eq!(tb("日本語", 6), "日本"); }
129
130 #[test]
131 fn test_truncate_chars() {
132 assert_eq!(tc("", 10), "");
134 assert_eq!(tc("hello", 10), "hello");
135 assert_eq!(tc("hello", 5), "hello");
136
137 assert_eq!(tc("hello world", 5), "hello");
139
140 assert_eq!(tc("a\u{1F600}b\u{1F600}c", 3), "a\u{1F600}b");
142
143 assert_eq!(tc("日本語テスト", 3), "日本語");
145
146 assert_eq!(tc("hello", 0), "");
148 }
149
150 #[cfg(not(target_env = "sgx"))]
153 #[test]
154 fn test_truncate_idempotent() {
155 proptest!(|(s: String, n in 0usize..=512)| {
156 let bytes_once = tb(&s, n);
157 let bytes_twice = tb(&bytes_once, n);
158 prop_assert_eq!(bytes_once, bytes_twice);
159
160 let chars_once = tc(&s, n);
161 let chars_twice = tc(&chars_once, n);
162 prop_assert_eq!(chars_once, chars_twice);
163 });
164 }
165
166 #[cfg(not(target_env = "sgx"))]
170 #[test]
171 fn test_truncate_length_ordering() {
172 proptest!(|(s: String, n in 0usize..=512)| {
173 let original_len = s.len();
174 let chars_len = tc(&s, n).len();
175 let bytes_len = tb(&s, n).len();
176
177 prop_assert!(original_len >= chars_len);
178 prop_assert!(chars_len >= bytes_len);
179 });
180 }
181
182 #[cfg(not(target_env = "sgx"))]
187 #[test]
188 fn test_truncate_prefix_recovery() {
189 proptest!(|(s: String, ascii in 0u8..=0x7f, c: char)| {
190 let mut with_ascii = s.clone();
191 with_ascii.push(char::from(ascii));
192 let bytes_recovered = tb(&with_ascii, s.len());
193 prop_assert_eq!(bytes_recovered, s.clone());
194
195 let mut with_char = s.clone();
196 with_char.push(c);
197 let chars_recovered = tc(&with_char, s.chars().count());
198 prop_assert_eq!(chars_recovered, s);
199 });
200 }
201}