Skip to content

Commit

Permalink
Merge pull request #751 from phip1611/str
Browse files Browse the repository at this point in the history
cstr[ing]16: convenience functions
  • Loading branch information
phip1611 authored Apr 5, 2023
2 parents 451d52b + 49116e6 commit 46069ef
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 15 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@

- 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::num_chars()`
- `CStr16::is_empty()`
- `CString16::new()`
- `CString16::is_empty()`
- `CString16::num_chars()`
- `CString16::replace_char()`
- `CString16::push()`
- `CString16::push_str()`
- `From<&CStr16>` for `CString16`
- `From<&CStr16>` for `String`
- `From<&CString16>` for `String`

### Changed

Expand All @@ -16,6 +29,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 character.

## uefi-macros - [Unreleased]

Expand Down
13 changes: 12 additions & 1 deletion uefi/src/data_types/chars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<char> for Char16 {
type Error = CharConversionError;

Expand Down Expand Up @@ -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) };
125 changes: 123 additions & 2 deletions uefi/src/data_types/owned_strs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -47,15 +49,79 @@ 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<Char16>);

impl CString16 {
/// Creates a new empty string with a terminating null character.
#[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;

fn try_from(input: &str) -> Result<Self, Self::Error> {
// Initially allocate one Char16 for each byte of the input, plus
// one for the null byte. This should be a good guess for ASCII-ish
// one for the null character. This should be a good guess for ASCII-ish
// input.
let mut output = Vec::with_capacity(input.len() + 1);

Expand Down Expand Up @@ -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<CString16, FromSliceWithNulError> {
Expand Down Expand Up @@ -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 character");
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\\\\")
}
}
66 changes: 54 additions & 12 deletions uefi/src/data_types/strs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ impl CStr16 {
/// # Safety
///
/// The function will start accessing memory from `ptr` until the first
/// null byte. It's the callers responsibility to ensure `ptr` points to
/// null character. It's the callers responsibility to ensure `ptr` points to
/// a valid string, in accessible memory.
#[must_use]
pub unsafe fn from_ptr<'ptr>(ptr: *const Char16) -> &'ptr Self {
Expand Down Expand Up @@ -232,7 +232,7 @@ impl CStr16 {
/// # Safety
///
/// It's the callers responsibility to ensure chars is a valid UCS-2
/// null-terminated string, with no interior null bytes.
/// null-terminated string, with no interior null characters.
#[must_use]
pub const unsafe fn from_u16_with_nul_unchecked(codes: &[u16]) -> &Self {
&*(codes as *const [u16] as *const Self)
Expand Down Expand Up @@ -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.num_chars()]
}

/// 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]) }
Expand All @@ -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. character
#[must_use]
pub const 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
}

/// 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
Expand Down Expand Up @@ -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::<alloc::string::String>()
}
}

impl<StrType: AsRef<str> + ?Sized> EqStrUntilNul<StrType> for CStr16 {
fn eq_str_until_nul(&self, other: &StrType) -> bool {
let other = other.as_ref();
Expand All @@ -391,7 +423,7 @@ impl<StrType: AsRef<str> + ?Sized> EqStrUntilNul<StrType> for CStr16 {
}
}

/// An iterator over `CStr16`.
/// An iterator over the [`Char16`]s in a [`CStr16`].
#[derive(Debug)]
pub struct CStr16Iter<'a> {
inner: &'a CStr16,
Expand Down Expand Up @@ -451,11 +483,11 @@ impl<'a> UnalignedSlice<'a, u16> {
/// get the other direction (`right.eq_str_until_nul(&left)`) for free. Hence, the relation is
/// reflexive.
pub trait EqStrUntilNul<StrType: ?Sized> {
/// Checks if the provided Rust string `StrType` is equal to [Self] until the first null-byte
/// is found. An exception is the terminating null-byte of [Self] which is ignored.
/// Checks if the provided Rust string `StrType` is equal to [Self] until the first null character
/// is found. An exception is the terminating null character of [Self] which is ignored.
///
/// As soon as the first null byte in either `&self` or `other` is found, this method returns.
/// Note that Rust strings are allowed to contain null-bytes that do not terminate the string.
/// As soon as the first null character in either `&self` or `other` is found, this method returns.
/// Note that Rust strings are allowed to contain null bytes that do not terminate the string.
/// Although this is rather unusual, you can compare `"foo\0bar"` with an instance of [Self].
/// In that case, only `foo"` is compared against [Self] (if [Self] is long enough).
fn eq_str_until_nul(&self, other: &StrType) -> bool;
Expand Down Expand Up @@ -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)]
Expand Down

0 comments on commit 46069ef

Please sign in to comment.