tiny_str/
lib.rs

1/*  Copyright (C) 2025 Saúl Valdelvira
2 *
3 *  This program is free software: you can redistribute it and/or modify
4 *  it under the terms of the GNU General Public License as published by
5 *  the Free Software Foundation, version 3.
6 *
7 *  This program is distributed in the hope that it will be useful,
8 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
9 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10 *  GNU General Public License for more details.
11 *
12 *  You should have received a copy of the GNU General Public License
13 *  along with this program.  If not, see <https://www.gnu.org/licenses/>. */
14
15//! Tiny string
16//!
17//! A string that can store a small amount of bytes on the stack.
18//!
19//! This struct provides a string-like API, but performs SSO (Small String Optimization)
20//! This means that a `TinyString<N>` stores up to N bytes on the stack.
21//! If the string grows bigger than that, it moves the contents to the heap.
22//!
23//! # Example
24//! ```
25//! use tiny_str::TinyString;
26//!
27//! let mut s = TinyString::<10>::new();
28//!
29//! for (i, c) in (b'0'..=b'9').enumerate() {
30//!     s.push(c as char);
31//!     assert_eq!(s.len(), i + 1);
32//! }
33//!
34//! // Up to this point, no heap allocations are needed.
35//! // The string is stored on the stack.
36//!
37//! s.push_str("abc"); // This moves the string to the heap
38//!
39//! assert_eq!(&s[..], "0123456789abc")
40//! ```
41//!
42//! # Memory layout
43//! TinyString is based on [TinyVec], just like [alloc::string::String] if based
44//! on [alloc::vec::Vec].
45//!
46//! You can read the [tiny_vec] crate documentation to learn about the internal
47//! representation of the data.
48
49#![no_std]
50
51use core::fmt::{self, Display};
52use core::ops::{Deref, DerefMut};
53use core::str::{self, Utf8Error};
54
55extern crate alloc;
56use alloc::vec::Vec;
57use alloc::boxed::Box;
58
59use tiny_vec::TinyVec;
60
61const MAX_N_STACK_ELEMENTS: usize = tiny_vec::n_elements_for_stack::<u8>();
62
63/// A string that can store a small amount of bytes on the stack.
64pub struct TinyString<const N: usize = MAX_N_STACK_ELEMENTS>(TinyVec<u8, N>);
65
66impl<const N: usize> TinyString<N> {
67
68    /// Creates a new [TinyString]
69    #[inline]
70    pub const fn new() -> Self {
71        Self(TinyVec::new())
72    }
73
74    /// Creates a new [TinyString] with the given capacity
75    pub fn with_capacity(cap: usize) -> Self {
76        Self(TinyVec::with_capacity(cap))
77    }
78
79    /// Creates a new [TinyString] from the given utf8 buffer.
80    ///
81    /// # Errors
82    /// If the byte buffer contains invalid uft8
83    pub fn from_utf8(utf8: TinyVec<u8, N>) -> Result<Self,Utf8Error> {
84        str::from_utf8(utf8.as_slice())?;
85        Ok(Self(utf8))
86    }
87
88    /// Creates a new [TinyString] from the given utf8 buffer.
89    ///
90    /// # Safety
91    /// The caller must ensure that the given contains valid utf8
92    #[inline(always)]
93    pub const unsafe fn from_utf8_unchecked(utf8: TinyVec<u8, N>) -> Self {
94        Self(utf8)
95    }
96
97    /// Returns the number of elements inside this string
98    #[inline]
99    pub const fn len(&self) -> usize { self.0.len() }
100
101    /// Returns true if the string is empty
102    #[inline]
103    pub const fn is_empty(&self) -> bool { self.0.is_empty() }
104
105    /// Returns the allocated capacity for this string
106    #[inline]
107    pub const fn capacity(&self) -> usize { self.0.capacity() }
108
109    /// Returns a str slice
110    #[inline]
111    pub const fn as_str(&self) -> &str {
112        unsafe { str::from_utf8_unchecked(self.0.as_slice()) }
113    }
114
115    /// Returns a mutable str slice
116    #[inline]
117    pub const fn as_mut_str(&mut self) -> &mut str {
118        unsafe { str::from_utf8_unchecked_mut(self.0.as_mut_slice()) }
119    }
120
121    /// Returns the string as a byte slice
122    #[inline]
123    pub const fn as_bytes(&self) -> &[u8] {
124        self.0.as_slice()
125    }
126
127    /// Pushes a character into the string
128    pub fn push(&mut self, c: char) {
129        let len = c.len_utf8();
130        if len == 1 {
131            self.0.push(c as u8);
132        } else {
133            let mut buf = [0_u8; 4];
134            c.encode_utf8(&mut buf);
135            self.0.extend_from_slice(&buf[..len]);
136        }
137    }
138
139    /// Returns the last char of this string, if present
140    ///
141    /// # Example
142    /// ```
143    /// use tiny_str::TinyString;
144    ///
145    /// let mut s = TinyString::<10>::new();
146    ///
147    /// s.push_str("abcd");
148    ///
149    /// assert_eq!(s.pop(), Some('d'));
150    /// assert_eq!(s, "abc");
151    /// ```
152    pub fn pop(&mut self) -> Option<char> {
153        let c = self.chars().next_back()?;
154        let new_len = self.len() - c.len_utf8();
155        unsafe {
156            self.0.set_len(new_len);
157        }
158        Some(c)
159    }
160
161    /// Pushes a str slice into this string
162    #[inline]
163    pub fn push_str(&mut self, s: &str) {
164        self.0.extend_from_slice_copied(s.as_bytes());
165    }
166
167    /// Shrinks the capacity of this string to fit exactly it's length
168    #[inline]
169    pub fn shrink_to_fit(&mut self) {
170        self.0.shrink_to_fit();
171    }
172
173    /// Reserves space for, at least, n bytes
174    #[inline]
175    pub fn reserve(&mut self, n: usize) {
176        self.0.reserve(n);
177    }
178
179    /// Reserves space for exactly n more bytes
180    #[inline]
181    pub fn reserve_exact(&mut self, n: usize) {
182        self.0.reserve_exact(n);
183    }
184
185    /// Converts this TinyString into a boxed str
186    ///
187    /// # Example
188    /// ```
189    /// use tiny_str::TinyString;
190    ///
191    /// let mut s = TinyString::<10>::new();
192    /// s.push_str("abc");
193    ///
194    /// let b = s.into_boxed_str();
195    /// assert_eq!(&*b, "abc");
196    /// ```
197    pub fn into_boxed_str(self) -> Box<str> {
198        let b = self.0.into_boxed_slice();
199        unsafe { alloc::str::from_boxed_utf8_unchecked(b) }
200    }
201}
202
203impl<const N: usize> Default for TinyString<N> {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209impl<const N: usize> Deref for TinyString<N> {
210    type Target = str;
211
212    fn deref(&self) -> &Self::Target {
213        self.as_str()
214    }
215}
216
217impl<const N: usize> DerefMut for TinyString<N> {
218    fn deref_mut(&mut self) -> &mut Self::Target {
219        self.as_mut_str()
220    }
221}
222
223impl<const N: usize> From<&str> for TinyString<N> {
224    fn from(value: &str) -> Self {
225        let mut s = Self::with_capacity(value.len());
226        s.push_str(value);
227        s
228    }
229}
230
231impl<const N: usize> TryFrom<&[u8]> for TinyString<N> {
232    type Error = Utf8Error;
233
234    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
235        str::from_utf8(value)?;
236        Ok(unsafe { Self::from_utf8_unchecked(TinyVec::from_slice_copied(value)) })
237    }
238}
239
240impl<const N: usize> TryFrom<TinyVec<u8, N>> for TinyString<N> {
241    type Error = Utf8Error;
242
243    fn try_from(value: TinyVec<u8, N>) -> Result<Self, Self::Error> {
244        Self::from_utf8(value)
245    }
246}
247
248impl<const N: usize> TryFrom<Vec<u8>> for TinyString<N> {
249    type Error = Utf8Error;
250
251    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
252        str::from_utf8(value.as_slice())?;
253        Ok(unsafe { Self::from_utf8_unchecked(TinyVec::from_vec(value)) })
254    }
255}
256
257impl<const N: usize> From<TinyString<N>> for TinyVec<u8, N> {
258    fn from(value: TinyString<N>) -> Self {
259        value.0
260    }
261}
262
263impl<const N: usize> From<TinyString<N>> for Vec<u8> {
264    fn from(value: TinyString<N>) -> Self {
265        value.0.into_vec()
266    }
267}
268
269impl<const N: usize> From<TinyString<N>> for Box<str> {
270    fn from(value: TinyString<N>) -> Self {
271        value.into_boxed_str()
272    }
273}
274
275impl<const N: usize> FromIterator<char> for TinyString<N> {
276    fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
277        let iter = iter.into_iter();
278        let cap = match iter.size_hint() {
279            (_, Some(n)) => n,
280            (n, _) => n,
281        };
282        let mut s = TinyString::with_capacity(cap);
283        for c in iter {
284            s.push(c);
285        }
286        s
287    }
288}
289
290impl<const N: usize, S> PartialEq<S> for TinyString<N>
291where
292    S: AsRef<[u8]>
293{
294    fn eq(&self, other: &S) -> bool {
295        self.as_bytes() == other.as_ref()
296    }
297}
298
299impl<const N: usize> Eq for TinyString<N> { }
300
301impl<const N: usize> AsRef<[u8]> for TinyString<N> {
302    fn as_ref(&self) -> &[u8] {
303        self.as_bytes()
304    }
305}
306
307impl<const N: usize> AsRef<str> for TinyString<N> {
308    fn as_ref(&self) -> &str {
309        self.as_str()
310    }
311}
312
313impl<const N: usize> AsMut<str> for TinyString<N> {
314    fn as_mut(&mut self) -> &mut str {
315        self.as_mut_str()
316    }
317}
318
319impl<const N: usize> fmt::Debug for TinyString<N> {
320    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
321        write!(f, "{:?}", self.bytes())
322    }
323}
324
325impl<const N: usize> Display for TinyString<N> {
326    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
327        write!(f, "{}", self.as_str())
328    }
329}
330
331#[cfg(test)]
332mod test;