Skip to content

Commit b8fa753

Browse files
authored
Merge pull request rust-embedded#639 from johann-cm/cstring_into_vec_or_string
Cstring into Vec or String
2 parents 78eab14 + 172359e commit b8fa753

File tree

2 files changed

+156
-2
lines changed

2 files changed

+156
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [Unreleased]
99
- Added `from_bytes_truncating_at_nul` to `CString`
10+
- Added `CString::{into_bytes, into_bytes_with_nul, into_string}`
1011

1112
## [v0.9.2] 2025-11-12
1213

src/c_string.rs

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
//! A fixed capacity [`CString`](https://doc.rust-lang.org/std/ffi/struct.CString.html).
22
3-
use crate::{vec::Vec, CapacityError, LenType};
3+
use crate::{vec::Vec, CapacityError, LenType, String};
44
use core::{
55
borrow::Borrow,
66
cmp::Ordering,
77
error::Error,
88
ffi::{c_char, CStr, FromBytesWithNulError},
9-
fmt,
9+
fmt::{self, Display},
1010
ops::Deref,
11+
str::Utf8Error,
1112
};
1213

1314
#[cfg(feature = "zeroize")]
@@ -350,6 +351,81 @@ impl<const N: usize, LenT: LenType> CString<N, LenT> {
350351
pub fn as_bytes(&self) -> &[u8] {
351352
&self.inner[..self.inner.len() - 1]
352353
}
354+
355+
/// Converts the [`CString`] into a [`String`] if it contains valid UTF-8 data.
356+
///
357+
/// On failure, ownership of the original [`CString`] is returned.
358+
///
359+
/// Equivalent of `std::ffi::CString::into_string`.
360+
///
361+
/// # Examples
362+
/// Valid UTF-8:
363+
///
364+
/// ```rust
365+
/// use heapless::CString;
366+
///
367+
/// let sparkle_heart = CString::<5>::from_bytes_with_nul(&[240, 159, 146, 150, 0]).unwrap();
368+
/// assert_eq!(sparkle_heart.into_string().unwrap(), "💖");
369+
/// ```
370+
///
371+
/// Invalid UTF-8:
372+
///
373+
/// ```rust
374+
/// use heapless::CString;
375+
///
376+
/// let hello_world = CString::<16>::from_bytes_with_nul(b"Hello \xF0\x90\x80World\0").unwrap();
377+
/// assert!(hello_world.into_string().is_err());
378+
/// ```
379+
pub fn into_string(self) -> Result<String<N, LenT>, IntoStringError<N, LenT>> {
380+
// `String::from_utf8(self.inner)` would be a great fit here,
381+
// but the error type of that method does not return ownership.
382+
if let Err(error) = core::str::from_utf8(self.as_bytes()) {
383+
return Err(IntoStringError { inner: self, error });
384+
}
385+
386+
// SAFETY: UTF-8 invariant has just been checked by `str::from_utf8`.
387+
Ok(unsafe { String::from_utf8_unchecked(self.into_bytes()) })
388+
}
389+
390+
#[inline]
391+
/// Consumes the [`CString`] and returns the underlying byte buffer.
392+
///
393+
/// The returned byte buffer *does* contain the trailing nul terminator.
394+
/// It is guaranteed that the returned buffer does not contain any interior nul bytes.
395+
///
396+
/// Equivalent of `std::ffi::CString::into_bytes_with_nul`.
397+
///
398+
/// # Examples
399+
/// ```rust
400+
/// use heapless::CString;
401+
/// let c_string = CString::<16>::from_bytes_with_nul(b"Hello World!\0").unwrap();
402+
///
403+
/// assert_eq!(c_string.into_bytes_with_nul(), b"Hello World!\0");
404+
/// ```
405+
pub fn into_bytes_with_nul(self) -> Vec<u8, N, LenT> {
406+
self.inner
407+
}
408+
409+
/// Consumes the [`CString`] and returns the underlying byte buffer.
410+
///
411+
/// The returned byte buffer does *not* contain the trailing nul terminator,
412+
/// and it guaranteed to not contain any interior nul bytes.
413+
///
414+
/// Equivalent of `std::ffi::CString::into_bytes`.
415+
///
416+
/// # Examples
417+
/// ```rust
418+
/// use heapless::CString;
419+
/// let c_string = CString::<16>::from_bytes_with_nul(b"Hello World!\0").unwrap();
420+
///
421+
/// assert_eq!(c_string.into_bytes(), b"Hello World!");
422+
/// ```
423+
pub fn into_bytes(self) -> Vec<u8, N, LenT> {
424+
let mut vec = self.into_bytes_with_nul();
425+
let _nul = vec.pop();
426+
debug_assert_eq!(_nul, Some(0u8));
427+
vec
428+
}
353429
}
354430

355431
impl<const N: usize, LenT: LenType> AsRef<CStr> for CString<N, LenT> {
@@ -415,6 +491,64 @@ impl<const N: usize, LenT: LenType> fmt::Debug for CString<N, LenT> {
415491
}
416492
}
417493

494+
#[derive(Debug, Clone, PartialEq)]
495+
/// An error indicating invalid UTF-8 when converting a [`CString`] into a [`String`].
496+
///
497+
/// This struct is created by [`CString::into_string()`].
498+
///
499+
/// Equivalent of `std::ffi::IntoStringError`.
500+
///
501+
/// Call [`Self::into_cstring`] to regain ownership of the [`CString`].
502+
///
503+
/// # Examples
504+
/// ```rust
505+
/// use heapless::CString;
506+
/// use heapless::c_string::IntoStringError;
507+
///
508+
/// // the byte slice contains invalid UTF-8
509+
/// let hello_world = CString::<16>::from_bytes_with_nul(b"Hello \xF0\x90\x80World\0").unwrap();
510+
/// let hello_world_clone = hello_world.clone();
511+
///
512+
/// let err: IntoStringError<16> = hello_world.into_string().unwrap_err();
513+
///
514+
/// assert_eq!(err.utf8_error().valid_up_to(), 6);
515+
/// assert_eq!(err.utf8_error().error_len(), Some(3));
516+
/// assert_eq!(err.into_cstring(), hello_world_clone);
517+
/// ```
518+
pub struct IntoStringError<const N: usize, LenT = usize>
519+
where
520+
LenT: LenType,
521+
{
522+
inner: CString<N, LenT>,
523+
error: Utf8Error,
524+
}
525+
526+
impl<const N: usize, LenT: LenType> IntoStringError<N, LenT> {
527+
#[inline]
528+
/// Consumes this error, returning original [`CString`] which generated the error.
529+
pub fn into_cstring(self) -> CString<N, LenT> {
530+
self.inner
531+
}
532+
533+
#[inline]
534+
/// Access the underlying UTF-8 error that was the cause of this error.
535+
pub fn utf8_error(&self) -> Utf8Error {
536+
self.error
537+
}
538+
}
539+
540+
impl<const N: usize, LenT: LenType> Display for IntoStringError<N, LenT> {
541+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542+
"CString contained non-utf8 bytes".fmt(f)
543+
}
544+
}
545+
546+
impl<const N: usize, LenT: LenType> Error for IntoStringError<N, LenT> {
547+
fn source(&self) -> Option<&(dyn Error + 'static)> {
548+
Some(&self.error)
549+
}
550+
}
551+
418552
/// An error to extend [`CString`] with bytes.
419553
#[derive(Debug)]
420554
pub enum ExtendError {
@@ -530,6 +664,25 @@ mod tests {
530664
Some(INITIAL_BYTES.len() + 5)
531665
);
532666
}
667+
668+
#[test]
669+
fn into_bytes_empty() {
670+
let c_string = CString::<16>::from_bytes_with_nul(b"\0").unwrap();
671+
assert_eq!(c_string.into_bytes(), b"");
672+
}
673+
674+
#[test]
675+
fn into_bytes_with_nul_empty() {
676+
let c_string = CString::<16>::from_bytes_with_nul(b"\0").unwrap();
677+
assert_eq!(c_string.into_bytes_with_nul(), b"\0");
678+
}
679+
680+
#[test]
681+
fn into_string_empty() {
682+
let c_string = CString::<16>::from_bytes_with_nul(b"\0").unwrap();
683+
assert_eq!(c_string.into_string().unwrap(), "");
684+
}
685+
533686
#[test]
534687
fn default() {
535688
assert_eq!(CString::<1>::default().as_c_str(), c"");

0 commit comments

Comments
 (0)