termesh_filesystem/
reader.rs1use std::path::Path;
9
10use crate::ignore_rules::{IgnoreOptions, IgnoreRules};
11use crate::service::{DirEntryInfo, FileSystemService, FsResult};
12
13pub const MAX_ENTRIES_PER_DIR: usize = 10_000;
18
19pub struct DirReader<'a> {
21 fs: &'a dyn FileSystemService,
22 rules: IgnoreRules,
23}
24
25impl<'a> DirReader<'a> {
26 pub fn new(fs: &'a dyn FileSystemService, root: &Path, options: IgnoreOptions) -> Self {
27 let rules = IgnoreRules::for_root(fs, root, options);
28 Self { fs, rules }
29 }
30
31 pub fn unfiltered(fs: &'a dyn FileSystemService) -> Self {
33 Self { fs, rules: IgnoreRules::disabled() }
34 }
35
36 pub fn rules(&self) -> &IgnoreRules {
37 &self.rules
38 }
39
40 pub fn service(&self) -> &dyn FileSystemService {
42 self.fs
43 }
44
45 pub fn read(&mut self, path: &Path) -> FsResult<Vec<DirEntryInfo>> {
50 self.rules.load_dir(self.fs, path);
51 let entries = self.fs.read_dir(path)?;
52 let mut kept = self.rules.filter(entries);
53 kept.truncate(MAX_ENTRIES_PER_DIR);
54 Ok(kept)
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use std::path::PathBuf;
62
63 use crate::service::EntryKind;
64 use termesh_core::{FsError, FsResult};
65
66 struct Fs {
67 entries: Vec<DirEntryInfo>,
68 gitignore: Option<&'static str>,
69 }
70
71 impl FileSystemService for Fs {
72 fn read_dir(&self, _: &Path) -> FsResult<Vec<DirEntryInfo>> {
73 Ok(self.entries.clone())
74 }
75 fn read_file(&self, path: &Path) -> FsResult<Vec<u8>> {
76 match (self.gitignore, path.file_name().and_then(|n| n.to_str())) {
77 (Some(c), Some(".gitignore")) => Ok(c.as_bytes().to_vec()),
78 _ => Err(FsError::NotFound(path.to_path_buf())),
79 }
80 }
81 fn create_file(&self, _: &Path) -> FsResult<()> {
82 Ok(())
83 }
84 fn write_file(&self, _: &Path, _: &[u8]) -> FsResult<()> {
85 Ok(())
86 }
87 fn create_dir(&self, _: &Path) -> FsResult<()> {
88 Ok(())
89 }
90 fn rename(&self, _: &Path, _: &Path) -> FsResult<()> {
91 Ok(())
92 }
93 fn remove_file(&self, _: &Path) -> FsResult<()> {
94 Ok(())
95 }
96 fn remove_dir_all(&self, _: &Path) -> FsResult<()> {
97 Ok(())
98 }
99 fn canonicalize(&self, p: &Path) -> FsResult<PathBuf> {
100 Ok(p.to_path_buf())
101 }
102 }
103
104 fn entry(name: &str, kind: EntryKind) -> DirEntryInfo {
105 DirEntryInfo { name: name.into(), path: PathBuf::from("/r").join(name), kind }
106 }
107
108 #[test]
109 fn ignored_and_hidden_entries_are_filtered_out() {
110 let fs = Fs {
111 entries: vec![
112 entry("src", EntryKind::Dir),
113 entry("target", EntryKind::Dir),
114 entry(".git", EntryKind::Dir),
115 entry("README.md", EntryKind::File),
116 ],
117 gitignore: Some("target\n"),
118 };
119 let mut reader = DirReader::new(&fs, Path::new("/r"), IgnoreOptions::default());
120 let names: Vec<String> = reader
121 .read(Path::new("/r"))
122 .unwrap()
123 .iter()
124 .map(|e| e.name.to_string_lossy().into_owned())
125 .collect();
126 assert_eq!(names, ["src", "README.md"]);
127 }
128
129 #[test]
130 fn an_unfiltered_reader_keeps_everything() {
131 let fs = Fs {
132 entries: vec![entry(".git", EntryKind::Dir), entry("target", EntryKind::Dir)],
133 gitignore: Some("target\n"),
134 };
135 let mut reader = DirReader::unfiltered(&fs);
136 assert_eq!(reader.read(Path::new("/r")).unwrap().len(), 2);
137 }
138
139 #[test]
140 fn oversized_directories_are_capped() {
141 let entries =
142 (0..MAX_ENTRIES_PER_DIR + 500).map(|i| entry(&format!("f{i}"), EntryKind::File));
143 let fs = Fs { entries: entries.collect(), gitignore: None };
144 let mut reader = DirReader::unfiltered(&fs);
145 assert_eq!(reader.read(Path::new("/r")).unwrap().len(), MAX_ENTRIES_PER_DIR);
146 }
147
148 #[test]
149 fn read_errors_propagate_rather_than_becoming_an_empty_listing() {
150 struct Denied;
151 impl FileSystemService for Denied {
152 fn read_dir(&self, p: &Path) -> FsResult<Vec<DirEntryInfo>> {
153 Err(FsError::PermissionDenied(p.to_path_buf()))
154 }
155 fn read_file(&self, p: &Path) -> FsResult<Vec<u8>> {
156 Err(FsError::NotFound(p.to_path_buf()))
157 }
158 fn create_file(&self, _: &Path) -> FsResult<()> {
159 Ok(())
160 }
161 fn write_file(&self, _: &Path, _: &[u8]) -> FsResult<()> {
162 Ok(())
163 }
164 fn create_dir(&self, _: &Path) -> FsResult<()> {
165 Ok(())
166 }
167 fn rename(&self, _: &Path, _: &Path) -> FsResult<()> {
168 Ok(())
169 }
170 fn remove_file(&self, _: &Path) -> FsResult<()> {
171 Ok(())
172 }
173 fn remove_dir_all(&self, _: &Path) -> FsResult<()> {
174 Ok(())
175 }
176 fn canonicalize(&self, p: &Path) -> FsResult<PathBuf> {
177 Ok(p.to_path_buf())
178 }
179 }
180 let mut reader = DirReader::unfiltered(&Denied);
181 assert!(reader.read(Path::new("/r")).is_err(), "an empty list would look like success");
182 }
183}