small_fixed_array/
string.rs

1use alloc::{
2    borrow::{Cow, ToOwned},
3    boxed::Box,
4    string::{String, ToString},
5    sync::Arc,
6};
7use core::{borrow::Borrow, fmt::Write as _, hash::Hash, str::FromStr};
8
9use crate::{
10    array::FixedArray,
11    inline::InlineString,
12    length::{InvalidStrLength, SmallLen, ValidLength},
13    r#static::StaticStr,
14};
15
16#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
17enum FixedStringRepr<LenT: ValidLength> {
18    Static(StaticStr<LenT>),
19    Heap(FixedArray<u8, LenT>),
20    Inline(InlineString<LenT::InlineStrRepr>),
21}
22
23#[cold]
24fn truncate_string(err: InvalidStrLength, max_len: usize) -> String {
25    let mut value = String::from(err.get_inner());
26    value.truncate(truncate_str(&value, max_len).len());
27    value
28}
29
30#[cold]
31fn truncate_str(string: &str, max_len: usize) -> &str {
32    for len in (0..=max_len).rev() {
33        if string.is_char_boundary(len) {
34            return &string[..len];
35        }
36    }
37
38    unreachable!("Len 0 is a char boundary")
39}
40
41/// A fixed size String with length provided at creation denoted in [`ValidLength`], by default [`u32`].
42///
43/// See module level documentation for more information.
44#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
45pub struct FixedString<LenT: ValidLength = SmallLen>(FixedStringRepr<LenT>);
46
47impl<LenT: ValidLength> FixedString<LenT> {
48    #[must_use]
49    pub fn new() -> Self {
50        Self::from_static_trunc("")
51    }
52
53    pub(crate) fn new_inline(val: &str) -> Option<Self> {
54        InlineString::from_str(val)
55            .map(FixedStringRepr::Inline)
56            .map(Self)
57    }
58
59    /// Converts a `&'static str` into a [`FixedString`].
60    ///
61    /// This method will not allocate, or copy the string data.
62    ///
63    /// See [`Self::from_string_trunc`] for truncation behaviour.
64    pub fn from_static_trunc(val: &'static str) -> Self {
65        Self(FixedStringRepr::Static(StaticStr::from_static_str(
66            truncate_str(val, LenT::MAX.to_usize()),
67        )))
68    }
69
70    /// Converts a `&str` into a [`FixedString`], allocating if the value cannot fit "inline".
71    ///
72    /// This method will be more efficent if you would otherwise clone a [`String`] to convert into [`FixedString`],
73    /// but should not be used in the case that [`String`] ownership could be transfered without reallocation.
74    ///
75    /// If the `&str` is `'static`, it is preferred to use [`Self::from_static_trunc`], which does not need to copy the data around.
76    ///
77    /// "Inline" refers to Small String Optimisation which allows for Strings with less than 9 to 11 characters
78    /// to be stored without allocation, saving a pointer size and an allocation.
79    ///
80    /// See [`Self::from_string_trunc`] for truncation behaviour.
81    #[must_use]
82    pub fn from_str_trunc(val: &str) -> Self {
83        if let Some(inline) = Self::new_inline(val) {
84            inline
85        } else {
86            Self::from_string_trunc(val.to_owned())
87        }
88    }
89
90    /// Converts a [`String`] into a [`FixedString`], **truncating** if the value is larger than `LenT`'s maximum.
91    ///
92    /// This allows for infallible conversion, but may be lossy in the case of a value above `LenT`'s max.
93    /// For lossless fallible conversion, convert to [`Box<str>`] using [`String::into_boxed_str`] and use [`TryFrom`].
94    #[must_use]
95    pub fn from_string_trunc(str: String) -> Self {
96        match str.into_boxed_str().try_into() {
97            Ok(val) => val,
98            Err(err) => Self::from_string_trunc(truncate_string(err, LenT::MAX.to_usize())),
99        }
100    }
101
102    /// Returns the length of the [`FixedString`].
103    #[must_use]
104    pub fn len(&self) -> LenT {
105        match &self.0 {
106            FixedStringRepr::Heap(a) => a.len(),
107            FixedStringRepr::Static(a) => a.len(),
108            FixedStringRepr::Inline(a) => a.len().into(),
109        }
110    }
111
112    /// Returns if the length is equal to 0.
113    #[must_use]
114    pub fn is_empty(&self) -> bool {
115        self.len() == LenT::ZERO
116    }
117
118    /// Converts `&`[`FixedString`] to `&str`, this conversion can be performed by [`core::ops::Deref`].
119    #[must_use]
120    pub fn as_str(&self) -> &str {
121        self
122    }
123
124    /// Converts [`FixedString`] to [`String`], this operation should be cheap.
125    #[must_use]
126    pub fn into_string(self) -> String {
127        self.into()
128    }
129
130    #[cfg(test)]
131    #[must_use]
132    pub(crate) fn is_inline(&self) -> bool {
133        matches!(self, Self(FixedStringRepr::Inline(_)))
134    }
135
136    #[cfg(test)]
137    #[must_use]
138    pub(crate) fn is_static(&self) -> bool {
139        matches!(self, Self(FixedStringRepr::Static(_)))
140    }
141}
142
143impl<LenT: ValidLength> core::ops::Deref for FixedString<LenT> {
144    type Target = str;
145
146    fn deref(&self) -> &Self::Target {
147        match &self.0 {
148            // SAFETY: Self holds the type invariant that the array is UTF-8.
149            FixedStringRepr::Heap(a) => unsafe { core::str::from_utf8_unchecked(a) },
150            FixedStringRepr::Static(a) => a.as_str(),
151            FixedStringRepr::Inline(a) => a.as_str(),
152        }
153    }
154}
155
156impl<LenT: ValidLength> Default for FixedString<LenT> {
157    fn default() -> Self {
158        FixedString::new()
159    }
160}
161
162impl<LenT: ValidLength> Clone for FixedString<LenT> {
163    fn clone(&self) -> Self {
164        match &self.0 {
165            FixedStringRepr::Heap(a) => Self(FixedStringRepr::Heap(a.clone())),
166            FixedStringRepr::Inline(a) => Self(FixedStringRepr::Inline(*a)),
167            FixedStringRepr::Static(a) => Self(FixedStringRepr::Static(*a)),
168        }
169    }
170
171    fn clone_from(&mut self, source: &Self) {
172        match (&mut self.0, &source.0) {
173            (FixedStringRepr::Heap(new), FixedStringRepr::Heap(src)) => new.clone_from(src),
174            #[allow(clippy::assigning_clones)]
175            _ => *self = source.clone(),
176        }
177    }
178}
179
180impl<LenT: ValidLength> Hash for FixedString<LenT> {
181    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
182        self.as_str().hash(state);
183    }
184}
185
186impl<LenT: ValidLength> PartialEq for FixedString<LenT> {
187    fn eq(&self, other: &Self) -> bool {
188        self.as_str() == other.as_str()
189    }
190}
191
192impl<LenT: ValidLength> Eq for FixedString<LenT> {}
193
194impl<LenT: ValidLength> PartialEq<String> for FixedString<LenT> {
195    fn eq(&self, other: &String) -> bool {
196        self.as_str().eq(other)
197    }
198}
199
200impl<LenT: ValidLength> PartialEq<&str> for FixedString<LenT> {
201    fn eq(&self, other: &&str) -> bool {
202        self.as_str().eq(*other)
203    }
204}
205
206impl<LenT: ValidLength> PartialEq<str> for FixedString<LenT> {
207    fn eq(&self, other: &str) -> bool {
208        self.as_str().eq(other)
209    }
210}
211
212impl<LenT: ValidLength> PartialEq<FixedString<LenT>> for &str {
213    fn eq(&self, other: &FixedString<LenT>) -> bool {
214        other == self
215    }
216}
217
218impl<LenT: ValidLength> PartialEq<FixedString<LenT>> for str {
219    fn eq(&self, other: &FixedString<LenT>) -> bool {
220        other == self
221    }
222}
223
224impl<LenT: ValidLength> PartialEq<FixedString<LenT>> for String {
225    fn eq(&self, other: &FixedString<LenT>) -> bool {
226        other == self
227    }
228}
229
230impl<LenT: ValidLength> core::cmp::PartialOrd for FixedString<LenT> {
231    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
232        Some(self.cmp(other))
233    }
234}
235
236impl<LenT: ValidLength> core::cmp::Ord for FixedString<LenT> {
237    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
238        self.as_str().cmp(other.as_str())
239    }
240}
241
242impl<LenT: ValidLength> core::fmt::Display for FixedString<LenT> {
243    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
244        f.write_str(self)
245    }
246}
247
248impl<LenT: ValidLength> core::fmt::Debug for FixedString<LenT> {
249    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
250        f.write_char('"')?;
251        f.write_str(self)?;
252        f.write_char('"')
253    }
254}
255
256impl<LenT: ValidLength> FromStr for FixedString<LenT> {
257    type Err = InvalidStrLength;
258
259    fn from_str(val: &str) -> Result<Self, Self::Err> {
260        if let Some(inline) = Self::new_inline(val) {
261            Ok(inline)
262        } else {
263            Self::try_from(Box::from(val))
264        }
265    }
266}
267
268impl<LenT: ValidLength> TryFrom<Box<str>> for FixedString<LenT> {
269    type Error = InvalidStrLength;
270
271    fn try_from(value: Box<str>) -> Result<Self, Self::Error> {
272        if let Some(inline) = Self::new_inline(&value) {
273            return Ok(inline);
274        }
275
276        match value.into_boxed_bytes().try_into() {
277            Ok(val) => Ok(Self(FixedStringRepr::Heap(val))),
278            Err(err) => Err(err
279                .try_into()
280                .expect("Box<str> -> Box<[u8]> should stay valid UTF8")),
281        }
282    }
283}
284
285impl<LenT: ValidLength> TryFrom<String> for FixedString<LenT> {
286    type Error = InvalidStrLength;
287
288    fn try_from(value: String) -> Result<Self, Self::Error> {
289        if let Some(inline) = Self::new_inline(&value) {
290            return Ok(inline);
291        }
292
293        value.into_boxed_str().try_into()
294    }
295}
296
297impl<LenT: ValidLength> From<char> for FixedString<LenT> {
298    fn from(value: char) -> Self {
299        use alloc::vec;
300
301        if let Some(value) = InlineString::from_char(value) {
302            return Self(FixedStringRepr::Inline(value));
303        }
304
305        let mut bytes = vec![0; value.len_utf8()].into_boxed_slice();
306
307        value.encode_utf8(&mut bytes);
308
309        let bytes = bytes
310            .try_into()
311            .expect("len_utf8 is at most 4, so it will fit in u8");
312
313        Self(FixedStringRepr::Heap(bytes))
314    }
315}
316
317impl<LenT: ValidLength> From<FixedString<LenT>> for String {
318    fn from(value: FixedString<LenT>) -> Self {
319        match value.0 {
320            // SAFETY: Self holds the type invariant that the array is UTF-8.
321            FixedStringRepr::Heap(a) => unsafe { String::from_utf8_unchecked(a.into()) },
322            FixedStringRepr::Inline(a) => a.as_str().to_string(),
323            FixedStringRepr::Static(a) => a.as_str().to_string(),
324        }
325    }
326}
327
328impl<'a, LenT: ValidLength> From<&'a FixedString<LenT>> for Cow<'a, str> {
329    fn from(value: &'a FixedString<LenT>) -> Self {
330        Cow::Borrowed(value.as_str())
331    }
332}
333
334impl<LenT: ValidLength> From<FixedString<LenT>> for Cow<'_, str> {
335    fn from(value: FixedString<LenT>) -> Self {
336        Cow::Owned(value.into_string())
337    }
338}
339
340impl<LenT: ValidLength> AsRef<str> for FixedString<LenT> {
341    fn as_ref(&self) -> &str {
342        self
343    }
344}
345
346impl<LenT: ValidLength> Borrow<str> for FixedString<LenT> {
347    fn borrow(&self) -> &str {
348        self
349    }
350}
351
352#[cfg(feature = "std")]
353impl<LenT: ValidLength> AsRef<std::path::Path> for FixedString<LenT> {
354    fn as_ref(&self) -> &std::path::Path {
355        self.as_str().as_ref()
356    }
357}
358
359#[cfg(feature = "std")]
360impl<LenT: ValidLength> AsRef<std::ffi::OsStr> for FixedString<LenT> {
361    fn as_ref(&self) -> &std::ffi::OsStr {
362        self.as_str().as_ref()
363    }
364}
365
366impl<LenT: ValidLength> From<FixedString<LenT>> for Arc<str> {
367    fn from(value: FixedString<LenT>) -> Self {
368        Arc::from(value.into_string())
369    }
370}
371
372#[cfg(feature = "to-arraystring")]
373impl to_arraystring::ToArrayString for &FixedString<u8> {
374    const MAX_LENGTH: usize = 255;
375    type ArrayString = to_arraystring::ArrayString<255>;
376
377    fn to_arraystring(self) -> Self::ArrayString {
378        Self::ArrayString::from(self).unwrap()
379    }
380}
381
382#[cfg(feature = "serde")]
383impl<'de, LenT: ValidLength> serde::Deserialize<'de> for FixedString<LenT> {
384    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
385        use core::marker::PhantomData;
386
387        struct Visitor<LenT: ValidLength>(PhantomData<LenT>);
388
389        impl<LenT: ValidLength> serde::de::Visitor<'_> for Visitor<LenT> {
390            type Value = FixedString<LenT>;
391
392            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
393                write!(formatter, "a string up to {} bytes long", LenT::MAX)
394            }
395
396            fn visit_str<E: serde::de::Error>(self, val: &str) -> Result<Self::Value, E> {
397                FixedString::from_str(val).map_err(E::custom)
398            }
399
400            fn visit_string<E: serde::de::Error>(self, val: String) -> Result<Self::Value, E> {
401                FixedString::try_from(val.into_boxed_str()).map_err(E::custom)
402            }
403        }
404
405        deserializer.deserialize_string(Visitor(PhantomData))
406    }
407}
408
409#[cfg(feature = "serde")]
410impl<LenT: ValidLength> serde::Serialize for FixedString<LenT> {
411    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
412        self.as_str().serialize(serializer)
413    }
414}
415
416#[cfg(test)]
417mod test {
418    use super::*;
419
420    fn check_u8_roundtrip_generic(to_fixed: fn(String) -> FixedString<u8>) {
421        for i in 0..=u8::MAX {
422            let original = "a".repeat(i.into());
423            let fixed = to_fixed(original);
424
425            assert!(fixed.bytes().all(|c| c == b'a'));
426            assert_eq!(fixed.len(), i);
427
428            if !fixed.is_static() {
429                assert_eq!(fixed.is_inline(), fixed.len() <= 9);
430            }
431        }
432    }
433
434    #[test]
435    fn test_truncating_behaviour() {
436        const STR: &str = "______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________🦀";
437
438        let string = FixedString::<u8>::from_static_trunc(STR);
439
440        let str = std::str::from_utf8(string.as_bytes()).expect("is utf8");
441
442        assert_eq!(str, string.as_str());
443        assert_ne!(STR, str);
444    }
445
446    #[test]
447    fn check_u8_roundtrip() {
448        check_u8_roundtrip_generic(|original| {
449            FixedString::<u8>::try_from(original.into_boxed_str()).unwrap()
450        });
451    }
452
453    #[test]
454    fn check_u8_roundtrip_static() {
455        check_u8_roundtrip_generic(|original| {
456            let static_str = Box::leak(original.into_boxed_str());
457            FixedString::from_static_trunc(static_str)
458        });
459    }
460
461    #[test]
462    #[cfg(feature = "serde")]
463    fn check_u8_roundtrip_serde() {
464        check_u8_roundtrip_generic(|original| {
465            serde_json::from_str(&alloc::format!("\"{original}\"")).unwrap()
466        });
467    }
468
469    #[test]
470    #[cfg(feature = "to-arraystring")]
471    fn check_u8_roundtrip_arraystring() {
472        use to_arraystring::ToArrayString;
473
474        check_u8_roundtrip_generic(|original| {
475            FixedString::from_str_trunc(
476                FixedString::from_string_trunc(original)
477                    .to_arraystring()
478                    .as_str(),
479            )
480        });
481    }
482
483    #[test]
484    fn check_sizes() {
485        type DoubleOpt<T> = Option<Option<T>>;
486
487        assert_eq!(core::mem::size_of::<Option<InlineString<[u8; 11]>>>(), 12);
488        assert_eq!(core::mem::align_of::<Option<InlineString<[u8; 11]>>>(), 1);
489        assert_eq!(core::mem::size_of::<Option<FixedArray<u8, u32>>>(), 12);
490        // https://github.com/rust-lang/rust/issues/119507
491        assert_eq!(core::mem::size_of::<DoubleOpt<FixedArray<u8, u32>>>(), 13);
492        assert_eq!(core::mem::align_of::<Option<FixedArray<u8, u32>>>(), 1);
493        // This sucks!! I want to fix this, soon.... this should so niche somehow.
494        assert_eq!(core::mem::size_of::<FixedStringRepr<u32>>(), 13);
495        assert_eq!(core::mem::align_of::<FixedStringRepr<u32>>(), 1);
496    }
497
498    #[test]
499    fn from_char_u8() {
500        let s: FixedString<u8> = 'a'.into();
501        assert_eq!(s.len(), 1);
502        assert!(s.is_inline());
503
504        let s: FixedString<u8> = '¼'.into();
505        assert_eq!(s.len(), 2);
506        assert!(s.is_inline());
507
508        let s: FixedString<u8> = 'âš¡'.into();
509        assert_eq!(s.len(), 3);
510        assert!(s.is_inline());
511
512        let s: FixedString<u8> = '🦀'.into();
513        assert_eq!(s.len(), 4);
514        #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))]
515        assert!(s.is_inline());
516    }
517
518    #[test]
519    fn from_char_u16() {
520        let s: FixedString<u16> = 'a'.into();
521        assert_eq!(s.len(), 1);
522        assert!(s.is_inline());
523
524        let s: FixedString<u16> = '¼'.into();
525        assert_eq!(s.len(), 2);
526        assert!(s.is_inline());
527
528        let s: FixedString<u16> = 'âš¡'.into();
529        assert_eq!(s.len(), 3);
530        assert!(s.is_inline());
531
532        let s: FixedString<u16> = '🦀'.into();
533        assert_eq!(s.len(), 4);
534        assert!(s.is_inline());
535    }
536
537    #[test]
538    #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))]
539    fn from_char_u32() {
540        let s: FixedString<u32> = 'a'.into();
541        assert_eq!(s.len(), 1);
542        assert!(s.is_inline());
543
544        let s: FixedString<u32> = '¼'.into();
545        assert_eq!(s.len(), 2);
546        assert!(s.is_inline());
547
548        let s: FixedString<u32> = 'âš¡'.into();
549        assert_eq!(s.len(), 3);
550        assert!(s.is_inline());
551
552        let s: FixedString<u32> = '🦀'.into();
553        assert_eq!(s.len(), 4);
554        assert!(s.is_inline());
555    }
556}