c8str/
lib.rs

1// SPDX-FileCopyrightText: 2024-2025 Maia S Ravn
2// SPDX-License-Identifier: Zlib OR MIT OR Apache-2.0
3
4#![doc = include_str!("../README.md")]
5#![no_std]
6#![cfg_attr(all(doc, not(doctest), feature = "nightly"), feature(doc_auto_cfg))]
7#![deny(missing_docs)]
8
9#[cfg(feature = "alloc")]
10const MAX_STACK_LEN: usize = 4096;
11
12/// Makes a new [`C8Str`] constant from a string literal. This macro adds the null terminator for
13/// you and checks validity at compile time.
14///
15/// ```rust
16/// # use c8str::{c8, C8Str};
17/// const MY_C8: &C8Str = c8!("hello");
18/// assert_eq!(MY_C8, C8Str::from_str_with_nul("hello\0").unwrap());
19/// ```
20///
21/// The macro will fail to compile if the argument contains null bytes:
22///
23/// ```compile_fail
24/// # use c8str::c8;
25/// c8!("doesn't compile\0");
26/// ```
27#[macro_export]
28macro_rules! c8 {
29    ($string:literal) => {{
30        const _: &::core::primitive::str = $string; // type check
31        const S: &$crate::C8Str =
32            $crate::__const_str_with_nul_to_c8_str(::core::concat!($string, "\0"));
33        S
34    }};
35}
36
37/// Make a new [`C8String`] from a string literal. This macro adds the null terminator for
38/// you and checks validity at compile time.
39///
40/// This is equivalent to calling `C8String::from(c8!("literal"))`. See [`c8!`] for more information.
41#[cfg(feature = "alloc")]
42#[macro_export]
43macro_rules! c8string {
44    ($string:literal) => {
45        $crate::C8String::from(c8!($string))
46    };
47}
48
49#[cfg(feature = "alloc")]
50/// Format and create a new [`C8String`]. This will panic if the formatted string
51/// contains any null bytes.
52///
53/// Usage is the same as for [`alloc::format!`].
54#[macro_export]
55macro_rules! c8format {
56    ($($fmt_args:tt)*) => {
57        $crate::C8String::from_string($crate::__reexports::format!($($fmt_args)*)).unwrap()
58    };
59}
60
61use core::{convert::Infallible, error::Error, ffi::CStr};
62
63#[cfg(feature = "alloc")]
64extern crate alloc;
65
66#[cfg(feature = "alloc")]
67use core::mem::MaybeUninit;
68
69#[cfg(feature = "alloc")]
70use alloc::{ffi::CString, string::String};
71
72#[doc(hidden)]
73pub mod __reexports {
74    #[cfg(feature = "alloc")]
75    pub use alloc::format;
76}
77
78mod c8str;
79
80#[doc(inline)]
81pub use c8str::C8Str;
82
83#[cfg(feature = "alloc")]
84mod c8string;
85
86#[cfg(feature = "alloc")]
87#[doc(inline)]
88pub use c8string::{C8String, StringType};
89
90#[cfg(test)]
91mod tests;
92
93use core::{
94    fmt::{self, Debug, Display},
95    num::NonZeroU32,
96    slice, str,
97};
98
99#[doc(hidden)]
100// used in `c8` macro
101pub const fn __const_str_with_nul_to_c8_str(str: &str) -> &C8Str {
102    match C8Str::from_str_with_nul(str) {
103        Ok(str) => str,
104        Err(e) => e.into_const_panic(),
105    }
106}
107
108const fn const_count_nonzero_bytes(bytes: &[u8]) -> usize {
109    let mut i = 0;
110    while i < bytes.len() {
111        if bytes[i] == 0 {
112            return i;
113        }
114        i += 1;
115    }
116    i
117}
118
119const fn const_byte_prefix(bytes: &[u8], len: usize) -> &[u8] {
120    debug_assert!(len <= bytes.len());
121    unsafe { slice::from_raw_parts(bytes.as_ptr(), len) }
122}
123
124const fn const_str_without_null_terminator(str: &str) -> &str {
125    debug_assert!(str.as_bytes()[str.len() - 1] == 0);
126    unsafe { str::from_utf8_unchecked(const_byte_prefix(str.as_bytes(), str.len() - 1)) }
127}
128
129const fn const_nonzero_bytes_with_null_terminator(bytes: &[u8]) -> Option<&[u8]> {
130    let nonzero_len = const_count_nonzero_bytes(bytes);
131    if nonzero_len < bytes.len() {
132        Some(const_byte_prefix(bytes, nonzero_len + 1))
133    } else {
134        None
135    }
136}
137
138const fn const_min(a: usize, b: usize) -> usize {
139    if a < b {
140        a
141    } else {
142        b
143    }
144}
145
146/// Common error type for most of this crate
147#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
148pub struct C8StrError(C8StrErrorEnum);
149
150impl C8StrError {
151    #[inline]
152    const fn not_utf8(at: usize) -> Self {
153        Self(C8StrErrorEnum::NotUtf8(
154            const_min(at, u32::MAX as usize) as u32
155        ))
156    }
157
158    #[inline]
159    const fn inner_zero(at: usize) -> Self {
160        Self(C8StrErrorEnum::InnerZero(
161            const_min(at, u32::MAX as usize) as u32
162        ))
163    }
164
165    #[allow(unused)] // used in test
166    #[inline]
167    const fn inner_zero_unknown() -> Self {
168        Self(C8StrErrorEnum::InnerZero(u32::MAX))
169    }
170
171    #[inline]
172    const fn missing_terminator() -> Self {
173        Self(C8StrErrorEnum::MissingTerminator)
174    }
175
176    /// Convert this error into a const compatible panic
177    #[inline]
178    const fn into_const_panic(self) -> ! {
179        match self.0 {
180            C8StrErrorEnum::NotUtf8(_) => panic!("string isn't valid utf-8"),
181            C8StrErrorEnum::InnerZero(_) => {
182                panic!("string contains null bytes before the end")
183            }
184            C8StrErrorEnum::MissingTerminator => {
185                panic!("string doesn't have a null terminator")
186            }
187        }
188    }
189}
190
191#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
192enum C8StrErrorEnum {
193    NotUtf8(u32),
194    InnerZero(u32),
195    MissingTerminator,
196}
197
198impl Debug for C8StrError {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        write!(f, "C8StrError(\"{self}\")")
201    }
202}
203
204impl Display for C8StrError {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        match &self.0 {
207            C8StrErrorEnum::NotUtf8(i) => {
208                write!(f, "invalid utf-8")?;
209                if *i != u32::MAX {
210                    write!(f, " at {i}")?;
211                }
212                Ok(())
213            }
214            C8StrErrorEnum::InnerZero(i) => {
215                write!(f, "null byte before the end")?;
216                if *i != u32::MAX {
217                    write!(f, " at {i}")?;
218                }
219                Ok(())
220            }
221            C8StrErrorEnum::MissingTerminator => {
222                write!(f, "missing null terminator")
223            }
224        }
225    }
226}
227
228impl Error for C8StrError {}
229
230/// Error returned from `TryFrom<char>::try_from` for `NonZeroChar` if the char was zero
231#[non_exhaustive]
232#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
233pub struct NonZeroCharError;
234
235impl Debug for NonZeroCharError {
236    #[inline]
237    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
238        f.write_str("NonZeroCharError")
239    }
240}
241
242impl Display for NonZeroCharError {
243    #[inline]
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        f.write_str("nonzero char")
246    }
247}
248
249impl Error for NonZeroCharError {}
250
251/// A `char` that is guaranteed to be nonzero
252#[repr(transparent)]
253#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
254pub struct NonZeroChar(NonZeroU32);
255
256impl NonZeroChar {
257    /// Create a new `NonZeroChar` if `ch` is nonzero
258    #[inline]
259    pub const fn new(ch: char) -> Option<Self> {
260        match NonZeroU32::new(ch as u32) {
261            Some(ch) => Some(Self(ch)),
262            None => None,
263        }
264    }
265
266    /// Create a new `NonZeroChar` assuming `ch` is nonzero
267    ///
268    /// # Safety
269    /// The char `ch` must not be equal to `'\0'`
270    #[inline]
271    pub const unsafe fn new_unchecked(ch: char) -> Self {
272        unsafe { Self(NonZeroU32::new_unchecked(ch as u32)) }
273    }
274
275    /// Get this `NonZeroChar` as a `char`
276    #[inline]
277    pub const fn get(self) -> char {
278        unsafe {
279            // Safety: the contained value is always a valid char
280            char::from_u32_unchecked(self.0.get())
281        }
282    }
283}
284
285impl Debug for NonZeroChar {
286    #[inline]
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        write!(f, "NonZeroChar({:?})", self.get())
289    }
290}
291
292impl Display for NonZeroChar {
293    #[inline]
294    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295        <char as Display>::fmt(&self.get(), f)
296    }
297}
298
299impl TryFrom<char> for NonZeroChar {
300    type Error = NonZeroCharError;
301
302    #[inline]
303    fn try_from(value: char) -> Result<Self, Self::Error> {
304        Self::new(value).ok_or(NonZeroCharError)
305    }
306}
307
308impl From<NonZeroChar> for char {
309    #[inline]
310    fn from(value: NonZeroChar) -> Self {
311        value.get()
312    }
313}
314
315/// Access an object as a `&C8Str`
316pub trait WithC8Str: Sized {
317    /// Error type
318    type Error;
319
320    /// Access this object as a `&C8Str`
321    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error>;
322}
323
324impl<'a, T: ?Sized> WithC8Str for &&'a T
325where
326    &'a T: WithC8Str,
327{
328    type Error = <&'a T as WithC8Str>::Error;
329
330    #[inline(always)]
331    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
332        (*self).with_c8_str(f)
333    }
334}
335
336impl<'a, T: ?Sized> WithC8Str for &mut &'a T
337where
338    &'a T: WithC8Str,
339{
340    type Error = <&'a T as WithC8Str>::Error;
341
342    #[inline(always)]
343    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
344        (*self).with_c8_str(f)
345    }
346}
347
348impl<'a, T: ?Sized> WithC8Str for &'a &'a mut T
349where
350    &'a T: WithC8Str,
351{
352    type Error = <&'a T as WithC8Str>::Error;
353
354    #[inline(always)]
355    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
356        (self as &T).with_c8_str(f)
357    }
358}
359
360impl<'a, T: ?Sized> WithC8Str for &'a mut &'a mut T
361where
362    &'a T: WithC8Str,
363{
364    type Error = <&'a T as WithC8Str>::Error;
365
366    #[inline(always)]
367    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
368        (self as &T).with_c8_str(f)
369    }
370}
371
372impl WithC8Str for &C8Str {
373    type Error = Infallible;
374
375    #[inline(always)]
376    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
377        Ok(f(self))
378    }
379}
380
381impl WithC8Str for &mut C8Str {
382    type Error = Infallible;
383
384    #[inline(always)]
385    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
386        Ok(f(self))
387    }
388}
389
390#[cfg(feature = "alloc")]
391impl WithC8Str for C8String {
392    type Error = Infallible;
393
394    #[inline(always)]
395    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
396        Ok(f(&self))
397    }
398}
399
400#[cfg(feature = "alloc")]
401impl WithC8Str for &C8String {
402    type Error = Infallible;
403
404    #[inline(always)]
405    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
406        Ok(f(self))
407    }
408}
409
410#[cfg(feature = "alloc")]
411impl WithC8Str for &mut C8String {
412    type Error = Infallible;
413
414    #[inline(always)]
415    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
416        Ok(f(self))
417    }
418}
419
420impl WithC8Str for &CStr {
421    type Error = C8StrError;
422
423    #[inline(always)]
424    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
425        Ok(f(C8Str::from_c_str(self)?))
426    }
427}
428
429impl WithC8Str for &mut CStr {
430    type Error = C8StrError;
431
432    #[inline(always)]
433    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
434        (&*self).with_c8_str(f)
435    }
436}
437
438#[cfg(feature = "alloc")]
439impl WithC8Str for CString {
440    type Error = C8StrError;
441
442    #[inline(always)]
443    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
444        Ok(f(&C8String::from_c_string(self)?))
445    }
446}
447
448#[cfg(feature = "alloc")]
449impl WithC8Str for &CString {
450    type Error = C8StrError;
451
452    #[inline(always)]
453    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
454        Ok(f(C8Str::from_c_str(self)?))
455    }
456}
457
458#[cfg(feature = "alloc")]
459impl WithC8Str for &mut CString {
460    type Error = C8StrError;
461
462    #[inline(always)]
463    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
464        Ok(f(C8Str::from_c_str(self)?))
465    }
466}
467
468#[cfg(feature = "alloc")]
469impl WithC8Str for &str {
470    type Error = C8StrError;
471
472    #[inline(always)]
473    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
474        if self.len() < MAX_STACK_LEN {
475            let mut buf: [MaybeUninit<u8>; MAX_STACK_LEN] =
476                [const { MaybeUninit::uninit() }; MAX_STACK_LEN];
477            let buf = unsafe {
478                let buf_ptr = buf.as_mut_ptr().cast::<u8>();
479                self.as_bytes()
480                    .as_ptr()
481                    .copy_to_nonoverlapping(buf_ptr, self.len());
482                buf_ptr.add(self.len()).write(0);
483                slice::from_raw_parts(buf_ptr, self.len() + 1)
484            };
485            Ok(f(C8Str::from_bytes_with_nul(buf)?))
486        } else {
487            Ok(f(&C8String::from_string(self)?))
488        }
489    }
490}
491
492#[cfg(feature = "alloc")]
493impl WithC8Str for &mut str {
494    type Error = C8StrError;
495
496    #[inline(always)]
497    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
498        (&*self).with_c8_str(f)
499    }
500}
501
502#[cfg(feature = "alloc")]
503impl WithC8Str for String {
504    type Error = C8StrError;
505
506    #[inline(always)]
507    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
508        Ok(f(&C8String::from_string(self)?))
509    }
510}
511
512#[cfg(feature = "alloc")]
513impl WithC8Str for &String {
514    type Error = C8StrError;
515
516    #[inline(always)]
517    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
518        Ok(f(&C8String::from_string(self.as_str())?))
519    }
520}
521
522#[cfg(feature = "alloc")]
523impl WithC8Str for &mut String {
524    type Error = C8StrError;
525
526    #[inline(always)]
527    fn with_c8_str<R>(self, f: impl FnOnce(&C8Str) -> R) -> Result<R, Self::Error> {
528        Ok(f(&C8String::from_string(self.as_str())?))
529    }
530}