Skip to main content

nros_log/
buffer.rs

1//! Call-site formatting buffer for the `nros_*!` macros.
2//!
3//! Wraps a `heapless::String<N>` where `N` is picked at compile time
4//! by the `buffer-size-<N>` Cargo feature family. Overflow truncates
5//! and appends `…` rather than dropping the record.
6
7use core::fmt::{self, Write};
8
9/// Returns the configured capacity in bytes.
10#[must_use]
11pub const fn format_buffer_capacity() -> usize {
12    if cfg!(feature = "buffer-size-1024") {
13        1024
14    } else if cfg!(feature = "buffer-size-512") {
15        512
16    } else if cfg!(feature = "buffer-size-128") {
17        128
18    } else {
19        256
20    }
21}
22
23const CAPACITY: usize = format_buffer_capacity();
24
25/// Stack-resident UTF-8 buffer for one log record's formatted body.
26///
27/// `Write` impl truncates + appends a single `…` (3 bytes) on
28/// overflow; subsequent writes drop silently. The truncated content
29/// is still valid UTF-8 (`…` is 3 bytes; we shrink first to make
30/// room rather than splitting a multi-byte sequence).
31pub struct FormatBuffer {
32    inner: heapless::String<CAPACITY>,
33    truncated: bool,
34}
35
36impl FormatBuffer {
37    /// Empty buffer at the configured capacity.
38    #[must_use]
39    pub fn new() -> Self {
40        Self {
41            inner: heapless::String::new(),
42            truncated: false,
43        }
44    }
45
46    /// Borrow the current contents as `&str`.
47    #[must_use]
48    pub fn as_str(&self) -> &str {
49        self.inner.as_str()
50    }
51
52    /// Whether the formatter saw an overflow on at least one write.
53    #[must_use]
54    pub fn truncated(&self) -> bool {
55        self.truncated
56    }
57}
58
59impl Default for FormatBuffer {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl Write for FormatBuffer {
66    fn write_str(&mut self, s: &str) -> fmt::Result {
67        if self.truncated {
68            return Ok(());
69        }
70        match self.inner.push_str(s) {
71            Ok(()) => Ok(()),
72            Err(()) => {
73                self.truncated = true;
74                // Reserve 3 bytes for the ellipsis. heapless::String
75                // exposes capacity / len; shrink without splitting a
76                // multi-byte UTF-8 boundary.
77                let target_len = CAPACITY.saturating_sub(3);
78                let bytes = self.inner.as_bytes();
79                let mut trunc_at = bytes.len().min(target_len);
80                while trunc_at > 0 && (bytes[trunc_at - 1] & 0b1100_0000) == 0b1000_0000 {
81                    trunc_at -= 1;
82                }
83                self.inner.truncate(trunc_at);
84                let _ = self.inner.push('\u{2026}');
85                Ok(())
86            }
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use core::fmt::Write;
95
96    #[test]
97    fn small_payload_fits() {
98        let mut b = FormatBuffer::new();
99        write!(b, "hello {}", 42).unwrap();
100        assert_eq!(b.as_str(), "hello 42");
101        assert!(!b.truncated());
102    }
103
104    #[test]
105    fn overflow_truncates_and_appends_ellipsis() {
106        let mut b = FormatBuffer::new();
107        let pad = "x".repeat(CAPACITY * 2);
108        write!(b, "{}", pad).unwrap();
109        assert!(b.truncated());
110        assert!(b.as_str().ends_with('\u{2026}'));
111        assert!(b.as_str().len() <= CAPACITY);
112    }
113
114    #[test]
115    fn capacity_matches_feature_default() {
116        // Default feature set selects 256.
117        assert_eq!(format_buffer_capacity(), 256);
118    }
119}