From e93168be01ad2078a5c6f299ba712c003b1cc6a6 Mon Sep 17 00:00:00 2001 From: Philipp Schuster Date: Sun, 2 Apr 2023 16:43:01 +0200 Subject: [PATCH] cstr[ing]16: convenience functions --- CHANGELOG.md | 15 ++++ uefi/src/data_types/chars.rs | 13 +++- uefi/src/data_types/owned_strs.rs | 123 +++++++++++++++++++++++++++++- uefi/src/data_types/strs.rs | 54 +++++++++++-- 4 files changed, 197 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e41fab760..3dfc319df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ - There is a new `fs` module that provides a high-level API for file-system access. The API is close to the `std::fs` module. +- Multiple convenience methods for `CString16` and `CStr16`, including: + - `CStr16::as_slice()` + - `CStr16::len()` + - `CStr16::is_empty()` + - `CStr16::char_replace_all_in_place()` + - `CString16::new()` + - `CString16::is_empty()` + - `CString16::len()` + - `CString16::char_replace_all_in_place()` + - `CString16::push()` + - `CString16::push_str()` + - `From<&CStr16>` for `CString16` + - `From<&CStr16>` for `String` + - `From<&CString16>` for `String` ### Changed @@ -16,6 +30,7 @@ - `Error::new` and `Error::from` now panic if the status is `SUCCESS`. - `Image::get_image_file_system` now returns a `fs::FileSystem` instead of the protocol. +- `CString16::default` now always contains a null byte. ## uefi-macros - [Unreleased] diff --git a/uefi/src/data_types/chars.rs b/uefi/src/data_types/chars.rs index 30f4beac1..e3a6275e7 100644 --- a/uefi/src/data_types/chars.rs +++ b/uefi/src/data_types/chars.rs @@ -65,6 +65,17 @@ pub const NUL_8: Char8 = Char8(0); #[repr(transparent)] pub struct Char16(u16); +impl Char16 { + /// Creates a UCS-2 character from a Rust character without checks. + /// + /// # Safety + /// The caller must be sure that the character is valid. + #[must_use] + pub const unsafe fn from_u16_unchecked(val: u16) -> Self { + Self(val) + } +} + impl TryFrom for Char16 { type Error = CharConversionError; @@ -125,4 +136,4 @@ impl fmt::Display for Char16 { } /// UCS-2 version of the NUL character -pub const NUL_16: Char16 = Char16(0); +pub const NUL_16: Char16 = unsafe { Char16::from_u16_unchecked(0) }; diff --git a/uefi/src/data_types/owned_strs.rs b/uefi/src/data_types/owned_strs.rs index a0b7dd4a9..f83794360 100644 --- a/uefi/src/data_types/owned_strs.rs +++ b/uefi/src/data_types/owned_strs.rs @@ -4,6 +4,8 @@ use crate::data_types::strs::EqStrUntilNul; use crate::data_types::UnalignedSlice; use crate::polyfill::vec_into_raw_parts; use alloc::borrow::{Borrow, ToOwned}; +use alloc::string::String; +use alloc::vec; use alloc::vec::Vec; use core::{fmt, ops}; @@ -47,9 +49,73 @@ impl core::error::Error for FromStrError {} /// let s = CString16::try_from("abc").unwrap(); /// assert_eq!(s.to_string(), "abc"); /// ``` -#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct CString16(Vec); +impl CString16 { + /// Creates a new empty string with a null-byte. + #[must_use] + pub fn new() -> Self { + Self(vec![NUL_16]) + } + + /// Inserts a character at the end of the string, right before the null + /// character. + /// + /// # Panics + /// Panics if the char is a null character. + pub fn push(&mut self, char: Char16) { + assert_ne!(char, NUL_16, "Pushing a null-character is illegal"); + let last_elem = self + .0 + .last_mut() + .expect("There should be at least a null character"); + *last_elem = char; + self.0.push(NUL_16); + } + + /// Extends the string with the given [`CStr16`]. The null character is + /// automatically kept at the end. + pub fn push_str(&mut self, str: &CStr16) { + str.as_slice() + .iter() + .copied() + .for_each(|char| self.push(char)); + } + + /// Replaces all chars in the string with the replace value in-place. + pub fn replace_char(&mut self, search: Char16, replace: Char16) { + assert_ne!(search, NUL_16, "Replacing a null character is illegal"); + assert_ne!( + replace, NUL_16, + "Replacing with a null character is illegal" + ); + self.0 + .as_mut_slice() + .iter_mut() + .filter(|char| **char == search) + .for_each(|char| *char = replace); + } + + /// Returns the number of characters without the trailing null character. + #[must_use] + pub fn num_chars(&self) -> usize { + self.0.len() - 1 + } + + /// Returns if the string is empty. This ignores the null character. + #[must_use] + pub fn is_empty(&self) -> bool { + self.num_chars() == 0 + } +} + +impl Default for CString16 { + fn default() -> Self { + CString16::new() + } +} + impl TryFrom<&str> for CString16 { type Error = FromStrError; @@ -112,6 +178,20 @@ impl<'a> TryFrom<&UnalignedSlice<'a, u16>> for CString16 { } } +impl From<&CStr16> for CString16 { + fn from(value: &CStr16) -> Self { + let vec = value.as_slice_with_nul().to_vec(); + Self(vec) + } +} + +impl From<&CString16> for String { + fn from(value: &CString16) -> Self { + let slice: &CStr16 = value.as_ref(); + String::from(slice) + } +} + impl<'a> UnalignedSlice<'a, u16> { /// Copies `self` to a new [`CString16`]. pub fn to_cstring16(&self) -> Result { @@ -256,4 +336,45 @@ mod tests { ] ); } + + /// This tests the following UCS-2 string functions: + /// - runtime constructor + /// - len() + /// - push() / push_str() + /// - to rust string + #[test] + fn test_push_str() { + let mut str1 = CString16::new(); + assert_eq!(str1.num_bytes(), 2, "Should have null-byte"); + assert_eq!(str1.num_chars(), 0); + str1.push(Char16::try_from('h').unwrap()); + str1.push(Char16::try_from('i').unwrap()); + assert_eq!(str1.num_chars(), 2); + + let mut str2 = CString16::new(); + str2.push(Char16::try_from('!').unwrap()); + + str2.push_str(str1.as_ref()); + assert_eq!(str2.num_chars(), 3); + + let rust_str = String::from(&str2); + assert_eq!(rust_str, "!hi"); + } + + #[test] + #[should_panic] + fn test_push_str_panic() { + CString16::new().push(NUL_16); + } + + #[test] + fn test_char_replace_all_in_place() { + let mut input = CString16::try_from("foo/bar/foobar//").unwrap(); + let search = Char16::try_from('/').unwrap(); + let replace = Char16::try_from('\\').unwrap(); + input.replace_char(search, replace); + + let input = String::from(&input); + assert_eq!(input, "foo\\bar\\foobar\\\\") + } } diff --git a/uefi/src/data_types/strs.rs b/uefi/src/data_types/strs.rs index c7da2f470..de9747eb5 100644 --- a/uefi/src/data_types/strs.rs +++ b/uefi/src/data_types/strs.rs @@ -309,26 +309,32 @@ impl CStr16 { }) } - /// Returns the inner pointer to this C string + /// Returns the inner pointer to this C16 string. #[must_use] pub const fn as_ptr(&self) -> *const Char16 { self.0.as_ptr() } - /// Get the underlying [`Char16`] slice, including the trailing null. + /// Get the underlying [`Char16`]s as slice without the trailing null. + #[must_use] + pub fn as_slice(&self) -> &[Char16] { + &self.0[..self.len()] + } + + /// Get the underlying [`Char16`]s as slice including the trailing null. #[must_use] pub const fn as_slice_with_nul(&self) -> &[Char16] { &self.0 } - /// Converts this C string to a u16 slice + /// Converts this C string to a u16 slice without the trailing null. #[must_use] pub fn to_u16_slice(&self) -> &[u16] { let chars = self.to_u16_slice_with_nul(); &chars[..chars.len() - 1] } - /// Converts this C string to a u16 slice containing the trailing 0 char + /// Converts this C string to a u16 slice containing the trailing null. #[must_use] pub const fn to_u16_slice_with_nul(&self) -> &[u16] { unsafe { &*(&self.0 as *const [Char16] as *const [u16]) } @@ -343,7 +349,19 @@ impl CStr16 { } } - /// Get the number of bytes in the string (including the trailing null character). + /// Returns the number of characters without the trailing null. + #[must_use] + pub const fn len(&self) -> usize { + self.0.len() - 1 + } + + /// Returns if the string is empty. This ignores the null byte. + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Get the number of bytes in the string (including the trailing null). #[must_use] pub const fn num_bytes(&self) -> usize { self.0.len() * 2 @@ -373,6 +391,20 @@ impl CStr16 { } } +#[cfg(feature = "alloc")] +impl From<&CStr16> for alloc::string::String { + fn from(value: &CStr16) -> Self { + value + .as_slice() + .iter() + .copied() + .map(u16::from) + .map(|int| int as u32) + .map(|int| char::from_u32(int).expect("Should be encodable as UTF-8")) + .collect::() + } +} + impl + ?Sized> EqStrUntilNul for CStr16 { fn eq_str_until_nul(&self, other: &StrType) -> bool { let other = other.as_ref(); @@ -391,7 +423,7 @@ impl + ?Sized> EqStrUntilNul for CStr16 { } } -/// An iterator over `CStr16`. +/// An iterator over the [`Char16`]s in a [`CStr16`]. #[derive(Debug)] pub struct CStr16Iter<'a> { inner: &'a CStr16, @@ -575,6 +607,16 @@ mod tests { ); } + #[test] + fn test_cstr16_as_slice() { + let string: &CStr16 = cstr16!("a"); + assert_eq!(string.as_slice(), &[Char16::try_from('a').unwrap()]); + assert_eq!( + string.as_slice_with_nul(), + &[Char16::try_from('a').unwrap(), NUL_16] + ); + } + // Code generation helper for the compare tests of our CStrX types against "str" and "String" // from the standard library. #[allow(non_snake_case)]