use std::io;
use std::path::Path;
use std::path::PathBuf;
use thiserror::Error;
use crate::hash::NON_MODULE_CONTENT;
#[derive(Debug, Error)]
pub enum ModuleWalkError {
#[error("symbolic link `{0}` resolves outside the module root")]
SymlinkEscapesRoot(String),
#[error("symbolic link `{0}` targets a directory")]
DirectorySymlink(String),
#[error("symbolic link target under `{0}` is not UTF-8")]
NonUtf8SymlinkTarget(String),
#[error("symbolic link `{0}` targets non-module content")]
SymlinkTargetsMetadata(String),
#[error("i/o error at `{path}`")]
Io {
path: PathBuf,
#[source]
source: io::Error,
},
}
#[derive(Clone, Debug, Default)]
pub struct TreeStats {
pub files: usize,
pub bytes: u64,
}
pub fn walk_module_tree<E>(
root: &Path,
visitor: &mut dyn FnMut(&Path, u64) -> Result<(), E>,
) -> Result<TreeStats, WalkError<E>> {
let canonical_root = std::fs::canonicalize(root).map_err(|source| {
WalkError::Walk(ModuleWalkError::Io {
path: root.to_path_buf(),
source,
})
})?;
let mut stats = TreeStats::default();
walk_recursive(&canonical_root, root, visitor, &mut stats)?;
Ok(stats)
}
#[derive(Debug)]
pub enum WalkError<E> {
Walk(ModuleWalkError),
Visitor(E),
}
impl<E> From<ModuleWalkError> for WalkError<E> {
fn from(e: ModuleWalkError) -> Self {
Self::Walk(e)
}
}
fn walk_recursive<E>(
module_root: &Path,
dir: &Path,
visitor: &mut dyn FnMut(&Path, u64) -> Result<(), E>,
stats: &mut TreeStats,
) -> Result<(), WalkError<E>> {
let entries = std::fs::read_dir(dir).map_err(|source| {
WalkError::Walk(ModuleWalkError::Io {
path: dir.to_path_buf(),
source,
})
})?;
for entry in entries {
let entry = entry.map_err(|source| {
WalkError::Walk(ModuleWalkError::Io {
path: dir.to_path_buf(),
source,
})
})?;
let name = entry.file_name();
if NON_MODULE_CONTENT.iter().any(|s| *s == name) {
continue;
}
let path = entry.path();
let meta = std::fs::symlink_metadata(&path).map_err(|source| {
WalkError::Walk(ModuleWalkError::Io {
path: path.to_path_buf(),
source,
})
})?;
if meta.file_type().is_symlink() {
handle_symlink(module_root, &path, visitor, stats)?;
continue;
}
if meta.is_dir() {
walk_recursive(module_root, &path, visitor, stats)?;
} else if meta.is_file() {
stats.files += 1;
stats.bytes = stats.bytes.saturating_add(meta.len());
visitor(&path, meta.len()).map_err(WalkError::Visitor)?;
}
}
Ok(())
}
fn handle_symlink<E>(
module_root: &Path,
path: &Path,
visitor: &mut dyn FnMut(&Path, u64) -> Result<(), E>,
stats: &mut TreeStats,
) -> Result<(), WalkError<E>> {
let target = std::fs::canonicalize(path).map_err(|source| {
WalkError::Walk(ModuleWalkError::Io {
path: path.to_path_buf(),
source,
})
})?;
if !target.starts_with(module_root) {
return Err(WalkError::Walk(ModuleWalkError::SymlinkEscapesRoot(
path.display().to_string(),
)));
}
if let Ok(rel) = target.strip_prefix(module_root) {
if rel.to_str().is_none() {
return Err(WalkError::Walk(ModuleWalkError::NonUtf8SymlinkTarget(
path.display().to_string(),
)));
}
let targets_metadata = rel
.components()
.any(|c| NON_MODULE_CONTENT.contains(&c.as_os_str().to_str().unwrap()));
if targets_metadata {
return Err(WalkError::Walk(ModuleWalkError::SymlinkTargetsMetadata(
path.display().to_string(),
)));
}
}
let target_meta = std::fs::metadata(path).map_err(|source| {
WalkError::Walk(ModuleWalkError::Io {
path: path.to_path_buf(),
source,
})
})?;
if target_meta.is_dir() {
return Err(WalkError::Walk(ModuleWalkError::DirectorySymlink(
path.display().to_string(),
)));
}
if target_meta.is_file() {
stats.files += 1;
stats.bytes = stats.bytes.saturating_add(target_meta.len());
visitor(path, target_meta.len()).map_err(WalkError::Visitor)?;
}
Ok(())
}