use std::path::Path;
use crate::ignore_rules::{IgnoreOptions, IgnoreRules};
use crate::service::{DirEntryInfo, FileSystemService, FsResult};
pub const MAX_ENTRIES_PER_DIR: usize = 10_000;
pub struct DirReader<'a> {
fs: &'a dyn FileSystemService,
rules: IgnoreRules,
}
impl<'a> DirReader<'a> {
pub fn new(fs: &'a dyn FileSystemService, root: &Path, options: IgnoreOptions) -> Self {
let rules = IgnoreRules::for_root(fs, root, options);
Self { fs, rules }
}
pub fn unfiltered(fs: &'a dyn FileSystemService) -> Self {
Self { fs, rules: IgnoreRules::disabled() }
}
pub fn rules(&self) -> &IgnoreRules {
&self.rules
}
pub fn service(&self) -> &dyn FileSystemService {
self.fs
}
pub fn read(&mut self, path: &Path) -> FsResult<Vec<DirEntryInfo>> {
self.rules.load_dir(self.fs, path);
let entries = self.fs.read_dir(path)?;
let mut kept = self.rules.filter(entries);
kept.truncate(MAX_ENTRIES_PER_DIR);
Ok(kept)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use crate::service::EntryKind;
use termesh_core::{FsError, FsResult};
struct Fs {
entries: Vec<DirEntryInfo>,
gitignore: Option<&'static str>,
}
impl FileSystemService for Fs {
fn read_dir(&self, _: &Path) -> FsResult<Vec<DirEntryInfo>> {
Ok(self.entries.clone())
}
fn read_file(&self, path: &Path) -> FsResult<Vec<u8>> {
match (self.gitignore, path.file_name().and_then(|n| n.to_str())) {
(Some(c), Some(".gitignore")) => Ok(c.as_bytes().to_vec()),
_ => Err(FsError::NotFound(path.to_path_buf())),
}
}
fn create_file(&self, _: &Path) -> FsResult<()> {
Ok(())
}
fn write_file(&self, _: &Path, _: &[u8]) -> FsResult<()> {
Ok(())
}
fn create_dir(&self, _: &Path) -> FsResult<()> {
Ok(())
}
fn rename(&self, _: &Path, _: &Path) -> FsResult<()> {
Ok(())
}
fn remove_file(&self, _: &Path) -> FsResult<()> {
Ok(())
}
fn remove_dir_all(&self, _: &Path) -> FsResult<()> {
Ok(())
}
fn canonicalize(&self, p: &Path) -> FsResult<PathBuf> {
Ok(p.to_path_buf())
}
}
fn entry(name: &str, kind: EntryKind) -> DirEntryInfo {
DirEntryInfo { name: name.into(), path: PathBuf::from("/r").join(name), kind }
}
#[test]
fn ignored_and_hidden_entries_are_filtered_out() {
let fs = Fs {
entries: vec![
entry("src", EntryKind::Dir),
entry("target", EntryKind::Dir),
entry(".git", EntryKind::Dir),
entry("README.md", EntryKind::File),
],
gitignore: Some("target\n"),
};
let mut reader = DirReader::new(&fs, Path::new("/r"), IgnoreOptions::default());
let names: Vec<String> = reader
.read(Path::new("/r"))
.unwrap()
.iter()
.map(|e| e.name.to_string_lossy().into_owned())
.collect();
assert_eq!(names, ["src", "README.md"]);
}
#[test]
fn an_unfiltered_reader_keeps_everything() {
let fs = Fs {
entries: vec![entry(".git", EntryKind::Dir), entry("target", EntryKind::Dir)],
gitignore: Some("target\n"),
};
let mut reader = DirReader::unfiltered(&fs);
assert_eq!(reader.read(Path::new("/r")).unwrap().len(), 2);
}
#[test]
fn oversized_directories_are_capped() {
let entries =
(0..MAX_ENTRIES_PER_DIR + 500).map(|i| entry(&format!("f{i}"), EntryKind::File));
let fs = Fs { entries: entries.collect(), gitignore: None };
let mut reader = DirReader::unfiltered(&fs);
assert_eq!(reader.read(Path::new("/r")).unwrap().len(), MAX_ENTRIES_PER_DIR);
}
#[test]
fn read_errors_propagate_rather_than_becoming_an_empty_listing() {
struct Denied;
impl FileSystemService for Denied {
fn read_dir(&self, p: &Path) -> FsResult<Vec<DirEntryInfo>> {
Err(FsError::PermissionDenied(p.to_path_buf()))
}
fn read_file(&self, p: &Path) -> FsResult<Vec<u8>> {
Err(FsError::NotFound(p.to_path_buf()))
}
fn create_file(&self, _: &Path) -> FsResult<()> {
Ok(())
}
fn write_file(&self, _: &Path, _: &[u8]) -> FsResult<()> {
Ok(())
}
fn create_dir(&self, _: &Path) -> FsResult<()> {
Ok(())
}
fn rename(&self, _: &Path, _: &Path) -> FsResult<()> {
Ok(())
}
fn remove_file(&self, _: &Path) -> FsResult<()> {
Ok(())
}
fn remove_dir_all(&self, _: &Path) -> FsResult<()> {
Ok(())
}
fn canonicalize(&self, p: &Path) -> FsResult<PathBuf> {
Ok(p.to_path_buf())
}
}
let mut reader = DirReader::unfiltered(&Denied);
assert!(reader.read(Path::new("/r")).is_err(), "an empty list would look like success");
}
}