ascii_read/
lib.rs

1use ascii::{AsciiChar, AsciiStr, AsciiString};
2use std::io;
3use std::io::{BufRead, BufReader, Read, StdinLock};
4use thiserror::Error;
5
6/// Error returned when reading from a type that implements [`AsciiBufRead`]. This may be
7/// an I/O error or an error when the sequence of `u8` are not all ASCII.
8#[derive(Error, Debug)]
9pub enum AsciiReadError {
10    #[error("io error")]
11    Io(#[from] io::Error),
12    #[error("ascii decoding error")]
13    Ascii(#[from] ascii::AsAsciiStrError),
14}
15
16pub trait AsciiBufRead: BufRead {
17    /// Read all bytes until a newline is reached, and append them to the provided buffer.
18    ///
19    /// This function will read bytes from the underlying stream until the newline delimiter
20    /// is found. Once found, all bytes, including the delimiter, will be appended to `buf`.
21    ///
22    /// If successful, this function returns the total number of bytes read.
23    ///
24    /// If this function returns [`Ok(0)`], the stream has reached `EOF`.
25    ///
26    /// [`Ok(0)`]: Ok
27    fn read_ascii_line(&mut self, buf: &mut AsciiString) -> Result<usize, AsciiReadError> {
28        let mut s = String::new();
29        let n = self.read_line(&mut s)?;
30        buf.push_str(AsciiStr::from_ascii(&s)?);
31        Ok(n)
32    }
33
34    /// Returns an iterator over the lines of this reader.
35    ///
36    /// The iterator yields instances of <code>[Result]<[AsciiString], [AsciiReadError]></code>.
37    /// Each string will *not* have a newline byte or `CRLF` at the end.
38    fn ascii_lines(self) -> AsciiLines<Self>
39    where
40        Self: Sized,
41    {
42        AsciiLines { buf: self }
43    }
44}
45
46impl<R: Read> AsciiBufRead for BufReader<R> {}
47impl AsciiBufRead for StdinLock<'_> {}
48
49/// An iterator over the lines of an instance of [`AsciiBufRead`].
50///
51/// This struct is created by calling [`ascii_lines`] on an [`AsciiBufRead`].
52///
53/// [`AsciiBufRead`]: AsciiBufRead
54/// [`ascii_lines`]: AsciiBufRead::ascii_lines
55#[derive(Debug)]
56pub struct AsciiLines<B> {
57    buf: B,
58}
59
60impl<B: AsciiBufRead> Iterator for AsciiLines<B> {
61    type Item = Result<AsciiString, AsciiReadError>;
62
63    fn next(&mut self) -> Option<Self::Item> {
64        let mut buf = AsciiString::new();
65        match self.buf.read_ascii_line(&mut buf) {
66            Ok(0) => None,
67            Ok(_n) => {
68                if buf[buf.len() - 1] == AsciiChar::LineFeed {
69                    let _ = buf.pop();
70                    if !buf.is_empty() && buf[buf.len() - 1] == AsciiChar::CarriageReturn {
71                        let _ = buf.pop();
72                    }
73                }
74                Some(Ok(buf))
75            }
76            Err(e) => Some(Err(e)),
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use std::env;
85    use std::fs::File;
86    use std::path::Path;
87
88    // TODO test StdinLock impl
89    // TODO test io and ascii errors
90    // TODO test Iterator impl
91
92    #[test]
93    fn test_read_ascii_line_bufreader() {
94        let manifest_path = Path::new(
95            &env::var("CARGO_MANIFEST_DIR")
96                .expect("Environment variable CARGO_MANIFEST_DIR not set."),
97        )
98        .join("resources/test/file.txt");
99        let file = File::open(&manifest_path).expect("Failed to open {manifest_path:?}");
100        let mut reader = BufReader::new(file);
101        let mut asciistring = AsciiString::new();
102        loop {
103            let n = reader
104                .read_ascii_line(&mut asciistring)
105                .expect("Panic at read_ascii_line()");
106            if n == 0 {
107                break;
108            }
109        }
110        assert_eq!(asciistring, "this is a test file\nsecond line\nhola\n");
111    }
112}