Skip to content
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
99 changes: 84 additions & 15 deletions crates/sigsafe/src/c_str.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use core::{ffi::c_char, marker::PhantomData, num::NonZeroUsize, ptr::NonNull, slice};
use core::{
ffi::c_char, iter::FusedIterator, marker::PhantomData, num::NonZeroUsize, ptr::NonNull, slice,
};

/// Marks a [`CStr`] whose length is not known.
#[derive(Clone, Copy)]
Expand All @@ -23,6 +25,35 @@ pub struct CStr<'a, R> {
lifetime: PhantomData<&'a c_char>,
}

/// An iterator over the non-NUL bytes of a thin C string.
#[derive(Clone)]
pub struct Bytes<'a> {
ptr: NonNull<u8>,
lifetime: PhantomData<&'a u8>,
}

impl Iterator for Bytes<'_> {
type Item = u8;

#[inline]
fn next(&mut self) -> Option<Self::Item> {
// SAFETY: `ptr` starts within a valid C string and is advanced only
// after reading a non-NUL byte, so it remains readable and never moves
// beyond the terminating NUL.
unsafe {
let byte = self.ptr.read();
if byte == 0 {
None
} else {
self.ptr = self.ptr.add(1);
Some(byte)
}
}
}
}

impl FusedIterator for Bytes<'_> {}

impl<R> CStr<'_, R> {
/// Returns a pointer to the first byte of this C string.
#[must_use]
Expand All @@ -46,6 +77,18 @@ impl Fat {
}

impl<'a> CStr<'a, Thin> {
/// Creates a thin C string view from a non-null pointer without finding
/// its length.
///
/// # Safety
///
/// `ptr` must point to an immutable NUL-terminated string that remains
/// valid for the lifetime of the returned view.
#[must_use]
pub const unsafe fn from_non_null(ptr: NonNull<c_char>) -> Self {
Self { ptr, repr: Thin { _private: () }, lifetime: PhantomData }
}

/// Creates a thin C string view without finding its length.
///
/// # Safety
Expand All @@ -54,27 +97,31 @@ impl<'a> CStr<'a, Thin> {
/// that remains valid for the lifetime of the returned view.
#[must_use]
pub const unsafe fn from_ptr(ptr: *const c_char) -> Self {
Self {
// SAFETY: upheld by the caller.
ptr: unsafe { NonNull::new_unchecked(ptr.cast_mut()) },
repr: Thin { _private: () },
lifetime: PhantomData,
}
// SAFETY: the caller guarantees that `ptr` is non-null.
let ptr = unsafe { NonNull::new_unchecked(ptr.cast_mut()) };
// SAFETY: the caller guarantees the remaining C string invariants.
unsafe { Self::from_non_null(ptr) }
}

/// Returns an iterator over the bytes before the terminating NUL.
#[inline]
#[must_use]
pub const fn bytes(self) -> Bytes<'a> {
Bytes { ptr: self.ptr.cast(), lifetime: PhantomData }
}

/// Counts through the terminating NUL and returns a length-retaining view.
#[inline]
#[must_use]
pub const fn count(self) -> CStr<'a, Fat> {
let mut count = 0;
// SAFETY: `CStr<Thin>` points to a NUL-terminated string.
while unsafe { self.as_ptr().add(count).read() } != 0 {
count += 1;
}
pub fn count(self) -> CStr<'a, Fat> {
let count = self.bytes().count();

CStr {
ptr: self.ptr,
repr: Fat {
// SAFETY: the count includes at least the terminating NUL.
// SAFETY: adding the terminator makes the represented length
// nonzero, and a valid allocation cannot contain `usize::MAX`
// non-NUL bytes.
len_with_nul: unsafe { NonZeroUsize::new_unchecked(count + 1) },
},
lifetime: PhantomData,
Expand Down Expand Up @@ -125,7 +172,7 @@ impl<'a> CStr<'a, Fat> {

#[cfg(test)]
mod tests {
use core::mem::size_of;
use core::{mem::size_of, ptr::NonNull};

use super::{CStr, Fat, Thin};

Expand All @@ -143,4 +190,26 @@ mod tests {
assert_eq!(fat.as_bytes_with_nul(), b"abc\0");
assert_eq!(counted.as_bytes_with_nul(), fat.as_bytes_with_nul());
}

#[test]
fn thin_view_accepts_a_checked_non_null_pointer() {
let ptr = NonNull::new(c"abc".as_ptr().cast_mut()).unwrap();
// SAFETY: the literal is an immutable NUL-terminated string.
let thin = unsafe { CStr::<Thin>::from_non_null(ptr) };

assert!(thin.bytes().eq(b"abc".iter().copied()));
}

#[test]
fn thin_bytes_exclude_the_nul_and_remain_fused() {
let mut bytes = {
// SAFETY: the literal is NUL-terminated and outlives the iterator.
let thin = unsafe { CStr::<Thin>::from_ptr(c"abc".as_ptr()) };
thin.bytes()
};

assert_eq!(bytes.by_ref().collect::<Vec<_>>(), b"abc");
assert_eq!(bytes.next(), None);
assert_eq!(bytes.next(), None);
}
}
2 changes: 1 addition & 1 deletion crates/sigsafe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub mod fs;
pub mod mm;
pub mod param;

pub use c_str::{CStr, Fat, Thin};
pub use c_str::{Bytes, CStr, Fat, Thin};
pub use rustix::{
fd::{AsRawFd, BorrowedFd},
fs::CWD,
Expand Down
Loading