Skip to main content

cfixed_string/
lib.rs

1use std::borrow::{Borrow, Cow};
2use std::ffi::{CStr, CString};
3use std::mem::MaybeUninit;
4use std::os::raw::c_char;
5use std::ptr;
6use std::{fmt, ops};
7
8const STRING_SIZE: usize = 512;
9
10/// This is a C String abstractions that presents a CStr like
11/// interface for interop purposes but tries to be little nicer
12/// by avoiding heap allocations if the string is within the
13/// generous bounds (512 bytes) of the statically sized buffer.
14/// Strings over this limit will be heap allocated, but the
15/// interface outside of this abstraction remains the same.
16#[allow(clippy::large_enum_variant)]
17pub enum CFixedString {
18    Local {
19        s: [c_char; STRING_SIZE],
20        len: usize,
21    },
22    Heap {
23        s: CString,
24        len: usize,
25    },
26}
27
28impl Default for CFixedString {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl CFixedString {
35    /// Creates an empty CFixedString, this is intended to be
36    /// used with write! or the `fmt::Write` trait
37    pub fn new() -> Self {
38        let data: [MaybeUninit<c_char>; STRING_SIZE] =
39            unsafe { MaybeUninit::uninit().assume_init() };
40
41        CFixedString::Local {
42            s: unsafe { std::mem::transmute::<[MaybeUninit<c_char>; STRING_SIZE], [c_char; STRING_SIZE]>(data) },
43            len: 0,
44        }
45    }
46
47    /// Create from str
48    #[allow(clippy::should_implement_trait)]
49    pub fn from_str<S: AsRef<str>>(s: S) -> Self {
50        Self::from(s.as_ref())
51    }
52
53    /// Returns the pointer to be passed down to the C code
54    pub fn as_ptr(&self) -> *const c_char {
55        match *self {
56            CFixedString::Local { ref s, .. } => s.as_ptr(),
57            CFixedString::Heap { ref s, .. } => s.as_ptr(),
58        }
59    }
60
61    /// Returns true if the string has been heap allocated
62    pub fn is_allocated(&self) -> bool {
63        !matches!(*self, CFixedString::Local { .. })
64    }
65
66    /// Converts a `CFixedString` into a `Cow<str>`.
67    ///
68    /// This function will calculate the length of this string (which normally
69    /// requires a linear amount of work to be done) and then return the
70    /// resulting slice as a `Cow<str>`, replacing any invalid UTF-8 sequences
71    /// with `U+FFFD REPLACEMENT CHARACTER`. If there are no invalid UTF-8
72    /// sequences, this will merely return a borrowed slice.
73    pub fn to_string(&self) -> Cow<'_, str> {
74        String::from_utf8_lossy(self.to_bytes())
75    }
76
77    /// Convert back to str. Unsafe as it uses `from_utf8_unchecked`
78    ///
79    /// # Safety
80    ///
81    /// The caller must ensure that the string contains valid UTF-8 data.
82    /// This is typically the case if the string was created from a valid
83    /// Rust `&str` via `from_str` or `From<&str>`.
84    pub unsafe fn as_str(&self) -> &str {
85        use std::slice;
86        use std::str;
87
88        match *self {
89            CFixedString::Local { ref s, len } => {
90                str::from_utf8_unchecked(slice::from_raw_parts(s.as_ptr() as *const u8, len))
91            }
92            CFixedString::Heap { ref s, len } => {
93                str::from_utf8_unchecked(slice::from_raw_parts(s.as_ptr() as *const u8, len))
94            }
95        }
96    }
97}
98
99impl<'a> From<&'a str> for CFixedString {
100    fn from(s: &'a str) -> Self {
101        use std::fmt::Write;
102
103        let mut string = CFixedString::new();
104        string.write_str(s).unwrap();
105        string
106    }
107}
108
109impl fmt::Write for CFixedString {
110    fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
111        unsafe {
112            let cur_len = self.as_str().len();
113
114            match cur_len + s.len() {
115                len if len < STRING_SIZE => match *self {
116                    CFixedString::Local {
117                        s: ref mut ls,
118                        len: ref mut lslen,
119                    } => {
120                        let ptr = ls.as_mut_ptr() as *mut u8;
121                        ptr::copy(s.as_ptr(), ptr.add(cur_len), s.len());
122                        *ptr.add(len) = 0;
123                        *lslen = len;
124                    }
125                    _ => unreachable!(),
126                },
127                len => {
128                    let mut heapstring = String::with_capacity(len + 1);
129
130                    heapstring.write_str(self.as_str())?;
131                    heapstring.write_str(s)?;
132
133                    *self = CFixedString::Heap {
134                        s: CString::new(heapstring).unwrap(),
135                        len,
136                    };
137                }
138            }
139        }
140
141        Ok(())
142    }
143}
144
145impl From<CFixedString> for String {
146    fn from(s: CFixedString) -> Self {
147        String::from_utf8_lossy(s.to_bytes()).into_owned()
148    }
149}
150
151impl ops::Deref for CFixedString {
152    type Target = CStr;
153
154    fn deref(&self) -> &CStr {
155        use std::slice;
156
157        match *self {
158            CFixedString::Local { ref s, len } => unsafe {
159                let bytes = slice::from_raw_parts(s.as_ptr() as *const u8, len + 1);
160                CStr::from_bytes_with_nul_unchecked(bytes)
161            },
162            CFixedString::Heap { ref s, .. } => s,
163        }
164    }
165}
166
167impl Borrow<CStr> for CFixedString {
168    fn borrow(&self) -> &CStr {
169        self
170    }
171}
172
173impl AsRef<CStr> for CFixedString {
174    fn as_ref(&self) -> &CStr {
175        self
176    }
177}
178
179impl Borrow<str> for CFixedString {
180    fn borrow(&self) -> &str {
181        unsafe { self.as_str() }
182    }
183}
184
185impl AsRef<str> for CFixedString {
186    fn as_ref(&self) -> &str {
187        unsafe { self.as_str() }
188    }
189}
190
191#[macro_export]
192macro_rules! format_c {
193    ($fmt:expr, $($args:tt)*) => ({
194        use std::fmt::Write;
195
196        let mut fixed = CFixedString::new();
197        write!(&mut fixed, $fmt, $($args)*).unwrap();
198        fixed
199    })
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use std::fmt::Write;
206
207    fn gen_string(len: usize) -> String {
208        let mut out = String::with_capacity(len);
209
210        for _ in 0..len / 16 {
211            out.write_str("zyxvutabcdef9876").unwrap();
212        }
213
214        for i in 0..len % 16 {
215            out.write_char(char::from(b'A' + i as u8)).unwrap();
216        }
217
218        assert_eq!(out.len(), len);
219        out
220    }
221
222    #[test]
223    fn test_empty_handler() {
224        let short_string = "";
225
226        let t = CFixedString::from_str(short_string);
227
228        assert!(!t.is_allocated());
229        assert_eq!(&t.to_string(), short_string);
230    }
231
232    #[test]
233    fn test_short_1() {
234        let short_string = "test_local";
235
236        let t = CFixedString::from_str(short_string);
237
238        assert!(!t.is_allocated());
239        assert_eq!(&t.to_string(), short_string);
240    }
241
242    #[test]
243    fn test_short_2() {
244        let short_string = "test_local stoheusthsotheost";
245
246        let t = CFixedString::from_str(short_string);
247
248        assert!(!t.is_allocated());
249        assert_eq!(&t.to_string(), short_string);
250    }
251
252    #[test]
253    fn test_511() {
254        // this string (width 511) buffer should just fit
255        let test_511_string = gen_string(511);
256
257        let t = CFixedString::from_str(&test_511_string);
258
259        assert!(!t.is_allocated());
260        assert_eq!(&t.to_string(), &test_511_string);
261    }
262
263    #[test]
264    fn test_512() {
265        // this string (width 512) buffer should not fit
266        let test_512_string = gen_string(512);
267
268        let t = CFixedString::from_str(&test_512_string);
269
270        assert!(t.is_allocated());
271        assert_eq!(&t.to_string(), &test_512_string);
272    }
273
274    #[test]
275    fn test_513() {
276        // this string (width 513) buffer should not fit
277        let test_513_string = gen_string(513);
278
279        let t = CFixedString::from_str(&test_513_string);
280
281        assert!(t.is_allocated());
282        assert_eq!(&t.to_string(), &test_513_string);
283    }
284
285    #[test]
286    fn test_to_owned() {
287        let short = "this is an amazing string";
288
289        let t = CFixedString::from_str(short);
290
291        assert!(!t.is_allocated());
292        assert_eq!(&String::from(t), short);
293
294        let long = gen_string(1025);
295
296        let t = CFixedString::from_str(&long);
297
298        assert!(t.is_allocated());
299        assert_eq!(&String::from(t), &long);
300    }
301
302    #[test]
303    fn test_short_format() {
304        let mut fixed = CFixedString::new();
305
306        write!(&mut fixed, "one_{}", 1).unwrap();
307        write!(&mut fixed, "_two_{}", "two").unwrap();
308        write!(
309            &mut fixed,
310            "_three_{}-{}-{:.3}",
311            23, "some string data", 56.789
312        )
313        .unwrap();
314
315        assert!(!fixed.is_allocated());
316        assert_eq!(
317            &fixed.to_string(),
318            "one_1_two_two_three_23-some string data-56.789"
319        );
320    }
321
322    #[test]
323    fn test_long_format() {
324        let mut fixed = CFixedString::new();
325        let mut string = String::new();
326
327        for i in 1..30 {
328            let genned = gen_string(i * i);
329
330            write!(&mut fixed, "{}_{}", i, genned).unwrap();
331            write!(&mut string, "{}_{}", i, genned).unwrap();
332        }
333
334        assert!(fixed.is_allocated());
335        assert_eq!(&fixed.to_string(), &string);
336    }
337
338    #[test]
339    fn test_short_fmt_macro() {
340        let first = 23;
341        let second = "#@!*()&^%_-+={}[]|\\/?><,.:;~`";
342        let third = u32::MAX;
343        let fourth = gen_string(512 - 45);
344
345        let fixed = format_c!("{}_{}_0x{:x}_{}", first, second, third, fourth);
346        let heaped = format!("{}_{}_0x{:x}_{}", first, second, third, fourth);
347
348        assert!(!fixed.is_allocated());
349        assert_eq!(&fixed.to_string(), &heaped);
350    }
351
352    #[test]
353    fn test_long_fmt_macro() {
354        let first = "";
355        let second = gen_string(510);
356        let third = 3;
357        let fourth = gen_string(513 * 8);
358
359        let fixed = format_c!("{}_{}{}{}", first, second, third, fourth);
360        let heaped = format!("{}_{}{}{}", first, second, third, fourth);
361
362        assert!(fixed.is_allocated());
363        assert_eq!(&fixed.to_string(), &heaped);
364    }
365}