Skip to main content

sel/source/
file.rs

1//! File-backed `Source`.
2
3use super::Source;
4use crate::error::SelError;
5use crate::{Line, Result};
6use std::fs::File;
7use std::io::{BufRead, BufReader};
8use std::path::{Path, PathBuf};
9
10#[derive(Debug)]
11pub struct FileSource {
12    reader: BufReader<File>,
13    label: String,
14    path: PathBuf,
15    line_no: u64,
16}
17
18impl FileSource {
19    pub fn open(path: &Path) -> Result<Self> {
20        let file = File::open(path).map_err(|source| SelError::Io {
21            path: path.display().to_string(),
22            source,
23        })?;
24        Ok(Self {
25            reader: BufReader::new(file),
26            label: path.display().to_string(),
27            path: path.to_path_buf(),
28            line_no: 0,
29        })
30    }
31
32    pub fn path(&self) -> &Path {
33        &self.path
34    }
35}
36
37impl Source for FileSource {
38    fn next_line(&mut self) -> Result<Option<Line>> {
39        let mut buf: Vec<u8> = Vec::new();
40        let n = self
41            .reader
42            .read_until(b'\n', &mut buf)
43            .map_err(|source| SelError::Io {
44                path: self.label.clone(),
45                source,
46            })?;
47        if n == 0 {
48            return Ok(None);
49        }
50        // Strip trailing \n and optional \r
51        if buf.ends_with(b"\n") {
52            buf.pop();
53            if buf.ends_with(b"\r") {
54                buf.pop();
55            }
56        }
57        self.line_no += 1;
58        Ok(Some(Line::new(self.line_no, buf)))
59    }
60
61    fn label(&self) -> &str {
62        &self.label
63    }
64
65    fn is_seekable(&self) -> bool {
66        true
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use std::io::Write;
74    use tempfile::NamedTempFile;
75
76    #[test]
77    fn reads_three_lines_numbered() {
78        let mut f = NamedTempFile::new().unwrap();
79        writeln!(f, "alpha").unwrap();
80        writeln!(f, "beta").unwrap();
81        writeln!(f, "gamma").unwrap();
82
83        let mut src = FileSource::open(f.path()).unwrap();
84        let l1 = src.next_line().unwrap().unwrap();
85        let l2 = src.next_line().unwrap().unwrap();
86        let l3 = src.next_line().unwrap().unwrap();
87        assert!(src.next_line().unwrap().is_none());
88
89        assert_eq!(l1.no, 1);
90        assert_eq!(&l1.bytes, b"alpha");
91        assert_eq!(l2.no, 2);
92        assert_eq!(&l2.bytes, b"beta");
93        assert_eq!(l3.no, 3);
94        assert_eq!(&l3.bytes, b"gamma");
95    }
96
97    #[test]
98    fn handles_crlf() {
99        let mut f = NamedTempFile::new().unwrap();
100        f.write_all(b"one\r\ntwo\r\n").unwrap();
101
102        let mut src = FileSource::open(f.path()).unwrap();
103        let l1 = src.next_line().unwrap().unwrap();
104        assert_eq!(&l1.bytes, b"one");
105    }
106
107    #[test]
108    fn nonexistent_file_returns_io_error_with_path() {
109        let err = FileSource::open(Path::new("/nonexistent-xyz-123")).unwrap_err();
110        let msg = format!("{err}");
111        assert!(msg.contains("nonexistent-xyz-123"));
112    }
113}