use futures::{Stream, StreamExt};
pub fn line_stream<S, B, E>(byte_stream: S) -> impl Stream<Item = std::result::Result<String, E>>
where
S: Stream<Item = std::result::Result<B, E>> + Send + 'static,
B: AsRef<[u8]> + Send + 'static,
E: Send + 'static,
{
futures::stream::unfold(
(byte_stream.boxed(), Vec::<u8>::new(), false),
|(mut stream, mut buf, mut ended)| async move {
loop {
if let Some(pos) = buf.iter().position(|&b| b == b'\n') {
let mut line: Vec<u8> = buf.drain(..=pos).collect();
line.pop(); if line.last() == Some(&b'\r') {
line.pop(); }
let decoded = String::from_utf8_lossy(&line).into_owned();
return Some((Ok(decoded), (stream, buf, ended)));
}
if ended {
if buf.is_empty() {
return None;
}
let decoded = String::from_utf8_lossy(&buf).into_owned();
buf.clear();
return Some((Ok(decoded), (stream, buf, true)));
}
match stream.next().await {
Some(Ok(chunk)) => buf.extend_from_slice(chunk.as_ref()),
Some(Err(e)) => {
buf.clear();
return Some((Err(e), (stream, buf, true)));
}
None => ended = true,
}
}
},
)
}
#[cfg(test)]
mod tests {
use super::line_stream;
use futures::StreamExt;
async fn collect_lines(chunks: Vec<&'static [u8]>) -> Vec<String> {
let items: Vec<std::result::Result<&'static [u8], std::io::Error>> =
chunks.into_iter().map(Ok).collect();
line_stream(futures::stream::iter(items))
.map(|r| r.unwrap())
.collect::<Vec<_>>()
.await
}
#[tokio::test]
async fn reassembles_line_split_across_chunks() {
let lines = collect_lines(vec![b"data: hel", b"lo\n"]).await;
assert_eq!(lines, vec!["data: hello".to_string()]);
}
#[tokio::test]
async fn reassembles_multibyte_utf8_split_across_chunks() {
let bytes = "你好".as_bytes();
let (head, tail) = bytes.split_at(2);
let head: &'static [u8] = Box::leak(head.to_vec().into_boxed_slice());
let mut tail_buf = tail.to_vec();
tail_buf.push(b'\n');
let tail: &'static [u8] = Box::leak(tail_buf.into_boxed_slice());
let lines = collect_lines(vec![head, tail]).await;
assert_eq!(lines, vec!["你好".to_string()]);
assert!(!lines[0].contains('\u{FFFD}'));
}
#[tokio::test]
async fn splits_multiple_lines_and_flushes_tail() {
let lines = collect_lines(vec![b"a\nb\nc"]).await;
assert_eq!(
lines,
vec!["a".to_string(), "b".to_string(), "c".to_string()]
);
}
#[tokio::test]
async fn strips_crlf_line_endings() {
let lines = collect_lines(vec![b"x\r\ny\r\n"]).await;
assert_eq!(lines, vec!["x".to_string(), "y".to_string()]);
}
}