Skip to main content

cold_string/
lib.rs

1#![allow(rustdoc::bare_urls)]
2#![doc = include_str!("../README.md")]
3#![allow(unknown_lints, unexpected_cfgs)]
4#![allow(unstable_name_collisions)]
5#![deny(unused_imports)]
6#![no_std]
7
8extern crate alloc;
9
10#[cfg(test)]
11extern crate std;
12
13use alloc::{
14    borrow::{Cow, ToOwned},
15    boxed::Box,
16    str::Utf8Error,
17    string::String,
18};
19use core::{
20    cmp::Ordering,
21    fmt,
22    hash::{Hash, Hasher},
23    iter::FromIterator,
24    ops::Deref,
25    str,
26};
27
28#[cfg(test)]
29use core::{mem, ptr};
30
31mod arc;
32mod encoded;
33mod heap;
34mod vint;
35
36pub use crate::arc::ArcColdString;
37pub use crate::arc::ArcColdString16;
38pub use crate::arc::ArcColdString32;
39pub use crate::arc::ArcColdString8;
40use crate::encoded::Encoded;
41
42#[cfg(feature = "rkyv")]
43mod rkyv;
44
45/// Compact representation of immutable UTF-8 strings. Optimized for memory usage and struct packing.
46///
47/// # Example
48/// ```
49/// let s = cold_string::ColdString::new("qwerty");
50/// assert_eq!(s.as_str(), "qwerty");
51/// ```
52/// ```
53/// use core::mem::size_of;
54/// use cold_string::ColdString;
55///
56/// assert_eq!(size_of::<ColdString>(), size_of::<usize>());
57/// assert_eq!(size_of::<Option<ColdString>>(), size_of::<ColdString>());
58/// ```
59#[repr(transparent)]
60pub struct ColdString {
61    encoded: Encoded<()>,
62}
63
64impl ColdString {
65    /// Convert a slice of bytes into a [`ColdString`].
66    ///
67    /// A [`ColdString`] is a contiguous collection of bytes (`u8`s) that is valid [`UTF-8`](https://en.wikipedia.org/wiki/UTF-8).
68    /// This method converts from an arbitrary contiguous collection of bytes into a
69    /// [`ColdString`], failing if the provided bytes are not `UTF-8`.
70    ///
71    /// # Examples
72    /// ### Valid UTF-8
73    /// ```
74    /// # use cold_string::ColdString;
75    /// let bytes = [240, 159, 166, 128, 240, 159, 146, 175];
76    /// let compact = ColdString::from_utf8(&bytes).expect("valid UTF-8");
77    ///
78    /// assert_eq!(compact, "🦀💯");
79    /// ```
80    ///
81    /// ### Invalid UTF-8
82    /// ```
83    /// # use cold_string::ColdString;
84    /// let bytes = [255, 255, 255];
85    /// let result = ColdString::from_utf8(&bytes);
86    ///
87    /// assert!(result.is_err());
88    /// ```
89    pub fn from_utf8<B: AsRef<[u8]>>(v: B) -> Result<Self, Utf8Error> {
90        Ok(Self::new(str::from_utf8(v.as_ref())?))
91    }
92
93    /// Converts a vector of bytes to a [`ColdString`] without checking that the string contains
94    /// valid UTF-8.
95    ///
96    /// See the safe version, [`ColdString::from_utf8`], for more details.
97    ///
98    /// # Examples
99    ///
100    /// Basic usage:
101    ///
102    /// ```
103    /// # use cold_string::ColdString;
104    /// // some bytes, in a vector
105    /// let sparkle_heart = [240, 159, 146, 150];
106    ///
107    /// let sparkle_heart = unsafe {
108    ///     ColdString::from_utf8_unchecked(&sparkle_heart)
109    /// };
110    ///
111    /// assert_eq!("💖", sparkle_heart);
112    /// ```
113    ///
114    /// # Safety
115    ///
116    /// `v` must contain valid UTF-8.
117    pub unsafe fn from_utf8_unchecked<B: AsRef<[u8]>>(v: B) -> Self {
118        Self::new(str::from_utf8_unchecked(v.as_ref()))
119    }
120
121    /// Creates a new [`ColdString`] from any type that implements `AsRef<str>`.
122    /// If the string is at most `core::mem::size_of::<usize>()` bytes, then it
123    /// will be inlined on the stack.
124    pub fn new<T: AsRef<str>>(x: T) -> Self {
125        let s = x.as_ref();
126        Self {
127            encoded: Encoded::new(s, ()),
128        }
129    }
130
131    /// Creates a new inline [`ColdString`] from `&'static str` at compile time.
132    ///
133    /// In a dynamic context you can use the method [`ColdString::new()`].
134    ///
135    /// # Panics
136    /// The string must be at most `core::mem::size_of::<usize>()`. Creating
137    /// a [`ColdString`] larger than that is not supported.
138    ///
139    ///
140    /// # Examples
141    /// ```
142    /// use cold_string::ColdString;
143    ///
144    /// const DEFAULT_NAME: ColdString = ColdString::new_inline_const("cold");
145    /// ```
146    #[rustversion::since(1.61)]
147    #[inline]
148    pub const fn new_inline_const(s: &str) -> Self {
149        Self {
150            encoded: Encoded::new_inline_const(s),
151        }
152    }
153
154    /// Returns `true` if the string bytes are inlined.
155    #[inline]
156    pub fn is_inline(&self) -> bool {
157        self.encoded.is_inline()
158    }
159
160    /// Returns the length of this `ColdString`, in bytes, not [`char`]s or
161    /// graphemes. In other words, it might not be what a human considers the
162    /// length of the string.
163    ///
164    /// # Examples
165    ///
166    /// ```
167    /// use cold_string::ColdString;
168    ///
169    /// let a = ColdString::from("foo");
170    /// assert_eq!(a.len(), 3);
171    ///
172    /// let fancy_f = String::from("ƒoo");
173    /// assert_eq!(fancy_f.len(), 4);
174    /// assert_eq!(fancy_f.chars().count(), 3);
175    /// ```
176    #[inline]
177    pub fn len(&self) -> usize {
178        self.encoded.len()
179    }
180
181    /// Returns a byte slice of this `ColdString`'s contents.
182    ///
183    /// The inverse of this method is [`from_utf8`].
184    ///
185    /// [`from_utf8`]: String::from_utf8
186    ///
187    /// # Examples
188    ///
189    /// ```
190    /// let s = cold_string::ColdString::from("hello");
191    ///
192    /// assert_eq!(&[104, 101, 108, 108, 111], s.as_bytes());
193    /// ```
194    #[inline]
195    pub fn as_bytes(&self) -> &[u8] {
196        self.encoded.as_bytes()
197    }
198
199    /// Returns a string slice containing the entire [`ColdString`].
200    ///
201    /// # Examples
202    /// ```
203    /// let s = cold_string::ColdString::new("hello");
204    ///
205    /// assert_eq!(s.as_str(), "hello");
206    /// ```
207    #[inline]
208    pub fn as_str(&self) -> &str {
209        unsafe { str::from_utf8_unchecked(self.as_bytes()) }
210    }
211
212    /// Returns `true` if this `ColdString` has a length of zero, and `false` otherwise.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// let v = cold_string::ColdString::new("");
218    /// assert!(v.is_empty());
219    /// ```
220    #[inline]
221    pub fn is_empty(&self) -> bool {
222        self.len() == 0
223    }
224}
225
226impl Default for ColdString {
227    fn default() -> Self {
228        Self::new("")
229    }
230}
231
232impl Deref for ColdString {
233    type Target = str;
234    fn deref(&self) -> &str {
235        self.as_str()
236    }
237}
238
239impl Drop for ColdString {
240    fn drop(&mut self) {
241        if !self.is_inline() {
242            // SAFETY: a non-inline `ColdString` uniquely owns its allocation.
243            unsafe { self.encoded.deallocate() }
244        }
245    }
246}
247
248impl Clone for ColdString {
249    fn clone(&self) -> Self {
250        if self.is_inline() {
251            Self {
252                encoded: self.encoded,
253            }
254        } else {
255            Self::new(self.as_str())
256        }
257    }
258}
259
260impl PartialEq for ColdString {
261    fn eq(&self, other: &Self) -> bool {
262        if self.is_inline() && other.is_inline() {
263            self.encoded.addr() == other.encoded.addr()
264        } else if !self.is_inline() && !other.is_inline() {
265            if self.encoded.heap_prefix() != other.encoded.heap_prefix() {
266                false
267            } else {
268                self.as_bytes() == other.as_bytes()
269            }
270        } else {
271            false
272        }
273    }
274}
275
276impl Eq for ColdString {}
277
278impl Hash for ColdString {
279    fn hash<H: Hasher>(&self, state: &mut H) {
280        self.as_str().hash(state)
281    }
282}
283
284impl fmt::Debug for ColdString {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        fmt::Debug::fmt(self.as_str(), f)
287    }
288}
289
290impl fmt::Display for ColdString {
291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292        fmt::Display::fmt(self.as_str(), f)
293    }
294}
295
296impl From<&str> for ColdString {
297    fn from(s: &str) -> Self {
298        Self::new(s)
299    }
300}
301
302impl From<String> for ColdString {
303    fn from(s: String) -> Self {
304        Self::new(&s)
305    }
306}
307
308impl From<ColdString> for String {
309    fn from(s: ColdString) -> Self {
310        s.as_str().to_owned()
311    }
312}
313
314impl From<ColdString> for Cow<'_, str> {
315    #[inline]
316    fn from(s: ColdString) -> Self {
317        Self::Owned(s.into())
318    }
319}
320
321impl<'a> From<&'a ColdString> for Cow<'a, str> {
322    #[inline]
323    fn from(s: &'a ColdString) -> Self {
324        Self::Borrowed(s)
325    }
326}
327
328impl<'a> From<Cow<'a, str>> for ColdString {
329    fn from(cow: Cow<'a, str>) -> Self {
330        Self::new(cow)
331    }
332}
333
334impl From<Box<str>> for ColdString {
335    #[inline]
336    #[track_caller]
337    fn from(b: Box<str>) -> Self {
338        Self::new(&b)
339    }
340}
341
342impl FromIterator<char> for ColdString {
343    fn from_iter<I: IntoIterator<Item = char>>(iter: I) -> Self {
344        Self::new(iter.into_iter().collect::<String>())
345    }
346}
347
348unsafe impl Send for ColdString {}
349unsafe impl Sync for ColdString {}
350
351impl core::borrow::Borrow<str> for ColdString {
352    fn borrow(&self) -> &str {
353        self.as_str()
354    }
355}
356
357impl PartialEq<str> for ColdString {
358    fn eq(&self, other: &str) -> bool {
359        self.as_str() == other
360    }
361}
362
363impl PartialEq<ColdString> for str {
364    fn eq(&self, other: &ColdString) -> bool {
365        other.eq(self)
366    }
367}
368
369impl PartialEq<&str> for ColdString {
370    fn eq(&self, other: &&str) -> bool {
371        self.eq(*other)
372    }
373}
374
375impl PartialEq<ColdString> for &str {
376    fn eq(&self, other: &ColdString) -> bool {
377        other.eq(*self)
378    }
379}
380
381impl AsRef<str> for ColdString {
382    #[inline]
383    fn as_ref(&self) -> &str {
384        self.as_str()
385    }
386}
387
388impl AsRef<[u8]> for ColdString {
389    #[inline]
390    fn as_ref(&self) -> &[u8] {
391        self.as_bytes()
392    }
393}
394
395impl Ord for ColdString {
396    fn cmp(&self, other: &Self) -> Ordering {
397        self.as_str().cmp(other.as_str())
398    }
399}
400
401impl PartialOrd for ColdString {
402    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
403        Some(self.cmp(other))
404    }
405}
406
407impl alloc::str::FromStr for ColdString {
408    type Err = core::convert::Infallible;
409    fn from_str(s: &str) -> Result<ColdString, Self::Err> {
410        Ok(ColdString::new(s))
411    }
412}
413
414#[cfg(feature = "serde")]
415impl serde::Serialize for ColdString {
416    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
417        serializer.serialize_str(self.as_str())
418    }
419}
420
421#[cfg(feature = "serde")]
422impl<'de> serde::Deserialize<'de> for ColdString {
423    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
424        let s = String::deserialize(d)?;
425        Ok(ColdString::new(&s))
426    }
427}
428
429#[cfg(test)]
430trait TestString:
431    Clone + Default + fmt::Debug + Eq + Hash + PartialEq<str> + for<'a> PartialEq<&'a str>
432{
433    fn new(s: &str) -> Self;
434    fn from_utf8(bytes: &[u8]) -> Result<Self, Utf8Error>;
435    fn new_inline(s: &str) -> Self;
436    fn is_inline(&self) -> bool;
437    fn is_empty(&self) -> bool;
438    fn len(&self) -> usize;
439    fn as_bytes(&self) -> &[u8];
440    fn as_str(&self) -> &str;
441    fn encoded_addr(&self) -> usize;
442}
443
444#[cfg(test)]
445impl TestString for ColdString {
446    fn new(s: &str) -> Self {
447        Self::new(s)
448    }
449
450    fn from_utf8(bytes: &[u8]) -> Result<Self, Utf8Error> {
451        Self::from_utf8(bytes)
452    }
453
454    fn new_inline(s: &str) -> Self {
455        Self::new_inline_const(s)
456    }
457
458    fn is_inline(&self) -> bool {
459        self.is_inline()
460    }
461
462    fn is_empty(&self) -> bool {
463        self.is_empty()
464    }
465
466    fn len(&self) -> usize {
467        self.len()
468    }
469
470    fn as_bytes(&self) -> &[u8] {
471        self.as_bytes()
472    }
473
474    fn as_str(&self) -> &str {
475        self.as_str()
476    }
477
478    fn encoded_addr(&self) -> usize {
479        self.encoded.addr()
480    }
481}
482
483#[cfg(test)]
484impl<A: arc::RefCount> TestString for arc::ArcColdStringInner<A> {
485    fn new(s: &str) -> Self {
486        Self::new(s)
487    }
488
489    fn from_utf8(bytes: &[u8]) -> Result<Self, Utf8Error> {
490        Self::from_utf8(bytes)
491    }
492
493    fn new_inline(s: &str) -> Self {
494        Self::new_inline_const(s)
495    }
496
497    fn is_inline(&self) -> bool {
498        self.is_inline()
499    }
500
501    fn is_empty(&self) -> bool {
502        self.is_empty()
503    }
504
505    fn len(&self) -> usize {
506        self.len()
507    }
508
509    fn as_bytes(&self) -> &[u8] {
510        self.as_bytes()
511    }
512
513    fn as_str(&self) -> &str {
514        self.as_str()
515    }
516
517    fn encoded_addr(&self) -> usize {
518        self.encoded_addr()
519    }
520}
521
522#[cfg(test)]
523macro_rules! each_string {
524    ($test:ident $(, $arg:expr)*) => {
525        $test::<ColdString>($($arg),*);
526        $test::<ArcColdString>($($arg),*);
527        $test::<ArcColdString8>($($arg),*);
528        $test::<ArcColdString16>($($arg),*);
529        $test::<ArcColdString32>($($arg),*);
530    };
531}
532
533#[cfg(all(test, feature = "serde"))]
534mod serde_tests {
535    use super::*;
536    use serde_test::{assert_tokens, Token};
537
538    fn assert_serde<T>(s: &'static str)
539    where
540        T: TestString + serde::Serialize + for<'de> serde::Deserialize<'de>,
541    {
542        assert_tokens(&T::new(s), &[Token::Str(s)]);
543    }
544
545    #[test]
546    fn test_serde_cold_string_inline() {
547        each_string!(assert_serde, "ferris");
548    }
549
550    #[test]
551    fn test_serde_cold_string_heap() {
552        let long_str = "This is a significantly longer string for heap testing";
553        each_string!(assert_serde, long_str);
554    }
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560    use core::hash::BuildHasher;
561    use hashbrown::hash_map::DefaultHashBuilder;
562
563    fn assert_layout<T: TestString>() {
564        assert_eq!(mem::size_of::<T>(), mem::size_of::<usize>());
565        assert_eq!(mem::size_of::<Option<T>>(), mem::size_of::<T>());
566    }
567
568    #[test]
569    fn test_layout() {
570        each_string!(assert_layout);
571    }
572
573    fn assert_default<T: TestString>() {
574        assert!(T::default().is_empty());
575        assert_eq!(T::default().len(), 0);
576        assert_eq!(T::default(), "");
577        assert_eq!(T::default(), T::new(""));
578    }
579
580    #[test]
581    fn test_default() {
582        each_string!(assert_default);
583    }
584
585    fn assert_utf8_validation<T: TestString>() {
586        for valid in ["", "🦀", "valid UTF-8 🦀 longer than one word"] {
587            assert_eq!(T::from_utf8(valid.as_bytes()).unwrap().as_str(), valid);
588        }
589
590        for invalid in [
591            &[0x80][..],
592            &[0xff][..],
593            &[0xc0, 0x80][..],
594            &[0xe2, 0x82][..],
595        ] {
596            assert!(T::from_utf8(invalid).is_err());
597        }
598    }
599
600    #[test]
601    fn test_utf8_validation() {
602        each_string!(assert_utf8_validation);
603    }
604
605    fn assert_correct<T: TestString>(s: &str)
606    where
607        str: PartialEq<T>,
608        for<'a> &'a str: PartialEq<T>,
609    {
610        let cs = T::new(s);
611        assert_eq!(s.len() <= mem::size_of::<usize>(), cs.is_inline());
612        assert_eq!(cs.len(), s.len(), "error for: {:?}", s);
613        assert_eq!(cs.as_bytes(), s.as_bytes());
614        assert_eq!(cs.as_str().as_bytes(), s.as_bytes());
615        assert_eq!(cs.clone(), cs);
616        let bh = DefaultHashBuilder::new();
617        let mut hasher1 = bh.build_hasher();
618        cs.hash(&mut hasher1);
619        let mut hasher2 = bh.build_hasher();
620        cs.clone().hash(&mut hasher2);
621        assert_eq!(hasher1.finish(), hasher2.finish());
622        assert_eq!(cs, s);
623        assert_eq!(s, cs);
624        assert_eq!(cs, *s);
625        assert_eq!(*s, cs);
626        assert_eq!(cs, cs.clone());
627        assert!(cs != T::new("unused-qwerty"));
628        let opt_s = Some(cs.clone());
629        assert_eq!(opt_s, Some(T::new(s)));
630        assert!(opt_s.is_some());
631    }
632
633    #[test]
634    fn it_works() {
635        for s in [
636            "1",
637            "12",
638            "123",
639            "1234",
640            "12345",
641            "123456",
642            "1234567",
643            "12345678",
644            "123456789",
645            str::from_utf8(&[240, 159, 146, 150]).unwrap(),
646            "✅",
647            "❤️",
648            "🦀💯",
649            "🦀",
650            "💯",
651            "abcd",
652            "test",
653            "",
654            "\0",
655            "\0\0",
656            "\0\0\0",
657            "\0\0\0\0",
658            "\0\0\0\0\0\0\0",
659            "\0\0\0\0\0\0\0\0",
660            "1234567",
661            "12345678",
662            "longer test",
663            str::from_utf8(&[103, 39, 240, 145, 167, 156, 194, 165]).unwrap(),
664            "AaAa0 ® ",
665            str::from_utf8(&[240, 158, 186, 128, 240, 145, 143, 151]).unwrap(),
666        ] {
667            each_string!(assert_correct, s);
668        }
669    }
670
671    fn char_from_leading_byte(b: u8) -> Option<char> {
672        match b {
673            0x00..=0x7F => Some(b as char),
674            0xC2..=0xDF => str::from_utf8(&[b, 0x91]).unwrap().chars().next(),
675            0xE0 => str::from_utf8(&[b, 0xA0, 0x91]).unwrap().chars().next(),
676            0xE1..=0xEC | 0xEE..=0xEF => str::from_utf8(&[b, 0x91, 0xA5]).unwrap().chars().next(),
677            0xED => str::from_utf8(&[b, 0x80, 0x91]).unwrap().chars().next(),
678            0xF0 => str::from_utf8(&[b, 0x90, 0x91, 0xA5])
679                .unwrap()
680                .chars()
681                .next(),
682            0xF1..=0xF3 => str::from_utf8(&[b, 0x91, 0xA5, 0x82])
683                .unwrap()
684                .chars()
685                .next(),
686            0xF4 => str::from_utf8(&[b, 0x80, 0x91, 0x82])
687                .unwrap()
688                .chars()
689                .next(),
690            _ => None,
691        }
692    }
693
694    #[test]
695    fn test_edges() {
696        let width = mem::size_of::<usize>();
697        for len in [width - 1, width, width + 1] {
698            for first_byte in 0u8..=255 {
699                let first_char = match char_from_leading_byte(first_byte) {
700                    Some(c) => c,
701                    None => continue,
702                };
703
704                let mut s = String::with_capacity(len);
705                s.push(first_char);
706
707                while s.len() < len {
708                    let c = core::char::from_digit((len - s.len()) as u32, 10).unwrap();
709                    s.push(c);
710                }
711
712                each_string!(assert_correct, &s);
713            }
714        }
715    }
716
717    fn assert_unaligned_placement<T: TestString>() {
718        for s_content in ["torture", "tor", "tortures", "tort", "torture torture"] {
719            let mut buffer = [0u8; 32];
720            for offset in 0..8 {
721                unsafe {
722                    let dst = buffer.as_mut_ptr().add(offset).cast::<T>();
723                    let s = T::new(s_content);
724                    ptr::write_unaligned(dst, s);
725                    let recovered = ptr::read_unaligned(dst);
726                    assert_eq!(recovered.as_str(), s_content);
727                }
728            }
729        }
730    }
731
732    #[test]
733    fn test_unaligned_placement() {
734        each_string!(assert_unaligned_placement);
735    }
736
737    #[test]
738    fn ensure_zero_repr() {
739        assert!(str::from_utf8(&Encoded::<()>::WORD_NUL_MAP.to_ne_bytes()).is_err());
740    }
741
742    fn assert_const_word_nul<T: TestString>() {
743        let nul = str::from_utf8(&encoded::WORD_NUL).unwrap();
744        let const_value = T::new_inline(nul);
745        let non_const = T::new(nul);
746        let cloned = non_const.clone();
747        assert_eq!(const_value.encoded_addr(), non_const.encoded_addr());
748        assert_eq!(const_value.encoded_addr(), cloned.encoded_addr());
749        // The sentinel returns a slice into the shared word-sized NUL array.
750        assert_eq!(
751            &const_value.as_str().as_bytes()[0] as *const u8,
752            (&encoded::WORD_NUL) as *const u8
753        );
754    }
755
756    #[test]
757    fn test_const_word_nul() {
758        each_string!(assert_const_word_nul);
759    }
760}