use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{ensure, Context, Result};
use rayon::prelude::*;
use crate::MAX_DIR_DEPTH;
pub(crate) const PARALLEL_THRESHOLD: usize = 16;
pub fn verify_and_copy_files(src: &Path, dst: &Path) -> Result<()> {
ensure!(
is_safe_path(src)?,
"Source directory is unsafe or inaccessible: {}",
src.display()
);
if !src.exists() {
anyhow::bail!("Source directory does not exist: {}", src.display());
}
if src.is_file() {
verify_file_safety(src)?;
}
fs::create_dir_all(dst).with_context(|| {
format!(
"Failed to create or access destination directory at path: {}",
dst.display()
)
})?;
copy_dir_all(src, dst).with_context(|| {
format!(
"Failed to copy files from source: {} to destination: {}",
src.display(),
dst.display()
)
})?;
Ok(())
}
pub fn verify_and_copy_files_async(src: &Path, dst: &Path) -> Result<()> {
if !src.exists() {
return Err(anyhow::anyhow!(
"Source directory does not exist: {}",
src.display()
));
}
fs::create_dir_all(dst).with_context(|| {
format!(
"Failed to create or access destination directory at path: {}",
dst.display()
)
})?;
copy_directory_recursive(src, dst)
}
fn copy_directory_recursive(src: &Path, dst: &Path) -> Result<()> {
let mut stack = vec![(src.to_path_buf(), dst.to_path_buf(), 0usize)];
while let Some((src_dir, dst_dir, depth)) = stack.pop() {
ensure!(
depth < MAX_DIR_DEPTH,
"Directory nesting exceeds maximum depth of {}: {}",
MAX_DIR_DEPTH,
src_dir.display()
);
for entry in fs::read_dir(&src_dir)? {
let entry = entry?;
copy_entry(&entry, &dst_dir, depth, &mut stack)?;
}
}
Ok(())
}
fn copy_entry(
entry: &fs::DirEntry,
dst_dir: &Path,
depth: usize,
stack: &mut Vec<(PathBuf, PathBuf, usize)>,
) -> Result<()> {
let src_path = entry.path();
let dst_path = dst_dir.join(entry.file_name());
if src_path.is_dir() {
fs::create_dir_all(&dst_path)?;
stack.push((src_path, dst_path, depth + 1));
} else {
verify_file_safety(&src_path)?;
_ = fs::copy(&src_path, &dst_path)?;
}
Ok(())
}
pub fn copy_dir_with_progress(src: &Path, dst: &Path) -> Result<()> {
if !src.exists() {
anyhow::bail!("Source directory does not exist: {}", src.display());
}
fs::create_dir_all(dst).with_context(|| {
format!("Failed to create destination directory: {}", dst.display())
})?;
let mut file_count: u64 = 0;
let mut stack = vec![(src.to_path_buf(), dst.to_path_buf(), 0usize)];
while let Some((src_dir, dst_dir, depth)) = stack.pop() {
ensure!(
depth < MAX_DIR_DEPTH,
"Directory nesting exceeds maximum depth of {}: {}",
MAX_DIR_DEPTH,
src_dir.display()
);
let entries: Vec<_> = fs::read_dir(&src_dir)
.context(format!(
"Failed to read source directory: {}",
src_dir.display()
))?
.collect::<std::io::Result<Vec<_>>>()?;
for entry in &entries {
let src_path = entry.path();
let dst_path = dst_dir.join(entry.file_name());
if src_path.is_dir() {
fs::create_dir_all(&dst_path)?;
stack.push((src_path, dst_path, depth + 1));
} else {
let _ = fs::copy(&src_path, &dst_path)?;
}
file_count += 1;
}
}
eprintln!("Copied {file_count} files");
Ok(())
}
pub fn is_safe_path(path: &Path) -> Result<bool> {
if !path.exists() {
let path_str = path.to_string_lossy();
if path_str.contains("..") {
return Ok(false);
}
return Ok(true); }
let _canonical = path
.canonicalize()
.context(format!("Failed to canonicalize path {}", path.display()))?;
Ok(true)
}
pub fn verify_file_safety(path: &Path) -> Result<()> {
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
let symlink_metadata = path.symlink_metadata().map_err(|e| {
anyhow::anyhow!(
"Failed to get symlink metadata for {}: {}",
path.display(),
e
)
})?;
if symlink_metadata.file_type().is_symlink() {
return Err(anyhow::anyhow!(
"Symlinks are not allowed: {}",
path.display()
));
}
if symlink_metadata.file_type().is_file()
&& symlink_metadata.len() > MAX_FILE_SIZE
{
return Err(anyhow::anyhow!(
"File exceeds maximum allowed size of {} bytes: {}",
MAX_FILE_SIZE,
path.display()
));
}
Ok(())
}
pub fn collect_files_recursive(
dir: &Path,
files: &mut Vec<PathBuf>,
) -> Result<()> {
let mut stack = vec![(dir.to_path_buf(), 0usize)];
while let Some((current_dir, depth)) = stack.pop() {
ensure!(
depth < MAX_DIR_DEPTH,
"Directory nesting exceeds maximum depth of {}: {}",
MAX_DIR_DEPTH,
current_dir.display()
);
for entry in fs::read_dir(¤t_dir)? {
let path = entry?.path();
if path.is_dir() {
stack.push((path, depth + 1));
} else {
files.push(path);
}
}
}
Ok(())
}
pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
let mut stack = vec![(src.to_path_buf(), dst.to_path_buf(), 0usize)];
while let Some((src_dir, dst_dir, depth)) = stack.pop() {
ensure!(
depth < MAX_DIR_DEPTH,
"Directory nesting exceeds maximum depth of {}: {}",
MAX_DIR_DEPTH,
src_dir.display()
);
let entries: Vec<_> =
fs::read_dir(&src_dir)?.collect::<std::io::Result<Vec<_>>>()?;
let (files, subdirs) = partition_entries(&entries, &dst_dir);
copy_files_maybe_parallel(&files, &dst_dir)?;
for (sub_src, sub_dst) in subdirs {
fs::create_dir_all(&sub_dst)?;
stack.push((sub_src, sub_dst, depth + 1));
}
}
Ok(())
}
fn partition_entries<'a>(
entries: &'a [fs::DirEntry],
dst_dir: &Path,
) -> (Vec<&'a fs::DirEntry>, Vec<(PathBuf, PathBuf)>) {
let mut subdirs = Vec::new();
let files: Vec<_> = entries
.iter()
.filter(|entry| {
let path = entry.path();
if path.is_dir() {
subdirs.push((path, dst_dir.join(entry.file_name())));
false
} else {
true
}
})
.collect();
(files, subdirs)
}
fn copy_files_maybe_parallel(
files: &[&fs::DirEntry],
dst_dir: &Path,
) -> Result<()> {
let copy_file = |entry: &&fs::DirEntry| -> Result<()> {
let src_path = entry.path();
let dst_path = dst_dir.join(entry.file_name());
verify_file_safety(&src_path)?;
_ = fs::copy(&src_path, &dst_path)?;
Ok(())
};
if files.len() >= PARALLEL_THRESHOLD {
files.par_iter().try_for_each(copy_file)?;
} else {
files.iter().try_for_each(copy_file)?;
}
Ok(())
}
pub fn copy_dir_all_async(src: &Path, dst: &Path) -> Result<()> {
internal_copy_dir_async(src, dst)
}
fn internal_copy_dir_async(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst)?;
let mut stack = vec![(src.to_path_buf(), dst.to_path_buf(), 0usize)];
while let Some((src_path, dst_path, depth)) = stack.pop() {
ensure!(
depth < MAX_DIR_DEPTH,
"Directory nesting exceeds maximum depth of {}: {}",
MAX_DIR_DEPTH,
src_path.display()
);
for entry in fs::read_dir(&src_path)? {
let entry = entry?;
let src_entry = entry.path();
let dst_entry = dst_path.join(entry.file_name());
if src_entry.is_dir() {
fs::create_dir_all(&dst_entry)?;
stack.push((src_entry, dst_entry, depth + 1));
} else {
verify_file_safety(&src_entry)?;
_ = fs::copy(&src_entry, &dst_entry)?;
}
}
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn copy_dir_all_copies_files() {
let src = tempdir().unwrap();
let dst = tempdir().unwrap();
fs::write(src.path().join("a.txt"), "hello").unwrap();
fs::write(src.path().join("b.txt"), "world").unwrap();
copy_dir_all(src.path(), dst.path()).unwrap();
assert_eq!(
fs::read_to_string(dst.path().join("a.txt")).unwrap(),
"hello"
);
assert_eq!(
fs::read_to_string(dst.path().join("b.txt")).unwrap(),
"world"
);
}
#[test]
fn copy_dir_all_nested_preserves_structure() {
let src = tempdir().unwrap();
let dst = tempdir().unwrap();
let nested = src.path().join("sub").join("deep");
fs::create_dir_all(&nested).unwrap();
fs::write(nested.join("file.txt"), "nested content").unwrap();
fs::write(src.path().join("root.txt"), "root").unwrap();
copy_dir_all(src.path(), dst.path()).unwrap();
assert_eq!(
fs::read_to_string(dst.path().join("sub/deep/file.txt")).unwrap(),
"nested content"
);
assert_eq!(
fs::read_to_string(dst.path().join("root.txt")).unwrap(),
"root"
);
}
#[test]
fn copy_dir_all_nonexistent_src_returns_error() {
let dst = tempdir().unwrap();
let fake_src = dst.path().join("does_not_exist");
let result = copy_dir_all(&fake_src, dst.path());
assert!(result.is_err());
}
#[test]
fn is_safe_path_normal_relative() {
let tmp = tempdir().unwrap();
let file = tmp.path().join("safe.txt");
fs::write(&file, "ok").unwrap();
assert!(is_safe_path(&file).unwrap());
}
#[test]
fn is_safe_path_with_dotdot_nonexistent() {
let path = Path::new("some/../../../etc/passwd");
assert!(!is_safe_path(path).unwrap());
}
#[test]
fn is_safe_path_with_dotdot_existing() {
let tmp = tempdir().unwrap();
let safe = tmp.path().join("a");
fs::create_dir_all(&safe).unwrap();
let dotdot_path = safe.join("..");
assert!(is_safe_path(&dotdot_path).unwrap());
}
#[test]
fn is_safe_path_absolute_existing() {
let tmp = tempdir().unwrap();
let file = tmp.path().join("abs.txt");
fs::write(&file, "data").unwrap();
assert!(is_safe_path(&file).unwrap());
}
#[test]
fn verify_file_safety_valid_file() {
let tmp = tempdir().unwrap();
let file = tmp.path().join("ok.txt");
fs::write(&file, "small file").unwrap();
assert!(verify_file_safety(&file).is_ok());
}
#[test]
fn verify_file_safety_nonexistent() {
let tmp = tempdir().unwrap();
let missing = tmp.path().join("nope.txt");
assert!(verify_file_safety(&missing).is_err());
}
#[test]
fn verify_file_safety_directory() {
let tmp = tempdir().unwrap();
assert!(verify_file_safety(tmp.path()).is_ok());
}
#[test]
fn collect_files_recursive_finds_all() {
let tmp = tempdir().unwrap();
let sub = tmp.path().join("sub");
fs::create_dir_all(&sub).unwrap();
fs::write(tmp.path().join("a.md"), "").unwrap();
fs::write(sub.join("b.md"), "").unwrap();
fs::write(sub.join("c.txt"), "").unwrap();
let mut files = Vec::new();
collect_files_recursive(tmp.path(), &mut files).unwrap();
assert_eq!(files.len(), 3);
}
#[test]
fn collect_files_recursive_empty_dir() {
let tmp = tempdir().unwrap();
let mut files = Vec::new();
collect_files_recursive(tmp.path(), &mut files).unwrap();
assert!(files.is_empty());
}
#[test]
fn collect_files_recursive_only_files_not_dirs() {
let tmp = tempdir().unwrap();
let sub = tmp.path().join("subdir");
fs::create_dir_all(&sub).unwrap();
fs::write(sub.join("only.txt"), "data").unwrap();
let mut files = Vec::new();
collect_files_recursive(tmp.path(), &mut files).unwrap();
assert_eq!(files.len(), 1);
assert!(files[0].ends_with("only.txt"));
}
#[test]
fn verify_and_copy_files_end_to_end() {
let src = tempdir().unwrap();
let dst = tempdir().unwrap();
let target = dst.path().join("output");
fs::write(src.path().join("page.html"), "<h1>Hi</h1>").unwrap();
verify_and_copy_files(src.path(), &target).unwrap();
assert_eq!(
fs::read_to_string(target.join("page.html")).unwrap(),
"<h1>Hi</h1>"
);
}
#[test]
fn copy_dir_with_progress_smoke() {
let src = tempdir().unwrap();
let dst = tempdir().unwrap();
fs::write(src.path().join("f.txt"), "data").unwrap();
copy_dir_with_progress(src.path(), &dst.path().join("out")).unwrap();
}
#[test]
fn copy_dir_with_progress_nonexistent_src() {
let tmp = tempdir().unwrap();
let fake = tmp.path().join("missing");
let result = copy_dir_with_progress(&fake, tmp.path());
assert!(result.is_err());
}
}