fmtbuf/
lib.rs

1//! # `fmtbuf`
2//! This library is intended to help write formatted text to fixed buffers.
3//!
4//! ```
5//! use fmtbuf::WriteBuf;
6//! use std::fmt::Write;
7//!
8//! let mut buf: [u8; 10] = [0; 10];
9//! let mut writer = WriteBuf::new(&mut buf);
10//! if let Err(e) = write!(&mut writer, "πŸš€πŸš€πŸš€") {
11//!     println!("write error: {e:?}");
12//! }
13//! let written_len = match writer.finish_with_or("!", "…") {
14//!     Ok(len) => len, // <- won't be hit since πŸš€πŸš€πŸš€ is 12 bytes
15//!     Err(len) => {
16//!         println!("writing was truncated");
17//!         len
18//!     }
19//! };
20//! let written = &buf[..written_len];
21//! assert_eq!("πŸš€β€¦", std::str::from_utf8(written).unwrap());
22//! ```
23//!
24//! A few things happened in that example:
25//!
26//! 1. We stared with a 10 byte buffer
27//! 2. Tried to write `"πŸš€πŸš€πŸš€"` to it, which is encoded as 3 `b"\xf0\x9f\x9a\x80"`s (12 bytes)
28//! 3. This can't fit into 10 bytes, so only `"πŸš€πŸš€"` is stored and the `writer` is noted as having truncated writes
29//! 4. We finish the buffer with `"!"` on success or `"…"` (a.k.a. `b"\xe2\x80\xa6"`) on truncation
30//! 5. Since we noted truncation in step #3, we try to write `"…"`, but this can not fit into the buffer either, since
31//!    8 (`"πŸš€πŸš€".len()`) + 3 (`"…".len()`) > 12 (`buf.len()`)
32//! 6. Roll the buffer back to the end of the first πŸš€, then add …, leaving us with `"πŸš€β€¦"`
33
34#![cfg_attr(not(feature = "std"), no_std)]
35
36mod utf8;
37
38use core::fmt;
39
40#[deprecated]
41pub use utf8::rfind_utf8_end;
42
43/// A write buffer pointing to a `&mut [u8]`.
44///
45/// ```
46/// use fmtbuf::WriteBuf;
47/// use std::fmt::Write;
48///
49/// // The buffer to write into. The contents can be uninitialized, but using a
50/// // bogus `\xff` sigil for demonstration.
51/// let mut buf: [u8; 128] = [0xff; 128];
52/// let mut writer = WriteBuf::new(&mut buf);
53///
54/// // Write data to the buffer.
55/// write!(writer, "some data: {}", 0x01a4).unwrap();
56///
57/// // Finish writing:
58/// let write_len = writer.finish().unwrap();
59/// let written = std::str::from_utf8(&buf[..write_len]).unwrap();
60/// assert_eq!(written, "some data: 420");
61/// ```
62pub struct WriteBuf<'a> {
63    target: &'a mut [u8],
64    position: usize,
65    reserve: usize,
66    truncated: bool,
67}
68
69impl<'a> WriteBuf<'a> {
70    /// Create an instance that will write to the given `target`. The contents of the target do not need to have been
71    /// initialized before this, as they will be overwritten by writing.
72    pub fn new(target: &'a mut [u8]) -> Self {
73        Self {
74            target,
75            position: 0,
76            reserve: 0,
77            truncated: false,
78        }
79    }
80
81    /// Create an instance that will write to the given `target` and `reserve` bytes at the end that will not be written
82    /// be `write_str` operations.
83    ///
84    /// The use of this constructor is to note that `reserve` bytes will always be written at the end of the buffer (by
85    /// the [`WriteBuf::finish_with`] family of functions), so `write_str` should not bother writing to it. This is
86    /// useful when you know that you will always `finish_with` a null terminator or other character.
87    ///
88    /// It is allowed to have `target.len() < reserve`, but this can never be written to.
89    pub fn with_reserve(target: &'a mut [u8], reserve: usize) -> Self {
90        Self {
91            target,
92            position: 0,
93            reserve,
94            truncated: false,
95        }
96    }
97
98    /// Get the position in the target buffer. The value is one past the end of written content and the next position to
99    /// be written to.
100    pub fn position(&self) -> usize {
101        self.position
102    }
103
104    /// Get if a truncated write has happened.
105    pub fn truncated(&self) -> bool {
106        self.truncated
107    }
108
109    /// Get the count of reserved bytes.
110    pub fn reserve(&self) -> usize {
111        self.reserve
112    }
113
114    /// Set the reserve bytes to `count`. If the written section has already encroached on the reserve space, this has
115    /// no immediate effect, but it will prevent future writes. If [`WriteBuf::truncated`] has already been triggered,
116    /// it will not be reset.
117    pub fn set_reserve(&mut self, count: usize) {
118        self.reserve = count;
119    }
120
121    /// Get the contents that have been written so far.
122    pub fn written_bytes(&self) -> &[u8] {
123        &self.target[..self.position]
124    }
125
126    /// Get the contents that have been written so far.
127    pub fn written(&self) -> &str {
128        #[cfg(debug_assertions)]
129        return core::str::from_utf8(self.written_bytes()).expect("contents of buffer should have been UTF-8 encoded");
130
131        // safety: The only way to write into the buffer is with valid UTF-8, so there is no reason to check the
132        // contents for validity. They're still checked in debug builds just in case, though.
133        #[cfg(not(debug_assertions))]
134        unsafe {
135            core::str::from_utf8_unchecked(self.written_bytes())
136        }
137    }
138
139    /// Finish writing to the buffer. This returns control of the target buffer to the caller (it is no longer mutably
140    /// borrowed) and returns the number of bytes written.
141    ///
142    /// # Returns
143    ///
144    /// In both the `Ok` and `Err` cases, the [`WriteBuf::position`] is returned. The `Ok` case indicates the truncation
145    /// did not occur, while `Err` indicates that it did.
146    pub fn finish(self) -> Result<usize, usize> {
147        if self.truncated() {
148            Err(self.position())
149        } else {
150            Ok(self.position())
151        }
152    }
153
154    /// Finish the buffer, adding the `suffix` to the end. A common use case for this is to add a null terminator.
155    ///
156    /// This operates slightly differently than the normal format writing function `write_str` in that the `suffix` is
157    /// always put at the end. The only case where this will not happen is when `suffix.len()` is less than the size of
158    /// the buffer originally provided. In this case, the last bit of `suffix` will be copied (starting at a valid UTF-8
159    /// sequence start; e.g.: writing `"πŸš€..."` to a 5 byte buffer will leave you with just `"..."`, no matter what was
160    /// written before).
161    ///
162    /// ```
163    /// use fmtbuf::WriteBuf;
164    ///
165    /// let mut buf: [u8; 4] = [0xff; 4];
166    /// let mut writer = WriteBuf::new(&mut buf);
167    ///
168    /// // Finish writing with too many bytes:
169    /// let write_len = writer.finish_with("12345").unwrap_err();
170    /// assert_eq!(write_len, 4);
171    /// let buf_str = std::str::from_utf8(&buf).unwrap();
172    /// assert_eq!(buf_str, "2345");
173    /// ```
174    ///
175    /// # Returns
176    ///
177    /// The returned value has the same meaning as [`WriteBuf::finish`].
178    pub fn finish_with(self, suffix: impl AsRef<[u8]>) -> Result<usize, usize> {
179        let suffix = suffix.as_ref();
180        self._finish_with(suffix, suffix)
181    }
182
183    /// Finish the buffer by adding `normal_suffix` if not truncated or `truncated_suffix` if the buffer will be
184    /// truncated. This operates the same as [`WriteBuf::finish_with`] in every other way.
185    pub fn finish_with_or(
186        self,
187        normal_suffix: impl AsRef<[u8]>,
188        truncated_suffix: impl AsRef<[u8]>,
189    ) -> Result<usize, usize> {
190        self._finish_with(normal_suffix.as_ref(), truncated_suffix.as_ref())
191    }
192
193    fn _finish_with(mut self, normal: &[u8], truncated: &[u8]) -> Result<usize, usize> {
194        let remaining = self.target.len() - self.position();
195
196        // If the truncated case is shorter than the normal case, then writing it might still work
197        for (suffix, should_test) in [(normal, !self.truncated), (truncated, true)] {
198            if !should_test {
199                continue;
200            }
201
202            // enough room in the buffer to write entire suffix, so just write it
203            if suffix.len() <= remaining {
204                self.target[self.position..self.position + suffix.len()].copy_from_slice(suffix);
205                self.position += suffix.len();
206                return if self.truncated() {
207                    Err(self.position())
208                } else {
209                    Ok(self.position())
210                };
211            }
212
213            // we attempted to perform a write, but rejected it
214            self.truncated = true;
215        }
216
217        let suffix = truncated;
218
219        // if the suffix is larger than the entire target buffer, copy the last N
220        if self.target.len() < suffix.len() {
221            let copyable_suffix = &suffix[suffix.len() - self.target.len()..];
222            let Some(valid_utf8_idx) = copyable_suffix
223                .iter()
224                .enumerate()
225                .find(|(_, cu)| utf8::utf8_char_width(**cu).is_some())
226                .map(|(idx, _)| idx)
227            else {
228                return Err(0);
229            };
230            let copyable_suffix = &copyable_suffix[valid_utf8_idx..];
231            self.target[..copyable_suffix.len()].copy_from_slice(copyable_suffix);
232            return Err(copyable_suffix.len());
233        }
234
235        // Scan backwards to find the position we should write to (can't interrupt a UTF-8 multibyte sequence)
236        let potential_end_idx = self.target.len() - suffix.len();
237        let write_idx = rfind_utf8_end(&self.target[..potential_end_idx]);
238        self.target[write_idx..write_idx + suffix.len()].copy_from_slice(suffix);
239        Err(write_idx + suffix.len())
240    }
241
242    fn _write(&mut self, input: &[u8]) -> fmt::Result {
243        if self.truncated() {
244            return Err(fmt::Error);
245        }
246
247        let remaining = self.target.len() - self.position();
248        if remaining < self.reserve() {
249            self.truncated = true;
250            return Err(fmt::Error);
251        }
252        let remaining = remaining - self.reserve();
253
254        let (input, result) = if remaining >= input.len() {
255            (input, Ok(()))
256        } else {
257            let to_write = &input[..remaining];
258            self.truncated = true;
259            (&input[..rfind_utf8_end(to_write)], Err(fmt::Error))
260        };
261
262        self.target[self.position..self.position + input.len()].copy_from_slice(input);
263        self.position += input.len();
264
265        result
266    }
267}
268
269impl<'a> fmt::Write for WriteBuf<'a> {
270    /// Append `s` to the target buffer.
271    ///
272    /// # Error
273    ///
274    /// An error is returned if the entirety of `s` can not fit in the target buffer or if a previous `write_str`
275    /// operation failed. If this occurs, as much as `s` that can fit into the buffer will be written up to the last
276    /// valid Unicode code point. In other words, if the target buffer have 6 writable bytes left and `s` is the two
277    /// code points `"β™‘πŸΆ"` (a.k.a.: the 7 byte `b"\xe2\x99\xa1\xf0\x9f\x90\xb6"`), then only `β™‘` will make it to the
278    /// output buffer, making the target of your β™‘ ambiguous.
279    ///
280    /// Truncation marks this buffer as truncated, which can be observed with [`WriteBuf::truncated`]. Future write
281    /// attempts will immediately return in `Err`. This also affects the behavior of [`WriteBuf::finish`] family of
282    /// functions, which will always return the `Err` case to indicate truncation. For [`WriteBuf::finish_with_or`],
283    /// the `normal_suffix` will not be attempted.
284    fn write_str(&mut self, s: &str) -> fmt::Result {
285        self._write(s.as_bytes())
286    }
287}
288
289#[cfg(test)]
290mod test {
291    use super::*;
292    use core::fmt::Write;
293
294    /// * `.0`: Input string
295    /// * `.1`: The end position if the last byte was chopped off
296    static TEST_CASES: &[(&str, usize)] = &[
297        ("", 0),
298        ("James", 4),
299        ("_ΓΈ", 1),
300        ("磨", 0),
301        ("here: 见/見", 10),
302        ("π¨‰Ÿε‘γ—‚θΆŠ", 10),
303        ("πŸš€", 0),
304        ("πŸš€πŸš€πŸš€", 8),
305        ("rocket: πŸš€", 8),
306    ];
307
308    #[test]
309    fn rfind_utf8_end_test() {
310        for (input, last_valid_idx_after_cut) in TEST_CASES.iter() {
311            let result = rfind_utf8_end(input.as_bytes());
312            assert_eq!(result, input.len(), "input=\"{input}\"");
313            if input.len() == 0 {
314                continue;
315            }
316            let input_truncated = &input.as_bytes()[..input.len() - 1];
317            let result = rfind_utf8_end(input_truncated);
318            assert_eq!(
319                result, *last_valid_idx_after_cut,
320                "input=\"{input}\" truncated={input_truncated:?}"
321            );
322        }
323    }
324
325    #[test]
326    fn format_enough_space() {
327        for (input, _) in TEST_CASES.iter() {
328            let mut buf: [u8; 128] = [0xff; 128];
329            let mut writer = WriteBuf::new(&mut buf);
330
331            writer.write_str(input).unwrap();
332            assert_eq!(input.len(), writer.position());
333            let last_idx = writer.finish().unwrap();
334            assert_eq!(input.len(), last_idx);
335        }
336    }
337
338    #[test]
339    fn format_enough_space_just_enough_reserved() {
340        for (input, _) in TEST_CASES.iter() {
341            let mut buf: [u8; 128] = [0xff; 128];
342            let mut writer = WriteBuf::with_reserve(&mut buf[..input.len() + 1], 1);
343
344            writer.write_str(input).unwrap();
345            assert_eq!(input.len(), writer.position());
346            let last_idx = writer.finish().unwrap();
347            assert_eq!(input.len(), last_idx);
348        }
349    }
350
351    #[test]
352    fn format_truncation() {
353        for (input, last_valid_idx_after_cut) in TEST_CASES.iter() {
354            if input.len() == 0 {
355                continue;
356            }
357
358            let mut buf: [u8; 128] = [0xff; 128];
359            let mut writer = WriteBuf::new(&mut buf[..input.len() - 1]);
360
361            writer.write_str(input).unwrap_err();
362            assert_eq!(*last_valid_idx_after_cut, writer.position());
363            assert!(writer.truncated());
364            write!(writer, "!!!").expect_err("writes should fail here");
365
366            let last_idx = writer.finish().unwrap_err();
367            assert_eq!(*last_valid_idx_after_cut, last_idx);
368        }
369    }
370
371    struct SimpleString {
372        storage: [u8; 128],
373        size: usize,
374    }
375
376    impl SimpleString {
377        pub fn from_segments(segments: &[&str]) -> Self {
378            let mut out = Self {
379                storage: [0; 128],
380                size: 0,
381            };
382            for segment in segments {
383                out.append(segment);
384            }
385            out
386        }
387
388        pub fn append(&mut self, value: &str) {
389            let value = value.as_bytes();
390            self.storage[self.size..self.size + value.len()].copy_from_slice(value);
391            self.size += value.len();
392        }
393
394        pub fn as_str(&self) -> &str {
395            core::str::from_utf8(&self.storage[..self.size]).unwrap()
396        }
397    }
398
399    impl From<&str> for SimpleString {
400        fn from(value: &str) -> Self {
401            Self::from_segments(&[value])
402        }
403    }
404
405    #[test]
406    fn finish_with_enough_space() {
407        for (input, _) in TEST_CASES.iter() {
408            let mut buf: [u8; 128] = [0xff; 128];
409            let mut writer = WriteBuf::new(&mut buf);
410
411            writer.write_str(input).unwrap();
412            let position = writer.finish_with(b".123").unwrap();
413            assert_eq!(position, input.len() + 4);
414            let expected_written = SimpleString::from_segments(&[input, ".123"]);
415            let actually_wriiten = core::str::from_utf8(&buf[..position]).unwrap();
416            assert_eq!(expected_written.as_str(), actually_wriiten);
417        }
418    }
419
420    #[test]
421    fn finish_with_overwrite() {
422        for (input, last_valid_idx_after_cut) in TEST_CASES.iter() {
423            if input.len() == 0 {
424                continue;
425            }
426
427            let mut buf: [u8; 128] = [0xff; 128];
428            let mut writer = WriteBuf::new(&mut buf[..input.len()]);
429
430            writer.write_str(input).unwrap();
431            let position = writer.finish_with("?").unwrap_err();
432            assert_eq!(position, last_valid_idx_after_cut + 1);
433            let expected_written = SimpleString::from_segments(&[
434                core::str::from_utf8(&input.as_bytes()[..*last_valid_idx_after_cut]).unwrap(),
435                "?",
436            ]);
437            let actually_wriiten = core::str::from_utf8(&buf[..position]).unwrap();
438            assert_eq!(expected_written.as_str(), actually_wriiten);
439        }
440    }
441
442    #[test]
443    fn finish_with_or_with_longer_normal_closer() {
444        let mut buf: [u8; 4] = [0xff; 4];
445        let writer = WriteBuf::new(&mut buf);
446
447        let written = writer.finish_with_or("0123456789", "abc").unwrap_err();
448        assert_eq!(written, 3);
449        assert_eq!("abc", core::str::from_utf8(&buf[..written]).unwrap());
450    }
451
452    #[test]
453    fn finish_with_full_overwrite_utf8() {
454        let mut buf: [u8; 4] = [0xff; 4];
455        let writer = WriteBuf::new(&mut buf);
456
457        let written = writer.finish_with("πŸš€12").unwrap_err();
458        assert_eq!(written, 2);
459        assert_eq!("12", core::str::from_utf8(&buf[..written]).unwrap());
460    }
461
462    #[test]
463    fn set_reserve_should_not_change_written() {
464        let mut buf: [u8; 10] = [0xff; 10];
465        let mut writer = WriteBuf::new(&mut buf);
466
467        write!(writer, "0123456789").unwrap();
468        assert_eq!("0123456789", writer.written());
469
470        writer.set_reserve(4);
471        assert_eq!("0123456789", writer.written());
472
473        writer.finish_with_or("", "!").unwrap();
474        assert_eq!("0123456789", core::str::from_utf8(&buf).unwrap());
475    }
476}
477
478#[cfg(doctest)]
479mod test_readme {
480    macro_rules! external_doc_test {
481        ($x:expr) => {
482            #[doc = $x]
483            extern "C" {}
484        };
485    }
486
487    external_doc_test!(include_str!("../README.md"));
488}