1use 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 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}