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