From 5a5ffa270861264ea25c7229cc03ba2a64291785 Mon Sep 17 00:00:00 2001 From: Johann Carl Meyer Date: Sun, 23 Nov 2025 15:58:24 +0100 Subject: [PATCH 1/6] implement a pendant to Cstr::from_bytes_until_nul --- src/c_string.rs | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/c_string.rs b/src/c_string.rs index 9ec011cf1c..413ca5622d 100644 --- a/src/c_string.rs +++ b/src/c_string.rs @@ -102,6 +102,68 @@ impl CString { Ok(string) } + /// Instantiates a [`CString`] copying from the given byte slice, until the first nul character. + /// `bytes` may contain any number of nul characters, or none at all. + /// + /// Fails if the given byte slice can't fit in `N`. + /// + /// # Examples + /// You can pass a byte array with many, or no nul-bytes as `bytes`. + /// + /// ```rust + /// use heapless::CString; + /// + /// let c_string = CString::<11>::from_bytes_until_nul(b"hey\0there!\0").unwrap(); + /// assert_eq!(c_string.as_c_str(), c"hey"); + /// + /// let c_string = CString::<11>::from_bytes_until_nul(b"string").unwrap(); + /// assert_eq!(c_string.as_c_str(), c"string"); + /// ``` + /// + /// If `bytes` is too long, an error is returned. + /// ```rust + /// use heapless::CString; + /// + /// assert!(CString::<3>::from_bytes_until_nul(b"hey\0").is_err()); + /// ``` + /// + /// `bytes` may also represent an empty string. + /// ```rust + /// use heapless::CString; + /// + /// assert_eq!(CString::<1>::from_bytes_until_nul(b"").unwrap().as_c_str(), c""); + /// assert_eq!(CString::<1>::from_bytes_until_nul(b"\0").unwrap().as_c_str(), c""); + /// ``` + pub fn from_bytes_until_nul(bytes: &[u8]) -> Result { + // Truncate `bytes` to before the first nul byte. + // `bytes` will not contain any nul bytes. + let bytes = match CStr::from_bytes_with_nul(bytes) { + Ok(_) => &bytes[..bytes.len() - 1], // bytes.len() > 0, as `bytes` is nul-terminated + Err(FromBytesWithNulError::InteriorNul { position }) => &bytes[..position], + Err(FromBytesWithNulError::NotNulTerminated) => bytes, + }; + + let mut string = Self::new(); + if let Some(capacity) = string.capacity_with_bytes(bytes) { + // Cannot store `bytes` due to insufficient capacity. + if capacity > N { + return Err(CapacityError.into()); + } + } + + // SAFETY: + // `string` is left in a valid state because + // the appended bytes do not contain any nul bytes, + // and we push a nul byte at the end. + // + // We've ensured above that there is enough space to push `bytes` + // and the nul byte. + unsafe { string.extend_from_bytes_unchecked(bytes) }?; + unsafe { string.inner.push_unchecked(0) }; + + Ok(string) + } + /// Builds a [`CString`] copying from a raw C string pointer. /// /// # Safety From 4c2a368922cd4c660fe42bfb0f5512d13bb02395 Mon Sep 17 00:00:00 2001 From: Johann Carl Meyer Date: Sun, 23 Nov 2025 16:11:52 +0100 Subject: [PATCH 2/6] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d563bc234b..c68c1389d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +- Added `from_bytes_until_nul` to `CString` ## [v0.9.2] 2025-11-12 From fec6cc0f6492f74675403e40865ff316f7dc3f2d Mon Sep 17 00:00:00 2001 From: Johann Carl Meyer Date: Sun, 23 Nov 2025 18:14:59 +0100 Subject: [PATCH 3/6] rename and add test case - rename from_bytes_until_nul to from_bytes_truncating_at_nul, in order to aviod confusion with Cstr::from_bytes_until_nul, which errors on non-nul terminated data. - add a test case for handling a nul-terminated slice without interior nul bytes - improve documentation --- src/c_string.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/c_string.rs b/src/c_string.rs index 413ca5622d..f6ad43defa 100644 --- a/src/c_string.rs +++ b/src/c_string.rs @@ -105,36 +105,42 @@ impl CString { /// Instantiates a [`CString`] copying from the given byte slice, until the first nul character. /// `bytes` may contain any number of nul characters, or none at all. /// + /// This method mimics [`CStr::from_bytes_until_nul`] with two important differences: + /// [`Self::from_bytes_truncating_at_nul`] copies the data, and it does not fail on non-nul terminated data. + /// /// Fails if the given byte slice can't fit in `N`. /// /// # Examples - /// You can pass a byte array with many, or no nul-bytes as `bytes`. + /// You can pass a byte array with one, many, or no nul bytes as `bytes`. /// /// ```rust /// use heapless::CString; /// - /// let c_string = CString::<11>::from_bytes_until_nul(b"hey\0there!\0").unwrap(); + /// let c_string = CString::<11>::from_bytes_truncating_at_nul(b"hey there!\0").unwrap(); + /// assert_eq!(c_string.as_c_str(), c"hey there!"); + /// + /// let c_string = CString::<11>::from_bytes_truncating_at_nul(b"hey\0there!\0").unwrap(); /// assert_eq!(c_string.as_c_str(), c"hey"); /// - /// let c_string = CString::<11>::from_bytes_until_nul(b"string").unwrap(); - /// assert_eq!(c_string.as_c_str(), c"string"); + /// let c_string = CString::<11>::from_bytes_truncating_at_nul(b"hey there!").unwrap(); + /// assert_eq!(c_string.as_c_str(), c"hey there!"); /// ``` /// /// If `bytes` is too long, an error is returned. /// ```rust /// use heapless::CString; /// - /// assert!(CString::<3>::from_bytes_until_nul(b"hey\0").is_err()); + /// assert!(CString::<3>::from_bytes_truncating_at_nul(b"hey\0").is_err()); /// ``` /// /// `bytes` may also represent an empty string. /// ```rust /// use heapless::CString; /// - /// assert_eq!(CString::<1>::from_bytes_until_nul(b"").unwrap().as_c_str(), c""); - /// assert_eq!(CString::<1>::from_bytes_until_nul(b"\0").unwrap().as_c_str(), c""); + /// assert_eq!(CString::<1>::from_bytes_truncating_at_nul(b"").unwrap().as_c_str(), c""); + /// assert_eq!(CString::<1>::from_bytes_truncating_at_nul(b"\0").unwrap().as_c_str(), c""); /// ``` - pub fn from_bytes_until_nul(bytes: &[u8]) -> Result { + pub fn from_bytes_truncating_at_nul(bytes: &[u8]) -> Result { // Truncate `bytes` to before the first nul byte. // `bytes` will not contain any nul bytes. let bytes = match CStr::from_bytes_with_nul(bytes) { From 2f3ef268d07e61068e597a9a948fc2ccbb20a72e Mon Sep 17 00:00:00 2001 From: Johann Carl Meyer Date: Sun, 23 Nov 2025 18:18:39 +0100 Subject: [PATCH 4/6] format --- src/c_string.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/c_string.rs b/src/c_string.rs index f6ad43defa..aa2cc46f67 100644 --- a/src/c_string.rs +++ b/src/c_string.rs @@ -106,7 +106,8 @@ impl CString { /// `bytes` may contain any number of nul characters, or none at all. /// /// This method mimics [`CStr::from_bytes_until_nul`] with two important differences: - /// [`Self::from_bytes_truncating_at_nul`] copies the data, and it does not fail on non-nul terminated data. + /// [`Self::from_bytes_truncating_at_nul`] copies the data, and it does not fail on + /// non-nul terminated data. /// /// Fails if the given byte slice can't fit in `N`. /// From f18153ef9fb4ab48dbed26d821addea633364827 Mon Sep 17 00:00:00 2001 From: Johann Carl Meyer Date: Sun, 23 Nov 2025 18:32:00 +0100 Subject: [PATCH 5/6] improve tests --- src/c_string.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/c_string.rs b/src/c_string.rs index aa2cc46f67..25d5291cd4 100644 --- a/src/c_string.rs +++ b/src/c_string.rs @@ -120,7 +120,7 @@ impl CString { /// let c_string = CString::<11>::from_bytes_truncating_at_nul(b"hey there!\0").unwrap(); /// assert_eq!(c_string.as_c_str(), c"hey there!"); /// - /// let c_string = CString::<11>::from_bytes_truncating_at_nul(b"hey\0there!\0").unwrap(); + /// let c_string = CString::<11>::from_bytes_truncating_at_nul(b"hey\0there\0!").unwrap(); /// assert_eq!(c_string.as_c_str(), c"hey"); /// /// let c_string = CString::<11>::from_bytes_truncating_at_nul(b"hey there!").unwrap(); From 68391063e3e32b285681d9d2cd18d73687b2a878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sosth=C3=A8ne=20Gu=C3=A9don?= Date: Tue, 25 Nov 2025 20:41:09 +0100 Subject: [PATCH 6/6] Fix changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c68c1389d0..5d65933181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] -- Added `from_bytes_until_nul` to `CString` +- Added `from_bytes_truncating_at_nul` to `CString` ## [v0.9.2] 2025-11-12