diff --git a/src/strings/mod.rs b/src/strings/mod.rs index 07b01df..9016eb7 100644 --- a/src/strings/mod.rs +++ b/src/strings/mod.rs @@ -1,6 +1,8 @@ pub mod experimental; pub mod fixed_string; +pub mod stack_string; pub mod shstring; pub use fixed_string::{FixedString, Error as FixedStringError}; +pub use stack_string::StackString; pub use shstring::{ShString, ShString22}; diff --git a/src/strings/stack_string.rs b/src/strings/stack_string.rs new file mode 100644 index 0000000..9ab08eb --- /dev/null +++ b/src/strings/stack_string.rs @@ -0,0 +1,278 @@ +use std::{ + borrow::{self, Cow}, + cmp::Ordering, + error, + fmt, + hash::{Hash, Hasher}, + ops, + str, +}; + +#[derive(Clone)] +pub struct StackString { + buf: [u8; N], + len: u8, +} + +impl StackString { + const MAX_LEN: u8 = { + #[allow(clippy::cast_possible_truncation, clippy::checked_conversions)] + if N <= u8::MAX as usize { + N as u8 + } else { + panic!("`N` must be within the bounds of `u8`") + } + }; + + /// Creates a new `StackString` from a given byte buffer and length. + /// + /// # Safety + /// + /// The first `len` bytes of `buf` (i.e. `buf[..len]`) must be valid UTF-8. + #[inline] + #[must_use] + pub const unsafe fn from_raw_parts(buf: [u8; N], len: u8) -> Self { + Self { buf, len } + } + + /// Returns a new empty `StackString`. + #[inline] + #[must_use] + pub const fn empty() -> Self { + // SAFETY: + // The first zero bytes of the buffer are valid UTF-8, because an empty byte slice is + // valid UTF-8. + unsafe { Self::from_raw_parts([0; N], 0) } + } + + #[inline] + pub fn new(s: &S) -> Result + where + S: AsRef + ?Sized, + { + // Convert the string to a byte slice, which is guaranteed to be valid UTF-8 since this is + // an invariant of `str`. + let s = >::as_ref(s).as_bytes(); + + // If the length of the string is greater than `Self::MAX_LEN`, it will not fit in the + // stack buffer so return `None`. + let len = u8::try_from(s.len()) + .ok() + .and_then(|len| (len <= Self::MAX_LEN).then_some(len)) + .ok_or(StackStringError { + max_len: N, + actual_len: s.len(), + })?; + + let mut buf = [0; N]; + buf[..usize::from(len)].copy_from_slice(s); + + // SAFETY: + // The first `len` bytes of the buffer are valid UTF-8 because the first `len` bytes of + // the buffer contain data copied from a `&str`, and `&str` is always valid UTF-8. + unsafe { Ok(Self::from_raw_parts(buf, len)) } + } + + /// Returns a string slice pointing to the underlying string data. + pub fn as_str(&self) -> &str { + // SAFETY: + // `len` being less than or equal to `N` is an invariant of `StackString`, so it is + // always within the bounds of `buf`. + let slice = unsafe { self.buf.get_unchecked(..usize::from(self.len)) }; + + // SAFETY: + // The first `len` bytes of `buf` being valid UTF-8 is an invariant of `StackString`. + unsafe { str::from_utf8_unchecked(slice) } + } + + /// Returns a mutable string slice pointing to the underlying string data. + pub fn as_str_mut(&mut self) -> &mut str { + // SAFETY: + // `len` being less than or equal to `N` is an invariant of `StackString`, so it is + // always within the bounds of `buf`. + let slice = unsafe { self.buf.get_unchecked_mut(..usize::from(self.len)) }; + + // SAFETY: + // The first `len` bytes of `buf` being valid UTF-8 is an invariant of `StackString`. + unsafe { str::from_utf8_unchecked_mut(slice) } + } + + pub fn into_string(self) -> String { + self.as_str().to_owned() + } + + pub fn len(&self) -> usize { + usize::from(self.len) + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } +} + +impl Default for StackString { + #[inline] + fn default() -> Self { + Self::empty() + } +} + +impl ops::Deref for StackString { + type Target = str; + + #[inline] + fn deref(&self) -> &Self::Target { + self.as_str() + } +} + +impl ops::DerefMut for StackString { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + self.as_str_mut() + } +} + +impl AsRef for StackString { + #[inline] + fn as_ref(&self) -> &str { + self + } +} + +impl AsMut for StackString { + #[inline] + fn as_mut(&mut self) -> &mut str { + self + } +} + +impl borrow::Borrow for StackString { + #[inline] + fn borrow(&self) -> &str { + self + } +} + +impl borrow::BorrowMut for StackString { + #[inline] + fn borrow_mut(&mut self) -> &mut str { + self + } +} + +impl<'a, const N: usize> TryFrom<&'a str> for StackString { + type Error = StackStringError; + + #[inline] + fn try_from(s: &'a str) -> Result { + Self::new(s) + } +} + +impl TryFrom for StackString { + type Error = StackStringError; + + #[inline] + fn try_from(s: String) -> Result { + Self::new(&s) + } +} + +impl<'a, const N: usize> TryFrom> for StackString { + type Error = StackStringError; + + #[inline] + fn try_from(s: Cow<'a, str>) -> Result { + Self::new(&s) + } +} + +impl From> for String { + #[inline] + fn from(s: StackString) -> Self { + s.into_string() + } +} + +impl PartialEq> for StackString { + #[inline] + fn eq(&self, other: &StackString) -> bool { + **self == **other + } +} + +impl Eq for StackString {} + +impl PartialOrd> for StackString { + #[inline] + fn partial_cmp(&self, other: &StackString) -> Option { + (**self).partial_cmp(&**other) + } +} + +impl Ord for StackString { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + (**self).cmp(&**other) + } +} + +impl Hash for StackString { + #[inline] + fn hash(&self, state: &mut H) { + (**self).hash(state); + } +} + +impl str::FromStr for StackString { + type Err = StackStringError; + + #[inline] + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + +impl fmt::Debug for StackString { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl fmt::Display for StackString { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&**self, f) + } +} + +#[derive(Debug)] +pub struct StackStringError { + max_len: usize, + actual_len: usize, +} + +impl StackStringError { + pub fn max_len(&self) -> usize { + self.max_len + } + + pub fn actual_len(&self) -> usize { + self.actual_len + } +} + +impl fmt::Display for StackStringError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "string of length {} exceeds limit for `StackString<{}>`", + self.actual_len, + self.max_len + ) + } +} + +impl error::Error for StackStringError {}