Skip to main content

freeswitch_log_parser/
chain.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4/// Iterator that concatenates named segments and tracks which line number
5/// each segment starts at. Pair with [`SegmentTracker`] to look up which
6/// segment a given line belongs to.
7pub struct TrackedChain {
8    segments: Vec<Box<dyn Iterator<Item = String>>>,
9    current: usize,
10    lines_emitted: u64,
11    starts: Rc<RefCell<Vec<u64>>>,
12    emit_sentinel: bool,
13}
14
15/// Handle for looking up which segment a line number belongs to.
16///
17/// Created alongside a [`TrackedChain`] — keep this while the chain is
18/// consumed by [`LogStream`](crate::LogStream).
19pub struct SegmentTracker {
20    filenames: Vec<String>,
21    starts: Rc<RefCell<Vec<u64>>>,
22}
23
24impl TrackedChain {
25    /// Build a tracked chain from named segments.
26    ///
27    /// Returns the iterator (feed to `LogStream::new()`) and a tracker handle
28    /// (use to look up segment boundaries after entries are yielded).
29    pub fn new(
30        segments: Vec<(String, Box<dyn Iterator<Item = String>>)>,
31    ) -> (Self, SegmentTracker) {
32        let (filenames, iters): (Vec<_>, Vec<_>) = segments.into_iter().unzip();
33        let starts = Rc::new(RefCell::new(if iters.is_empty() {
34            Vec::new()
35        } else {
36            vec![1u64]
37        }));
38        let tracker = SegmentTracker {
39            filenames: filenames.clone(),
40            starts: starts.clone(),
41        };
42        let chain = TrackedChain {
43            segments: iters,
44            current: 0,
45            lines_emitted: 0,
46            starts,
47            emit_sentinel: false,
48        };
49        (chain, tracker)
50    }
51}
52
53/// Marks the seam between two chained segments. In band because the iterator
54/// yields plain `String`s, so it is a line no log can produce: a real line with
55/// a leading NUL still carries text after it.
56pub(crate) const SEGMENT_BOUNDARY: &str = "\x00";
57
58impl Iterator for TrackedChain {
59    type Item = String;
60
61    fn next(&mut self) -> Option<String> {
62        if self.emit_sentinel {
63            self.emit_sentinel = false;
64            return Some(SEGMENT_BOUNDARY.to_string());
65        }
66        loop {
67            if self.current >= self.segments.len() {
68                return None;
69            }
70            if let Some(line) = self.segments[self.current].next() {
71                self.lines_emitted += 1;
72                return Some(line);
73            }
74            self.current += 1;
75            if self.current < self.segments.len() {
76                self.starts.borrow_mut().push(self.lines_emitted + 1);
77                self.emit_sentinel = true;
78                return self.next();
79            }
80        }
81    }
82}
83
84impl SegmentTracker {
85    /// Look up which segment a line number belongs to.
86    ///
87    /// Returns `(segment_index, filename)` or `None` for line number 0.
88    pub fn segment_for_line(&self, line_number: u64) -> Option<(usize, &str)> {
89        if line_number == 0 {
90            return None;
91        }
92        let starts = self.starts.borrow();
93        let idx = starts.partition_point(|&s| s <= line_number);
94        if idx == 0 {
95            return None;
96        }
97        let seg = idx - 1;
98        Some((seg, &self.filenames[seg]))
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    fn seg(name: &str, lines: Vec<&str>) -> (String, Box<dyn Iterator<Item = String>>) {
107        let owned: Vec<String> = lines.into_iter().map(String::from).collect();
108        (name.to_string(), Box::new(owned.into_iter()))
109    }
110
111    #[test]
112    fn single_segment() {
113        let (chain, tracker) = TrackedChain::new(vec![seg("a.log", vec!["x", "y", "z"])]);
114        let lines: Vec<_> = chain.collect();
115        assert_eq!(lines, ["x", "y", "z"]);
116        assert_eq!(tracker.segment_for_line(1), Some((0, "a.log")));
117        assert_eq!(tracker.segment_for_line(3), Some((0, "a.log")));
118    }
119
120    #[test]
121    fn two_segments() {
122        let (chain, tracker) = TrackedChain::new(vec![
123            seg("a.log", vec!["a1", "a2"]),
124            seg("b.log", vec!["b1"]),
125        ]);
126        let lines: Vec<_> = chain.collect();
127        assert_eq!(lines, ["a1", "a2", "\x00", "b1"]);
128        assert_eq!(tracker.segment_for_line(1), Some((0, "a.log")));
129        assert_eq!(tracker.segment_for_line(2), Some((0, "a.log")));
130        assert_eq!(tracker.segment_for_line(3), Some((1, "b.log")));
131    }
132
133    #[test]
134    fn empty_segment_skipped() {
135        let (chain, tracker) = TrackedChain::new(vec![
136            seg("a.log", vec!["a1"]),
137            seg("empty.log", vec![]),
138            seg("c.log", vec!["c1"]),
139        ]);
140        let lines: Vec<_> = chain.collect();
141        assert_eq!(lines, ["a1", "\x00", "\x00", "c1"]);
142        assert_eq!(tracker.segment_for_line(1), Some((0, "a.log")));
143        assert_eq!(tracker.segment_for_line(2), Some((2, "c.log")));
144    }
145
146    #[test]
147    fn line_zero_returns_none() {
148        let (_chain, tracker) = TrackedChain::new(vec![seg("a.log", vec!["x"])]);
149        assert_eq!(tracker.segment_for_line(0), None);
150    }
151
152    #[test]
153    fn empty_chain() {
154        let (chain, tracker) = TrackedChain::new(vec![]);
155        let lines: Vec<String> = chain.collect();
156        assert!(lines.is_empty());
157        assert_eq!(tracker.segment_for_line(1), None);
158    }
159}