use std::{cmp, ffi, fmt, hash, marker::PhantomData, ptr, slice};
#[derive(Copy, Clone)]
#[repr(transparent)]
pub struct NullTermStr<'s> {
start: *const ffi::c_char,
_ph: PhantomData<&'s ffi::c_char>,
}
impl<'s> NullTermStr<'s> {
#[inline]
pub const unsafe fn from_ptr(start: *const ffi::c_char) -> Self {
Self {
start,
_ph: PhantomData,
}
}
#[inline]
pub const fn as_ptr(&self) -> *const ffi::c_char { self.start }
#[inline]
pub fn measure(self) -> MeasuredNullTermStr<'s> {
let n = unsafe { libc::strlen(self.start) };
unsafe { MeasuredNullTermStr::given_measurement(self, n) }
}
#[inline]
pub fn match_dir_entries_unmeasured(&self) -> bool {
let mut p: *const u8 = self.start.cast();
match unsafe { p.read() } {
0 => return false,
b'.' => (),
_ => return false,
}
p = unsafe { p.add(1) };
match unsafe { p.read() } {
0 => return true,
b'.' => (),
_ => return false,
}
p = unsafe { p.add(1) };
match unsafe { p.read() } {
0 => true,
_ => false,
}
}
}
impl cmp::PartialEq for NullTermStr<'_> {
fn eq(&self, rhs: &Self) -> bool { ptr::eq(self.start, rhs.start) }
}
impl cmp::Eq for NullTermStr<'_> {}
impl hash::Hash for NullTermStr<'_> {
fn hash<H>(&self, state: &mut H)
where H: hash::Hasher {
ptr::hash(self.start, state);
}
}
impl fmt::Debug for NullTermStr<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s: &ffi::CStr = self.measure().into();
f.debug_tuple("NullTermStr")
.field(&self.start)
.field(&s)
.finish()
}
}
#[derive(Copy, Clone)]
pub struct MeasuredNullTermStr<'s> {
s: NullTermStr<'s>,
n: usize,
}
impl<'s> MeasuredNullTermStr<'s> {
#[inline]
pub const unsafe fn given_measurement(s: NullTermStr<'s>, n: usize) -> Self { Self { s, n } }
#[inline]
pub fn from_bytes_with_nul(s: &'s [u8]) -> Self {
assert!(!s.is_empty(), "expected non-empty slice");
assert_eq!(s[s.len() - 1], 0, "slice must end with null byte");
assert_eq!(
memchr::memchr(0, s),
Some(s.len() - 1),
"slice must contain no internal null bytes"
);
unsafe { Self::from_bytes_with_nul_unchecked(s) }
}
#[inline]
pub const unsafe fn from_bytes_with_nul_unchecked(s: &'s [u8]) -> Self {
let n = unsafe { s.len().unchecked_sub(1) };
Self {
s: unsafe { NullTermStr::from_ptr(s.as_ptr().cast()) },
n,
}
}
#[inline]
pub const fn as_bytes(&self) -> &'s [u8] {
unsafe { slice::from_raw_parts(self.as_ptr().cast(), self.len()) }
}
#[inline]
pub fn as_os_str(&self) -> &'s ffi::OsStr {
use std::os::unix::ffi::OsStrExt;
ffi::OsStr::from_bytes(self.as_bytes())
}
#[inline]
const fn as_ptr(&self) -> *const ffi::c_char { self.as_unmeasured().as_ptr() }
#[inline]
pub const fn len(&self) -> usize { self.n }
#[inline]
pub const fn is_empty(&self) -> bool { self.len() == 0 }
#[inline]
pub const fn len_with_nul(&self) -> usize { self.n.checked_add(1).unwrap() }
#[inline]
pub const fn as_bytes_with_nul(&self) -> &'s [u8] {
unsafe { slice::from_raw_parts(self.as_ptr().cast(), self.len_with_nul()) }
}
#[inline]
pub const fn as_unmeasured(&self) -> NullTermStr<'s> { self.s }
#[inline]
pub fn clone_into(&self, v: &mut NullTermString) {
let NullTermString(v) = v;
v.clear();
let src = self.as_bytes_with_nul();
v.reserve(src.len());
unsafe {
cfg_if::cfg_if! {
if #[cfg(feature = "nightly")] {
v.spare_capacity_mut()[..src.len()].write_copy_of_slice(src);
} else {
v.as_mut_ptr()
.copy_from_nonoverlapping(src.as_ptr(), src.len());
}
}
v.set_len(src.len());
}
}
}
impl<'s> From<&'s [u8]> for MeasuredNullTermStr<'s> {
fn from(s: &'s [u8]) -> Self { Self::from_bytes_with_nul(s) }
}
impl<'s> From<MeasuredNullTermStr<'s>> for &'s [u8] {
fn from(s: MeasuredNullTermStr<'s>) -> Self { s.as_bytes_with_nul() }
}
impl<'s> From<&'s ffi::CStr> for MeasuredNullTermStr<'s> {
fn from(s: &'s ffi::CStr) -> Self {
unsafe { Self::from_bytes_with_nul_unchecked(s.to_bytes_with_nul()) }
}
}
impl<'s> From<MeasuredNullTermStr<'s>> for &'s ffi::CStr {
fn from(s: MeasuredNullTermStr<'s>) -> &'s ffi::CStr {
unsafe { ffi::CStr::from_bytes_with_nul_unchecked(s.as_bytes_with_nul()) }
}
}
impl cmp::PartialEq for MeasuredNullTermStr<'_> {
fn eq(&self, rhs: &Self) -> bool { self.as_bytes().eq(rhs.as_bytes()) }
}
impl cmp::Eq for MeasuredNullTermStr<'_> {}
impl cmp::PartialOrd for MeasuredNullTermStr<'_> {
fn partial_cmp(&self, rhs: &Self) -> Option<cmp::Ordering> { Some(self.cmp(rhs)) }
}
impl cmp::Ord for MeasuredNullTermStr<'_> {
fn cmp(&self, rhs: &Self) -> cmp::Ordering { self.as_bytes().cmp(rhs.as_bytes()) }
}
impl hash::Hash for MeasuredNullTermStr<'_> {
fn hash<H>(&self, state: &mut H)
where H: hash::Hasher {
self.as_bytes_with_nul().hash(state);
}
}
impl fmt::Debug for MeasuredNullTermStr<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s: &ffi::CStr = (*self).into();
f.debug_tuple("MeasuredNullTermStr")
.field(&self.s.start)
.field(&self.n)
.field(&s)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NullTermString(Vec<u8>);
impl NullTermString {
pub const fn new() -> Self { Self(Vec::new()) }
pub fn with_capacity(n: usize) -> Self { Self(Vec::with_capacity(n)) }
}
impl Default for NullTermString {
fn default() -> Self { Self::new() }
}
impl From<NullTermString> for ffi::CString {
fn from(x: NullTermString) -> Self {
let NullTermString(v) = x;
unsafe { ffi::CString::from_vec_with_nul_unchecked(v) }
}
}
impl From<ffi::CString> for NullTermString {
fn from(x: ffi::CString) -> Self { Self(x.into_bytes_with_nul()) }
}
impl fmt::Debug for NullTermString {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s: ffi::CString = self.clone().into();
f.debug_tuple("NullTermString").field(&s).finish()
}
}
pub trait AsNullTermStr {
fn as_null_term_str(&self) -> MeasuredNullTermStr<'_>;
}
impl AsNullTermStr for MeasuredNullTermStr<'_> {
fn as_null_term_str(&self) -> MeasuredNullTermStr<'_> { *self }
}
impl AsNullTermStr for NullTermString {
fn as_null_term_str(&self) -> MeasuredNullTermStr<'_> {
unsafe { MeasuredNullTermStr::from_bytes_with_nul_unchecked(&self.0) }
}
}
#[cfg(test)]
mod test {
use proptest::{prelude::*, string::bytes_regex};
use super::*;
#[test]
fn matches_dir_entries() {
let s = NullTermString(vec![b'.', 0]);
let s = s.as_null_term_str().as_unmeasured();
assert!(s.match_dir_entries_unmeasured());
let s = NullTermString(vec![b'.', b'.', 0]);
let s = s.as_null_term_str().as_unmeasured();
assert!(s.match_dir_entries_unmeasured());
}
proptest! {
#[test]
fn not_dir_entries(
mut s in bytes_regex("(?s-u:[^\x00]*)").unwrap()
.prop_filter("not '.' or '..'",
|v| !(&v[..] == b"." || &v[..] == b".."))
) {
s.push(0);
let s = NullTermString(s);
let s = s.as_null_term_str().as_unmeasured();
prop_assert!(!s.match_dir_entries_unmeasured());
}
#[test]
fn cstring_roundtrip(s in any::<Vec<u8>>()) {
let n1 = NullTermString(s);
let c1: ffi::CString = n1.clone().into();
let n2: NullTermString = c1.into();
prop_assert_eq!(n1, n2);
}
#[test]
fn nonnull_roundtrip(mut s in bytes_regex("(?s-u:[^\x00]*)").unwrap()) {
s.push(0);
let s = NullTermString(s);
let c: ffi::CString = s.clone().into();
let c2: &ffi::CStr = s.as_null_term_str().into();
prop_assert_eq!(c.as_c_str(), c2);
}
#[test]
fn slice_roundtrip(mut s in bytes_regex("(?s-u:[^\x00]*)").unwrap()) {
s.push(0);
let s = NullTermString(s);
let s: MeasuredNullTermStr = s.as_null_term_str();
let sl: &[u8] = s.into();
let s2: MeasuredNullTermStr = sl.into();
prop_assert_eq!(s, s2);
let sl2: &[u8] = s2.into();
prop_assert_eq!(sl, sl2);
}
#[test]
fn nonnull_ref(s in bytes_regex("(?s-u:[^\x00]*)").unwrap()) {
let mut t = s.clone();
t.push(0);
let v = NullTermString(t.clone());
prop_assert_eq!(&t[..], v.as_null_term_str().as_bytes_with_nul());
prop_assert_eq!(&s[..], v.as_null_term_str().as_bytes());
}
#[test]
fn nonnull_measure(mut s in bytes_regex("(?s-u:[^\x00]*)").unwrap()) {
s.push(0);
let s = NullTermString(s);
let s = s.as_null_term_str();
prop_assert_eq!(s, s.as_unmeasured().measure());
}
#[test]
fn nonnull_clone_into(mut s in bytes_regex("(?s-u:[^\x00]*)").unwrap()) {
s.push(0);
let v = NullTermString(s);
let s = v.as_null_term_str();
let mut v2 = NullTermString(Vec::new());
s.clone_into(&mut v2);
let s2 = v2.as_null_term_str();
prop_assert_eq!(s, s2);
prop_assert_eq!(v, v2);
}
}
}