From 9bc464686f4c9ca7d5fdb3499c4b65fee098436f Mon Sep 17 00:00:00 2001 From: 4lDO2 <4lDO2@protonmail.com> Date: Mon, 6 Oct 2025 10:53:00 +0200 Subject: [PATCH] Generalize CStr into NulStr to allow wide strings. --- src/c_str.rs | 199 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 137 insertions(+), 62 deletions(-) diff --git a/src/c_str.rs b/src/c_str.rs index 0dafbc1f51..c8f1134238 100644 --- a/src/c_str.rs +++ b/src/c_str.rs @@ -7,30 +7,79 @@ use alloc::{ string::String, }; -use crate::{ - header::string::{strchr, strchrnul, strlen}, - platform::types::c_char, -}; +use crate::platform::types::{c_char, c_int, wchar_t}; + +mod private { + pub trait Sealed {} +} +#[derive(Clone, Copy, Debug)] +pub enum Thin {} + +//#[derive(Clone, Copy, Debug)] +//pub enum Wide {} + +impl private::Sealed for Thin {} +//impl private::Sealed for Wide {} + +pub trait Kind: private::Sealed + Copy { + /// c_char or wchar_t + type C: Copy; + // u8 or u32 + type Char: Copy + Into + PartialEq; + + const NUL: Self::Char; + + fn r2c(c: Self::Char) -> Self::C; + fn c2r(c: Self::C) -> Self::Char; + + unsafe fn strlen(s: *const Self::C) -> usize; + unsafe fn strchr(s: *const Self::C, c: Self::C) -> *const Self::C; + unsafe fn strchrnul(s: *const Self::C, c: Self::C) -> *const Self::C; +} +impl Kind for Thin { + type C = c_char; + type Char = u8; + + const NUL: Self::Char = 0; + + unsafe fn strlen(s: *const c_char) -> usize { + crate::header::string::strlen(s) + } + unsafe fn strchr(s: *const c_char, c: c_char) -> *const c_char { + crate::header::string::strchr(s, c.into()) + } + unsafe fn strchrnul(s: *const c_char, c: c_char) -> *const c_char { + crate::header::string::strchrnul(s, c.into()) + } + fn r2c(c: u8) -> c_char { + c as _ + } + fn c2r(c: c_char) -> u8 { + c as _ + } +} /// Safe wrapper for immutable borrowed C strings, guaranteed to be the same layout as `*const u8`. #[derive(Clone, Copy)] #[repr(transparent)] -pub struct CStr<'a> { - ptr: NonNull, +pub struct NulStr<'a, T: Kind> { + ptr: NonNull, _marker: PhantomData<&'a [u8]>, } +pub type CStr<'a> = NulStr<'a, Thin>; +//pub type WStr<'a> = NulStr<'a, Wide>; -impl<'a> CStr<'a> { +impl<'a, T: Kind> NulStr<'a, T> { /// Safety /// /// The ptr must be valid up to and including the first NUL byte from the base ptr. - pub const unsafe fn from_ptr(ptr: *const c_char) -> Self { + pub const unsafe fn from_ptr(ptr: *const T::C) -> Self { Self { - ptr: NonNull::new_unchecked(ptr as *mut c_char), + ptr: NonNull::new_unchecked(ptr as *mut T::C), _marker: PhantomData, } } - pub unsafe fn from_nullable_ptr(ptr: *const c_char) -> Option { + pub unsafe fn from_nullable_ptr(ptr: *const T::C) -> Option { if ptr.is_null() { None } else { @@ -41,17 +90,20 @@ impl<'a> CStr<'a> { /// that byte and a `CStr` starting at that byte. #[inline] #[doc(alias = "strchrnul")] - pub fn find_get_subslice_or_all(self, c: u8) -> Result<(&'a [u8], Self), (&'a [u8], Self)> { + pub fn find_get_subslice_or_all( + self, + c: T::Char, + ) -> Result<(&'a [T::Char], Self), (&'a [T::Char], Self)> { // SAFETY: strchrnul expects self.as_ptr() to be valid up to and including its last NUL // byte - let found = unsafe { strchrnul(self.as_ptr(), c.into()) }; + let found = unsafe { T::strchrnul(self.as_ptr(), T::r2c(c)) }; // SAFETY: the pointer returned from strchrnul is always a substring of this string, and // hence always valid as a CStr. let found = unsafe { Self::from_ptr(found) }; let until = unsafe { self.slice_until_substr(found) }; - if found.first() == 0 { + if found.first() == T::NUL { // The character was not found, and we got the end of the string instead. Err((until, found)) } else { @@ -62,18 +114,18 @@ impl<'a> CStr<'a> { /// /// `substr` must be contained within `self` #[inline] - pub unsafe fn slice_until_substr(self, substr: CStr<'_>) -> &'a [u8] { + pub unsafe fn slice_until_substr(self, substr: NulStr<'_, T>) -> &'a [T::Char] { let index = unsafe { // SAFETY: the sub-pointer as returned by strchr must be derived from the same // allocation substr.as_ptr().offset_from(self.as_ptr()) as usize }; - unsafe { core::slice::from_raw_parts(self.as_ptr().cast::(), index) } + unsafe { core::slice::from_raw_parts(self.as_ptr().cast::(), index) } } /// Look for the closest occurence of `c`, and if found, split the string into a slice up to /// that byte and a `CStr` starting at that byte. #[inline] - pub fn find_get_subslice(self, c: u8) -> Option<(&'a [u8], Self)> { + pub fn find_get_subslice(self, c: T::Char) -> Option<(&'a [T::Char], Self)> { let rest = self.find(c)?; // SAFETY: the output of strchr is obviously a substring if it doesn't return NULL @@ -83,11 +135,11 @@ impl<'a> CStr<'a> { /// found. #[doc(alias = "strchr")] #[inline] - pub fn find(self, c: u8) -> Option { + pub fn find(self, c: T::Char) -> Option { unsafe { // SAFETY: the only requirement is for self.as_ptr() to be valid up to and including // the nearest NUL byte, which this type requires - let ret = strchr(self.as_ptr(), c.into()); + let ret = T::strchr(self.as_ptr(), T::r2c(c)); // SAFETY: strchr must either return NULL (not found) or a substring of self, which can // never exceed the nearest NUL byte of self Self::from_nullable_ptr(ret) @@ -96,82 +148,105 @@ impl<'a> CStr<'a> { // TODO: strrchr, strchrnul wrappers #[inline] - pub fn contains(self, c: u8) -> bool { + pub fn contains(self, c: T::Char) -> bool { self.find(c).is_some() } #[inline] - pub fn first(self) -> u8 { + pub fn first(self) -> T::Char { unsafe { // SAFETY: Self must be valid up to and including its nearest NUL byte, which certainly // implies its readable length is nonzero (string is empty if this first byte is 0). - self.ptr.read() as u8 + T::c2r(self.ptr.read()) } } /// Split this string into `Some((first_byte, string_after_that))` or `None` if empty. #[inline] - pub fn split_first(self) -> Option<(u8, CStr<'a>)> { - if self.first() == 0 { + pub fn split_first(self) -> Option<(T::Char, Self)> { + if self.first() == T::NUL { return None; } Some((self.first(), unsafe { - CStr::from_ptr(self.as_ptr().add(1)) + Self::from_ptr(self.as_ptr().add(1)) })) } - pub fn to_bytes_with_nul(self) -> &'a [u8] { + pub fn to_chars_with_nul(self) -> &'a [T::Char] { unsafe { // SAFETY: The string must be valid at least until (and including) the NUL byte. - let len = strlen(self.ptr.as_ptr()); + let len = T::strlen(self.ptr.as_ptr()); core::slice::from_raw_parts(self.ptr.as_ptr().cast(), len + 1) } } - pub fn to_bytes(self) -> &'a [u8] { - let s = self.to_bytes_with_nul(); + pub fn to_chars(self) -> &'a [T::Char] { + let s = self.to_chars_with_nul(); &s[..s.len() - 1] } + pub const fn as_ptr(self) -> *const T::C { + self.ptr.as_ptr() + } + pub const unsafe fn from_chars_with_nul_unchecked(chars: &'a [T::Char]) -> Self { + Self::from_ptr(chars.as_ptr().cast()) + } + pub fn from_chars_with_nul(chars: &'a [T::Char]) -> Result { + if chars.last() != Some(&T::NUL) || chars[..chars.len() - 1].contains(&T::NUL) { + return Err(FromCharsWithNulError); + } + + Ok(unsafe { Self::from_chars_with_nul_unchecked(chars) }) + } + pub fn from_chars_until_nul(chars: &'a [T::Char]) -> Result { + if !chars.contains(&T::NUL) { + return Err(FromCharsUntilNulError); + } + + Ok(unsafe { Self::from_chars_with_nul_unchecked(chars) }) + } + /// Scan the string to get its length. + #[doc(alias = "strlen")] + pub fn len(self) -> usize { + self.to_chars().len() + } + #[inline] + pub fn is_empty(&self) -> bool { + self.first() == T::NUL + } +} +impl<'a> CStr<'a> { + pub fn to_owned_cstring(self) -> CString { + CString::from(unsafe { core::ffi::CStr::from_ptr(self.ptr.as_ptr()) }) + } + pub fn borrow(string: &'a CString) -> Self { + unsafe { Self::from_ptr(string.as_ptr()) } + } + #[inline] + pub fn to_bytes(self) -> &'a [u8] { + self.to_chars() + } + #[inline] + pub fn to_bytes_with_nul(self) -> &'a [u8] { + self.to_chars_with_nul() + } pub fn to_str(self) -> Result<&'a str, Utf8Error> { core::str::from_utf8(self.to_bytes()) } pub fn to_string_lossy(self) -> Cow<'a, str> { String::from_utf8_lossy(self.to_bytes()) } - pub const fn as_ptr(self) -> *const c_char { - self.ptr.as_ptr() - } + #[inline] pub const unsafe fn from_bytes_with_nul_unchecked(bytes: &'a [u8]) -> Self { - Self::from_ptr(bytes.as_ptr().cast()) - } - pub fn from_bytes_with_nul(bytes: &'a [u8]) -> Result { - if bytes.last() != Some(&b'\0') || bytes[..bytes.len() - 1].contains(&b'\0') { - return Err(FromBytesWithNulError); - } - - Ok(unsafe { Self::from_bytes_with_nul_unchecked(bytes) }) - } - pub fn from_bytes_until_nul(bytes: &'a [u8]) -> Result { - if !bytes.contains(&b'\0') { - return Err(FromBytesUntilNulError); - } - - Ok(unsafe { Self::from_bytes_with_nul_unchecked(bytes) }) - } - pub fn to_owned_cstring(self) -> CString { - CString::from(unsafe { core::ffi::CStr::from_ptr(self.ptr.as_ptr()) }) - } - pub fn borrow(string: &'a CString) -> Self { - unsafe { Self::from_ptr(string.as_ptr()) } - } - #[doc(alias = "strlen")] - pub fn count_bytes(&self) -> usize { - self.to_bytes().len() + Self::from_chars_with_nul_unchecked(bytes) } #[inline] - pub fn is_empty(&self) -> bool { - self.first() == 0 + pub fn from_bytes_with_nul(bytes: &'a [u8]) -> Result { + Self::from_chars_with_nul(bytes) + } + #[inline] + pub fn from_bytes_until_nul(bytes: &'a [u8]) -> Result { + Self::from_chars_until_nul(bytes) } } -unsafe impl Send for CStr<'_> {} -unsafe impl Sync for CStr<'_> {} +unsafe impl Send for NulStr<'_, T> {} +unsafe impl Sync for NulStr<'_, T> {} impl From<&core::ffi::CStr> for CStr<'_> { fn from(s: &core::ffi::CStr) -> Self { @@ -183,9 +258,9 @@ impl From<&core::ffi::CStr> for CStr<'_> { } #[derive(Debug)] -pub struct FromBytesWithNulError; +pub struct FromCharsWithNulError; #[derive(Debug)] -pub struct FromBytesUntilNulError; +pub struct FromCharsUntilNulError; pub use alloc::ffi::CString;