diff --git a/src/length.rs b/src/length.rs index e349c9c..bbbe90f 100644 --- a/src/length.rs +++ b/src/length.rs @@ -70,6 +70,13 @@ impl InvalidStrLength { pub fn get_inner(self) -> Box { self.original } + + pub(crate) unsafe fn from_invalid_length_unchecked(value: InvalidLength) -> Self { + Self { + type_name: value.type_name, + original: unsafe { alloc::str::from_boxed_utf8_unchecked(value.original) }, + } + } } #[cfg(feature = "std")] @@ -86,23 +93,6 @@ impl core::fmt::Display for InvalidStrLength { } } -impl TryFrom> for InvalidStrLength { - type Error = core::str::Utf8Error; - - fn try_from(value: InvalidLength) -> Result { - let original = if let Err(err) = core::str::from_utf8(&value.original) { - return Err(err); - } else { - unsafe { alloc::str::from_boxed_utf8_unchecked(value.original) } - }; - - Ok(Self { - original, - type_name: value.type_name, - }) - } -} - #[doc(hidden)] pub trait NonZero: sealed::NonZeroSealed + Into + Sized + Copy + PartialEq + Debug diff --git a/src/string.rs b/src/string.rs index dd2cc52..a265575 100644 --- a/src/string.rs +++ b/src/string.rs @@ -1,6 +1,7 @@ use alloc::{ - borrow::{Cow, ToOwned}, + borrow::{Cow}, boxed::Box, + rc::Rc, string::String, sync::Arc, }; @@ -70,35 +71,51 @@ impl FixedString { Self(FixedStringRepr::Static(StaticStr::from_static_str(val))) } - /// Converts a `&str` into a [`FixedString`], allocating if the value cannot fit "inline". + /// Converts a string into a [`FixedString`], **truncating** if the value is larger than `LenT`'s maximum. /// - /// This method will be more efficent if you would otherwise clone a [`String`] to convert into [`FixedString`], - /// but should not be used in the case that [`String`] ownership could be transfered without reallocation. - /// - /// If the `&str` is `'static`, it is preferred to use [`Self::from_static_trunc`], which does not need to copy the data around. + /// If the `&str` value cannot fit "inline" it gets allocated. For owned types the allocation can get reused. /// /// "Inline" refers to Small String Optimisation which allows for Strings with less than 9 to 11 characters /// to be stored without allocation, saving a pointer size and an allocation. /// - /// See [`Self::from_string_trunc`] for truncation behaviour. + /// If the `&str` is `'static`, it is preferred to use [`Self::from_static_trunc`], which does not need to copy the data around. + /// + /// This allows for infallible conversion, but may be lossy in the case of a value above `LenT`'s max. + /// For lossless fallible conversion use [`TryFrom`] or [`Self::try_from_string`]. #[must_use] - pub fn from_str_trunc(val: &str) -> Self { - if let Some(inline) = Self::new_inline(val) { - inline - } else { - Self::from_string_trunc(val.to_owned()) + pub fn from_string_trunc(str: S) -> Self + where + S: AsRef, + Box: From, + { + match Self::try_from_string::(str) { + Ok(val) => val, + Err(err) => { + Self::from_string_trunc::(truncate_string(err, LenT::MAX.to_usize())) + } } } - /// Converts a [`String`] into a [`FixedString`], **truncating** if the value is larger than `LenT`'s maximum. + /// Converts a string into a [`FixedString`]. /// - /// This allows for infallible conversion, but may be lossy in the case of a value above `LenT`'s max. - /// For lossless fallible conversion, convert to [`Box`] using [`String::into_boxed_str`] and use [`TryFrom`]. - #[must_use] - pub fn from_string_trunc(str: String) -> Self { - match str.try_into() { - Ok(val) => val, - Err(err) => Self::from_string_trunc(truncate_string(err, LenT::MAX.to_usize())), + /// # Errors + /// + /// This function will return an error if str is longer than `LenT`'s maximum. + pub fn try_from_string(str: S) -> Result + where + S: AsRef, + Box: From, + { + if let Some(inline) = Self::new_inline(str.as_ref()) { + return Ok(inline); + } + + match Box::::from(str).into_boxed_bytes().try_into() { + Ok(val) => Ok(Self(FixedStringRepr::Heap(val))), + Err(err) => Err( + // SAFETY: Box -> Box<[u8]> -> Box always works + unsafe { InvalidStrLength::from_invalid_length_unchecked(err) }, + ), } } @@ -258,11 +275,7 @@ impl FromStr for FixedString { type Err = InvalidStrLength; fn from_str(val: &str) -> Result { - if let Some(inline) = Self::new_inline(val) { - Ok(inline) - } else { - Self::try_from(Box::from(val)) - } + Self::try_from_string(val) } } @@ -270,16 +283,7 @@ impl TryFrom> for FixedString { type Error = InvalidStrLength; fn try_from(value: Box) -> Result { - if let Some(inline) = Self::new_inline(&value) { - return Ok(inline); - } - - match value.into_boxed_bytes().try_into() { - Ok(val) => Ok(Self(FixedStringRepr::Heap(val))), - Err(err) => Err(err - .try_into() - .expect("Box -> Box<[u8]> should stay valid UTF8")), - } + Self::try_from_string(value) } } @@ -287,11 +291,39 @@ impl TryFrom for FixedString { type Error = InvalidStrLength; fn try_from(value: String) -> Result { - if let Some(inline) = Self::new_inline(&value) { - return Ok(inline); - } + Self::try_from_string(value) + } +} + +impl TryFrom<&str> for FixedString { + type Error = InvalidStrLength; + + fn try_from(value: &str) -> Result { + Self::try_from_string(value) + } +} + +impl TryFrom> for FixedString { + type Error = InvalidStrLength; + + fn try_from(value: Cow<'_, str>) -> Result { + Self::try_from_string(value) + } +} + +impl TryFrom> for FixedString { + type Error = InvalidStrLength; - value.into_boxed_str().try_into() + fn try_from(value: Rc) -> Result { + Self::try_from_string(value.as_ref()) + } +} + +impl TryFrom> for FixedString { + type Error = InvalidStrLength; + + fn try_from(value: Arc) -> Result { + Self::try_from_string(value.as_ref()) } } @@ -373,9 +405,33 @@ impl AsRef for FixedString { } } +/// With reference counted pointers the allocation can't be re-used, +/// so allocating a Box just to copy from there to a new alloc +/// should be avoided. Instead construct the rc from the &str directly. +fn fixed_string_into_ref_counted(value: FixedString) -> RC +where + LenT: ValidLength, + RC: From> + 'static, + for<'a> RC: From<&'a str>, +{ + if matches!(value.0, FixedStringRepr::Heap(_)) { + // Move existing allocation to Arc/Rc + RC::from(Box::::from(value)) + } else { + // Just construct the Arc/Rc directly + RC::from(&*value) + } +} + impl From> for Arc { fn from(value: FixedString) -> Self { - Arc::from(value.into_string()) + fixed_string_into_ref_counted(value) + } +} + +impl From> for Rc { + fn from(value: FixedString) -> Self { + fixed_string_into_ref_counted(value) } } @@ -404,11 +460,11 @@ impl<'de, LenT: ValidLength> serde::Deserialize<'de> for FixedString { } fn visit_str(self, val: &str) -> Result { - FixedString::from_str(val).map_err(E::custom) + FixedString::try_from_string(val).map_err(E::custom) } fn visit_string(self, val: String) -> Result { - FixedString::try_from(val.into_boxed_str()).map_err(E::custom) + FixedString::try_from_string(val).map_err(E::custom) } } @@ -426,6 +482,7 @@ impl serde::Serialize for FixedString { #[cfg(test)] mod test { use super::*; + use core::fmt::Debug; fn check_u8_roundtrip_generic(to_fixed: fn(String) -> FixedString) { for i in 0..=u8::MAX { @@ -539,7 +596,7 @@ mod test { use to_arraystring::ToArrayString; check_u8_roundtrip_generic(|original| { - FixedString::from_str_trunc( + FixedString::::from_string_trunc( FixedString::from_string_trunc(original) .to_arraystring() .as_str(), @@ -636,4 +693,111 @@ mod test { assert_eq!(s.len(), 4); assert!(s.is_inline()); } + + fn try_from_rountrip(value: S) + where + LenT: ValidLength, + FixedString: TryFrom, + as TryFrom>::Error: Debug, + S: AsRef, + S: From>, + Box: From, + { + let string = value.as_ref().to_string(); + + let fixed_str: FixedString = value.try_into().expect("Try into should work"); + + assert_eq!(fixed_str, string); + + let value: S = fixed_str.into(); + + assert_eq!(value.as_ref(), string); + + let fixed_str = FixedString::::from_string_trunc(value); + + assert_eq!(fixed_str, string); + + let fixed_str = FixedString::::from_string_trunc::>(fixed_str); + + let value: S = fixed_str.into(); + + let fixed_str = FixedString::::try_from_string(value).expect("try_from_string works"); + + assert_eq!(fixed_str, string); + + let fixed_str = FixedString::::try_from_string::>(fixed_str) + .expect("try_from_string works"); + + assert_eq!(fixed_str, string); + } + + #[test] + fn test_try_from_string() { + let value = "Hello, world!"; + + try_from_rountrip::(value.into()); + try_from_rountrip::(value.into()); + #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))] + try_from_rountrip::(value.into()); + } + + #[test] + fn test_try_from_boxed_str() { + let value = "Hello, world!"; + + try_from_rountrip::>(value.into()); + try_from_rountrip::>(value.into()); + #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))] + try_from_rountrip::>(value.into()); + } + + #[test] + fn test_try_from_owned_cow_string() { + let owned_cow: Cow<'static, str> = Cow::Owned("Hello, world!".into()); + + #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))] + try_from_rountrip::>(owned_cow.clone()); + try_from_rountrip::>(owned_cow.clone()); + try_from_rountrip::>(owned_cow); + } + + #[test] + fn test_try_from_cow_string() { + let owned_cow: Cow<'_, str> = Cow::Borrowed("Hello, world!"); + + #[cfg(any(target_pointer_width = "64", target_pointer_width = "32"))] + try_from_rountrip::>(owned_cow.clone()); + try_from_rountrip::>(owned_cow.clone()); + try_from_rountrip::>(owned_cow); + } + + #[test] + fn test_try_from_rc_str() { + let static_string = "Test string"; + + let rc_string: Rc = static_string.into(); + + let value: FixedString:: = rc_string.try_into().unwrap(); + + assert_eq!(static_string, value); + + let rc_string: Rc = value.into(); + + assert_eq!(static_string, rc_string.as_ref()); + } + + #[test] + fn test_try_from_arc_str() { + let static_string = "Test string"; + + let arc_string: Arc = static_string.into(); + + let value: FixedString:: = arc_string.try_into().unwrap(); + + assert_eq!(static_string, value); + + let arc_string: Arc = value.into(); + + assert_eq!(static_string, arc_string.as_ref()); + } }