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