Skip to main content

fbl/
string_buffer.rs

1// Copyright 2026 The Fuchsia Authors
2//
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file or at
5// https://opensource.org/licenses/MIT
6
7use core::ffi::{CStr, FromBytesWithNulError};
8use core::ops::{Deref, DerefMut};
9
10/// A fixed-size buffer for assembling a string.
11///
12/// `StringBuffer` is designed to resemble `std::string` except that it
13/// does not allocate heap storage.
14///
15/// # Note on Generic Parameter `N`
16///
17/// In C++, `StringBuffer<M>` has a capacity of `M` characters and stores `M + 1` bytes
18/// (including the null terminator).
19/// In Rust, to avoid unstable features (`generic_const_exprs`), the generic parameter `N`
20/// represents the **total size** of the backing array.
21/// Therefore, a C++ `StringBuffer<M>` corresponds to a Rust `StringBuffer<{M + 1}>`.
22/// The Rust `StringBuffer<N>` can hold up to `N - 1` characters.
23#[repr(C)]
24pub struct StringBuffer<const N: usize> {
25    /// The number of active characters in the buffer.
26    ///
27    /// - `length < N` to leave room for the null terminator.
28    length: usize,
29
30    /// The backing storage for the string.
31    ///
32    /// - `data[length]` is always `0` (null terminator).
33    /// - Elements from `0` to `length - 1` are part of the string.
34    data: [u8; N],
35}
36
37// On 64-bit: usize is 8 bytes. [u8; 8] is 8 bytes. Total 16 bytes. No padding.
38zr::static_assert!(core::mem::size_of::<StringBuffer<8>>() == 16);
39zr::static_assert!(core::mem::align_of::<StringBuffer<8>>() == core::mem::align_of::<usize>());
40
41impl<const N: usize> StringBuffer<N> {
42    const ASSERT_N_POSITIVE: () = assert!(N > 0);
43
44    /// Creates an empty string buffer.
45    pub const fn new() -> Self {
46        let _ = Self::ASSERT_N_POSITIVE;
47
48        let data = [0; N];
49        Self { length: 0, data }
50    }
51
52    /// Creates a string buffer containing exactly one character and a null
53    /// terminator.
54    pub const fn with_char(c: u8) -> Self {
55        assert!(N >= 2, "N must be at least 2 to hold a char and null terminator");
56        let mut data = [0; N];
57        data[0] = c;
58        Self { length: 1, data }
59    }
60
61    /// Returns the capacity of the buffer (max characters it can hold).
62    ///
63    /// The capacity is `N - 1` because we need 1 byte for the null terminator.
64    pub const fn capacity(&self) -> usize {
65        N - 1
66    }
67
68    /// Returns a reference to the contents as a CStr.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if the buffer contains interior null bytes.
73    pub fn as_cstr(&self) -> Result<&CStr, FromBytesWithNulError> {
74        CStr::from_bytes_with_nul(&self.data[..=self.length])
75    }
76
77    /// Clears the string buffer.
78    pub fn clear(&mut self) {
79        self.length = 0;
80        self.data[0] = 0;
81    }
82
83    /// Clears existing data from the buffer and sets the buffer to the new value, plus a null
84    /// terminator.
85    ///
86    /// The `data` does not need to be null terminated. A terminating `0` will always be appended
87    /// to the resulting string.
88    ///
89    /// # Panics
90    ///
91    /// Panics if `data.len() >= N`.
92    pub fn set(&mut self, data: &[u8]) {
93        let len = data.len();
94        assert!(len < N, "The data and a null terminator must fit within the array.");
95        self.data[..len].copy_from_slice(data);
96        self.length = len;
97        self.data[self.length] = 0;
98    }
99
100    /// Resizes the string buffer.
101    ///
102    /// If the current length is less than `count`, additional characters are appended
103    /// with the value `ch`.
104    ///
105    /// If the current length is greater than `count`, the string is truncated.
106    ///
107    /// # Panics
108    ///
109    /// Panics if `count >= N`.
110    pub fn resize(&mut self, count: usize, ch: u8) {
111        assert!(count < N, "Must have room for count bytes an a null terminator within the array.");
112        if self.length < count {
113            self.data[self.length..count].fill(ch);
114        }
115        self.length = count;
116        self.data[self.length] = 0;
117    }
118
119    /// Remove the first `count` characters from the string buffer.
120    ///
121    /// # Panics
122    ///
123    /// Panics if `count > self.len()`.
124    pub fn remove_prefix(&mut self, count: usize) {
125        assert!(count <= self.length, "Cannot remove more than current length");
126        self.length -= count;
127        self.data.copy_within(count..count + self.length, 0);
128        self.data[self.length] = 0;
129    }
130
131    /// Appends a single character.
132    ///
133    /// The result is truncated if the appended content does not fit completely.
134    pub fn append_char(&mut self, ch: u8) -> &mut Self {
135        if self.length < self.capacity() {
136            self.data[self.length] = ch;
137            self.length += 1;
138            self.data[self.length] = 0;
139        }
140        self
141    }
142
143    /// Appends content to the string buffer from a byte slice.
144    ///
145    /// The result is truncated if the appended content does not fit completely.
146    pub fn append(&mut self, data: &[u8]) -> &mut Self {
147        let remaining = self.capacity() - self.length;
148        let len = core::cmp::min(data.len(), remaining);
149        self.data[self.length..self.length + len].copy_from_slice(&data[..len]);
150        self.length += len;
151        self.data[self.length] = 0;
152        self
153    }
154}
155
156impl<const N: usize> Default for StringBuffer<N> {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162impl<const N: usize> core::fmt::Write for StringBuffer<N> {
163    /// Appends to the string buffer from the given string.
164    ///
165    /// The result is truncated if the appended content does not fit completely.
166    fn write_str(&mut self, s: &str) -> core::fmt::Result {
167        self.append(s.as_bytes());
168        Ok(())
169    }
170}
171
172impl<const N: usize> Deref for StringBuffer<N> {
173    type Target = [u8];
174
175    fn deref(&self) -> &Self::Target {
176        &self.data[..self.length]
177    }
178}
179
180impl<const N: usize> DerefMut for StringBuffer<N> {
181    fn deref_mut(&mut self) -> &mut Self::Target {
182        &mut self.data[..self.length]
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use core::fmt::Write;
190
191    #[test]
192    fn test_empty() {
193        let sb: StringBuffer<11> = StringBuffer::new(); // Capacity 10
194        assert_eq!(sb.len(), 0);
195        assert_eq!(sb.capacity(), 10);
196        assert!(sb.is_empty());
197        assert_eq!(sb.data[0], 0);
198    }
199
200    #[test]
201    fn test_with_char() {
202        let sb: StringBuffer<11> = StringBuffer::with_char(b'a');
203        assert_eq!(sb.len(), 1);
204        assert_eq!(&sb[..], b"a");
205    }
206
207    #[test]
208    fn test_append() {
209        let mut sb: StringBuffer<11> = StringBuffer::new();
210        sb.append(b"hello");
211        assert_eq!(sb.len(), 5);
212        assert_eq!(&sb[..], b"hello");
213
214        sb.append(b" world");
215        assert_eq!(sb.len(), 10);
216        assert_eq!(&sb[..], b"hello worl"); // Truncated
217    }
218
219    #[test]
220    fn test_append_char() {
221        let mut sb: StringBuffer<6> = StringBuffer::new(); // Capacity 5
222        sb.append_char(b'a').append_char(b'b');
223        assert_eq!(&sb[..], b"ab");
224    }
225
226    #[test]
227    fn test_clear() {
228        let mut sb: StringBuffer<11> = StringBuffer::new();
229        sb.append(b"hello");
230        sb.clear();
231        assert_eq!(sb.len(), 0);
232        assert!(sb.is_empty());
233    }
234
235    #[test]
236    fn test_set() {
237        let mut sb: StringBuffer<11> = StringBuffer::new();
238        sb.set(b"hello");
239        assert_eq!(&sb[..], b"hello");
240    }
241
242    #[test]
243    fn test_resize() {
244        let mut sb: StringBuffer<11> = StringBuffer::new();
245        sb.append(b"hello");
246        sb.resize(3, b' ');
247        assert_eq!(&sb[..], b"hel");
248
249        sb.resize(6, b'x');
250        assert_eq!(&sb[..], b"helxxx");
251    }
252
253    #[test]
254    fn test_remove_prefix() {
255        let mut sb: StringBuffer<11> = StringBuffer::new();
256        sb.append(b"hello");
257        sb.remove_prefix(2);
258        assert_eq!(&sb[..], b"llo");
259    }
260
261    #[test]
262    fn test_write_macro() {
263        let mut sb: StringBuffer<11> = StringBuffer::new();
264        write!(sb, "test {}", 123).unwrap();
265        assert_eq!(&sb[..], b"test 123");
266
267        write!(sb, "more").unwrap();
268        assert_eq!(&sb[..], b"test 123mo"); // Truncated
269    }
270
271    #[test]
272    fn test_index() {
273        let mut sb: StringBuffer<11> = StringBuffer::new();
274        sb.append(b"hello");
275        assert_eq!(sb[0], b'h');
276        assert_eq!(sb[4], b'o');
277    }
278
279    #[test]
280    fn test_default() {
281        let sb: StringBuffer<11> = Default::default();
282        assert_eq!(sb.len(), 0);
283        assert_eq!(sb.capacity(), 10);
284    }
285
286    #[test]
287    fn test_constructor_zero() {
288        let sb: StringBuffer<1> = StringBuffer::new(); // Capacity 0
289        assert_eq!(sb.len(), 0);
290        assert_eq!(sb.capacity(), 0);
291        assert!(sb.is_empty());
292    }
293
294    #[test]
295    fn test_modify() {
296        let mut sb: StringBuffer<11> = StringBuffer::new();
297        sb.append(b"hello");
298        sb[0] = b'j';
299        assert_eq!(&sb[..], b"jello");
300    }
301
302    #[test]
303    fn test_deref() {
304        let mut sb: StringBuffer<11> = StringBuffer::new();
305        sb.append(b"hello");
306        let slice: &[u8] = &sb;
307        assert_eq!(slice, b"hello");
308
309        let slice_mut: &mut [u8] = &mut sb;
310        slice_mut[0] = b'H';
311        assert_eq!(&sb[..], b"Hello");
312    }
313
314    #[test]
315    fn test_resize_to_max() {
316        let mut sb: StringBuffer<11> = StringBuffer::new();
317        sb.resize(10, b'x');
318        assert_eq!(sb.len(), 10);
319        assert_eq!(&sb[..], b"xxxxxxxxxx");
320        // Verify null terminator is at index 10
321        assert_eq!(sb.data[10], 0);
322    }
323
324    #[test]
325    fn test_as_cstr_success() {
326        let mut sb: StringBuffer<11> = StringBuffer::new();
327        sb.set(b"hello");
328        let cstr = sb.as_cstr().unwrap();
329        assert_eq!(cstr.to_bytes(), b"hello");
330    }
331
332    #[test]
333    fn test_as_cstr_interior_null() {
334        let mut sb: StringBuffer<11> = StringBuffer::new();
335        sb.set(b"a\0b");
336        assert!(sb.as_cstr().is_err());
337    }
338
339    #[test]
340    fn test_append_chaining() {
341        let mut sb: StringBuffer<11> = StringBuffer::new();
342        sb.append(b"hello").append(b" world");
343        assert_eq!(&sb[..], b"hello worl"); // Truncated
344    }
345}