use std::io::{BufRead, Result};
pub trait LinesLossyExt: Sized {
fn lines_lossy(self) -> LinesLossy<Self>;
}
impl<T: BufRead> LinesLossyExt for T {
fn lines_lossy(self) -> LinesLossy<Self> {
LinesLossy { buf: self }
}
}
#[derive(Debug)]
pub struct LinesLossy<B> {
buf: B,
}
impl<B: BufRead> Iterator for LinesLossy<B> {
type Item = Result<String>;
fn next(&mut self) -> Option<Result<String>> {
let mut buf = Vec::new();
match self.buf.read_until(b'\n', &mut buf) {
Ok(0) => None,
Ok(_n) => {
if buf.last() == Some(&b'\n') {
buf.pop();
if buf.last() == Some(&b'\r') {
buf.pop();
}
}
let text = match String::from_utf8(buf) {
Ok(s) => s,
Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
};
Some(Ok(text))
}
Err(e) => Some(Err(e)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
fn test_case(input: &[u8], output: &[&str]) {
let rdr = Cursor::new(input);
let lines1 = rdr.lines_lossy().collect::<Result<Vec<String>>>().unwrap();
let lines2 = output
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>();
assert_eq!(lines1, lines2);
}
#[test]
fn copied_from_std_io() {
test_case(b"12\r", &["12\r"]);
test_case(b"12\r\n\n", &["12", ""]);
}
#[test]
fn basic() {
test_case(b"", &[]);
test_case(b"hello\nworld", &["hello", "world"]);
test_case(b"hello\r\nworld", &["hello", "world"]);
test_case(b"hello\nworld\n", &["hello", "world"]);
test_case(b"hello\r\nworld\r\n", &["hello", "world"]);
}
#[test]
fn lossy() {
test_case(b"what\xaas\nup", &["what\u{fffd}s", "up"]);
test_case(
b"\xaa\xbb\xcc\r\n\xee \xff",
&["\u{fffd}\u{fffd}\u{fffd}", "\u{fffd} \u{fffd}"],
);
}
}