|
1 | 1 | //! A fixed capacity [`CString`](https://doc.rust-lang.org/std/ffi/struct.CString.html). |
2 | 2 |
|
3 | | -use crate::{vec::Vec, CapacityError, LenType}; |
| 3 | +use crate::{vec::Vec, CapacityError, LenType, String}; |
4 | 4 | use core::{ |
5 | 5 | borrow::Borrow, |
6 | 6 | cmp::Ordering, |
7 | 7 | error::Error, |
8 | 8 | ffi::{c_char, CStr, FromBytesWithNulError}, |
9 | | - fmt, |
| 9 | + fmt::{self, Display}, |
10 | 10 | ops::Deref, |
| 11 | + str::Utf8Error, |
11 | 12 | }; |
12 | 13 |
|
13 | 14 | #[cfg(feature = "zeroize")] |
@@ -350,6 +351,81 @@ impl<const N: usize, LenT: LenType> CString<N, LenT> { |
350 | 351 | pub fn as_bytes(&self) -> &[u8] { |
351 | 352 | &self.inner[..self.inner.len() - 1] |
352 | 353 | } |
| 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 | + } |
353 | 429 | } |
354 | 430 |
|
355 | 431 | 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> { |
415 | 491 | } |
416 | 492 | } |
417 | 493 |
|
| 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 | + |
418 | 552 | /// An error to extend [`CString`] with bytes. |
419 | 553 | #[derive(Debug)] |
420 | 554 | pub enum ExtendError { |
@@ -530,6 +664,25 @@ mod tests { |
530 | 664 | Some(INITIAL_BYTES.len() + 5) |
531 | 665 | ); |
532 | 666 | } |
| 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 | + |
533 | 686 | #[test] |
534 | 687 | fn default() { |
535 | 688 | assert_eq!(CString::<1>::default().as_c_str(), c""); |
|
0 commit comments