Skip to main content

freeswitch_log_parser/
attached.rs

1//! Compact contiguous storage for the raw continuation lines that follow a
2//! primary log entry.
3//!
4//! Lines are stored end-to-end in `buf` separated by `\n`. The separator is
5//! never exposed to callers — [`AttachedLines::iter`] and
6//! [`AttachedLines::get`] return `&str` slices that exclude it.
7
8/// One entry's attached lines outgrew the `u32` offsets addressing them.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct AttachedOverflow;
11
12impl std::fmt::Display for AttachedOverflow {
13    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14        f.write_str("attached lines exceed the 4 GiB an entry can address")
15    }
16}
17
18impl std::error::Error for AttachedOverflow {}
19
20/// Compact storage for the raw continuation lines of a log entry.
21///
22/// Iteration yields each pushed line as `&str` in insertion order. The
23/// type is API-equivalent to a read-only `[String]` for the patterns used
24/// in this crate (`len`, `is_empty`, `iter`, `get`, indexed access via
25/// `get(i)`).
26#[derive(Debug, Default, Clone, PartialEq, Eq)]
27pub struct AttachedLines {
28    buf: String,
29    offsets: Vec<u32>,
30}
31
32impl AttachedLines {
33    /// Create an empty `AttachedLines` with no allocations.
34    pub fn new() -> Self {
35        Self::default()
36    }
37
38    /// Number of stored lines.
39    pub fn len(&self) -> usize {
40        self.offsets.len()
41    }
42
43    /// `true` when no lines have been pushed.
44    pub fn is_empty(&self) -> bool {
45        self.offsets.is_empty()
46    }
47
48    /// Bytes the stored lines occupy, separators included.
49    pub fn byte_len(&self) -> usize {
50        self.buf.len()
51    }
52
53    /// Append a line. The trailing `\n` separator is added internally and is
54    /// not part of the line returned by [`Self::get`] or [`Self::iter`].
55    ///
56    /// Fails once the buffer would outgrow the `u32` offsets that address it.
57    /// `mod_logfile`'s write budget bounds one physical line, not how many join
58    /// an entry, so that range is the only limit here — a caller wanting a
59    /// smaller one sets it on the stream
60    /// ([`LogStream::max_attached_bytes`](crate::LogStream::max_attached_bytes)).
61    /// A line that does not fit is not stored.
62    pub fn push(&mut self, line: &str) -> Result<(), AttachedOverflow> {
63        let start = u32::try_from(self.buf.len()).map_err(|_| AttachedOverflow)?;
64        self.offsets.push(start);
65        self.buf.push_str(line);
66        self.buf.push('\n');
67        Ok(())
68    }
69
70    /// Borrow the i-th line, or `None` if out of range.
71    pub fn get(&self, i: usize) -> Option<&str> {
72        let start = *self.offsets.get(i)? as usize;
73        let end = self
74            .offsets
75            .get(i + 1)
76            .map(|&o| o as usize - 1)
77            .unwrap_or_else(|| self.buf.len() - 1);
78        Some(&self.buf[start..end])
79    }
80
81    /// Iterate over the stored lines in insertion order.
82    pub fn iter(&self) -> AttachedLinesIter<'_> {
83        AttachedLinesIter {
84            lines: self,
85            pos: 0,
86        }
87    }
88}
89
90impl<'a> IntoIterator for &'a AttachedLines {
91    type Item = &'a str;
92    type IntoIter = AttachedLinesIter<'a>;
93
94    fn into_iter(self) -> Self::IntoIter {
95        self.iter()
96    }
97}
98
99/// Iterator over the lines of an [`AttachedLines`].
100#[derive(Debug, Clone)]
101pub struct AttachedLinesIter<'a> {
102    lines: &'a AttachedLines,
103    pos: usize,
104}
105
106impl<'a> Iterator for AttachedLinesIter<'a> {
107    type Item = &'a str;
108
109    fn next(&mut self) -> Option<&'a str> {
110        let line = self.lines.get(self.pos)?;
111        self.pos += 1;
112        Some(line)
113    }
114
115    fn size_hint(&self) -> (usize, Option<usize>) {
116        let remaining = self.lines.len() - self.pos;
117        (remaining, Some(remaining))
118    }
119}
120
121impl ExactSizeIterator for AttachedLinesIter<'_> {}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn empty_default() {
129        let a = AttachedLines::new();
130        assert_eq!(a.len(), 0);
131        assert!(a.is_empty());
132        assert!(a.get(0).is_none());
133        assert_eq!(a.iter().count(), 0);
134    }
135
136    #[test]
137    fn push_and_iterate_preserves_order_and_content() {
138        let mut a = AttachedLines::new();
139        a.push("first").expect("fits");
140        a.push("").expect("fits");
141        a.push("third line").expect("fits");
142        assert_eq!(a.len(), 3);
143        assert!(!a.is_empty());
144        assert_eq!(a.get(0), Some("first"));
145        assert_eq!(a.get(1), Some(""));
146        assert_eq!(a.get(2), Some("third line"));
147        assert!(a.get(3).is_none());
148        let collected: Vec<&str> = a.iter().collect();
149        assert_eq!(collected, vec!["first", "", "third line"]);
150    }
151
152    #[test]
153    fn intoiter_for_ref_works_in_for_loop() {
154        let mut a = AttachedLines::new();
155        a.push("a").expect("fits");
156        a.push("b").expect("fits");
157        let mut out = Vec::new();
158        for line in &a {
159            out.push(line.to_string());
160        }
161        assert_eq!(out, vec!["a".to_string(), "b".to_string()]);
162    }
163
164    #[test]
165    fn lines_with_embedded_separators_round_trip() {
166        // The parser never feeds embedded newlines today, but the API should not
167        // corrupt content that happens to contain them — the offset table
168        // delimits by index, not by scanning for '\n'.
169        let mut a = AttachedLines::new();
170        a.push("has\nnewline").expect("fits");
171        a.push("plain").expect("fits");
172        assert_eq!(a.get(0), Some("has\nnewline"));
173        assert_eq!(a.get(1), Some("plain"));
174    }
175
176    #[test]
177    fn allocation_pattern_is_logarithmic_not_per_line() {
178        // Push 200 typical CHANNEL_DATA variable lines. Buffer capacity should
179        // grow logarithmically (capacity-doubling), not 200 separate allocations.
180        let mut a = AttachedLines::new();
181        for i in 0..200 {
182            a.push(&format!(
183                "variable_some_long_name_{i}: [a typical value here]"
184            ))
185            .expect("fits");
186        }
187        assert_eq!(a.len(), 200);
188        // Round-trip check: every line readable in order.
189        for (i, line) in a.iter().enumerate() {
190            assert!(line.starts_with(&format!("variable_some_long_name_{i}")));
191        }
192    }
193}