diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 42eba2decf32..3e1e7416d405 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,7 +18,7 @@ jobs: - stable - beta - nightly - - "1.63.0" + - "1.64.0" conf: - { name: "cairo", features: "png,pdf,svg,ps,use_glib,v1_18,freetype,script,xcb,xlib,win32-surface", nightly: "--features 'png,pdf,svg,ps,use_glib,v1_18,freetype,script,xcb,xlib,win32-surface'", test_sys: true } - { name: "gdk-pixbuf", features: "v2_42", nightly: "--all-features", test_sys: true } @@ -100,7 +100,7 @@ jobs: - stable - beta - nightly - - "1.63.0" + - "1.64.0" steps: - uses: actions/checkout@v3 - uses: actions-rs/toolchain@v1 diff --git a/glib/Cargo.toml b/glib/Cargo.toml index 39f0a448fab1..5070a0cdc142 100644 --- a/glib/Cargo.toml +++ b/glib/Cargo.toml @@ -13,7 +13,7 @@ exclude = [ "gir-files/*", ] edition = "2021" -rust-version = "1.63" +rust-version = "1.64" [lib] name = "glib" @@ -34,6 +34,7 @@ rs-log = { package = "log", version = "0.4", optional = true } smallvec = "1.0" thiserror = "1" gio_ffi = { package = "gio-sys", path = "../gio/sys", optional = true } +memchr = "2.5.0" [dev-dependencies] tempfile = "3" diff --git a/glib/src/convert.rs b/glib/src/convert.rs index d9a303f0b7ae..3ddf21ae9fe5 100644 --- a/glib/src/convert.rs +++ b/glib/src/convert.rs @@ -2,7 +2,9 @@ use std::{io, os::raw::c_char, path::PathBuf, ptr}; -use crate::{translate::*, ConvertError, Error, GStr, GString, NormalizeMode, Slice}; +use crate::{ + translate::*, ConvertError, Error, GString, IntoGStr, IntoOptionalGStr, NormalizeMode, Slice, +}; // rustdoc-stripper-ignore-next /// A wrapper for [`ConvertError`](crate::ConvertError) that can hold an offset into the input @@ -36,24 +38,26 @@ impl CvtError { #[doc(alias = "g_convert")] pub fn convert( str_: &[u8], - to_codeset: &str, - from_codeset: &str, + to_codeset: impl IntoGStr, + from_codeset: impl IntoGStr, ) -> Result<(Slice, usize), CvtError> { assert!(str_.len() <= isize::MAX as usize); let mut bytes_read = 0; let mut bytes_written = 0; let mut error = ptr::null_mut(); - let result = unsafe { - ffi::g_convert( - str_.as_ptr(), - str_.len() as isize, - to_codeset.to_glib_none().0, - from_codeset.to_glib_none().0, - &mut bytes_read, - &mut bytes_written, - &mut error, - ) - }; + let result = to_codeset.run_with_gstr(|to_codeset| { + from_codeset.run_with_gstr(|from_codeset| unsafe { + ffi::g_convert( + str_.as_ptr(), + str_.len() as isize, + to_codeset.to_glib_none().0, + from_codeset.to_glib_none().0, + &mut bytes_read, + &mut bytes_written, + &mut error, + ) + }) + }); if result.is_null() { Err(CvtError::new(unsafe { from_glib_full(error) }, bytes_read)) } else { @@ -65,26 +69,30 @@ pub fn convert( #[doc(alias = "g_convert_with_fallback")] pub fn convert_with_fallback( str_: &[u8], - to_codeset: &str, - from_codeset: &str, - fallback: Option<&str>, + to_codeset: impl IntoGStr, + from_codeset: impl IntoGStr, + fallback: Option, ) -> Result<(Slice, usize), CvtError> { assert!(str_.len() <= isize::MAX as usize); let mut bytes_read = 0; let mut bytes_written = 0; let mut error = ptr::null_mut(); - let result = unsafe { - ffi::g_convert_with_fallback( - str_.as_ptr(), - str_.len() as isize, - to_codeset.to_glib_none().0, - from_codeset.to_glib_none().0, - fallback.to_glib_none().0, - &mut bytes_read, - &mut bytes_written, - &mut error, - ) - }; + let result = to_codeset.run_with_gstr(|to_codeset| { + from_codeset.run_with_gstr(|from_codeset| { + fallback.run_with_gstr(|fallback| unsafe { + ffi::g_convert_with_fallback( + str_.as_ptr(), + str_.len() as isize, + to_codeset.to_glib_none().0, + from_codeset.to_glib_none().0, + fallback.to_glib_none().0, + &mut bytes_read, + &mut bytes_written, + &mut error, + ) + }) + }) + }); if result.is_null() { Err(CvtError::new(unsafe { from_glib_full(error) }, bytes_read)) } else { @@ -117,10 +125,12 @@ unsafe impl Send for IConv {} impl IConv { #[doc(alias = "g_iconv_open")] #[allow(clippy::unnecessary_lazy_evaluations)] - pub fn new(to_codeset: &str, from_codeset: &str) -> Option { - let iconv = unsafe { - ffi::g_iconv_open(to_codeset.to_glib_none().0, from_codeset.to_glib_none().0) - }; + pub fn new(to_codeset: impl IntoGStr, from_codeset: impl IntoGStr) -> Option { + let iconv = to_codeset.run_with_gstr(|to_codeset| { + from_codeset.run_with_gstr(|from_codeset| unsafe { + ffi::g_iconv_open(to_codeset.to_glib_none().0, from_codeset.to_glib_none().0) + }) + }); (iconv as isize != -1).then(|| Self(iconv)) } #[doc(alias = "g_convert_with_iconv")] @@ -209,20 +219,23 @@ pub fn filename_charsets() -> (bool, Vec) { } #[doc(alias = "g_filename_from_utf8")] -pub fn filename_from_utf8(utf8string: &str) -> Result<(PathBuf, usize), CvtError> { - let len = utf8string.len() as isize; +pub fn filename_from_utf8(utf8string: impl IntoGStr) -> Result<(PathBuf, usize), CvtError> { let mut bytes_read = 0; let mut bytes_written = std::mem::MaybeUninit::uninit(); let mut error = ptr::null_mut(); - let ret = unsafe { - ffi::g_filename_from_utf8( - utf8string.to_glib_none().0, - len, - &mut bytes_read, - bytes_written.as_mut_ptr(), - &mut error, - ) - }; + let ret = utf8string.run_with_gstr(|utf8string| { + assert!(utf8string.len() <= isize::MAX as usize); + let len = utf8string.len() as isize; + unsafe { + ffi::g_filename_from_utf8( + utf8string.to_glib_none().0, + len, + &mut bytes_read, + bytes_written.as_mut_ptr(), + &mut error, + ) + } + }); if error.is_null() { Ok(unsafe { ( @@ -265,20 +278,22 @@ pub fn filename_to_utf8( } #[doc(alias = "g_locale_from_utf8")] -pub fn locale_from_utf8(utf8string: &GStr) -> Result<(Slice, usize), CvtError> { - assert!(utf8string.len() <= isize::MAX as usize); +pub fn locale_from_utf8(utf8string: impl IntoGStr) -> Result<(Slice, usize), CvtError> { let mut bytes_read = 0; let mut bytes_written = std::mem::MaybeUninit::uninit(); let mut error = ptr::null_mut(); - let ret = unsafe { - ffi::g_locale_from_utf8( - utf8string.as_ptr(), - utf8string.len() as isize, - &mut bytes_read, - bytes_written.as_mut_ptr(), - &mut error, - ) - }; + let ret = utf8string.run_with_gstr(|utf8string| { + assert!(utf8string.len() <= isize::MAX as usize); + unsafe { + ffi::g_locale_from_utf8( + utf8string.as_ptr(), + utf8string.len() as isize, + &mut bytes_read, + bytes_written.as_mut_ptr(), + &mut error, + ) + } + }); if error.is_null() { Ok(unsafe { ( @@ -393,7 +408,7 @@ mod tests { assert!(super::convert(b"Hello", "utf-8", "ascii").is_ok()); assert!(super::convert(b"He\xaallo", "utf-8", "ascii").is_err()); assert_eq!( - super::convert_with_fallback(b"H\xc3\xa9llo", "ascii", "utf-8", None) + super::convert_with_fallback(b"H\xc3\xa9llo", "ascii", "utf-8", crate::NONE_STR) .unwrap() .0 .as_slice(), diff --git a/glib/src/functions.rs b/glib/src/functions.rs index f812c2312caa..4bec745318a7 100644 --- a/glib/src/functions.rs +++ b/glib/src/functions.rs @@ -14,8 +14,7 @@ use std::ptr; // #[cfg(windows)] // #[cfg(any(feature = "v2_58", feature = "dox"))] // use std::os::windows::io::AsRawHandle; -use crate::translate::*; -use crate::GString; +use crate::{translate::*, GStr}; #[cfg(not(windows))] use crate::{Error, Pid, SpawnFlags}; @@ -213,7 +212,7 @@ pub fn spawn_async_with_pipes< /// charset if available. #[doc(alias = "g_get_charset")] #[doc(alias = "get_charset")] -pub fn charset() -> (bool, Option) { +pub fn charset() -> (bool, Option<&'static GStr>) { unsafe { let mut out_charset = ptr::null(); let is_utf8 = from_glib(ffi::g_get_charset(&mut out_charset)); diff --git a/glib/src/gstring.rs b/glib/src/gstring.rs index db54ab97a236..6e23b7259f51 100644 --- a/glib/src/gstring.rs +++ b/glib/src/gstring.rs @@ -3,13 +3,13 @@ use std::{ borrow::{Borrow, Cow}, cmp::Ordering, - ffi::{CStr, CString, OsStr}, + ffi::{CStr, CString, OsStr, OsString}, fmt, hash, marker::PhantomData, mem, ops::Deref, os::raw::{c_char, c_void}, - path::Path, + path::{Path, PathBuf}, ptr, slice, }; @@ -26,35 +26,120 @@ pub struct GStr(str); impl GStr { // rustdoc-stripper-ignore-next - /// Creates a GLib string wrapper from a string slice. The string slice must be terminated with - /// a nul byte. + /// Creates a GLib string wrapper from a byte slice. /// - /// This function will cast the provided bytes to a `GStr` wrapper after ensuring - /// that the string slice is nul-terminated and does not contain any interior nul bytes. - pub fn from_str_with_nul(s: &str) -> Result<&Self, std::ffi::FromBytesWithNulError> { - let bytes = s.as_bytes(); - CStr::from_bytes_with_nul(bytes)?; - Ok(unsafe { Self::from_bytes_with_nul_unchecked(bytes) }) + /// This function will cast the provided bytes to a `GStr` wrapper after ensuring that the byte + /// slice is valid UTF-8 and is nul-terminated. + #[inline] + pub fn from_utf8_with_nul(bytes: &[u8]) -> Result<&Self, GStrError> { + Self::check_trailing_nul(bytes)?; + std::str::from_utf8(bytes)?; + Ok(unsafe { mem::transmute(bytes) }) + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string wrapper from a byte slice, checking for interior nul-bytes. + /// + /// This function will cast the provided bytes to a `GStr` wrapper after ensuring that the byte + /// slice is valid UTF-8, is nul-terminated, and does not contain any interior nul-bytes. + #[inline] + pub fn from_utf8_with_nul_checked(bytes: &[u8]) -> Result<&Self, GStrError> { + Self::check_nuls(bytes)?; + std::str::from_utf8(bytes)?; + Ok(unsafe { mem::transmute(bytes) }) } // rustdoc-stripper-ignore-next /// Unsafely creates a GLib string wrapper from a byte slice. /// /// This function will cast the provided `bytes` to a `GStr` wrapper without performing any - /// sanity checks. The provided slice **must** be valid UTF-8, nul-terminated and not contain - /// any interior nul bytes. + /// sanity checks. + /// + /// # Safety + /// + /// The provided slice **must** be valid UTF-8 and nul-terminated. It is undefined behavior to + /// pass a slice that does not uphold those conditions. #[inline] - pub const unsafe fn from_bytes_with_nul_unchecked(bytes: &[u8]) -> &Self { + pub const unsafe fn from_utf8_with_nul_unchecked(bytes: &[u8]) -> &Self { debug_assert!(!bytes.is_empty() && bytes[bytes.len() - 1] == 0); + debug_assert!(std::str::from_utf8(bytes).is_ok()); mem::transmute(bytes) } // rustdoc-stripper-ignore-next + /// Creates a GLib string wrapper from a byte slice, truncating it at the first nul-byte. + /// + /// This function will cast the provided bytes to a `GStr` wrapper after ensuring that the byte + /// slice is valid UTF-8 and contains at least one nul-byte. + #[inline] + pub fn from_utf8_until_nul(bytes: &[u8]) -> Result<&Self, GStrError> { + let nul_pos = memchr::memchr(0, bytes).ok_or(GStrError::NoTrailingNul)?; + let bytes = unsafe { bytes.get_unchecked(..nul_pos + 1) }; + std::str::from_utf8(bytes)?; + Ok(unsafe { mem::transmute(bytes) }) + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string wrapper from a string slice. + /// + /// The string slice must be terminated with a nul-byte. + /// + /// This function will cast the provided bytes to a `GStr` wrapper after ensuring + /// that the string slice is nul-terminated. + #[inline] + pub fn from_str_with_nul(s: &str) -> Result<&Self, GStrError> { + Self::check_trailing_nul(s)?; + Ok(unsafe { mem::transmute(s) }) + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string wrapper from a string slice, checking for interior nul-bytes. + /// + /// The string slice must be terminated with a nul-byte. + /// + /// This function will cast the provided bytes to a `GStr` wrapper after ensuring + /// that the string slice is nul-terminated and does not contain any interior nul-bytes. + #[inline] + pub fn from_str_with_nul_checked(s: &str) -> Result<&Self, GStrError> { + Self::check_nuls(s)?; + Ok(unsafe { mem::transmute(s) }) + } + // rustdoc-stripper-ignore-next + /// Unsafely creates a GLib string wrapper from a string slice. The string slice must be + /// terminated with a nul-byte. + /// + /// This function will cast the provided string slice to a `GStr` without performing any sanity + /// checks. + /// + /// # Safety + /// + /// The provided string slice **must** be nul-terminated. It is undefined behavior to pass a + /// slice that does not uphold those conditions. + #[inline] + pub const unsafe fn from_str_with_nul_unchecked(s: &str) -> &Self { + debug_assert!(!s.is_empty() && s.as_bytes()[s.len() - 1] == 0); + mem::transmute(s) + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string wrapper from a string slice, truncating it at the first nul-byte. + /// + /// The string slice must contain at least one nul-byte. + /// + /// This function will cast the provided bytes to a `GStr` wrapper after ensuring + /// that the string slice contains at least one nul-byte. + #[inline] + pub fn from_str_until_nul(s: &str) -> Result<&Self, GStrError> { + let b = s.as_bytes(); + let nul_pos = memchr::memchr(0, b).ok_or(GStrError::NoTrailingNul)?; + let s = unsafe { std::str::from_utf8_unchecked(b.get_unchecked(..nul_pos + 1)) }; + Ok(unsafe { mem::transmute(s) }) + } + // rustdoc-stripper-ignore-next /// Wraps a raw C string with a safe GLib string wrapper. The provided C string **must** be - /// valid UTF-8 and nul-terminated. All constraints from [`std::ffi::CStr::from_ptr`] also - /// apply here. + /// valid UTF-8 and nul-terminated. All constraints from [`CStr::from_ptr`] also apply here. + /// + /// # Safety + /// + /// See [`CStr::from_ptr`](CStr::from_ptr#safety). #[inline] pub unsafe fn from_ptr<'a>(ptr: *const c_char) -> &'a Self { let cstr = CStr::from_ptr(ptr); - Self::from_bytes_with_nul_unchecked(cstr.to_bytes_with_nul()) + Self::from_utf8_with_nul_unchecked(cstr.to_bytes_with_nul()) } // rustdoc-stripper-ignore-next /// Wraps a raw C string with a safe GLib string wrapper. The provided C string **must** be @@ -66,7 +151,7 @@ impl GStr { pub unsafe fn from_ptr_lossy<'a>(ptr: *const c_char) -> Cow<'a, Self> { let mut end_ptr = ptr::null(); if ffi::g_utf8_validate(ptr as *const _, -1, &mut end_ptr) != ffi::GFALSE { - Cow::Borrowed(Self::from_bytes_with_nul_unchecked(slice::from_raw_parts( + Cow::Borrowed(Self::from_utf8_with_nul_unchecked(slice::from_raw_parts( ptr as *const u8, end_ptr.offset_from(ptr) as usize + 1, ))) @@ -83,7 +168,7 @@ impl GStr { /// This function is the equivalent of [`GStr::to_bytes`] except that it will retain the /// trailing nul terminator instead of chopping it off. #[inline] - pub fn to_bytes_with_nul(&self) -> &[u8] { + pub const fn as_bytes_with_nul(&self) -> &[u8] { self.0.as_bytes() } // rustdoc-stripper-ignore-next @@ -92,7 +177,7 @@ impl GStr { /// The returned slice will **not** contain the trailing nul terminator that this GLib /// string has. #[inline] - pub fn to_bytes(&self) -> &[u8] { + pub const fn as_bytes(&self) -> &[u8] { self.as_str().as_bytes() } // rustdoc-stripper-ignore-next @@ -107,27 +192,116 @@ impl GStr { /// writes to it) causes undefined behavior. It is your responsibility to make /// sure that the underlying memory is not freed too early. #[inline] - pub fn as_ptr(&self) -> *const c_char { + pub const fn as_ptr(&self) -> *const c_char { self.0.as_ptr() as *const _ } // rustdoc-stripper-ignore-next /// Converts this GLib string to a string slice. #[inline] - pub fn as_str(&self) -> &str { - // Clip off the nul byte - &self.0[0..self.0.len() - 1] + pub const fn as_str(&self) -> &str { + // Clip off the nul-byte + unsafe { + std::str::from_utf8_unchecked(std::slice::from_raw_parts( + self.as_ptr() as *const _, + self.0.len() - 1, + )) + } } // rustdoc-stripper-ignore-next - /// Converts this GLib string to a C string slice. + /// Converts this GLib string to a C string slice, checking for interior nul-bytes. + /// + /// Returns `Err` if the string contains any interior nul-bytes. #[inline] - pub fn as_c_str(&self) -> &CStr { - unsafe { CStr::from_bytes_with_nul_unchecked(self.to_bytes_with_nul()) } + pub fn to_cstr(&self) -> Result<&CStr, GStrInteriorNulError> { + Self::check_interior_nuls(self.as_bytes())?; + Ok(unsafe { self.to_cstr_unchecked() }) + } + // rustdoc-stripper-ignore-next + /// Converts this GLib string to a C string slice, truncating it at the first nul-byte. + #[inline] + pub fn to_cstr_until_nul(&self) -> &CStr { + let b = self.as_bytes_with_nul(); + let nul_pos = memchr::memchr(0, b).unwrap(); + unsafe { CStr::from_bytes_with_nul_unchecked(b.get_unchecked(..nul_pos + 1)) } + } + // rustdoc-stripper-ignore-next + /// Converts this GLib string to a C string slice, without checking for interior nul-bytes. + /// + /// # Safety + /// + /// `self` **must** not contain any interior nul-bytes besides the final terminating nul-byte. + /// It is undefined behavior to call this on a string that contains interior nul-bytes. + #[inline] + pub const unsafe fn to_cstr_unchecked(&self) -> &CStr { + CStr::from_bytes_with_nul_unchecked(self.as_bytes_with_nul()) } #[doc(alias = "g_utf8_collate")] #[doc(alias = "utf8_collate")] - pub fn collate(&self, other: impl AsRef) -> Ordering { - unsafe { ffi::g_utf8_collate(self.as_ptr(), other.as_ref().as_ptr()) }.cmp(&0) + pub fn collate(&self, other: impl IntoGStr) -> Ordering { + other.run_with_gstr(|other| { + unsafe { ffi::g_utf8_collate(self.to_glib_none().0, other.to_glib_none().0) }.cmp(&0) + }) + } + + fn check_nuls(s: impl AsRef<[u8]>) -> Result<(), GStrError> { + let s = s.as_ref(); + if let Some(nul_pos) = memchr::memchr(0, s) { + if s.len() == nul_pos + 1 { + Ok(()) + } else { + Err(GStrInteriorNulError(nul_pos).into()) + } + } else { + Err(GStrError::NoTrailingNul) + } + } + #[inline] + fn check_trailing_nul(s: impl AsRef<[u8]>) -> Result<(), GStrError> { + if let Some(c) = s.as_ref().last().copied() { + if c == 0 { + return Ok(()); + } + } + Err(GStrError::NoTrailingNul) + } + // rustdoc-stripper-ignore-next + /// Returns `Err` if the string slice contains any nul-bytes. + #[inline] + pub(crate) fn check_interior_nuls(s: impl AsRef<[u8]>) -> Result<(), GStrInteriorNulError> { + if let Some(nul_pos) = memchr::memchr(0, s.as_ref()) { + Err(GStrInteriorNulError(nul_pos)) + } else { + Ok(()) + } + } + pub const NONE: Option<&'static GStr> = None; +} + +// rustdoc-stripper-ignore-next +/// Error type holding all possible failures when creating a [`GStr`] reference. +#[derive(thiserror::Error, Debug)] +pub enum GStrError { + #[error(transparent)] + InvalidUtf8(#[from] std::str::Utf8Error), + #[error(transparent)] + InteriorNul(#[from] GStrInteriorNulError), + #[error("data provided is not nul terminated")] + NoTrailingNul, +} + +// rustdoc-stripper-ignore-next +/// Error type indicating that a buffer had unexpected nul-bytes. +#[derive(thiserror::Error, Copy, Clone, PartialEq, Eq, Debug)] +#[error("data provided contains an interior nul-byte at byte pos {0}")] +pub struct GStrInteriorNulError(usize); + +impl GStrInteriorNulError { + // rustdoc-stripper-ignore-next + /// Returns the position of the nul-byte in the slice that caused the conversion to fail. + #[inline] + pub fn nul_position(&self) -> usize { + self.0 } } @@ -135,7 +309,7 @@ impl GStr { /// Converts a static string literal into a static nul-terminated string. /// /// The expanded expression has type [`&'static GStr`]. This macro will panic if the -/// string literal contains any interior nul bytes. +/// string literal contains any interior nul-bytes. /// /// # Examples /// @@ -144,7 +318,7 @@ impl GStr { /// use glib::{gstr, GStr, GString}; /// /// const MY_STRING: &GStr = gstr!("Hello"); -/// assert_eq!(MY_STRING.to_bytes_with_nul()[5], 0u8); +/// assert_eq!(MY_STRING.as_bytes_with_nul()[5], 0u8); /// let owned: GString = MY_STRING.to_owned(); /// assert_eq!(MY_STRING, owned); /// # } @@ -154,7 +328,7 @@ impl GStr { #[macro_export] macro_rules! gstr { ($s:literal) => { - unsafe { $crate::GStr::from_bytes_with_nul_unchecked($crate::cstr_bytes!($s)) } + unsafe { $crate::GStr::from_utf8_with_nul_unchecked($crate::cstr_bytes!($s)) } }; } @@ -165,12 +339,19 @@ impl Default for &GStr { } } +impl fmt::Display for GStr { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.as_str()) + } +} + impl<'a> TryFrom<&'a CStr> for &'a GStr { type Error = std::str::Utf8Error; #[inline] fn try_from(s: &'a CStr) -> Result { s.to_str()?; - Ok(unsafe { GStr::from_bytes_with_nul_unchecked(s.to_bytes_with_nul()) }) + Ok(unsafe { GStr::from_utf8_with_nul_unchecked(s.to_bytes_with_nul()) }) } } @@ -186,13 +367,13 @@ impl PartialEq for GStr { } } -impl<'a> PartialEq<&'a str> for GStr { - fn eq(&self, other: &&'a str) -> bool { +impl PartialEq<&str> for GStr { + fn eq(&self, other: &&str) -> bool { self.as_str() == *other } } -impl<'a> PartialEq for &'a str { +impl PartialEq for &str { fn eq(&self, other: &GStr) -> bool { *self == other.as_str() } @@ -246,12 +427,6 @@ impl AsRef for GStr { } } -impl AsRef for GStr { - fn as_ref(&self) -> &CStr { - self.as_c_str() - } -} - impl AsRef for GStr { fn as_ref(&self) -> &OsStr { OsStr::new(self.as_str()) @@ -266,7 +441,7 @@ impl AsRef for GStr { impl AsRef<[u8]> for GStr { fn as_ref(&self) -> &[u8] { - self.to_bytes() + self.as_bytes() } } @@ -283,7 +458,24 @@ impl ToOwned for GStr { #[inline] fn to_owned(&self) -> Self::Owned { - GString(Inner::Native(Some(self.as_c_str().to_owned()))) + let b = self.as_bytes_with_nul(); + if self.len() < INLINE_LEN { + let mut data = <[u8; INLINE_LEN]>::default(); + let b = self.as_bytes(); + unsafe { data.get_unchecked_mut(..b.len()) }.copy_from_slice(b); + return GString(Inner::Inline { + len: self.len() as u8, + data, + }); + } + let inner = unsafe { + let copy = ffi::g_strndup(b.as_ptr() as *const c_char, b.len()); + Inner::Foreign { + ptr: ptr::NonNull::new_unchecked(copy), + len: b.len() - 1, + } + }; + GString(inner) } } @@ -297,14 +489,52 @@ impl StaticType for GStr { } } +#[doc(hidden)] +impl FromGlibPtrNone<*const u8> for &GStr { + #[inline] + unsafe fn from_glib_none(ptr: *const u8) -> Self { + assert!(!ptr.is_null(), "provided C string is NULL"); + let cstr = CStr::from_ptr(ptr as *const _); + // Also check if it's valid UTF-8 + GStr::from_str_with_nul_unchecked(cstr.to_str().unwrap()) + } +} + +#[doc(hidden)] +impl FromGlibPtrNone<*const i8> for &GStr { + #[inline] + unsafe fn from_glib_none(ptr: *const i8) -> Self { + from_glib_none(ptr as *const u8) + } +} + +#[doc(hidden)] +impl FromGlibPtrNone<*mut u8> for &GStr { + #[inline] + unsafe fn from_glib_none(ptr: *mut u8) -> Self { + from_glib_none(ptr as *const u8) + } +} + +#[doc(hidden)] +impl FromGlibPtrNone<*mut i8> for &GStr { + #[inline] + unsafe fn from_glib_none(ptr: *mut i8) -> Self { + from_glib_none(ptr as *const u8) + } +} + unsafe impl<'a> FromValue<'a> for &'a GStr { type Checker = crate::value::GenericValueTypeOrNoneChecker; unsafe fn from_value(value: &'a Value) -> Self { let ptr = gobject_ffi::g_value_get_string(value.to_glib_none().0); let cstr = CStr::from_ptr(ptr); - assert!(cstr.to_str().is_ok()); - GStr::from_bytes_with_nul_unchecked(cstr.to_bytes_with_nul()) + assert!( + cstr.to_str().is_ok(), + "C string in glib::Value is not valid utf-8" + ); + GStr::from_utf8_with_nul_unchecked(cstr.to_bytes_with_nul()) } } @@ -369,37 +599,253 @@ impl<'a> ToGlibPtr<'a, *mut c_char> for GStr { } } +// size_of::() minus two bytes for length and enum discriminant +const INLINE_LEN: usize = + mem::size_of::>>() + mem::size_of::() - mem::size_of::() * 2; + // rustdoc-stripper-ignore-next /// A type representing an owned, C-compatible, nul-terminated UTF-8 string. /// /// `GString` is to &[GStr] as [`String`] is to &[str]: the former in /// each pair are owned strings; the latter are borrowed references. /// -/// This type is very similar to [`std::ffi::CString`], but with one added constraint: the string -/// must also be valid UTF-8. +/// This type is similar to [`std::ffi::CString`], but with some special behavior. When debug +/// assertions are enabled, [From]<[String]> will panic if there are interior +/// nul-bytes. In production builds, no checks will be made for interior nul-bytes, and strings +/// that contain interior nul-bytes will simply end at first nul-byte when converting to a C +/// string. +/// +/// The constructors beginning with `from_utf8` `and `from_string` can also be used to further +/// control how interior nul-bytes are handled. pub struct GString(Inner); + enum Inner { - Native(Option), + Native(Box), Foreign { ptr: ptr::NonNull, len: usize, }, + Inline { + len: u8, + data: [u8; INLINE_LEN], + }, } unsafe impl Send for GString {} unsafe impl Sync for GString {} impl GString { + // rustdoc-stripper-ignore-next + /// Creates a new empty [`GString`]. + /// + /// Does not allocate. + #[inline] + pub fn new() -> Self { + Self(Inner::Inline { + len: 0, + data: Default::default(), + }) + } + // rustdoc-stripper-ignore-next + /// Formats an [`Arguments`](std::fmt::Arguments) into a [`GString`]. + /// + /// This function is the same as [`std::fmt::format`], except it returns a [`GString`]. The + /// [`Arguments`](std::fmt::Arguments) instance can be created with the + /// [`format_args!`](std::format_args) macro. + /// + /// Please note that using [`gformat!`] might be preferable. + pub fn format(args: fmt::Arguments) -> Self { + if let Some(s) = args.as_str() { + return Self::from(s); + } + + let mut s = crate::GStringBuilder::default(); + fmt::Write::write_fmt(&mut s, args).unwrap(); + s.into_string() + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string by consuming a byte vector. + /// + /// Takes ownership of `bytes`. Returns `Err` if it contains invalid UTF-8. + /// + /// A trailing nul-byte will be appended by this function. + #[inline] + pub fn from_utf8(bytes: Vec) -> Result { + Ok(Self::from_string_unchecked(String::from_utf8(bytes)?)) + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string by consuming a byte vector, checking for interior nul-bytes. + /// + /// Takes ownership of `bytes`, as long as it is valid UTF-8 and does not contain any interior + /// nul-bytes. Otherwise, `Err` is returned. + /// + /// A trailing nul-byte will be appended by this function. + #[inline] + pub fn from_utf8_checked(bytes: Vec) -> Result>> { + Ok(Self::from_string_checked(String::from_utf8(bytes)?) + .map_err(|e| GStringInteriorNulError(e.0.into_bytes(), e.1))?) + } + // rustdoc-stripper-ignore-next + /// Unsafely creates a GLib string by consuming a byte vector, without checking for UTF-8 or + /// interior nul-bytes. + /// + /// A trailing nul-byte will be appended by this function. + /// + /// # Safety + /// + /// The byte vector **must** not contain invalid UTF-8 characters. It is undefined behavior to + /// pass a vector that contains invalid UTF-8. + #[inline] + pub unsafe fn from_utf8_unchecked(mut v: Vec) -> Self { + if v.is_empty() { + Self::new() + } else { + v.reserve_exact(1); + v.push(0); + Self(Inner::Native(String::from_utf8_unchecked(v).into())) + } + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string by consuming a nul-terminated byte vector, without checking for + /// interior nul-bytes. + /// + /// Takes ownership of `bytes`. Returns `Err` if it contains invalid UTF-8 or does not have a + /// trailing nul-byte. + #[inline] + pub fn from_utf8_with_nul(bytes: Vec) -> Result>> { + let s = String::from_utf8(bytes)?; + if s.as_bytes().last().copied() != Some(0u8) { + return Err(GStringNoTrailingNulError(s.into_bytes()).into()); + } + if s.len() == 1 { + Ok(Self::new()) + } else { + Ok(Self(Inner::Native(s.into()))) + } + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string by consuming a nul-terminated byte vector. + /// + /// Takes ownership of `bytes`. Returns `Err` if it contains invalid UTF-8, does not have a + /// trailing nul-byte, or contains interior nul-bytes. + #[inline] + pub fn from_utf8_with_nul_checked(bytes: Vec) -> Result>> { + let s = Self::from_utf8_with_nul(bytes)?; + if let Err(e) = GStr::check_interior_nuls(&s) { + return Err(GStringInteriorNulError(s.into_bytes(), e).into()); + } + Ok(s) + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string by consuming a byte vector, without checking for UTF-8, a trailing + /// nul-byte, or interior nul-bytes. + /// + /// # Safety + /// + /// The byte vector **must** not contain invalid UTF-8 characters, and **must** have a trailing + /// nul-byte. It is undefined behavior to pass a vector that does not uphold those conditions. + #[inline] + pub unsafe fn from_utf8_with_nul_unchecked(v: Vec) -> Self { + debug_assert!(!v.is_empty() && v[v.len() - 1] == 0); + let s = if cfg!(debug_assertions) { + let s = String::from_utf8(v).unwrap(); + GStr::check_interior_nuls(&s[..s.len() - 1]).unwrap(); + s + } else { + String::from_utf8_unchecked(v) + }; + if s.len() == 1 { + Self::new() + } else { + Self(Inner::Native(s.into())) + } + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string by consuming a nul-terminated byte vector, truncating it at the first + /// nul-byte. + /// + /// Takes ownership of `bytes`. Returns `Err` if it contains invalid UTF-8 or does not contain + /// at least one nul-byte. + pub fn from_utf8_until_nul(mut bytes: Vec) -> Result>> { + let nul_pos = if let Some(nul_pos) = memchr::memchr(0, &bytes) { + nul_pos + } else { + return Err(GStringNoTrailingNulError(bytes).into()); + }; + if nul_pos == 0 { + Ok(Self::new()) + } else { + if let Err(e) = std::str::from_utf8(unsafe { bytes.get_unchecked(..nul_pos) }) { + return Err(GStringUtf8Error(bytes, e).into()); + } + bytes.truncate(nul_pos + 1); + let s = unsafe { String::from_utf8_unchecked(bytes) }; + Ok(Self(Inner::Native(s.into()))) + } + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string by consuming a string, checking for interior nul-bytes. + /// + /// Takes ownership of `s`, as long as it does not contain any interior nul-bytes. Otherwise, + /// `Err` is returned. + /// + /// A trailing nul-byte will be appended by this function. + #[inline] + pub fn from_string_checked(s: String) -> Result> { + if let Err(e) = GStr::check_interior_nuls(&s) { + return Err(GStringInteriorNulError(s, e)); + } + Ok(Self::from_string_unchecked(s)) + } + // rustdoc-stripper-ignore-next + /// Creates a GLib string by consuming a string, without checking for interior nul-bytes. + /// + /// A trailing nul-byte will be appended by this function. + #[inline] + pub fn from_string_unchecked(mut s: String) -> Self { + if s.is_empty() { + Self::new() + } else { + s.reserve_exact(1); + s.push('\0'); + Self(Inner::Native(s.into())) + } + } + // rustdoc-stripper-ignore-next + /// Wraps a raw C string with a safe GLib string wrapper. The provided C string **must** be + /// nul-terminated. All constraints from [`std::ffi::CStr::from_ptr`] also apply here. + /// + /// If the string is valid UTF-8 then it is directly returned otherwise a copy is created with + /// every invalid character replaced by the Unicode replacement character (U+FFFD). + #[inline] + pub unsafe fn from_ptr_lossy<'a>(ptr: *const c_char) -> Cow<'a, GStr> { + GStr::from_ptr_lossy(ptr) + } + + // rustdoc-stripper-ignore-next + /// Wraps a raw C string with a safe GLib string wrapper. The provided C string **must** be + /// nul-terminated. All constraints from [`std::ffi::CStr::from_ptr`] also apply here. + /// + /// `len` is the length without the nul-terminator, i.e. if `len == 0` is passed then `*ptr` + /// must be the nul-terminator. + pub unsafe fn from_ptr_and_len_unchecked(ptr: *const c_char, len: usize) -> Self { + assert!(!ptr.is_null()); + + GString(Inner::Foreign { + ptr: ptr::NonNull::new_unchecked(ptr as *mut _), + len, + }) + } + // rustdoc-stripper-ignore-next /// Return the `GString` as string slice. + #[inline] pub fn as_str(&self) -> &str { unsafe { let (ptr, len) = match self.0 { - Inner::Native(ref cstr) => { - let cstr = cstr.as_ref().unwrap(); - (cstr.as_ptr() as *const u8, cstr.to_bytes().len()) - } + Inner::Native(ref s) => (s.as_ptr() as *const u8, s.len() - 1), Inner::Foreign { ptr, len } => (ptr.as_ptr() as *const u8, len), + Inner::Inline { len, ref data } => (data.as_ptr(), len as usize), }; if len == 0 { "" @@ -412,91 +858,230 @@ impl GString { // rustdoc-stripper-ignore-next /// Extracts the [`GStr`] containing the entire string. + #[inline] pub fn as_gstr(&self) -> &GStr { let bytes = match self.0 { - Inner::Native(ref cstr) => cstr.as_ref().unwrap().to_bytes_with_nul(), + Inner::Native(ref s) => s.as_bytes(), Inner::Foreign { len, .. } if len == 0 => &[0], Inner::Foreign { ptr, len } => unsafe { slice::from_raw_parts(ptr.as_ptr() as *const _, len + 1) }, + Inner::Inline { len, ref data } => unsafe { data.get_unchecked(..len as usize + 1) }, }; - unsafe { GStr::from_bytes_with_nul_unchecked(bytes) } + unsafe { GStr::from_utf8_with_nul_unchecked(bytes) } } // rustdoc-stripper-ignore-next /// Return the underlying pointer of the `GString`. + #[inline] pub fn as_ptr(&self) -> *const c_char { match self.0 { - Inner::Native(ref cstr) => cstr.as_ref().unwrap().as_ptr() as *const _, + Inner::Native(ref s) => s.as_ptr() as *const _, Inner::Foreign { ptr, .. } => ptr.as_ptr(), + Inner::Inline { ref data, .. } => data.as_ptr() as *const _, } } // rustdoc-stripper-ignore-next - /// Wraps a raw C string with a safe GLib string wrapper. The provided C string **must** be - /// nul-terminated. All constraints from [`std::ffi::CStr::from_ptr`] also apply here. + /// Consumes the `GString` and returns the underlying byte buffer. /// - /// If the string is valid UTF-8 then it is directly returned otherwise a copy is created with - /// every invalid character replaced by the Unicode replacement character (U+FFFD). - #[inline] - pub unsafe fn from_ptr_lossy<'a>(ptr: *const c_char) -> Cow<'a, GStr> { - GStr::from_ptr_lossy(ptr) + /// The returned buffer is not guaranteed to contain a trailing nul-byte. + pub fn into_bytes(mut self) -> Vec { + match &mut self.0 { + Inner::Native(s) => { + let mut s = String::from(mem::replace(s, "".into())); + let _nul = s.pop(); + debug_assert_eq!(_nul, Some('\0')); + s.into_bytes() + } + Inner::Foreign { ptr, len } => { + let bytes = unsafe { slice::from_raw_parts(ptr.as_ptr() as *const u8, *len - 1) }; + bytes.to_owned() + } + Inner::Inline { len, data } => { + unsafe { data.get_unchecked(..*len as usize) }.to_owned() + } + } } // rustdoc-stripper-ignore-next - /// Wraps a raw C string with a safe GLib string wrapper. The provided C string **must** be - /// nul-terminated. All constraints from [`std::ffi::CStr::from_ptr`] also apply here. - /// - /// `len` is the length without the nul-terminator, i.e. if `len == 0` is passed then `*ptr` - /// must be the nul-terminator. - pub unsafe fn from_ptr_and_len_unchecked(ptr: *const c_char, len: usize) -> Self { - assert!(!ptr.is_null()); + /// Consumes the `GString` and returns the underlying byte buffer, with trailing nul-byte. + pub fn into_bytes_with_nul(mut self) -> Vec { + match &mut self.0 { + Inner::Native(s) => str::into_boxed_bytes(mem::replace(s, "".into())).into(), + Inner::Foreign { ptr, len } => { + let bytes = unsafe { slice::from_raw_parts(ptr.as_ptr() as *const u8, *len) }; + bytes.to_owned() + } + Inner::Inline { len, data } => { + unsafe { data.get_unchecked(..*len as usize + 1) }.to_owned() + } + } + } +} - GString(Inner::Foreign { - ptr: ptr::NonNull::new_unchecked(ptr as *mut _), - len, - }) +// rustdoc-stripper-ignore-next +/// Creates a [`GString`] using interpolation of runtime expressions. +/// +/// This macro is the same as [`std::format!`] except it returns a [`GString`]. It is faster than +/// creating a [`String`] and then converting it manually to a [`GString`]. +#[macro_export] +macro_rules! gformat { + ($($arg:tt)*) => { GString::format(std::format_args!($($arg)*)) }; +} + +// rustdoc-stripper-ignore-next +/// Error type indicating that a buffer did not have a trailing nul-byte. +/// +/// `T` is the type of the value the conversion was attempted from. +#[derive(thiserror::Error, Clone, PartialEq, Eq, Debug)] +#[error("data provided is not nul terminated")] +pub struct GStringNoTrailingNulError(T); + +impl GStringNoTrailingNulError { + // rustdoc-stripper-ignore-next + /// Returns the original value that was attempted to convert to [`GString`]. + #[inline] + pub fn into_inner(self) -> T { + self.0 } } -impl IntoGlibPtr<*mut c_char> for GString { +// rustdoc-stripper-ignore-next +/// Error type indicating that a buffer had unexpected nul-bytes. +/// +/// `T` is the type of the value the conversion was attempted from. +#[derive(thiserror::Error, Clone, PartialEq, Eq, Debug)] +#[error("{1}")] +pub struct GStringInteriorNulError(T, GStrInteriorNulError); + +impl GStringInteriorNulError { // rustdoc-stripper-ignore-next - /// Transform into a `NUL`-terminated raw C string pointer. - unsafe fn into_glib_ptr(self) -> *mut c_char { - match self.0 { - Inner::Native(ref cstr) => { - let cstr = cstr.as_ref().unwrap(); + /// Returns the original value that was attempted to convert to [`GString`]. + #[inline] + pub fn into_inner(self) -> T { + self.0 + } + // rustdoc-stripper-ignore-next + /// Fetch a [`GStrInteriorNulError`] to get more details about the conversion failure. + #[inline] + pub fn nul_error(&self) -> GStrInteriorNulError { + self.1 + } +} - let ptr = cstr.as_ptr(); - let len = cstr.to_bytes().len(); +// rustdoc-stripper-ignore-next +/// Error type indicating that a buffer had invalid UTF-8. +/// +/// `T` is the type of the value the conversion was attempted from. +#[derive(thiserror::Error, Clone, PartialEq, Eq, Debug)] +#[error("{1}")] +pub struct GStringUtf8Error(T, std::str::Utf8Error); - let copy = ffi::g_malloc(len + 1) as *mut c_char; - ptr::copy_nonoverlapping(ptr as *const c_char, copy, len + 1); - ptr::write(copy.add(len), 0); +impl GStringUtf8Error { + // rustdoc-stripper-ignore-next + /// Returns the original value that was attempted to convert to [`GString`]. + #[inline] + pub fn into_inner(self) -> T { + self.0 + } + // rustdoc-stripper-ignore-next + /// Fetch a [`Utf8Error`](std::str::Utf8Error) to get more details about the conversion + /// failure. + #[inline] + pub fn utf8_error(&self) -> std::str::Utf8Error { + self.1 + } +} - copy +// rustdoc-stripper-ignore-next +/// Error type holding all possible failures when creating a [`GString`]. +#[derive(thiserror::Error, Debug)] +pub enum GStringFromError { + #[error(transparent)] + NoTrailingNul(#[from] GStringNoTrailingNulError), + #[error(transparent)] + InteriorNul(#[from] GStringInteriorNulError), + #[error(transparent)] + InvalidUtf8(#[from] GStringUtf8Error), + #[error("unable to convert")] + Unspecified(T), +} + +impl GStringFromError { + pub fn into_inner(self) -> T { + match self { + Self::NoTrailingNul(GStringNoTrailingNulError(t)) => t, + Self::InteriorNul(GStringInteriorNulError(t, _)) => t, + Self::InvalidUtf8(GStringUtf8Error(t, _)) => t, + Self::Unspecified(t) => t, + } + } + #[inline] + fn convert(self, func: impl FnOnce(T) -> R) -> GStringFromError { + match self { + Self::NoTrailingNul(GStringNoTrailingNulError(t)) => { + GStringFromError::NoTrailingNul(GStringNoTrailingNulError(func(t))) } + Self::InteriorNul(GStringInteriorNulError(t, e)) => { + GStringFromError::InteriorNul(GStringInteriorNulError(func(t), e)) + } + Self::InvalidUtf8(GStringUtf8Error(t, e)) => { + GStringFromError::InvalidUtf8(GStringUtf8Error(func(t), e)) + } + Self::Unspecified(t) => GStringFromError::Unspecified(func(t)), + } + } +} + +impl From for GStringFromError> { + #[inline] + fn from(e: std::string::FromUtf8Error) -> Self { + let ue = e.utf8_error(); + Self::InvalidUtf8(GStringUtf8Error(e.into_bytes(), ue)) + } +} + +impl IntoGlibPtr<*mut c_char> for GString { + // rustdoc-stripper-ignore-next + /// Transform into a nul-terminated raw C string pointer. + unsafe fn into_glib_ptr(self) -> *mut c_char { + match self.0 { + Inner::Native(ref s) => ffi::g_strndup(s.as_ptr() as *const _, s.len()), Inner::Foreign { ptr, .. } => { let _s = mem::ManuallyDrop::new(self); ptr.as_ptr() } + Inner::Inline { len, ref data } => { + ffi::g_strndup(data.as_ptr() as *const _, len as usize) + } } } } +impl Default for GString { + #[inline] + fn default() -> Self { + Self::new() + } +} + impl Clone for GString { + #[inline] fn clone(&self) -> GString { self.as_str().into() } } impl fmt::Debug for GString { + #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { <&str as fmt::Debug>::fmt(&self.as_str(), f) } } impl Drop for GString { + #[inline] fn drop(&mut self) { if let Inner::Foreign { ptr, .. } = self.0 { unsafe { @@ -507,138 +1092,161 @@ impl Drop for GString { } impl fmt::Display for GString { + #[inline] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(self.as_str()) } } impl hash::Hash for GString { + #[inline] fn hash(&self, state: &mut H) { self.as_str().hash(state) } } impl Borrow for GString { + #[inline] fn borrow(&self) -> &GStr { self.as_gstr() } } impl Borrow for GString { + #[inline] fn borrow(&self) -> &str { self.as_str() } } impl Ord for GString { + #[inline] fn cmp(&self, other: &GString) -> Ordering { self.as_str().cmp(other.as_str()) } } impl PartialOrd for GString { + #[inline] fn partial_cmp(&self, other: &GString) -> Option { Some(self.cmp(other)) } } impl PartialEq for GString { + #[inline] fn eq(&self, other: &GString) -> bool { self.as_str() == other.as_str() } } impl PartialEq for String { + #[inline] fn eq(&self, other: &GString) -> bool { self.as_str() == other.as_str() } } impl PartialEq for GString { + #[inline] fn eq(&self, other: &GStr) -> bool { self.as_str() == other.as_str() } } impl PartialEq<&GStr> for GString { + #[inline] fn eq(&self, other: &&GStr) -> bool { self.as_str() == other.as_str() } } impl PartialEq for GString { + #[inline] fn eq(&self, other: &str) -> bool { self.as_str() == other } } impl PartialEq<&str> for GString { + #[inline] fn eq(&self, other: &&str) -> bool { self.as_str() == *other } } impl PartialEq for &GStr { + #[inline] fn eq(&self, other: &GString) -> bool { self.as_str() == other.as_str() } } impl PartialEq for &str { + #[inline] fn eq(&self, other: &GString) -> bool { *self == other.as_str() } } impl PartialEq for GString { + #[inline] fn eq(&self, other: &String) -> bool { self.as_str() == other.as_str() } } impl PartialEq for str { + #[inline] fn eq(&self, other: &GString) -> bool { self == other.as_str() } } impl PartialEq for GStr { + #[inline] fn eq(&self, other: &GString) -> bool { self.as_str() == other.as_str() } } impl PartialOrd for String { + #[inline] fn partial_cmp(&self, other: &GString) -> Option { Some(self.cmp(&String::from(other.as_str()))) } } impl PartialOrd for GString { + #[inline] fn partial_cmp(&self, other: &String) -> Option { Some(self.as_str().cmp(other.as_str())) } } impl PartialOrd for GStr { + #[inline] fn partial_cmp(&self, other: &GString) -> Option { Some(self.as_str().cmp(other)) } } impl PartialOrd for GString { + #[inline] fn partial_cmp(&self, other: &GStr) -> Option { Some(self.as_str().cmp(other.as_str())) } } impl PartialOrd for str { + #[inline] fn partial_cmp(&self, other: &GString) -> Option { Some(self.cmp(other)) } } impl PartialOrd for GString { + #[inline] fn partial_cmp(&self, other: &str) -> Option { Some(self.as_str().cmp(other)) } @@ -647,36 +1255,35 @@ impl PartialOrd for GString { impl Eq for GString {} impl AsRef for GString { + #[inline] fn as_ref(&self) -> &GStr { self.as_gstr() } } impl AsRef for GString { + #[inline] fn as_ref(&self) -> &str { self.as_str() } } -impl AsRef for GString { - fn as_ref(&self) -> &CStr { - self.as_gstr().as_c_str() - } -} - impl AsRef for GString { + #[inline] fn as_ref(&self) -> &OsStr { OsStr::new(self.as_str()) } } impl AsRef for GString { + #[inline] fn as_ref(&self) -> &Path { Path::new(self.as_str()) } } impl AsRef<[u8]> for GString { + #[inline] fn as_ref(&self) -> &[u8] { self.as_str().as_bytes() } @@ -685,6 +1292,7 @@ impl AsRef<[u8]> for GString { impl Deref for GString { type Target = str; + #[inline] fn deref(&self) -> &str { self.as_str() } @@ -693,17 +1301,23 @@ impl Deref for GString { impl From for String { #[inline] fn from(mut s: GString) -> Self { - match s.0 { - Inner::Foreign { len, .. } if len == 0 => String::new(), + match &mut s.0 { + Inner::Native(s) => { + // Moves the underlying string + let mut s = String::from(mem::replace(s, "".into())); + let _nul = s.pop(); + debug_assert_eq!(_nul, Some('\0')); + s + } + Inner::Foreign { len, .. } if *len == 0 => String::new(), Inner::Foreign { ptr, len } => unsafe { // Creates a copy - let slice = slice::from_raw_parts(ptr.as_ptr() as *const u8, len); + let slice = slice::from_raw_parts(ptr.as_ptr() as *const u8, *len); std::str::from_utf8_unchecked(slice).into() }, - Inner::Native(ref mut cstr) => { - // Moves the underlying string - cstr.take().unwrap().into_string().unwrap() - } + Inner::Inline { len, data } => unsafe { + std::str::from_utf8_unchecked(data.get_unchecked(..*len as usize)).to_owned() + }, } } } @@ -712,19 +1326,60 @@ impl From for Box { #[inline] fn from(s: GString) -> Self { // Potentially creates a copy - let st: String = s.into(); - st.into_boxed_str() + String::from(s).into() + } +} + +impl From for Vec { + #[inline] + fn from(value: GString) -> Vec { + value.into_bytes_with_nul() + } +} + +impl TryFrom for CString { + type Error = GStringInteriorNulError; + #[inline] + fn try_from(value: GString) -> Result { + if let Some(nul_pos) = memchr::memchr(0, value.as_bytes()) { + return Err(GStringInteriorNulError( + value, + GStrInteriorNulError(nul_pos), + )); + } + let v = value.into_bytes_with_nul(); + Ok(unsafe { CString::from_vec_with_nul_unchecked(v) }) + } +} + +impl From for OsString { + #[inline] + fn from(s: GString) -> Self { + OsString::from(String::from(s)) + } +} + +impl From for PathBuf { + #[inline] + fn from(s: GString) -> Self { + PathBuf::from(OsString::from(s)) } } impl From for GString { #[inline] - fn from(s: String) -> Self { + fn from(mut s: String) -> Self { // Moves the content of the String - unsafe { + if cfg!(debug_assertions) { + GStr::check_interior_nuls(&s).unwrap(); + } + if s.is_empty() { + Self::new() + } else { + s.reserve_exact(1); + s.push('\0'); // No check for valid UTF-8 here - let cstr = CString::from_vec_unchecked(s.into_bytes()); - GString(Inner::Native(Some(cstr))) + Self(Inner::Native(s.into())) } } } @@ -737,6 +1392,16 @@ impl From> for GString { } } +impl<'a> From> for GString { + #[inline] + fn from(s: Cow<'a, str>) -> Self { + match s { + Cow::Borrowed(s) => Self::from(s), + Cow::Owned(s) => Self::from(s), + } + } +} + impl From<&GStr> for GString { #[inline] fn from(s: &GStr) -> GString { @@ -747,13 +1412,22 @@ impl From<&GStr> for GString { impl From<&str> for GString { #[inline] fn from(s: &str) -> Self { + if cfg!(debug_assertions) { + GStr::check_interior_nuls(s).unwrap(); + } + if s.len() < INLINE_LEN { + let mut data = <[u8; INLINE_LEN]>::default(); + let b = s.as_bytes(); + unsafe { data.get_unchecked_mut(..b.len()) }.copy_from_slice(b); + return Self(Inner::Inline { + len: b.len() as u8, + data, + }); + } // Allocates with the GLib allocator unsafe { // No check for valid UTF-8 here - let copy = ffi::g_malloc(s.len() + 1) as *mut c_char; - ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, copy, s.len()); - ptr::write(copy.add(s.len()), 0); - + let copy = ffi::g_strndup(s.as_ptr() as *const c_char, s.len()); GString(Inner::Foreign { ptr: ptr::NonNull::new_unchecked(copy), len: s.len(), @@ -762,32 +1436,66 @@ impl From<&str> for GString { } } -impl From> for GString { +impl TryFrom for GString { + type Error = GStringUtf8Error; #[inline] - fn from(s: Vec) -> Self { - // Moves the content of the Vec - // Also check if it's valid UTF-8 - let cstring = CString::new(s).expect("CString::new failed"); - cstring.into() + fn try_from(value: CString) -> Result { + if value.as_bytes().is_empty() { + Ok(Self::new()) + } else { + // Moves the content of the CString + // Also check if it's valid UTF-8 + let s = String::from_utf8(value.into_bytes_with_nul()).map_err(|e| { + let err = e.utf8_error(); + GStringUtf8Error( + unsafe { CString::from_vec_with_nul_unchecked(e.into_bytes()) }, + err, + ) + })?; + Ok(Self(Inner::Native(s.into()))) + } } } -impl From for GString { +impl TryFrom for GString { + type Error = GStringFromError; #[inline] - fn from(s: CString) -> Self { - // Moves the content of the CString - // Also check if it's valid UTF-8 - assert!(s.to_str().is_ok()); - Self(Inner::Native(Some(s))) + fn try_from(value: OsString) -> Result { + Self::from_string_checked(value.into_string().map_err(GStringFromError::Unspecified)?) + .map_err(|e| GStringFromError::from(e).convert(OsString::from)) } } -impl From<&CStr> for GString { +impl TryFrom for GString { + type Error = GStringFromError; #[inline] - fn from(c: &CStr) -> Self { - // Creates a copy with the GLib allocator - // Also check if it's valid UTF-8 - c.to_str().unwrap().into() + fn try_from(value: PathBuf) -> Result { + GString::try_from(value.into_os_string()).map_err(|e| e.convert(PathBuf::from)) + } +} + +impl TryFrom<&CStr> for GString { + type Error = std::str::Utf8Error; + #[inline] + fn try_from(value: &CStr) -> Result { + // Check if it's valid UTF-8 + value.to_str()?; + let gstr = unsafe { GStr::from_utf8_with_nul_unchecked(value.to_bytes_with_nul()) }; + Ok(gstr.to_owned()) + } +} + +impl<'a> From> for GString { + #[inline] + fn from(s: Cow<'a, GStr>) -> Self { + s.into_owned() + } +} + +impl<'a> From<&'a GString> for Cow<'a, GStr> { + #[inline] + fn from(s: &'a GString) -> Self { + Cow::Borrowed(s.as_gstr()) } } @@ -807,12 +1515,15 @@ impl<'a> From<&'a GStr> for Cow<'a, GStr> { impl FromGlibPtrFull<*mut u8> for GString { #[inline] unsafe fn from_glib_full(ptr: *mut u8) -> Self { - assert!(!ptr.is_null()); + assert!(!ptr.is_null(), "provided C string is NULL"); - // Check for valid UTF-8 here let cstr = CStr::from_ptr(ptr as *const _); - assert!(cstr.to_str().is_ok()); - GString(Inner::Foreign { + // Check for valid UTF-8 here + assert!( + cstr.to_str().is_ok(), + "provided C string is not valid utf-8" + ); + Self(Inner::Foreign { ptr: ptr::NonNull::new_unchecked(ptr as *mut _), len: cstr.to_bytes().len(), }) @@ -847,7 +1558,7 @@ impl FromGlibPtrFull<*const i8> for GString { impl FromGlibPtrNone<*const u8> for GString { #[inline] unsafe fn from_glib_none(ptr: *const u8) -> Self { - assert!(!ptr.is_null()); + assert!(!ptr.is_null(), "provided C string is NULL"); let cstr = CStr::from_ptr(ptr as *const _); // Also check if it's valid UTF-8 cstr.to_str().unwrap().into() @@ -887,7 +1598,7 @@ impl FromGlibPtrBorrow<*const u8> for GString { // Check for valid UTF-8 here let cstr = CStr::from_ptr(ptr as *const _); assert!(cstr.to_str().is_ok()); - Borrowed::new(GString(Inner::Foreign { + Borrowed::new(Self(Inner::Foreign { ptr: ptr::NonNull::new_unchecked(ptr as *mut _), len: cstr.to_bytes().len(), })) @@ -982,7 +1693,7 @@ impl<'a> ToGlibPtr<'a, *mut i8> for GString { impl FromGlibContainer<*const c_char, *const i8> for GString { unsafe fn from_glib_none_num(ptr: *const i8, num: usize) -> Self { if num == 0 || ptr.is_null() { - return Self::from(""); + return Self::default(); } let slice = slice::from_raw_parts(ptr as *const u8, num); // Also check if it's valid UTF-8 @@ -991,7 +1702,7 @@ impl FromGlibContainer<*const c_char, *const i8> for GString { unsafe fn from_glib_container_num(ptr: *const i8, num: usize) -> Self { if num == 0 || ptr.is_null() { - return Self::from(""); + return Self::default(); } // Check if it's valid UTF-8 @@ -1006,7 +1717,7 @@ impl FromGlibContainer<*const c_char, *const i8> for GString { unsafe fn from_glib_full_num(ptr: *const i8, num: usize) -> Self { if num == 0 || ptr.is_null() { - return Self::from(""); + return Self::default(); } // Check if it's valid UTF-8 @@ -1070,6 +1781,7 @@ impl GlibPtrDefault for GString { } impl StaticType for GString { + #[inline] fn static_type() -> Type { String::static_type() } @@ -1084,22 +1796,26 @@ impl crate::value::ValueTypeOptional for GString {} unsafe impl<'a> crate::value::FromValue<'a> for GString { type Checker = crate::value::GenericValueTypeOrNoneChecker; + #[inline] unsafe fn from_value(value: &'a Value) -> Self { Self::from(<&str>::from_value(value)) } } impl crate::value::ToValue for GString { + #[inline] fn to_value(&self) -> Value { <&str>::to_value(&self.as_str()) } + #[inline] fn value_type(&self) -> Type { String::static_type() } } impl crate::value::ToValueOptional for GString { + #[inline] fn to_value_optional(s: Option<&Self>) -> Value { ::to_value_optional(s.as_ref().map(|s| s.as_str())) } @@ -1116,6 +1832,7 @@ impl From for Value { } impl StaticType for Vec { + #[inline] fn static_type() -> Type { >::static_type() } @@ -1144,6 +1861,7 @@ impl ToValue for Vec { } } + #[inline] fn value_type(&self) -> Type { >::static_type() } @@ -1168,6 +1886,92 @@ impl From> for Value { impl_from_glib_container_as_vec_string!(GString, *const c_char); impl_from_glib_container_as_vec_string!(GString, *mut c_char); +// rustdoc-stripper-ignore-next +/// A trait to accept both &[str] or &[GStr] as an argument. +pub trait IntoGStr { + fn run_with_gstr T>(self, f: F) -> T; +} + +impl IntoGStr for &GStr { + #[inline] + fn run_with_gstr T>(self, f: F) -> T { + f(self) + } +} + +impl IntoGStr for GString { + #[inline] + fn run_with_gstr T>(self, f: F) -> T { + f(self.as_gstr()) + } +} + +impl IntoGStr for &GString { + #[inline] + fn run_with_gstr T>(self, f: F) -> T { + f(self.as_gstr()) + } +} + +// Limit borrowed from rust std CStr optimization: +// https://github.com/rust-lang/rust/blob/master/library/std/src/sys/common/small_c_string.rs#L10 +const MAX_STACK_ALLOCATION: usize = 384; + +impl IntoGStr for &str { + #[inline] + fn run_with_gstr T>(self, f: F) -> T { + if self.len() < MAX_STACK_ALLOCATION { + let mut s = mem::MaybeUninit::<[u8; MAX_STACK_ALLOCATION]>::uninit(); + let ptr = s.as_mut_ptr() as *mut u8; + let gs = unsafe { + ptr::copy_nonoverlapping(self.as_ptr(), ptr, self.len()); + ptr.add(self.len()).write(0); + GStr::from_utf8_with_nul_unchecked(slice::from_raw_parts(ptr, self.len() + 1)) + }; + f(gs) + } else { + f(GString::from(self).as_gstr()) + } + } +} + +impl IntoGStr for String { + #[inline] + fn run_with_gstr T>(self, f: F) -> T { + if self.len() < MAX_STACK_ALLOCATION { + self.as_str().run_with_gstr(f) + } else { + f(GString::from(self).as_gstr()) + } + } +} + +impl IntoGStr for &String { + #[inline] + fn run_with_gstr T>(self, f: F) -> T { + self.as_str().run_with_gstr(f) + } +} + +pub const NONE_STR: Option<&'static str> = None; + +// rustdoc-stripper-ignore-next +/// A trait to accept both [Option]<&[str]> or [Option]<&[GStr]> as +/// an argument. +pub trait IntoOptionalGStr { + fn run_with_gstr) -> T>(self, f: F) -> T; +} + +impl IntoOptionalGStr for Option { + #[inline] + fn run_with_gstr) -> T>(self, f: F) -> T { + match self { + Some(t) => t.run_with_gstr(|s| f(Some(s))), + None => f(None), + } + } +} + #[cfg(test)] #[allow(clippy::disallowed_names)] mod tests { @@ -1219,7 +2023,7 @@ mod tests { #[test] fn test_gstring_from_cstring() { let cstr = CString::new("foo").unwrap(); - let gstring = GString::from(cstr); + let gstring = GString::try_from(cstr).unwrap(); assert_eq!(gstring.as_str(), "foo"); let foo: Box = gstring.into(); assert_eq!(foo.as_ref(), "foo"); @@ -1228,7 +2032,7 @@ mod tests { #[test] fn test_string_from_gstring_from_cstring() { let cstr = CString::new("foo").unwrap(); - let gstring = GString::from(cstr); + let gstring = GString::try_from(cstr).unwrap(); assert_eq!(gstring.as_str(), "foo"); let s = String::from(gstring); assert_eq!(s, "foo"); @@ -1237,7 +2041,7 @@ mod tests { #[test] fn test_vec_u8_to_gstring() { let v: &[u8] = b"foo"; - let s: GString = Vec::from(v).into(); + let s: GString = GString::from_utf8(Vec::from(v)).unwrap(); assert_eq!(s.as_str(), "foo"); } @@ -1307,4 +2111,21 @@ mod tests { assert_ne!(ptr, gstring.as_ptr() as *const _); } } + + #[test] + fn gformat() { + let s = gformat!("bla bla {} bla", 123); + assert_eq!(s, "bla bla 123 bla"); + } + + #[test] + fn layout() { + // ensure the inline variant is not wider than the other variants + enum NoInline { + _Native(Box), + _Foreign(ptr::NonNull, usize), + } + assert_eq!(mem::size_of::(), mem::size_of::()); + assert_eq!(mem::size_of::(), mem::size_of::()); + } } diff --git a/glib/src/gstring_builder.rs b/glib/src/gstring_builder.rs index 63d2643753f2..dbbb3341833b 100644 --- a/glib/src/gstring_builder.rs +++ b/glib/src/gstring_builder.rs @@ -2,7 +2,7 @@ use std::{cmp, fmt, hash, mem, ops, ptr, slice, str}; -use crate::translate::*; +use crate::{translate::*, GStr}; wrapper! { // rustdoc-stripper-ignore-next @@ -12,7 +12,7 @@ wrapper! { pub struct GStringBuilder(BoxedInline); match fn { - copy => |ptr| ffi::g_string_new((*ptr).str), + copy => |ptr| ffi::g_string_new_len((*ptr).str, (*ptr).len.try_into().unwrap()), free => |ptr| ffi::g_string_free(ptr, ffi::GTRUE), init => |ptr| unsafe { let inner = ffi::GString { @@ -111,7 +111,7 @@ impl GStringBuilder { } // rustdoc-stripper-ignore-next - /// Returns `&str` slice. + /// Returns &[str] slice. pub fn as_str(&self) -> &str { unsafe { let ptr: *const u8 = self.inner.str as _; @@ -125,7 +125,21 @@ impl GStringBuilder { } // rustdoc-stripper-ignore-next - /// Returns `&str` slice. + /// Returns &[GStr] slice. + pub fn as_gstr(&self) -> &GStr { + unsafe { + let ptr: *const u8 = self.inner.str as _; + let len: usize = self.inner.len; + if len == 0 { + return Default::default(); + } + let slice = slice::from_raw_parts(ptr, len + 1); + GStr::from_utf8_with_nul_unchecked(slice) + } + } + + // rustdoc-stripper-ignore-next + /// Finalizes the builder, converting it to a [`GString`]. #[must_use = "String returned from the builder should probably be used"] pub fn into_string(self) -> crate::GString { unsafe { @@ -173,6 +187,18 @@ impl PartialEq for str { } } +impl PartialEq for GStringBuilder { + fn eq(&self, other: &GStr) -> bool { + self.as_gstr() == other + } +} + +impl PartialEq for GStr { + fn eq(&self, other: &GStringBuilder) -> bool { + self == other.as_gstr() + } +} + impl Eq for GStringBuilder {} impl cmp::PartialOrd for GStringBuilder { @@ -193,6 +219,18 @@ impl cmp::PartialOrd for str { } } +impl cmp::PartialOrd for GStringBuilder { + fn partial_cmp(&self, other: &GStr) -> Option { + Some(self.as_gstr().cmp(other)) + } +} + +impl cmp::PartialOrd for GStr { + fn partial_cmp(&self, other: &GStringBuilder) -> Option { + Some(self.cmp(other.as_gstr())) + } +} + impl cmp::Ord for GStringBuilder { fn cmp(&self, other: &Self) -> cmp::Ordering { self.as_str().cmp(other.as_str()) @@ -220,6 +258,12 @@ impl AsRef for GStringBuilder { } } +impl AsRef for GStringBuilder { + fn as_ref(&self) -> &GStr { + self.as_gstr() + } +} + impl ops::Deref for GStringBuilder { type Target = str; diff --git a/glib/src/lib.rs b/glib/src/lib.rs index cec0b55ce79c..47b67b07e3f6 100644 --- a/glib/src/lib.rs +++ b/glib/src/lib.rs @@ -138,7 +138,7 @@ pub use self::source::*; #[macro_use] pub mod translate; mod gstring; -pub use self::gstring::{GStr, GString}; +pub use self::gstring::*; mod gstring_builder; pub use self::gstring_builder::GStringBuilder; pub mod types; diff --git a/glib/src/translate.rs b/glib/src/translate.rs index 71714fdad534..d83ef44689ff 100644 --- a/glib/src/translate.rs +++ b/glib/src/translate.rs @@ -133,12 +133,15 @@ #[cfg(not(windows))] use std::os::unix::prelude::*; use std::{ + borrow::Cow, char, cmp::{Eq, Ordering, PartialEq}, collections::HashMap, error::Error, ffi::{CStr, CString, OsStr, OsString}, - fmt, mem, + fmt, + marker::PhantomData, + mem, path::{Path, PathBuf}, ptr, }; @@ -501,18 +504,71 @@ impl<'a, P: Ptr, T: ?Sized + ToGlibPtr<'a, P>> ToGlibPtr<'a, P> for &'a T { } } -impl<'a> ToGlibPtr<'a, *const c_char> for str { - type Storage = CString; +#[doc(hidden)] +#[derive(Debug)] +pub enum CowStash { + Borrowed(B), + Owned(O), +} + +impl<'a, P: Ptr, T> ToGlibPtr<'a, P> for Cow<'a, T> +where + T: ToOwned + ?Sized + ToGlibPtr<'a, P>, + T::Owned: ToGlibPtr<'a, P>, +{ + type Storage = CowStash>::Storage>; + + #[inline] + fn to_glib_none(&'a self) -> Stash<'a, P, Self> { + match self { + Cow::Borrowed(v) => { + let s = v.to_glib_none(); + Stash(s.0, CowStash::Borrowed(s.1)) + } + Cow::Owned(v) => { + let s = v.to_glib_none(); + Stash(s.0, CowStash::Owned(s.1)) + } + } + } #[inline] + fn to_glib_full(&self) -> P { + match self { + Cow::Borrowed(v) => v.to_glib_full(), + Cow::Owned(v) => v.to_glib_full(), + } + } +} + +impl<'a> ToGlibPtr<'a, *const c_char> for str { + type Storage = Cow<'static, [u8]>; + fn to_glib_none(&'a self) -> Stash<'a, *const c_char, Self> { - let tmp = - CString::new(self).expect("str::ToGlibPtr<*const c_char>: unexpected '\0' character"); - Stash(tmp.as_ptr(), tmp) + static EMPTY_STRING: &[u8] = &[0]; + + let bytes = if self.is_empty() { + Cow::Borrowed(EMPTY_STRING) + } else { + if cfg!(debug_assertions) { + crate::GStr::check_interior_nuls(self).unwrap(); + } + let mut bytes = Vec::with_capacity(self.len() + 1); + unsafe { + ptr::copy_nonoverlapping(self.as_ptr(), bytes.as_mut_ptr(), self.len()); + bytes.as_mut_ptr().add(self.len()).write(0); + bytes.set_len(self.len() + 1); + } + Cow::Owned(bytes) + }; + Stash(bytes.as_ptr() as *const c_char, bytes) } #[inline] fn to_glib_full(&self) -> *const c_char { + if cfg!(debug_assertions) { + crate::GStr::check_interior_nuls(self).unwrap(); + } unsafe { ffi::g_strndup(self.as_ptr() as *const c_char, self.len() as size_t) as *const c_char } @@ -520,53 +576,122 @@ impl<'a> ToGlibPtr<'a, *const c_char> for str { } impl<'a> ToGlibPtr<'a, *mut c_char> for str { - type Storage = CString; + type Storage = Cow<'static, [u8]>; #[inline] fn to_glib_none(&'a self) -> Stash<'a, *mut c_char, Self> { - let tmp = - CString::new(self).expect("str::ToGlibPtr<*mut c_char>: unexpected '\0' character"); - Stash(tmp.as_ptr() as *mut c_char, tmp) + let s = ToGlibPtr::<*const c_char>::to_glib_none(self); + Stash(s.0 as *mut _, s.1) } #[inline] fn to_glib_full(&self) -> *mut c_char { - unsafe { ffi::g_strndup(self.as_ptr() as *mut c_char, self.len() as size_t) } + ToGlibPtr::<*const c_char>::to_glib_full(self) as *mut _ } } impl<'a> ToGlibPtr<'a, *const c_char> for String { - type Storage = CString; + type Storage = Cow<'static, [u8]>; #[inline] fn to_glib_none(&self) -> Stash<'a, *const c_char, String> { - let tmp = CString::new(&self[..]) - .expect("String::ToGlibPtr<*const c_char>: unexpected '\0' character"); - Stash(tmp.as_ptr(), tmp) + let s = ToGlibPtr::to_glib_none(self.as_str()); + Stash(s.0, s.1) } #[inline] fn to_glib_full(&self) -> *const c_char { - unsafe { - ffi::g_strndup(self.as_ptr() as *const c_char, self.len() as size_t) as *const c_char - } + ToGlibPtr::to_glib_full(self.as_str()) } } impl<'a> ToGlibPtr<'a, *mut c_char> for String { - type Storage = CString; + type Storage = Cow<'static, [u8]>; #[inline] fn to_glib_none(&self) -> Stash<'a, *mut c_char, String> { - let tmp = CString::new(&self[..]) - .expect("String::ToGlibPtr<*mut c_char>: unexpected '\0' character"); - Stash(tmp.as_ptr() as *mut c_char, tmp) + let s = ToGlibPtr::to_glib_none(self.as_str()); + Stash(s.0, s.1) + } + + #[inline] + fn to_glib_full(&self) -> *mut c_char { + ToGlibPtr::to_glib_full(self.as_str()) + } +} + +impl<'a> ToGlibPtr<'a, *const c_char> for CStr { + type Storage = PhantomData<&'a Self>; + + #[inline] + fn to_glib_none(&'a self) -> Stash<'a, *const c_char, Self> { + Stash(self.as_ptr(), PhantomData) + } + + #[inline] + fn to_glib_full(&self) -> *const c_char { + unsafe { + ffi::g_strndup( + self.as_ptr() as *const c_char, + self.to_bytes().len() as size_t, + ) as *const c_char + } + } +} + +impl<'a> ToGlibPtr<'a, *mut c_char> for CStr { + type Storage = PhantomData<&'a Self>; + + #[inline] + fn to_glib_none(&'a self) -> Stash<'a, *mut c_char, Self> { + Stash(self.as_ptr() as *mut c_char, PhantomData) + } + + #[inline] + fn to_glib_full(&self) -> *mut c_char { + unsafe { + ffi::g_strndup( + self.as_ptr() as *const c_char, + self.to_bytes().len() as size_t, + ) as *mut c_char + } + } +} + +impl<'a> ToGlibPtr<'a, *const c_char> for CString { + type Storage = PhantomData<&'a Self>; + + #[inline] + fn to_glib_none(&'a self) -> Stash<'a, *const c_char, Self> { + Stash(self.as_ptr(), PhantomData) + } + + #[inline] + fn to_glib_full(&self) -> *const c_char { + unsafe { + ffi::g_strndup( + self.as_ptr() as *const c_char, + self.as_bytes().len() as size_t, + ) as *const c_char + } + } +} + +impl<'a> ToGlibPtr<'a, *mut c_char> for CString { + type Storage = PhantomData<&'a Self>; + + #[inline] + fn to_glib_none(&'a self) -> Stash<'a, *mut c_char, Self> { + Stash(self.as_ptr() as *mut c_char, PhantomData) } #[inline] fn to_glib_full(&self) -> *mut c_char { unsafe { - ffi::g_strndup(self.as_ptr() as *const c_char, self.len() as size_t) as *mut c_char + ffi::g_strndup( + self.as_ptr() as *const c_char, + self.as_bytes().len() as size_t, + ) as *mut c_char } } } diff --git a/glib/src/utils.rs b/glib/src/utils.rs index 49606579f361..71a0a54c1ca1 100644 --- a/glib/src/utils.rs +++ b/glib/src/utils.rs @@ -5,20 +5,22 @@ use std::{ mem, ptr, }; -use crate::translate::*; +use crate::{translate::*, GString, IntoGStr, IntoOptionalGStr}; // rustdoc-stripper-ignore-next /// Same as [`get_prgname()`]. /// /// [`get_prgname()`]: fn.get_prgname.html #[doc(alias = "get_program_name")] -pub fn program_name() -> Option { +#[inline] +pub fn program_name() -> Option { prgname() } #[doc(alias = "g_get_prgname")] #[doc(alias = "get_prgname")] -pub fn prgname() -> Option { +#[inline] +pub fn prgname() -> Option { unsafe { from_glib_none(ffi::g_get_prgname()) } } @@ -26,13 +28,15 @@ pub fn prgname() -> Option { /// Same as [`set_prgname()`]. /// /// [`set_prgname()`]: fn.set_prgname.html -pub fn set_program_name(name: Option<&str>) { +#[inline] +pub fn set_program_name(name: Option) { set_prgname(name) } #[doc(alias = "g_set_prgname")] -pub fn set_prgname(name: Option<&str>) { - unsafe { ffi::g_set_prgname(name.to_glib_none().0) } +#[inline] +pub fn set_prgname(name: Option) { + name.run_with_gstr(|name| unsafe { ffi::g_set_prgname(name.to_glib_none().0) }) } #[doc(alias = "g_environ_getenv")] @@ -130,50 +134,60 @@ pub fn is_canonical_pspec_name(name: &str) -> bool { #[doc(alias = "g_uri_escape_string")] pub fn uri_escape_string( - unescaped: &str, - reserved_chars_allowed: Option<&str>, + unescaped: impl IntoGStr, + reserved_chars_allowed: Option, allow_utf8: bool, ) -> crate::GString { - unsafe { - from_glib_full(ffi::g_uri_escape_string( - unescaped.to_glib_none().0, - reserved_chars_allowed.to_glib_none().0, - allow_utf8.into_glib(), - )) - } + unescaped.run_with_gstr(|unescaped| { + reserved_chars_allowed.run_with_gstr(|reserved_chars_allowed| unsafe { + from_glib_full(ffi::g_uri_escape_string( + unescaped.to_glib_none().0, + reserved_chars_allowed.to_glib_none().0, + allow_utf8.into_glib(), + )) + }) + }) } #[doc(alias = "g_uri_unescape_string")] pub fn uri_unescape_string( - escaped_string: &str, - illegal_characters: Option<&str>, + escaped_string: impl IntoGStr, + illegal_characters: Option, ) -> Option { - unsafe { - from_glib_full(ffi::g_uri_unescape_string( - escaped_string.to_glib_none().0, - illegal_characters.to_glib_none().0, - )) - } + escaped_string.run_with_gstr(|escaped_string| { + illegal_characters.run_with_gstr(|illegal_characters| unsafe { + from_glib_full(ffi::g_uri_unescape_string( + escaped_string.to_glib_none().0, + illegal_characters.to_glib_none().0, + )) + }) + }) } #[doc(alias = "g_uri_parse_scheme")] -pub fn uri_parse_scheme(uri: &str) -> Option { - unsafe { from_glib_full(ffi::g_uri_parse_scheme(uri.to_glib_none().0)) } +pub fn uri_parse_scheme(uri: impl IntoGStr) -> Option { + uri.run_with_gstr(|uri| unsafe { + from_glib_full(ffi::g_uri_parse_scheme(uri.to_glib_none().0)) + }) } #[doc(alias = "g_uri_unescape_segment")] pub fn uri_unescape_segment( - escaped_string: Option<&str>, - escaped_string_end: Option<&str>, - illegal_characters: Option<&str>, + escaped_string: Option, + escaped_string_end: Option, + illegal_characters: Option, ) -> Option { - unsafe { - from_glib_full(ffi::g_uri_unescape_segment( - escaped_string.to_glib_none().0, - escaped_string_end.to_glib_none().0, - illegal_characters.to_glib_none().0, - )) - } + escaped_string.run_with_gstr(|escaped_string| { + escaped_string_end.run_with_gstr(|escaped_string_end| { + illegal_characters.run_with_gstr(|illegal_characters| unsafe { + from_glib_full(ffi::g_uri_unescape_segment( + escaped_string.to_glib_none().0, + escaped_string_end.to_glib_none().0, + illegal_characters.to_glib_none().0, + )) + }) + }) + }) } #[cfg(test)] @@ -246,16 +260,19 @@ mod tests { ); assert_eq!(crate::uri_parse_scheme("foo"), None); - let escaped = crate::uri_escape_string("&foo", None, true); + let escaped = crate::uri_escape_string("&foo", crate::NONE_STR, true); assert_eq!(escaped, GString::from("%26foo")); - let unescaped = crate::uri_unescape_string(escaped.as_str(), None); + let unescaped = crate::uri_unescape_string(escaped.as_str(), crate::GStr::NONE); assert_eq!(unescaped, Some(GString::from("&foo"))); assert_eq!( - crate::uri_unescape_segment(Some("/foo"), None, None), + crate::uri_unescape_segment(Some("/foo"), crate::NONE_STR, crate::NONE_STR), Some(GString::from("/foo")) ); - assert_eq!(crate::uri_unescape_segment(Some("/foo%"), None, None), None); + assert_eq!( + crate::uri_unescape_segment(Some("/foo%"), crate::NONE_STR, crate::NONE_STR), + None + ); } }