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