use alloc::{ffi::CString, string::String};
use core::ffi::CStr;
use widestring::error::ContainsNul;
use widestring::{WideCStr, WideCString};
#[derive(Debug, Clone)]
pub enum Str {
Ascii(CString),
Unicode(WideCString),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StrError {
nul_position: usize,
}
impl StrError {
#[must_use]
pub fn nul_position(&self) -> usize {
self.nul_position
}
}
impl<C> From<ContainsNul<C>> for StrError {
fn from(value: ContainsNul<C>) -> Self {
Self {
nul_position: value.nul_position(),
}
}
}
impl core::fmt::Display for StrError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"interior NUL found while converting string at position {}",
self.nul_position
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for StrError {}
impl Str {
#[allow(clippy::needless_pass_by_value)]
pub fn try_from_string(value: String) -> Result<Self, StrError> {
Self::try_from_str(value.as_str())
}
pub fn try_from_str(value: &str) -> Result<Self, StrError> {
WideCString::from_str(value)
.map(Self::Unicode)
.map_err(StrError::from)
}
#[must_use]
pub fn from_string_lossy(mut value: String) -> Self {
if let Ok(str_value) = Self::try_from_str(value.as_str()) {
return str_value;
}
value.retain(|c| c != '\0');
Self::Unicode(unsafe { WideCString::from_str_unchecked(value) })
}
#[must_use]
pub fn from_str_lossy(value: &str) -> Self {
if let Ok(str_value) = Self::try_from_str(value) {
str_value
} else {
let sanitized: String = value.chars().filter(|c| *c != '\0').collect();
Self::Unicode(unsafe { WideCString::from_str_unchecked(sanitized) })
}
}
}
impl TryFrom<String> for Str {
type Error = StrError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from_string(value)
}
}
impl TryFrom<&str> for Str {
type Error = StrError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::try_from_str(value)
}
}
impl From<CString> for Str {
fn from(v: CString) -> Self {
Self::Ascii(v)
}
}
impl From<&CStr> for Str {
fn from(v: &CStr) -> Self {
CString::from(v).into()
}
}
impl From<WideCString> for Str {
fn from(v: WideCString) -> Self {
Self::Unicode(v)
}
}
impl From<&WideCStr> for Str {
fn from(v: &WideCStr) -> Self {
WideCString::from(v).into()
}
}
#[cfg(test)]
mod tests {
use super::{Str, StrError};
use alloc::string::ToString;
#[test]
fn try_from_str_rejects_interior_nul() {
let error = Str::try_from_str("abc\0def").unwrap_err();
assert_eq!(error.nul_position(), 3);
assert_eq!(
error.to_string(),
"interior NUL found while converting string at position 3"
);
}
#[test]
fn from_str_lossy_removes_interior_nuls() {
let value = Str::from_str_lossy("abc\0def\0ghi");
assert!(matches!(value, Str::Unicode(s) if s.to_string_lossy() == "abcdefghi"));
}
#[test]
fn str_error_can_be_constructed_from_widestring_error() {
let error: StrError = widestring::WideCString::from_str("a\0b")
.unwrap_err()
.into();
assert_eq!(error.nul_position(), 1);
}
}