use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use clap::ValueEnum;
use crate::{Dirs, Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
pub enum NotOnDirs {
#[default]
Error,
BeforeFirst,
}
pub struct Config {
pub base_dir: std::path::PathBuf,
pub all_target: bool,
pub current: Option<PathBuf>,
pub not_on_dirs: NotOnDirs,
}
impl Config {
pub fn new<P: AsRef<Path>>(base_dir: P, all_target: bool) -> Self {
Config {
base_dir: base_dir.as_ref().to_path_buf(),
all_target,
current: None,
not_on_dirs: NotOnDirs::default(),
}
}
pub fn new_with_wd<P1: AsRef<Path>, P2: AsRef<Path>>(base_dir: P1, all_target: bool, current: P2) -> Self {
Config {
base_dir: base_dir.as_ref().to_path_buf(),
all_target,
current: Some(current.as_ref().to_path_buf()),
not_on_dirs: NotOnDirs::default(),
}
}
#[must_use]
pub fn not_on_dirs(mut self, action: NotOnDirs) -> Self {
self.not_on_dirs = action;
self
}
}
pub struct DirsFactory {
}
impl DirsFactory {
pub fn create<P: AsRef<Path>>(base_dir: P) -> Result<Dirs> {
Self::create_with(&Config::new(base_dir, false))
}
pub fn create_with(config: &Config) -> Result<Dirs> {
let base_dir = &config.base_dir;
if base_dir == Path::new(".") {
log::info!("DirsFactory::create_with: Using current directory as base_dir");
let cwd = std::env::current_dir().map_err(Error::Io)?;
create_dirs_from_base_path(&cwd, config)
} else if base_dir.exists() && base_dir.is_dir() {
create_dirs_from_base_path(base_dir, config)
} else if base_dir.exists() && !base_dir.is_dir() {
log::error!("{}: Not a directory.", base_dir.display());
Err(Error::NotDir(base_dir.to_path_buf()))
} else {
log::error!("{}: directory not found", base_dir.display());
Err(Error::NotFound(base_dir.to_path_buf()))
}
}
pub fn create_from_file<P: AsRef<Path>>(file: P, config: &Config) -> Result<Dirs> {
let file = file.as_ref();
if file == Path::new("-") {
log::info!("Reading directories from stdin");
return Self::create_from_reader(
Box::new(std::io::stdin().lock()),
config
);
}
if !file.exists() {
log::error!(
"Dirs::new_from_file: Not found: {}, pwd: {}",
file.display(),
std::env::current_dir().unwrap().display()
);
Err(Error::NotFound(file.to_path_buf()))
} else if file.is_dir() {
log::error!("Dirs::new_from_file: Not a file: {}", file.display());
Err(Error::NotFile(file.to_path_buf()))
} else {
build_from_list_file(file, config)
}
}
pub fn create_from_reader(reader: Box<dyn std::io::Read>, config: &Config) -> Result<Dirs> {
let mut base_dir = config.base_dir.clone();
let mut dirs = vec![];
let mut current = config.current.clone();
let buf_reader = BufReader::new(reader);
for line in buf_reader.lines() {
let line = line.map_err(Error::Io)?;
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
} else if line.starts_with("base_dir:") || line.starts_with("parent:") {
base_dir = std::path::PathBuf::from(
line.split_once(':').unwrap().1.trim(),
);
} else if line.starts_with("current:") {
let child = PathBuf::from(line.split_once(':').unwrap().1.trim());
append_dirs(&mut dirs, child.clone(), config);
current = Some(child);
} else {
let path = base_dir.join(line);
append_dirs(&mut dirs, path, config);
}
}
let current = if current.is_some() {
resolve_current(&base_dir, &dirs, ¤t, config.not_on_dirs)?
} else {
find_cwd(&dirs)
};
Ok(Dirs {
entries: dirs,
parent: base_dir,
current,
})
}
}
fn append_dirs(dirs: &mut Vec<PathBuf>, path: PathBuf, config: &Config) {
if path.exists() {
dirs.push(path);
} else if config.all_target {
log::info!("append_dirs: Non-existent directory: {}", path.display());
dirs.push(path);
} else {
log::info!("append_dirs: Skipping non-existent directory: {}", path.display());
}
}
fn build_from_list_file<P: AsRef<Path>>(file: P, config: &Config) -> Result<Dirs> {
let file = file.as_ref();
if let Ok(f) = std::fs::File::open(file) {
let reader = BufReader::new(f);
DirsFactory::create_from_reader(Box::new(reader), config)
} else {
log::error!("build_from_list_file: I/O error: {}", file.display());
Err(Error::Io(std::io::Error::last_os_error()))
}
}
fn create_dirs_from_base_path(parent: &Path, config: &Config) -> Result<Dirs> {
log::trace!("build_dirs_from_base_path(parent={parent:?})");
let mut errs = vec![];
let entries = collect_dirs(parent, &mut errs);
if errs.is_empty() {
let current = resolve_current(parent, &entries, &config.current, config.not_on_dirs)?;
log::info!("build_dirs: siblings={}, current={current:?}", entries.len());
Ok(Dirs {
entries,
parent: parent.to_path_buf(),
current,
})
} else {
Err(Error::Array(errs))
}
}
fn resolve_current(
base_dir: &Path,
dirs: &[PathBuf],
current: &Option<PathBuf>,
not_on_dirs: NotOnDirs,
) -> Result<Option<usize>> {
let index = find_current(base_dir, dirs, current);
match (index, current) {
(None, Some(current)) if not_on_dirs == NotOnDirs::Error => {
log::error!("{}: not in the target directories", current.display());
Err(Error::NotFound(current.clone()))
}
_ => Ok(index),
}
}
pub(super) fn find_current(base_dir: &Path, dirs: &[PathBuf], current: &Option<PathBuf>) -> Option<usize> {
let Some(current) = current else {
log::debug!("find_current: no current directory was given");
return None;
};
let wd = current.strip_prefix(base_dir)
.unwrap_or(current);
let index = dirs.iter().position(|dir|{
let td = dir.strip_prefix(base_dir);
td.map(|d| wd == d).unwrap_or(false)
});
if index.is_none() {
log::warn!("find_current: current directory not found in siblings");
}
index
}
fn find_cwd(dirs: &[PathBuf]) -> Option<usize> {
let cwd = std::env::current_dir()
.and_then(std::fs::canonicalize)
.ok()?;
let index = dirs
.iter()
.position(|dir| std::fs::canonicalize(dir).is_ok_and(|d| d == cwd));
match index {
Some(index) => log::debug!("find_cwd: the current directory is at {index}"),
None => log::debug!("find_cwd: the current directory is not in the list"),
}
index
}
fn collect_dirs(parent: &Path, errs: &mut Vec<Error>) -> Vec<PathBuf> {
log::trace!("collect_dirs(parent={})", parent.display());
let mut dirs = vec![];
match parent.read_dir() {
Ok(entries) => {
for entry in entries {
match entry {
Ok(entry) => {
let path = entry.path();
if path.is_dir() {
dirs.push(path);
}
}
Err(e) => {
log::error!("collect_dirs: I/O error: {e}");
errs.push(Error::Io(e));
}
}
}
}
Err(e) => {
log::error!("collect_dirs: {}: {e}", parent.display());
errs.push(Error::Io(e));
}
}
if log::log_enabled!(log::Level::Warn) && dirs.is_empty() {
log::warn!("collect_dirs: no directories under {}", parent.display());
}
dirs.sort();
dirs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_dirs() {
let dirs = DirsFactory::create("testdata/basic")
.expect("Failed to create Dirs from testdata/basic");
assert_eq!(dirs.len(), 26);
assert_eq!(dirs.parent, PathBuf::from("testdata/basic"));
assert!(dirs.current().is_none());
}
#[test]
fn test_create_dirs_with() {
let config = Config::new_with_wd(PathBuf::from("testdata/basic"), false, "c");
let dirs = DirsFactory::create_with(&config)
.expect("Failed to create Dirs from testdata/basic");
assert_eq!(dirs.len(), 26);
assert_eq!(dirs.parent, PathBuf::from("testdata/basic"));
let wd = dirs.current().expect("no current directory");
assert_eq!(wd.path(), Path::new("testdata/basic/c"));
assert_eq!(wd.index(), 2);
}
#[test]
fn test_create_dirs_from_file() {
let config = Config::new(PathBuf::from("testdata/basic"), false);
let dirs = DirsFactory::create_from_file("testdata/basic/dirlist.txt", &config)
.expect("Failed to create Dirs from testdata/basic/dirlist.txt");
assert_eq!(dirs.len(), 3);
assert_eq!(dirs.parent, PathBuf::from("testdata/basic"));
assert!(dirs.current().is_none());
}
#[test]
fn test_create_dirs_from_reader_with_current() {
let list = "parent: testdata/basic\na\ncurrent: testdata/basic/b\nc\n";
let dirs = DirsFactory::create_from_reader(
Box::new(std::io::Cursor::new(list)),
&Config::new(".", false),
)
.expect("Failed to create Dirs from the reader");
assert_eq!(dirs.len(), 3);
let current = dirs.current().expect("no current directory");
assert_eq!(current.path(), Path::new("testdata/basic/b"));
assert_eq!(current.index(), 1);
}
#[test]
fn test_create_dirs_from_reader_on_cwd() {
use crate::Nextable;
let cwd = std::env::current_dir().unwrap();
let name = cwd.file_name().unwrap().to_string_lossy().to_string();
let list = format!("parent: ..\n{name}\nzzz_no_such_dir\n");
let dirs = DirsFactory::create_from_reader(
Box::new(std::io::Cursor::new(list)),
&Config::new(".", true), )
.expect("Failed to create Dirs from the reader");
assert_eq!(dirs.len(), 2);
assert_eq!(dirs.current().map(|d| d.index()), Some(0));
assert_eq!(
dirs.next(crate::NexterType::Next).map(|d| d.path().to_path_buf()),
Some(PathBuf::from("../zzz_no_such_dir"))
);
}
#[test]
fn test_create_dirs_from_reader_without_current() {
let list = "parent: testdata/basic\na\nb\n";
let dirs = DirsFactory::create_from_reader(
Box::new(std::io::Cursor::new(list)),
&Config::new(".", false),
)
.expect("Failed to create Dirs from the reader");
assert_eq!(dirs.len(), 2);
assert!(dirs.current().is_none());
}
#[test]
fn test_not_on_dirs() {
let config = Config::new_with_wd("testdata/worried", false, "testdata/basic/c");
let e = DirsFactory::create_with(&config)
.expect_err("the current directory is not in testdata/worried");
assert!(matches!(e, Error::NotFound(_)), "{e}");
let config = config.not_on_dirs(NotOnDirs::BeforeFirst);
let dirs = DirsFactory::create_with(&config)
.expect("before-first accepts the current directory out of the targets");
assert!(dirs.current().is_none());
assert_eq!(dirs.len(), 2);
}
#[test]
fn test_not_on_dirs_does_not_affect_the_unknown_current() {
let list = "parent: testdata/basic\na\nb\n";
let dirs = DirsFactory::create_from_reader(
Box::new(std::io::Cursor::new(list)),
&Config::new(".", false), )
.expect("the unknown current directory is not an error");
assert!(dirs.current().is_none());
}
#[test]
fn test_fail_to_create_dirs_by_non_existent_dir() {
let _ = DirsFactory::create("src/not_exist_dir")
.expect_err("Expected error when creating Dirs from a non-existent directory");
}
#[test]
fn test_fail_to_create_dirs_by_non_existent_file() {
let _ = DirsFactory::create_from_file("src/not_exist_file.txt", &Config::new(".", false))
.expect_err("Expected error when creating Dirs from a non-existent file");
}
#[test]
fn test_fail_to_create_dirs_by_gives_file() {
let _ = DirsFactory::create("src/lib.rs")
.expect_err("Expected error when creating Dirs from a file");
}
#[test]
fn test_fail_to_create_dirs_from_file_by_gives_dir() {
let _ = DirsFactory::create_from_file("src", &Config::new(".", false))
.expect_err("Expected error when creating Dirs from a directory");
}
}