use std::{
collections::BTreeMap,
ffi::OsString,
path::{Path, PathBuf},
};
use crate::PathSegment;
pub(crate) fn from_path_with_walker<'a, TDir, TDirEntry>(
directory: impl AsRef<Path>,
walker: ignore::Walk,
make_dir: impl Fn(&Path) -> Result<TDir, DirectoryFromPathError>,
make_entry_dir: impl Fn(TDir) -> TDirEntry,
make_entry_file: impl Fn(&Path) -> Result<TDirEntry, DirectoryFromPathError>,
make_entry_symlink: impl Fn(&Path) -> Result<Option<TDirEntry>, DirectoryFromPathError>,
get_children: impl Fn(&mut TDir) -> &mut BTreeMap<PathSegment, TDirEntry>,
get_dir_from_entry: impl Fn(&mut TDirEntry) -> Option<&mut TDir>,
) -> Result<TDir, DirectoryFromPathError>
where
TDir: 'a,
TDirEntry: 'a,
{
let mut result = make_dir(directory.as_ref())?;
for entry in walker {
let entry = entry?;
let mut leaf_entry = if entry.path().symlink_metadata()?.file_type().is_symlink() {
make_entry_symlink(entry.path())?
} else {
None
};
if leaf_entry.is_none() && !entry.path().try_exists()? {
return Err(DirectoryFromPathError::BrokenSymlink(
entry.path().to_owned(),
));
}
if leaf_entry.is_none() && entry.path().is_file() {
leaf_entry = Some(make_entry_file(entry.path())?);
}
let path = entry.path().strip_prefix(directory.as_ref()).map_err(|_| {
DirectoryFromPathError::StripPrefix {
path: entry.path().to_owned(),
root: directory.as_ref().to_owned(),
}
})?;
let mut segments = path.iter().peekable();
let mut cursor = &mut result;
let mut full_path = directory.as_ref().to_owned();
while let Some(segment_str) = segments.next() {
let segment = segment_str
.to_str()
.ok_or_else(|| DirectoryFromPathError::NonUtf8Path(segment_str.to_owned()))?
.parse::<PathSegment>()?;
let is_last = segments.peek().is_none();
if is_last && leaf_entry.is_some() {
get_children(cursor).insert(
segment,
leaf_entry
.take()
.expect("leaf entry existence was just checked"),
);
} else {
full_path.push(segment_str);
let entry = match get_children(cursor).entry(segment) {
std::collections::btree_map::Entry::Occupied(o) => o.into_mut(),
std::collections::btree_map::Entry::Vacant(v) => {
let dir = make_dir(&full_path)?;
v.insert(make_entry_dir(dir))
}
};
let Some(dir) = get_dir_from_entry(entry) else {
break;
};
cursor = dir;
}
}
}
Ok(result)
}
#[derive(thiserror::Error, Debug)]
pub enum DirectoryFromPathError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to strip root path {root} from entry {path}")]
StripPrefix {
path: PathBuf,
root: PathBuf,
},
#[error("Non-UTF8 path {0:?}")]
NonUtf8Path(OsString),
#[error("Path segment error: {0}")]
PathSegment(#[from] crate::PathSegmentError),
#[error("Ignore error: {0}")]
Ignore(ignore::Error),
#[error("Broken symlink at: {0}")]
BrokenSymlink(PathBuf),
}
impl From<ignore::Error> for DirectoryFromPathError {
fn from(e: ignore::Error) -> Self {
match e {
ignore::Error::Io(io) => Self::Io(io),
_ => DirectoryFromPathError::Ignore(e),
}
}
}