Skip to main content

conmonrs/container_log/
line_buffer.rs

1//! Collecting the output into whole lines, see [`LineBuffer`].
2
3use super::{Line, Text, strip_line_ending};
4use crate::container_io::{Pipe, READ_BUFFER_SIZE};
5use bytes::BytesMut;
6
7/// Collects the output of both pipes into whole lines for journald, which
8/// writes one entry per line.
9///
10/// A read chunk may end anywhere, in the middle of a line, between "\r" and
11/// "\n" or inside a multi byte character. Keeping the incomplete tail until
12/// the next chunk of the same pipe arrives avoids split lines and garbled
13/// characters. A line which grows beyond [`LineBuffer::MAX_LINE`] is written
14/// as a partial one, cut at a character boundary, and so is a line which
15/// waits too long for its ending, see [`LineBuffer::flush`].
16#[derive(Debug, Default)]
17pub(super) struct LineBuffer {
18    pub(super) stdout: BytesMut,
19    stderr: BytesMut,
20}
21
22impl LineBuffer {
23    /// Longer lines get written as partial entries.
24    pub(super) const MAX_LINE: usize = 16 * 1024;
25
26    fn tail(&mut self, pipe: Pipe) -> &mut BytesMut {
27        match pipe {
28            Pipe::StdOut => &mut self.stdout,
29            Pipe::StdErr => &mut self.stderr,
30        }
31    }
32
33    /// Whether a line of the pipe waits for its ending.
34    pub(super) fn has_pending(&self, pipe: Pipe) -> bool {
35        !match pipe {
36            Pipe::StdOut => &self.stdout,
37            Pipe::StdErr => &self.stderr,
38        }
39        .is_empty()
40    }
41
42    /// Add a chunk and return the lines it completes. The whole lines of the
43    /// chunk borrow it, only the end of a line which started in an earlier
44    /// chunk and the start of one which continues in a later one get copied
45    /// into the buffer of the pipe.
46    pub(super) fn push<'a>(&mut self, pipe: Pipe, bytes: &'a [u8]) -> Vec<Line<'a>> {
47        let mut lines = vec![];
48        let mut ends = memchr::memchr_iter(b'\n', bytes).map(|end| end + 1);
49        let mut start = 0;
50        let tail = self.tail(pipe);
51        if !tail.is_empty() {
52            let end = ends.next();
53            start = end.unwrap_or(bytes.len());
54            tail.extend_from_slice(&bytes[..start]);
55            if end.is_some() {
56                let line = tail.split().freeze();
57                lines.push(Line {
58                    text: Text::Buffered(line.slice(..strip_line_ending(&line).len())),
59                    partial: false,
60                });
61            }
62        }
63
64        for end in ends {
65            lines.push(Line {
66                text: Text::Chunk(strip_line_ending(&bytes[start..end])),
67                partial: false,
68            });
69            start = end;
70        }
71        tail.extend_from_slice(&bytes[start..]);
72        lines.extend(Self::cut_long(tail));
73        lines
74    }
75
76    /// Cut a line which grew too long without its ending, see
77    /// [`LineBuffer::cut`], and release the buffer once empty.
78    fn cut_long(tail: &mut BytesMut) -> Option<Line<'static>> {
79        let line = (tail.len() >= Self::MAX_LINE).then(|| Self::cut(tail));
80        Self::shrink(tail);
81        line
82    }
83
84    /// Cut the tail at a point which keeps an unfinished multi byte character
85    /// and a "\r" waiting for its "\n" in the buffer. Invalid bytes are not
86    /// held back, they would stay in front forever.
87    fn cut(tail: &mut BytesMut) -> Line<'static> {
88        let mut end = Self::complete_len(tail);
89        if tail[..end].ends_with(b"\r") {
90            end -= 1;
91        }
92        Line {
93            text: Text::Buffered(tail.split_to(end).freeze()),
94            partial: true,
95        }
96    }
97
98    /// The length of the bytes without the unfinished multi byte character
99    /// they end with, if any. Only the last character matters, which starts
100    /// within the last three bytes if it is unfinished, an invalid byte in
101    /// front of it does not.
102    fn complete_len(bytes: &[u8]) -> usize {
103        let window = bytes.len().saturating_sub(3);
104        let Some(start) = bytes[window..]
105            .iter()
106            .rposition(|b| b & 0xc0 != 0x80)
107            .map(|i| window + i)
108        else {
109            return bytes.len();
110        };
111        match str::from_utf8(&bytes[start..]) {
112            Err(e) if e.error_len().is_none() => start + e.valid_up_to(),
113            _ => bytes.len(),
114        }
115    }
116
117    /// A line longer than a read chunk leaves a large buffer behind, release
118    /// it once drained. An idle container keeps at most a chunk of it.
119    fn shrink(tail: &mut BytesMut) {
120        if tail.is_empty() && tail.capacity() > READ_BUFFER_SIZE {
121            *tail = BytesMut::new();
122        }
123    }
124
125    /// Return the line which waits for its ending as a partial one. The
126    /// output paused, a prompt or progress output without a newline would
127    /// otherwise stay invisible.
128    pub(super) fn flush(&mut self, pipe: Pipe) -> Vec<Line<'static>> {
129        // Like a long line, it keeps an unfinished multi byte character and
130        // a "\r" back for the next chunk, see [`LineBuffer::cut`].
131        let tail = self.tail(pipe);
132        if tail.is_empty() {
133            return vec![];
134        }
135        let line = Self::cut(tail);
136        Self::shrink(tail);
137        if line.text.is_empty() {
138            return vec![];
139        }
140        vec![line]
141    }
142
143    /// The output of the pipe ended, return the line which still waits for
144    /// its ending as a partial one.
145    pub(super) fn finish(&mut self, pipe: Pipe) -> Vec<Line<'static>> {
146        let tail = self.tail(pipe);
147        if tail.is_empty() {
148            return vec![];
149        }
150        // Without a newline, so there is no line ending to strip.
151        let text = Text::Buffered(tail.split().freeze());
152        Self::shrink(tail);
153        vec![Line {
154            text,
155            partial: true,
156        }]
157    }
158}
159
160#[cfg(test)]
161mod tests;