From e95a1858963be34bcd6b320a3e3a2922c8e6b430 Mon Sep 17 00:00:00 2001 From: Artemciy Date: Fri, 27 Feb 2026 18:58:40 +0300 Subject: [PATCH] Hide as_mut. Remove costly UTF-8 checks. Bump edition to 2024. #37 --- .gitignore | 2 ++ Cargo.toml | 5 ++-- src/inline_string.rs | 64 +++----------------------------------------- src/lib.rs | 15 +++++++---- src/string_ext.rs | 6 ++--- 5 files changed, 22 insertions(+), 70 deletions(-) diff --git a/.gitignore b/.gitignore index a9d37c5..722fab7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ target Cargo.lock + +/.vscode diff --git a/Cargo.toml b/Cargo.toml index 14375ff..5744076 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ name = "inlinable_string" description = "The `inlinable_string` crate provides the `InlinableString` type -- an owned, grow-able UTF-8 string that stores small strings inline and avoids heap-allocation -- and the `StringExt` trait which abstracts string operations over both `std::string::String` and `InlinableString` (or even your own custom string type)." -version = "0.1.15" -edition = "2018" +version = "0.1.16" +edition = "2024" license = "Apache-2.0/MIT" keywords = ["string", "inline", "inlinable"] readme = "./README.md" @@ -21,6 +21,7 @@ version = "1" [features] nightly = [] no_std = [] +as_mut = [] [dev-dependencies] serde_test = "1" diff --git a/src/inline_string.rs b/src/inline_string.rs index dbc137a..c70e571 100644 --- a/src/inline_string.rs +++ b/src/inline_string.rs @@ -68,7 +68,6 @@ pub struct NotEnoughSpaceError; impl AsRef for InlineString { fn as_ref(&self) -> &str { - self.assert_sanity(); unsafe { str::from_utf8_unchecked(&self.bytes[..self.len()]) } } } @@ -80,18 +79,18 @@ impl AsRef<[u8]> for InlineString { } } +#[cfg(feature = "as_mut")] impl AsMut for InlineString { fn as_mut(&mut self) -> &mut str { - self.assert_sanity(); let length = self.len(); unsafe { str::from_utf8_unchecked_mut(&mut self.bytes[..length]) } } } +#[cfg(feature = "as_mut")] impl AsMut<[u8]> for InlineString { #[inline] fn as_mut(&mut self) -> &mut [u8] { - self.assert_sanity(); let length = self.len(); &mut self.bytes[0..length] } @@ -113,15 +112,12 @@ impl<'a> From<&'a str> for InlineString { ptr::copy_nonoverlapping(string.as_ptr(), ss.bytes.as_mut_ptr(), string_len); } ss.length = string_len as u8; - - ss.assert_sanity(); ss } } impl fmt::Display for InlineString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - self.assert_sanity(); write!(f, "{}", self as &str) } } @@ -147,7 +143,6 @@ impl ops::Index> for InlineString { #[inline] fn index(&self, index: ops::Range) -> &str { - self.assert_sanity(); &self[..][index] } } @@ -157,7 +152,6 @@ impl ops::Index> for InlineString { #[inline] fn index(&self, index: ops::RangeTo) -> &str { - self.assert_sanity(); &self[..][index] } } @@ -167,7 +161,6 @@ impl ops::Index> for InlineString { #[inline] fn index(&self, index: ops::RangeFrom) -> &str { - self.assert_sanity(); &self[..][index] } } @@ -177,7 +170,6 @@ impl ops::Index for InlineString { #[inline] fn index(&self, _index: ops::RangeFull) -> &str { - self.assert_sanity(); unsafe { str::from_utf8_unchecked(&self.bytes[..self.len()]) } } } @@ -185,7 +177,6 @@ impl ops::Index for InlineString { impl ops::IndexMut> for InlineString { #[inline] fn index_mut(&mut self, index: ops::Range) -> &mut str { - self.assert_sanity(); &mut self[..][index] } } @@ -193,7 +184,6 @@ impl ops::IndexMut> for InlineString { impl ops::IndexMut> for InlineString { #[inline] fn index_mut(&mut self, index: ops::RangeTo) -> &mut str { - self.assert_sanity(); &mut self[..][index] } } @@ -201,7 +191,6 @@ impl ops::IndexMut> for InlineString { impl ops::IndexMut> for InlineString { #[inline] fn index_mut(&mut self, index: ops::RangeFrom) -> &mut str { - self.assert_sanity(); &mut self[..][index] } } @@ -209,7 +198,6 @@ impl ops::IndexMut> for InlineString { impl ops::IndexMut for InlineString { #[inline] fn index_mut(&mut self, _index: ops::RangeFull) -> &mut str { - self.assert_sanity(); let length = self.len(); unsafe { str::from_utf8_unchecked_mut(&mut self.bytes[..length]) } } @@ -220,7 +208,6 @@ impl ops::Deref for InlineString { #[inline] fn deref(&self) -> &str { - self.assert_sanity(); unsafe { str::from_utf8_unchecked(&self.bytes[..self.len()]) } } } @@ -228,7 +215,6 @@ impl ops::Deref for InlineString { impl ops::DerefMut for InlineString { #[inline] fn deref_mut(&mut self) -> &mut str { - self.assert_sanity(); let length = self.len(); unsafe { str::from_utf8_unchecked_mut(&mut self.bytes[..length]) } } @@ -244,8 +230,6 @@ impl Default for InlineString { impl PartialEq for InlineString { #[inline] fn eq(&self, rhs: &InlineString) -> bool { - self.assert_sanity(); - rhs.assert_sanity(); PartialEq::eq(&self[..], &rhs[..]) } } @@ -273,19 +257,6 @@ impl_eq! { InlineString, &'a str } impl_eq! { borrow::Cow<'a, str>, InlineString } impl InlineString { - #[cfg_attr(feature = "nightly", allow(inline_always))] - #[inline(always)] - fn assert_sanity(&self) { - debug_assert!( - self.length as usize <= INLINE_STRING_CAPACITY, - "inlinable_string: internal error: length greater than capacity" - ); - debug_assert!( - str::from_utf8(&self.bytes[0..self.length as usize]).is_ok(), - "inlinable_string: internal error: contents are not valid UTF-8!" - ); - } - /// Creates a new string buffer initialized with the empty string. /// /// # Examples @@ -317,7 +288,6 @@ impl InlineString { /// ``` #[inline] pub fn into_bytes(mut self) -> [u8; INLINE_STRING_CAPACITY] { - self.assert_sanity(); for i in self.len()..INLINE_STRING_CAPACITY { self.bytes[i] = 0; } @@ -337,8 +307,6 @@ impl InlineString { /// ``` #[inline] pub fn push_str(&mut self, string: &str) -> Result<(), NotEnoughSpaceError> { - self.assert_sanity(); - let string_len = string.len(); let new_length = self.len() + string_len; @@ -354,8 +322,6 @@ impl InlineString { ); } self.length = new_length as u8; - - self.assert_sanity(); Ok(()) } @@ -374,8 +340,6 @@ impl InlineString { /// ``` #[inline] pub fn push(&mut self, ch: char) -> Result<(), NotEnoughSpaceError> { - self.assert_sanity(); - let char_len = ch.len_utf8(); let new_length = self.len() + char_len; @@ -388,8 +352,6 @@ impl InlineString { ch.encode_utf8(&mut slice); } self.length = new_length as u8; - - self.assert_sanity(); Ok(()) } @@ -405,7 +367,6 @@ impl InlineString { /// ``` #[inline] pub fn as_bytes(&self) -> &[u8] { - self.assert_sanity(); &self.bytes[0..self.len()] } @@ -427,8 +388,6 @@ impl InlineString { /// ``` #[inline] pub fn truncate(&mut self, new_len: usize) { - self.assert_sanity(); - assert!( self.is_char_boundary(new_len), "inlinable_string::InlineString::truncate: new_len is not a character @@ -437,7 +396,6 @@ impl InlineString { assert!(new_len <= self.len()); self.length = new_len as u8; - self.assert_sanity(); } /// Removes the last character from the string buffer and returns it. @@ -456,13 +414,10 @@ impl InlineString { /// ``` #[inline] pub fn pop(&mut self) -> Option { - self.assert_sanity(); - match self.char_indices().rev().next() { None => None, Some((idx, ch)) => { self.length = idx as u8; - self.assert_sanity(); Some(ch) } } @@ -488,7 +443,6 @@ impl InlineString { /// ``` #[inline] pub fn remove(&mut self, idx: usize) -> char { - self.assert_sanity(); assert!(idx < self.len()); let ch = self @@ -513,8 +467,6 @@ impl InlineString { ); } self.length -= char_len as u8; - - self.assert_sanity(); ch } @@ -528,6 +480,7 @@ impl InlineString { return Err(NotEnoughSpaceError); } + unsafe { let ptr = self.bytes.as_mut_ptr().add(idx); // Shift the latter part. @@ -538,6 +491,7 @@ impl InlineString { ); // Copy the bytes into the buffer. ptr::copy(bytes.as_ptr(), self.bytes.as_mut_ptr().add(idx), amt); + } // `amt` is less than `u8::MAX` becuase `INLINE_STRING_CAPACITY < u8::MAX` holds. self.length += amt as u8; @@ -562,7 +516,6 @@ impl InlineString { /// this function will panic. #[inline] pub fn insert(&mut self, idx: usize, ch: char) -> Result<(), NotEnoughSpaceError> { - self.assert_sanity(); assert!(idx <= self.len()); let mut bits = [0; 4]; @@ -571,8 +524,6 @@ impl InlineString { unsafe { self.insert_bytes(idx, bits)?; } - - self.assert_sanity(); Ok(()) } @@ -589,14 +540,12 @@ impl InlineString { /// ``` #[inline] pub fn insert_str(&mut self, idx: usize, string: &str) -> Result<(), NotEnoughSpaceError> { - self.assert_sanity(); assert!(idx <= self.len()); unsafe { self.insert_bytes(idx, string.as_bytes())?; } - self.assert_sanity(); Ok(()) } @@ -622,7 +571,6 @@ impl InlineString { /// ``` #[inline] pub unsafe fn as_mut_slice(&mut self) -> &mut [u8] { - self.assert_sanity(); &mut self.bytes[0..self.length as usize] } @@ -638,7 +586,6 @@ impl InlineString { /// ``` #[inline] pub fn len(&self) -> usize { - self.assert_sanity(); self.length as usize } @@ -656,7 +603,6 @@ impl InlineString { /// ``` #[inline] pub fn is_empty(&self) -> bool { - self.assert_sanity(); self.length == 0 } @@ -673,9 +619,7 @@ impl InlineString { /// ``` #[inline] pub fn clear(&mut self) { - self.assert_sanity(); self.length = 0; - self.assert_sanity(); } } diff --git a/src/lib.rs b/src/lib.rs index ab96c32..4672ce2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ //! //! // This method can work on strings potentially stored inline on the stack, //! // on the heap, or plain old `std::string::String`s! -//! fn takes_a_string_reference(string: &mut StringExt) { +//! fn takes_a_string_reference<'a, T: StringExt<'a>>(string: &mut T) { //! // Do something with the string... //! string.push_str("it works!"); //! } @@ -214,6 +214,11 @@ impl AsRef for InlinableString { } } +// The only mutation operations that are sound are those that guarantee the byte count stays the same +// and the result stays valid UTF-8 (like make_ascii_uppercase). Arbitrary mutation +// through an AsMut impl exposes the internal storage to callers who may not uphold that invariant. + +#[cfg(feature = "as_mut")] impl AsMut for InlinableString { fn as_mut(&mut self) -> &mut str { match *self { @@ -453,12 +458,12 @@ impl<'a> StringExt<'a> for InlinableString { #[inline] unsafe fn from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> Self { - InlinableString::Heap(String::from_raw_parts(buf, length, capacity)) + unsafe {InlinableString::Heap(String::from_raw_parts(buf, length, capacity))} } #[inline] unsafe fn from_utf8_unchecked(bytes: Vec) -> Self { - InlinableString::Heap(String::from_utf8_unchecked(bytes)) + unsafe {InlinableString::Heap(String::from_utf8_unchecked(bytes))} } #[inline] @@ -657,10 +662,10 @@ impl<'a> StringExt<'a> for InlinableString { #[inline] unsafe fn as_mut_slice(&mut self) -> &mut [u8] { - match *self { + unsafe {match *self { InlinableString::Heap(ref mut s) => &mut s.as_mut_vec()[..], InlinableString::Inline(ref mut s) => s.as_mut_slice(), - } + }} } #[inline] diff --git a/src/string_ext.rs b/src/string_ext.rs index 18047d8..dcaad51 100644 --- a/src/string_ext.rs +++ b/src/string_ext.rs @@ -498,12 +498,12 @@ impl<'a> StringExt<'a> for String { #[inline] unsafe fn from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> Self { - String::from_raw_parts(buf, length, capacity) + unsafe {String::from_raw_parts(buf, length, capacity)} } #[inline] unsafe fn from_utf8_unchecked(bytes: Vec) -> Self { - String::from_utf8_unchecked(bytes) + unsafe {String::from_utf8_unchecked(bytes)} } #[inline] @@ -573,7 +573,7 @@ impl<'a> StringExt<'a> for String { #[inline] unsafe fn as_mut_slice(&mut self) -> &mut [u8] { - &mut *(self.as_mut_str() as *mut str as *mut [u8]) + unsafe {&mut *(self.as_mut_str() as *mut str as *mut [u8])} } #[inline]