equt_md/
strings.rs

1use std::borrow::{Borrow, ToOwned};
2use std::convert::{AsRef, TryFrom};
3use std::fmt;
4use std::hash::{Hash, Hasher};
5use std::ops::Deref;
6use std::str::from_utf8;
7
8const MAX_INLINE_STR_LEN: usize = 3 * std::mem::size_of::<isize>() - 1;
9
10/// Returned when trying to convert a `&str` into a `InlineStr`
11/// but it fails because it doesn't fit.
12#[derive(Debug)]
13pub struct StringTooLongError;
14
15/// An inline string that can contain almost three words
16/// of utf-8 text.
17#[derive(Debug, Clone, Copy, Eq)]
18pub struct InlineStr {
19    inner: [u8; MAX_INLINE_STR_LEN],
20}
21
22impl<'a> AsRef<str> for InlineStr {
23    fn as_ref(&self) -> &str {
24        self.deref()
25    }
26}
27
28impl Hash for InlineStr {
29    fn hash<H: Hasher>(&self, state: &mut H) {
30        self.deref().hash(state);
31    }
32}
33
34impl From<char> for InlineStr {
35    fn from(c: char) -> Self {
36        let mut inner = [0u8; MAX_INLINE_STR_LEN];
37        c.encode_utf8(&mut inner);
38        inner[MAX_INLINE_STR_LEN - 1] = c.len_utf8() as u8;
39        Self { inner }
40    }
41}
42
43impl<'a> std::cmp::PartialEq<InlineStr> for InlineStr {
44    fn eq(&self, other: &InlineStr) -> bool {
45        self.deref() == other.deref()
46    }
47}
48
49impl TryFrom<&str> for InlineStr {
50    type Error = StringTooLongError;
51
52    fn try_from(s: &str) -> Result<InlineStr, StringTooLongError> {
53        let len = s.len();
54        if len < MAX_INLINE_STR_LEN {
55            let mut inner = [0u8; MAX_INLINE_STR_LEN];
56            inner[..len].copy_from_slice(s.as_bytes());
57            inner[MAX_INLINE_STR_LEN - 1] = len as u8;
58            Ok(Self { inner })
59        } else {
60            Err(StringTooLongError)
61        }
62    }
63}
64
65impl Deref for InlineStr {
66    type Target = str;
67
68    fn deref(&self) -> &str {
69        let len = self.inner[MAX_INLINE_STR_LEN - 1] as usize;
70        from_utf8(&self.inner[..len]).unwrap()
71    }
72}
73
74impl fmt::Display for InlineStr {
75    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
76        write!(f, "{}", self.as_ref())
77    }
78}
79
80/// A copy-on-write string that can be owned, borrowed
81/// or inlined.
82///
83/// It is three words long.
84#[derive(Debug, Eq)]
85pub enum CowStr<'a> {
86    /// An owned, immutable string.
87    Boxed(Box<str>),
88    /// A borrowed string.
89    Borrowed(&'a str),
90    /// A short inline string.
91    Inlined(InlineStr),
92}
93
94impl<'a> AsRef<str> for CowStr<'a> {
95    fn as_ref(&self) -> &str {
96        self.deref()
97    }
98}
99
100impl<'a> Hash for CowStr<'a> {
101    fn hash<H: Hasher>(&self, state: &mut H) {
102        self.deref().hash(state);
103    }
104}
105
106impl<'a> std::clone::Clone for CowStr<'a> {
107    fn clone(&self) -> Self {
108        match self {
109            CowStr::Boxed(s) => match InlineStr::try_from(&**s) {
110                Ok(inline) => CowStr::Inlined(inline),
111                Err(..) => CowStr::Boxed(s.clone()),
112            },
113            CowStr::Borrowed(s) => CowStr::Borrowed(s),
114            CowStr::Inlined(s) => CowStr::Inlined(*s),
115        }
116    }
117}
118
119impl<'a> std::cmp::PartialEq<CowStr<'a>> for CowStr<'a> {
120    fn eq(&self, other: &CowStr) -> bool {
121        self.deref() == other.deref()
122    }
123}
124
125impl<'a> From<&'a str> for CowStr<'a> {
126    fn from(s: &'a str) -> Self {
127        CowStr::Borrowed(s)
128    }
129}
130
131impl<'a> From<String> for CowStr<'a> {
132    fn from(s: String) -> Self {
133        CowStr::Boxed(s.into_boxed_str())
134    }
135}
136
137impl<'a> From<char> for CowStr<'a> {
138    fn from(c: char) -> Self {
139        CowStr::Inlined(c.into())
140    }
141}
142
143impl<'a> Deref for CowStr<'a> {
144    type Target = str;
145
146    fn deref(&self) -> &str {
147        match self {
148            CowStr::Boxed(ref b) => &*b,
149            CowStr::Borrowed(b) => b,
150            CowStr::Inlined(ref s) => s.deref(),
151        }
152    }
153}
154
155impl<'a> Borrow<str> for CowStr<'a> {
156    fn borrow(&self) -> &str {
157        self.deref()
158    }
159}
160
161impl<'a> CowStr<'a> {
162    pub fn into_string(self) -> String {
163        match self {
164            CowStr::Boxed(b) => b.into(),
165            CowStr::Borrowed(b) => b.to_owned(),
166            CowStr::Inlined(s) => s.deref().to_owned(),
167        }
168    }
169}
170
171impl<'a> fmt::Display for CowStr<'a> {
172    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
173        write!(f, "{}", self.as_ref())
174    }
175}
176
177#[cfg(test)]
178mod test_special_string {
179    use super::*;
180
181    #[test]
182    fn inlinestr_ascii() {
183        let s: InlineStr = 'a'.into();
184        assert_eq!("a", s.deref());
185    }
186
187    #[test]
188    fn inlinestr_unicode() {
189        let s: InlineStr = '🍔'.into();
190        assert_eq!("🍔", s.deref());
191    }
192
193    #[test]
194    fn cowstr_size() {
195        let size = std::mem::size_of::<CowStr>();
196        let word_size = std::mem::size_of::<isize>();
197        assert_eq!(3 * word_size, size);
198    }
199
200    #[test]
201    fn cowstr_char_to_string() {
202        let c = '藏';
203        let smort: CowStr = c.into();
204        let owned: String = smort.to_string();
205        let expected = "藏".to_owned();
206        assert_eq!(expected, owned);
207    }
208
209    #[test]
210    fn max_inline_str_len_atleast_five() {
211        // we need 4 bytes to store a char and then one more to store
212        // its length
213        assert!(MAX_INLINE_STR_LEN >= 5);
214    }
215
216    #[test]
217    #[cfg(target_pointer_width = "64")]
218    fn inlinestr_fits_twentytwo() {
219        let s = "0123456789abcdefghijkl";
220        let stack_str = InlineStr::try_from(s).unwrap();
221        assert_eq!(stack_str.deref(), s);
222    }
223
224    #[test]
225    #[cfg(target_pointer_width = "64")]
226    fn inlinestr_not_fits_twentythree() {
227        let s = "0123456789abcdefghijklm";
228        let _stack_str = InlineStr::try_from(s).unwrap_err();
229    }
230
231    #[test]
232    #[cfg(target_pointer_width = "64")]
233    fn small_boxed_str_clones_to_stack() {
234        let s = "0123456789abcde".to_owned();
235        let smort: CowStr = s.into();
236        let smort_clone = smort.clone();
237
238        if let CowStr::Inlined(..) = smort_clone {
239        } else {
240            panic!("Expected a Inlined variant!");
241        }
242    }
243}