Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cstr[ing]16: convenience functions #751

Merged
merged 1 commit into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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