#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RemoteLine {
Update(String),
Line(String),
}
#[derive(Debug, Default)]
pub struct SidebandLines {
pending: Vec<u8>,
}
impl SidebandLines {
#[must_use]
pub fn new() -> Self {
Self {
pending: Vec::new(),
}
}
pub fn feed<F>(&mut self, chunk: &[u8], mut emit: F)
where
F: FnMut(RemoteLine),
{
if chunk.is_empty() {
return;
}
self.pending.extend_from_slice(chunk);
while let Some(i) = self.pending.iter().position(|&b| b == b'\r' || b == b'\n') {
let delim = self.pending[i];
let line = self.pending.drain(..i).collect::<Vec<_>>();
self.pending.drain(..1);
let text = String::from_utf8_lossy(&line).into_owned();
let item = if delim == b'\r' {
RemoteLine::Update(text)
} else {
RemoteLine::Line(text)
};
emit(item);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn collect(chunks: &[&[u8]]) -> Vec<RemoteLine> {
let mut splitter = SidebandLines::new();
let mut out = Vec::new();
for chunk in chunks {
splitter.feed(*chunk, |line| out.push(line));
}
out
}
#[test]
fn cr_updates() {
assert_eq!(
collect(&[b"one\r two\r"]),
vec![
RemoteLine::Update("one".into()),
RemoteLine::Update(" two".into()),
]
);
}
#[test]
fn lf_lines() {
assert_eq!(
collect(&[b"first\nsecond\n"]),
vec![
RemoteLine::Line("first".into()),
RemoteLine::Line("second".into()),
]
);
}
#[test]
fn split_across_chunks() {
assert_eq!(
collect(&[b"hel", b"lo\rwor", b"ld\n"]),
vec![
RemoteLine::Update("hello".into()),
RemoteLine::Line("world".into()),
]
);
}
#[test]
fn invalid_utf8_lossy() {
let mut splitter = SidebandLines::new();
let mut out = Vec::new();
splitter.feed(b"\xFF\xFEok\r", |line| out.push(line));
assert_eq!(out, vec![RemoteLine::Update("\u{FFFD}\u{FFFD}ok".into())]);
}
}