null_terminated_str/
borrowed.rs

1use std::{
2    cmp::Ordering,
3    ffi::CStr,
4    fmt,
5    ops::Deref,
6    os::raw::c_char,
7    str::{from_utf8, from_utf8_unchecked, Utf8Error},
8};
9
10use super::NullTerminatedString;
11
12const fn report_err() -> &'static NullTerminatedStr {
13    const EMPTY_ARR: [&NullTerminatedStr; 0] = [];
14    #[allow(unconditional_panic)]
15    EMPTY_ARR[0]
16}
17
18#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
19#[repr(transparent)]
20pub struct NullTerminatedStr(CStr);
21
22impl NullTerminatedStr {
23    pub const fn as_c_str(&self) -> &CStr {
24        &self.0
25    }
26
27    pub const fn as_ptr(&self) -> *const c_char {
28        self.as_c_str().as_ptr()
29    }
30
31    /// # Safety
32    ///
33    /// The `CStr` (excluding the null byte)
34    /// must be valid utf-8 str.
35    pub const unsafe fn from_cstr_unchecked(cstr: &CStr) -> &Self {
36        // Safety: NullTerminatedStr is transparent
37        // newtype of CStr
38        &*(cstr as *const CStr as *const Self)
39    }
40
41    pub fn from_cstr(cstr: &CStr) -> Result<&Self, Utf8Error> {
42        from_utf8(cstr.to_bytes())?;
43        Ok(unsafe { Self::from_cstr_unchecked(cstr) })
44    }
45
46    /// Return `true` if the str has and only has one null byte
47    /// at the end of the string.
48    const fn is_null_terminated(s: &str) -> bool {
49        let bytes = s.as_bytes();
50
51        if bytes.is_empty() {
52            return false;
53        }
54
55        let mut i = 0;
56        let n = bytes.len() - 1;
57
58        // Check last byte is null byte
59        if bytes[n] != b'\0' {
60            return false;
61        }
62
63        // Ensure there is no internal null byte.
64        while i < n {
65            if bytes[i] == b'\0' {
66                return false;
67            }
68            i += 1;
69        }
70
71        true
72    }
73
74    /// This function creates a `NullTerminatedStr`
75    /// from `s` which must have only one null byte
76    /// at the end of the string.
77    ///
78    /// If not, then this function would panic.
79    pub const fn from_const_str(s: &str) -> &Self {
80        if let Some(null_str) = Self::try_from_str(s) {
81            null_str
82        } else {
83            report_err()
84        }
85    }
86
87    /// This function tries creates a `NullTerminatedStr`
88    /// from `s` which must have only one null byte
89    /// at the end of the string.
90    ///
91    /// If not, then this function would return `None`.
92    ///
93    /// ```rust
94    /// use null_terminated_str::NullTerminatedStr;
95    /// use std::ops::Deref;
96    ///
97    /// // Empty string is rejected
98    /// assert_eq!(
99    ///     NullTerminatedStr::try_from_str(""),
100    ///     None,
101    /// );
102    ///
103    /// // String without null byte is rejected
104    /// assert_eq!(
105    ///     NullTerminatedStr::try_from_str("ha"),
106    ///     None,
107    /// );
108    ///
109    /// // String with internal null byte is rejected
110    /// assert_eq!(
111    ///     NullTerminatedStr::try_from_str("h\0a\0"),
112    ///     None,
113    /// );
114    ///
115    /// // String without trailing null byte is also rejected
116    /// assert_eq!(
117    ///     NullTerminatedStr::try_from_str("h\0a"),
118    ///     None,
119    /// );
120    /// ```
121    pub const fn try_from_str(s: &str) -> Option<&Self> {
122        if Self::is_null_terminated(s) {
123            Some(unsafe {
124                Self::from_cstr_unchecked(CStr::from_bytes_with_nul_unchecked(s.as_bytes()))
125            })
126        } else {
127            None
128        }
129    }
130}
131
132impl Deref for NullTerminatedStr {
133    type Target = str;
134
135    fn deref(&self) -> &Self::Target {
136        unsafe { from_utf8_unchecked(self.0.to_bytes()) }
137    }
138}
139
140impl PartialEq<str> for NullTerminatedStr {
141    fn eq(&self, other: &str) -> bool {
142        self.deref().eq(other)
143    }
144}
145
146impl PartialEq<NullTerminatedStr> for str {
147    fn eq(&self, other: &NullTerminatedStr) -> bool {
148        self.eq(other.deref())
149    }
150}
151
152impl PartialOrd<str> for NullTerminatedStr {
153    fn partial_cmp(&self, other: &str) -> Option<Ordering> {
154        self.deref().partial_cmp(other)
155    }
156}
157
158impl PartialOrd<NullTerminatedStr> for str {
159    fn partial_cmp(&self, other: &NullTerminatedStr) -> Option<Ordering> {
160        self.partial_cmp(other.deref())
161    }
162}
163
164impl fmt::Display for NullTerminatedStr {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        self.deref().fmt(f)
167    }
168}
169
170impl<'a> TryFrom<&'a CStr> for &'a NullTerminatedStr {
171    type Error = Utf8Error;
172
173    fn try_from(cstr: &'a CStr) -> Result<Self, Self::Error> {
174        NullTerminatedStr::from_cstr(cstr)
175    }
176}
177
178impl ToOwned for NullTerminatedStr {
179    type Owned = NullTerminatedString;
180
181    fn to_owned(&self) -> Self::Owned {
182        let cstring = self.as_c_str().to_owned();
183        unsafe { NullTerminatedString::from_cstring_unchecked(cstring) }
184    }
185}
186
187impl<'a> From<&'a NullTerminatedStr> for &'a CStr {
188    fn from(null_str: &'a NullTerminatedStr) -> Self {
189        null_str.as_c_str()
190    }
191}
192
193impl AsRef<NullTerminatedStr> for NullTerminatedStr {
194    fn as_ref(&self) -> &NullTerminatedStr {
195        self
196    }
197}
198
199impl AsRef<str> for NullTerminatedStr {
200    fn as_ref(&self) -> &str {
201        self.deref()
202    }
203}
204
205impl AsRef<CStr> for NullTerminatedStr {
206    fn as_ref(&self) -> &CStr {
207        self.as_c_str()
208    }
209}