From 7d7ec08187753b2be08838346524efc3c3e2fd6d Mon Sep 17 00:00:00 2001 From: Joshix Date: Fri, 3 Oct 2025 09:00:00 +0000 Subject: [PATCH 01/12] add generic FIxedString::try_from_string --- src/length.rs | 7 +++++++ src/string.rs | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/length.rs b/src/length.rs index e349c9c..d93d48a 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")] diff --git a/src/string.rs b/src/string.rs index dd2cc52..0c5f998 100644 --- a/src/string.rs +++ b/src/string.rs @@ -102,6 +102,29 @@ impl FixedString { } } + /// Converts a string into a [`FixedString`]. + /// + /// # 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) }, + ), + } + } + /// Returns the length of the [`FixedString`]. #[must_use] pub fn len(&self) -> LenT { From 9f5f3b9d804afa5c4f4105c4bba1a21fb6546e56 Mon Sep 17 00:00:00 2001 From: Joshix Date: Fri, 3 Oct 2025 09:00:00 +0000 Subject: [PATCH 02/12] use generic try_from_string in different places --- src/string.rs | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/src/string.rs b/src/string.rs index 0c5f998..64374d9 100644 --- a/src/string.rs +++ b/src/string.rs @@ -281,11 +281,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) } } @@ -293,16 +289,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) } } @@ -310,11 +297,7 @@ impl TryFrom for FixedString { type Error = InvalidStrLength; fn try_from(value: String) -> Result { - if let Some(inline) = Self::new_inline(&value) { - return Ok(inline); - } - - value.into_boxed_str().try_into() + Self::try_from_string(value) } } @@ -427,11 +410,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) } } From 72e878f45cfd44ba8ed69e5c5c7b7fc727f67cf3 Mon Sep 17 00:00:00 2001 From: Joshix Date: Fri, 3 Oct 2025 10:00:00 +0000 Subject: [PATCH 03/12] improve conversion into rc types --- src/string.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/string.rs b/src/string.rs index 64374d9..213a1e8 100644 --- a/src/string.rs +++ b/src/string.rs @@ -1,6 +1,7 @@ use alloc::{ borrow::{Cow, ToOwned}, boxed::Box, + rc::Rc, string::String, sync::Arc, }; @@ -381,7 +382,25 @@ impl AsRef for FixedString { impl From> for Arc { fn from(value: FixedString) -> Self { - Arc::from(value.into_string()) + if matches!(value.0, FixedStringRepr::Heap(_)) { + // Move existing allocation to Rc + Self::from(Box::::from(value)) + } else { + // Just construct the Rc directly + Self::from(&*value) + } + } +} + +impl From> for Rc { + fn from(value: FixedString) -> Self { + if matches!(value.0, FixedStringRepr::Heap(_)) { + // Move existing allocation to Rc + Self::from(Box::::from(value)) + } else { + // Just construct the Rc directly + Self::from(&*value) + } } } From 7f6dcd0312e848585f68b99420eb32e41af09273 Mon Sep 17 00:00:00 2001 From: Joshix Date: Fri, 3 Oct 2025 10:00:00 +0000 Subject: [PATCH 04/12] dry out code --- src/string.rs | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/string.rs b/src/string.rs index 213a1e8..163ab88 100644 --- a/src/string.rs +++ b/src/string.rs @@ -380,27 +380,32 @@ 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 { - if matches!(value.0, FixedStringRepr::Heap(_)) { - // Move existing allocation to Rc - Self::from(Box::::from(value)) - } else { - // Just construct the Rc directly - Self::from(&*value) - } + fixed_string_into_ref_counted(value) } } impl From> for Rc { fn from(value: FixedString) -> Self { - if matches!(value.0, FixedStringRepr::Heap(_)) { - // Move existing allocation to Rc - Self::from(Box::::from(value)) - } else { - // Just construct the Rc directly - Self::from(&*value) - } + fixed_string_into_ref_counted(value) } } From 59308cb6a325d33c1719fbaf67baddfe1aa5bbcf Mon Sep 17 00:00:00 2001 From: Joshix Date: Fri, 3 Oct 2025 10:00:00 +0000 Subject: [PATCH 05/12] make FixedString::from_string_trunc generic --- src/string.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/string.rs b/src/string.rs index 163ab88..882844d 100644 --- a/src/string.rs +++ b/src/string.rs @@ -87,19 +87,23 @@ impl FixedString { if let Some(inline) = Self::new_inline(val) { inline } else { - Self::from_string_trunc(val.to_owned()) + Self::from_string_trunc(val) } } /// Converts a [`String`] into a [`FixedString`], **truncating** if the value is larger than `LenT`'s maximum. /// /// 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`]. + /// For lossless fallible conversion use [`TryFrom`] or [`Self::try_from_string`]. #[must_use] - pub fn from_string_trunc(str: String) -> Self { - match str.try_into() { + 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())), + Err(err) => Self::from_string_trunc::(truncate_string(err, LenT::MAX.to_usize())), } } From 7d3ad015139a67a89d03112ec14dedbf8e718746 Mon Sep 17 00:00:00 2001 From: Joshix Date: Fri, 3 Oct 2025 10:00:00 +0000 Subject: [PATCH 06/12] remove FixedString::from_str_trunc as from_string_trunc covers that --- src/string.rs | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/string.rs b/src/string.rs index 882844d..c9cb554 100644 --- a/src/string.rs +++ b/src/string.rs @@ -71,27 +71,14 @@ 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. - #[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) - } - } - - /// Converts a [`String`] into a [`FixedString`], **truncating** if the value is larger than `LenT`'s maximum. + /// 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`]. @@ -573,7 +560,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(), From bb6bcf838ae3edb8519e23caa241641ab1a4ec19 Mon Sep 17 00:00:00 2001 From: Joshix Date: Fri, 3 Oct 2025 10:00:00 +0000 Subject: [PATCH 07/12] implement TryFrom<&str> and TryFrom> for FixedString --- src/string.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/string.rs b/src/string.rs index c9cb554..221ed7c 100644 --- a/src/string.rs +++ b/src/string.rs @@ -293,6 +293,22 @@ impl TryFrom for FixedString { } } +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 From for FixedString { fn from(value: char) -> Self { use alloc::vec; From ad20476f0054dc17cedef5ea3826ada296514f1b Mon Sep 17 00:00:00 2001 From: Joshix Date: Fri, 3 Oct 2025 10:00:00 +0000 Subject: [PATCH 08/12] implement TryFrom> and TryFrom> for FixedString --- src/string.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/string.rs b/src/string.rs index 221ed7c..70a37c7 100644 --- a/src/string.rs +++ b/src/string.rs @@ -309,6 +309,22 @@ impl TryFrom> for FixedString { } } +impl TryFrom> for FixedString { + type Error = InvalidStrLength; + + 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()) + } +} + impl From for FixedString { fn from(value: char) -> Self { use alloc::vec; From f748148b1a619075d5c78383492f98bc16c0d712 Mon Sep 17 00:00:00 2001 From: Joshix Date: Fri, 3 Oct 2025 11:00:00 +0000 Subject: [PATCH 09/12] remove unused TryFrom implementation --- src/length.rs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/length.rs b/src/length.rs index d93d48a..bbbe90f 100644 --- a/src/length.rs +++ b/src/length.rs @@ -93,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 From f8c0337beb577d5359a675325c6020f53894270d Mon Sep 17 00:00:00 2001 From: Joshix Date: Fri, 3 Oct 2025 11:00:00 +0000 Subject: [PATCH 10/12] cargo fmt --- src/string.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/string.rs b/src/string.rs index 70a37c7..97cfcc0 100644 --- a/src/string.rs +++ b/src/string.rs @@ -86,11 +86,13 @@ impl FixedString { pub fn from_string_trunc(str: S) -> Self where S: AsRef, - Box: From + Box: From, { match Self::try_from_string::(str) { Ok(val) => val, - Err(err) => Self::from_string_trunc::(truncate_string(err, LenT::MAX.to_usize())), + Err(err) => { + Self::from_string_trunc::(truncate_string(err, LenT::MAX.to_usize())) + } } } @@ -407,9 +409,10 @@ impl AsRef for FixedString { /// 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> +where + LenT: ValidLength, + RC: From> + 'static, + for<'a> RC: From<&'a str>, { if matches!(value.0, FixedStringRepr::Heap(_)) { // Move existing allocation to Arc/Rc From 4012eb63b17fb4ac8a2f5bcbdf91fbd4320ba8c2 Mon Sep 17 00:00:00 2001 From: Joshix Date: Fri, 3 Oct 2025 11:00:00 +0000 Subject: [PATCH 11/12] add some tests --- src/string.rs | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/src/string.rs b/src/string.rs index 97cfcc0..1c44dbd 100644 --- a/src/string.rs +++ b/src/string.rs @@ -482,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 { @@ -692,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()); + } } From e66364ecad36ca72d5d3cfc1f23d5d8bd8452055 Mon Sep 17 00:00:00 2001 From: Joshix Date: Sun, 12 Oct 2025 10:00:00 +0000 Subject: [PATCH 12/12] remove import --- src/string.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/string.rs b/src/string.rs index 1c44dbd..a265575 100644 --- a/src/string.rs +++ b/src/string.rs @@ -1,5 +1,5 @@ use alloc::{ - borrow::{Cow, ToOwned}, + borrow::{Cow}, boxed::Box, rc::Rc, string::String,