Skip to main content

const_serialize/
str.rs

1use crate::*;
2use std::{char, fmt::Debug, hash::Hash, mem::MaybeUninit};
3
4const MAX_STR_SIZE: usize = 256;
5
6/// A string that is stored in a constant sized buffer that can be serialized and deserialized at compile time
7#[derive(Clone, Copy)]
8pub struct ConstStr {
9    bytes: [MaybeUninit<u8>; MAX_STR_SIZE],
10    len: u32,
11}
12
13impl Debug for ConstStr {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        f.debug_struct("ConstStr")
16            .field("str", &self.as_str())
17            .finish()
18    }
19}
20
21#[cfg(feature = "serde")]
22mod serde_bytes {
23    use serde::{Deserialize, Serialize, Serializer};
24
25    use crate::ConstStr;
26
27    impl Serialize for ConstStr {
28        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
29        where
30            S: Serializer,
31        {
32            serializer.serialize_str(self.as_str())
33        }
34    }
35
36    impl<'de> Deserialize<'de> for ConstStr {
37        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
38        where
39            D: serde::Deserializer<'de>,
40        {
41            let s = String::deserialize(deserializer)?;
42            Ok(ConstStr::new(&s))
43        }
44    }
45}
46
47unsafe impl SerializeConst for ConstStr {
48    const MEMORY_LAYOUT: Layout = Layout::List(ListLayout::new(
49        std::mem::size_of::<Self>(),
50        std::mem::offset_of!(Self, len),
51        PrimitiveLayout {
52            size: std::mem::size_of::<u32>(),
53        },
54        std::mem::offset_of!(Self, bytes),
55        ArrayLayout {
56            len: MAX_STR_SIZE,
57            item_layout: &Layout::Primitive(PrimitiveLayout {
58                size: std::mem::size_of::<u8>(),
59            }),
60        },
61    ));
62}
63
64impl ConstStr {
65    /// Create a new constant string
66    pub const fn new(s: &str) -> Self {
67        let str_bytes = s.as_bytes();
68        let mut bytes = [MaybeUninit::uninit(); MAX_STR_SIZE];
69        let mut i = 0;
70        while i < str_bytes.len() {
71            bytes[i] = MaybeUninit::new(str_bytes[i]);
72            i += 1;
73        }
74        Self {
75            bytes,
76            len: str_bytes.len() as u32,
77        }
78    }
79
80    /// Get the bytes of the initialized portion of the string
81    const fn bytes(&self) -> &[u8] {
82        // Safety: All bytes up to the pointer are initialized
83        unsafe {
84            &*(self.bytes.split_at(self.len as usize).0 as *const [MaybeUninit<u8>]
85                as *const [u8])
86        }
87    }
88
89    /// Get a reference to the string
90    pub const fn as_str(&self) -> &str {
91        let str_bytes = self.bytes();
92        match std::str::from_utf8(str_bytes) {
93            Ok(s) => s,
94            Err(_) => panic!(
95                "Invalid utf8; ConstStr should only ever be constructed from valid utf8 strings"
96            ),
97        }
98    }
99
100    /// Get the length of the string
101    pub const fn len(&self) -> usize {
102        self.len as usize
103    }
104
105    /// Check if the string is empty
106    pub const fn is_empty(&self) -> bool {
107        self.len == 0
108    }
109
110    /// Push a character onto the string
111    pub const fn push(self, byte: char) -> Self {
112        assert!(byte.is_ascii(), "Only ASCII bytes are supported");
113        let (bytes, len) = char_to_bytes(byte);
114        let (str, _) = bytes.split_at(len);
115        let Ok(str) = std::str::from_utf8(str) else {
116            panic!("Invalid utf8; char_to_bytes should always return valid utf8 bytes")
117        };
118        self.push_str(str)
119    }
120
121    /// Push a str onto the string
122    pub const fn push_str(self, str: &str) -> Self {
123        let Self { mut bytes, len } = self;
124        assert!(
125            str.len() + len as usize <= MAX_STR_SIZE,
126            "String is too long"
127        );
128        let str_bytes = str.as_bytes();
129        let new_len = len as usize + str_bytes.len();
130        let mut i = 0;
131        while i < str_bytes.len() {
132            bytes[len as usize + i] = MaybeUninit::new(str_bytes[i]);
133            i += 1;
134        }
135        Self {
136            bytes,
137            len: new_len as u32,
138        }
139    }
140
141    /// Split the string at a byte index. The byte index must be a char boundary
142    pub const fn split_at(self, index: usize) -> (Self, Self) {
143        let (left, right) = self.bytes().split_at(index);
144        let left = match std::str::from_utf8(left) {
145            Ok(s) => s,
146            Err(_) => {
147                panic!("Invalid utf8; you cannot split at a byte that is not a char boundary")
148            }
149        };
150        let right = match std::str::from_utf8(right) {
151            Ok(s) => s,
152            Err(_) => {
153                panic!("Invalid utf8; you cannot split at a byte that is not a char boundary")
154            }
155        };
156        (Self::new(left), Self::new(right))
157    }
158
159    /// Split the string at the last occurrence of a character
160    pub const fn rsplit_once(&self, char: char) -> Option<(Self, Self)> {
161        let str = self.as_str();
162        let mut index = str.len() - 1;
163        // First find the bytes we are searching for
164        let (char_bytes, len) = char_to_bytes(char);
165        let (char_bytes, _) = char_bytes.split_at(len);
166        let bytes = str.as_bytes();
167
168        // Then walk backwards from the end of the string
169        loop {
170            let byte = bytes[index];
171            // Look for char boundaries in the string and check if the bytes match
172            if let Some(char_boundary_len) = utf8_char_boundary_to_char_len(byte) {
173                // Split up the string into three sections: [before_char, in_char, after_char]
174                let (before_char, after_index) = bytes.split_at(index);
175                let (in_char, after_char) = after_index.split_at(char_boundary_len as usize);
176                if in_char.len() != char_boundary_len as usize {
177                    panic!("in_char.len() should always be equal to char_boundary_len as usize")
178                }
179                // Check if the bytes for the current char and the target char match
180                let mut in_char_eq = true;
181                let mut i = 0;
182                let min_len = if in_char.len() < char_bytes.len() {
183                    in_char.len()
184                } else {
185                    char_bytes.len()
186                };
187                while i < min_len {
188                    in_char_eq &= in_char[i] == char_bytes[i];
189                    i += 1;
190                }
191                // If they do, convert the bytes to strings and return the split strings
192                if in_char_eq {
193                    let Ok(before_char_str) = std::str::from_utf8(before_char) else {
194                        panic!(
195                            "Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary"
196                        )
197                    };
198                    let Ok(after_char_str) = std::str::from_utf8(after_char) else {
199                        panic!(
200                            "Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary"
201                        )
202                    };
203                    return Some((Self::new(before_char_str), Self::new(after_char_str)));
204                }
205            }
206            match index.checked_sub(1) {
207                Some(new_index) => index = new_index,
208                None => return None,
209            }
210        }
211    }
212
213    /// Split the string at the first occurrence of a character
214    pub const fn split_once(&self, char: char) -> Option<(Self, Self)> {
215        let str = self.as_str();
216        let mut index = 0;
217        // First find the bytes we are searching for
218        let (char_bytes, len) = char_to_bytes(char);
219        let (char_bytes, _) = char_bytes.split_at(len);
220        let bytes = str.as_bytes();
221
222        // Then walk forwards from the start of the string
223        while index < bytes.len() {
224            let byte = bytes[index];
225            // Look for char boundaries in the string and check if the bytes match
226            if let Some(char_boundary_len) = utf8_char_boundary_to_char_len(byte) {
227                // Split up the string into three sections: [before_char, in_char, after_char]
228                let (before_char, after_index) = bytes.split_at(index);
229                let (in_char, after_char) = after_index.split_at(char_boundary_len as usize);
230                if in_char.len() != char_boundary_len as usize {
231                    panic!("in_char.len() should always be equal to char_boundary_len as usize")
232                }
233                // Check if the bytes for the current char and the target char match
234                let mut in_char_eq = true;
235                let mut i = 0;
236                let min_len = if in_char.len() < char_bytes.len() {
237                    in_char.len()
238                } else {
239                    char_bytes.len()
240                };
241                while i < min_len {
242                    in_char_eq &= in_char[i] == char_bytes[i];
243                    i += 1;
244                }
245                // If they do, convert the bytes to strings and return the split strings
246                if in_char_eq {
247                    let Ok(before_char_str) = std::str::from_utf8(before_char) else {
248                        panic!(
249                            "Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary"
250                        )
251                    };
252                    let Ok(after_char_str) = std::str::from_utf8(after_char) else {
253                        panic!(
254                            "Invalid utf8; utf8_char_boundary_to_char_len should only return Some when the byte is a character boundary"
255                        )
256                    };
257                    return Some((Self::new(before_char_str), Self::new(after_char_str)));
258                }
259            }
260            index += 1
261        }
262        None
263    }
264}
265
266impl PartialEq for ConstStr {
267    fn eq(&self, other: &Self) -> bool {
268        self.as_str() == other.as_str()
269    }
270}
271
272impl Eq for ConstStr {}
273
274impl PartialOrd for ConstStr {
275    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
276        Some(self.cmp(other))
277    }
278}
279
280impl Ord for ConstStr {
281    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
282        self.as_str().cmp(other.as_str())
283    }
284}
285
286impl Hash for ConstStr {
287    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
288        self.as_str().hash(state);
289    }
290}
291
292#[test]
293fn test_rsplit_once() {
294    let str = ConstStr::new("hello world");
295    assert_eq!(
296        str.rsplit_once(' '),
297        Some((ConstStr::new("hello"), ConstStr::new("world")))
298    );
299
300    let unicode_str = ConstStr::new("hi😀hello😀world😀world");
301    assert_eq!(
302        unicode_str.rsplit_once('😀'),
303        Some((ConstStr::new("hi😀hello😀world"), ConstStr::new("world")))
304    );
305    assert_eq!(unicode_str.rsplit_once('❌'), None);
306
307    for _ in 0..100 {
308        let random_str: String = (0..rand::random::<u8>() % 50)
309            .map(|_| rand::random::<char>())
310            .collect();
311        let konst = ConstStr::new(&random_str);
312        let mut seen_chars = std::collections::HashSet::new();
313        for char in random_str.chars().rev() {
314            let (char_bytes, len) = char_to_bytes(char);
315            let char_bytes = &char_bytes[..len];
316            assert_eq!(char_bytes, char.to_string().as_bytes());
317            if seen_chars.contains(&char) {
318                continue;
319            }
320            seen_chars.insert(char);
321            let (correct_left, correct_right) = random_str.rsplit_once(char).unwrap();
322            let (left, right) = konst.rsplit_once(char).unwrap();
323            println!("splitting {random_str:?} at {char:?}");
324            assert_eq!(left.as_str(), correct_left);
325            assert_eq!(right.as_str(), correct_right);
326        }
327    }
328}
329
330const CONTINUED_CHAR_MASK: u8 = 0b10000000;
331const BYTE_CHAR_BOUNDARIES: [u8; 4] = [0b00000000, 0b11000000, 0b11100000, 0b11110000];
332
333// Const version of https://doc.rust-lang.org/src/core/char/methods.rs.html#1765-1797
334const fn char_to_bytes(char: char) -> ([u8; 4], usize) {
335    let code = char as u32;
336    let len = char.len_utf8();
337    let mut bytes = [0; 4];
338    match len {
339        1 => {
340            bytes[0] = code as u8;
341        }
342        2 => {
343            bytes[0] = ((code >> 6) & 0x1F) as u8 | BYTE_CHAR_BOUNDARIES[1];
344            bytes[1] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK;
345        }
346        3 => {
347            bytes[0] = ((code >> 12) & 0x0F) as u8 | BYTE_CHAR_BOUNDARIES[2];
348            bytes[1] = ((code >> 6) & 0x3F) as u8 | CONTINUED_CHAR_MASK;
349            bytes[2] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK;
350        }
351        4 => {
352            bytes[0] = ((code >> 18) & 0x07) as u8 | BYTE_CHAR_BOUNDARIES[3];
353            bytes[1] = ((code >> 12) & 0x3F) as u8 | CONTINUED_CHAR_MASK;
354            bytes[2] = ((code >> 6) & 0x3F) as u8 | CONTINUED_CHAR_MASK;
355            bytes[3] = (code & 0x3F) as u8 | CONTINUED_CHAR_MASK;
356        }
357        _ => panic!(
358            "encode_utf8: need more than 4 bytes to encode the unicode character, but the buffer has 4 bytes"
359        ),
360    };
361    (bytes, len)
362}
363
364#[test]
365fn fuzz_char_to_bytes() {
366    use std::char;
367    for _ in 0..100 {
368        let char = rand::random::<char>();
369        let (bytes, len) = char_to_bytes(char);
370        let str = std::str::from_utf8(&bytes[..len]).unwrap();
371        assert_eq!(char.to_string(), str);
372    }
373}
374
375const fn utf8_char_boundary_to_char_len(byte: u8) -> Option<u8> {
376    match byte {
377        0b00000000..=0b01111111 => Some(1),
378        0b11000000..=0b11011111 => Some(2),
379        0b11100000..=0b11101111 => Some(3),
380        0b11110000..=0b11111111 => Some(4),
381        _ => None,
382    }
383}
384
385#[test]
386fn fuzz_utf8_byte_to_char_len() {
387    for _ in 0..100 {
388        let random_string: String = (0..rand::random::<u8>())
389            .map(|_| rand::random::<char>())
390            .collect();
391        let bytes = random_string.as_bytes();
392        let chars: std::collections::HashMap<_, _> = random_string.char_indices().collect();
393        for (i, byte) in bytes.iter().enumerate() {
394            match utf8_char_boundary_to_char_len(*byte) {
395                Some(char_len) => {
396                    let char = chars
397                        .get(&i)
398                        .unwrap_or_else(|| panic!("{byte:b} is not a character boundary"));
399                    assert_eq!(char.len_utf8(), char_len as usize);
400                }
401                None => {
402                    assert!(!chars.contains_key(&i), "{byte:b} is a character boundary");
403                }
404            }
405        }
406    }
407}