#![doc = include_str!("../README.md")]
#![doc(
html_favicon_url = "https://cloudcdn.pro/shokunin/images/favicon.ico",
html_logo_url = "https://cloudcdn.pro/shokunin/images/logos/shokunin.svg",
html_root_url = "https://docs.rs/ssg"
)]
#![crate_name = "ssg"]
#![crate_type = "lib"]
use std::{
fs::{self, File},
io::Write,
path::{Path, PathBuf},
};
use crate::cmd::{Cli, SsgConfig};
use anyhow::{anyhow, ensure, Context, Result};
use dtt::datetime::DateTime;
use http_handle::Server;
use indicatif::{ProgressBar, ProgressStyle};
use log::{info, LevelFilter};
use rayon::prelude::*;
use staticdatagen::compile;
use tokio::fs as async_fs;
pub mod cache;
pub mod cmd;
pub mod livereload;
pub mod plugin;
pub mod plugins;
pub mod process;
pub mod schema;
pub mod search;
pub mod seo;
pub mod stream;
pub mod watch;
pub use staticdatagen;
#[derive(Debug, Clone)]
pub struct Paths {
pub site: PathBuf,
pub content: PathBuf,
pub build: PathBuf,
pub template: PathBuf,
}
impl Paths {
pub fn builder() -> PathsBuilder {
PathsBuilder::default()
}
pub fn default_paths() -> Self {
Self {
site: PathBuf::from("public"),
content: PathBuf::from("content"),
build: PathBuf::from("build"),
template: PathBuf::from("templates"),
}
}
}
impl Paths {
pub fn validate(&self) -> Result<()> {
for (name, path) in [
("site", &self.site),
("content", &self.content),
("build", &self.build),
("template", &self.template),
] {
let path_str = path.to_string_lossy();
if path_str.contains("..") {
anyhow::bail!(
"{} path contains directory traversal: {}",
name,
path.display()
);
}
if path_str.contains("//") {
anyhow::bail!(
"{} path contains invalid double slashes: {}",
name,
path.display()
);
}
if path.exists() {
let metadata = path.symlink_metadata().context(
format!("Failed to get metadata for {}", name),
)?;
if metadata.file_type().is_symlink() {
anyhow::bail!(
"{} path is a symlink which is not allowed: {}",
name,
path.display()
);
}
}
}
Ok(())
}
}
#[derive(Debug, Default, Clone)]
pub struct PathsBuilder {
pub site: Option<PathBuf>,
pub content: Option<PathBuf>,
pub build: Option<PathBuf>,
pub template: Option<PathBuf>,
}
impl PathsBuilder {
pub fn site<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.site = Some(path.into());
self
}
pub fn content<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.content = Some(path.into());
self
}
pub fn build_dir<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.build = Some(path.into());
self
}
pub fn template<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.template = Some(path.into());
self
}
pub fn relative_to<P: AsRef<Path>>(self, base: P) -> Self {
let base = base.as_ref();
self.site(base.join("public"))
.content(base.join("content"))
.build_dir(base.join("build"))
.template(base.join("templates"))
}
pub fn build(self) -> Result<Paths> {
let paths = Paths {
site: self.site.unwrap_or_else(|| PathBuf::from("public")),
content: self
.content
.unwrap_or_else(|| PathBuf::from("content")),
build: self.build.unwrap_or_else(|| PathBuf::from("build")),
template: self
.template
.unwrap_or_else(|| PathBuf::from("templates")),
};
paths.validate()?;
Ok(paths)
}
}
const DEFAULT_LOG_LEVEL: &str = "info";
const ENV_LOG_LEVEL: &str = "SSG_LOG_LEVEL";
pub const MAX_DIR_DEPTH: usize = 128;
const PARALLEL_THRESHOLD: usize = 16;
fn initialize_logging() -> Result<()> {
let log_level = std::env::var(ENV_LOG_LEVEL)
.unwrap_or_else(|_| DEFAULT_LOG_LEVEL.to_string());
let level = match log_level.to_lowercase().as_str() {
"error" => LevelFilter::Error,
"warn" => LevelFilter::Warn,
"info" => LevelFilter::Info,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
_ => LevelFilter::Info,
};
env_logger::Builder::new()
.filter_level(level)
.format_timestamp_millis()
.init();
info!("Logging initialized at level: {}", log_level);
Ok(())
}
fn resolve_build_and_site_dirs(
config: &SsgConfig,
) -> (PathBuf, PathBuf) {
let site_dir = config
.serve_dir
.clone()
.unwrap_or_else(|| config.output_dir.clone());
let build_dir = if site_dir == config.output_dir {
config.output_dir.with_extension("build-tmp")
} else {
config.output_dir.clone()
};
(build_dir, site_dir)
}
pub async fn run() -> Result<()> {
initialize_logging()?;
info!("Starting site generation process");
let matches = Cli::build().get_matches();
let config = SsgConfig::from_matches(&matches)?;
println!("Configuration loaded: {:?}", config);
let (build_dir, site_dir) = resolve_build_and_site_dirs(&config);
compile_site(&build_dir, &config.content_dir, &site_dir, &config.template_dir)?;
serve_site(&site_dir)
}
pub fn serve_site(site_dir: &Path) -> Result<()> {
let root = site_dir
.to_str()
.ok_or_else(|| {
anyhow!(
"Site directory path contains invalid UTF-8: {:?}",
site_dir
)
})?
.to_string();
let addr = format!("{}:{}", cmd::DEFAULT_HOST, cmd::DEFAULT_PORT);
let server = Server::new(&addr, &root);
let _ = server.start();
Ok(())
}
pub fn compile_site(
build_dir: &Path,
content_dir: &Path,
site_dir: &Path,
template_dir: &Path,
) -> Result<()> {
compile(build_dir, content_dir, site_dir, template_dir).map_err(|e| {
eprintln!(" Error compiling site: {:?}", e);
anyhow!("Failed to compile site: {:?}", e)
})
}
pub fn verify_and_copy_files(src: &Path, dst: &Path) -> Result<()> {
ensure!(
is_safe_path(src)?,
"Source directory is unsafe or inaccessible: {:?}",
src
);
if !src.exists() {
anyhow::bail!("Source directory does not exist: {:?}", src);
}
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))?;
copy_dir_all(src, dst).with_context(|| {
format!("Failed to copy files from source: {:?} to destination: {:?}", src, dst)
})?;
Ok(())
}
pub async fn verify_and_copy_files_async(
src: &Path,
dst: &Path,
) -> Result<()> {
if !src.exists() {
return Err(anyhow::anyhow!(
"Source directory does not exist: {:?}",
src
));
}
async_fs::create_dir_all(dst).await.with_context(|| format!(
"Failed to create or access destination directory at path: {:?}",
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 mut entries = async_fs::read_dir(&src_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let src_path = entry.path();
let dst_path = dst_dir.join(entry.file_name());
if src_path.is_dir() {
async_fs::create_dir_all(&dst_path).await?;
stack.push((src_path, dst_path, depth + 1));
} else {
verify_file_safety(&src_path)?;
_ = async_fs::copy(&src_path, &dst_path).await?;
}
}
}
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 progress_bar = ProgressBar::new_spinner();
progress_bar.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] {pos} files {msg}")
.map_err(|e| anyhow::anyhow!("Invalid progress bar template: {}", e))?
.progress_chars("#>-"),
);
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)?;
}
progress_bar.inc(1);
}
}
progress_bar.finish_with_message("Copy complete.");
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 create_log_file(file_path: &str) -> Result<File> {
File::create(file_path).context("Failed to create log file")
}
pub fn log_initialization(
log_file: &mut File,
date: &DateTime,
) -> Result<()> {
writeln!(
log_file,
"[{}] INFO process: System initialization complete",
date
)
.context("Failed to write banner log")
}
pub fn log_arguments(
log_file: &mut File,
date: &DateTime,
) -> Result<()> {
writeln!(
log_file,
"[{}] INFO process: Arguments processed",
date
)
.context("Failed to write arguments log")
}
pub fn create_directories(paths: &Paths) -> Result<()> {
for (name, path) in [
("content", &paths.content),
("build", &paths.build),
("site", &paths.site),
("template", &paths.template),
] {
fs::create_dir_all(path).with_context(|| {
format!("Failed to create or access {} directory at path: {:?}", name, path)
})?;
}
if !is_safe_path(&paths.content)?
|| !is_safe_path(&paths.build)?
|| !is_safe_path(&paths.site)?
|| !is_safe_path(&paths.template)?
{
anyhow::bail!("One or more paths are unsafe. Ensure paths do not contain '..' and are accessible.");
}
Ok(())
}
pub async fn handle_server(
log_file: &mut File,
date: &DateTime,
paths: &Paths,
serve_dir: &PathBuf,
) -> Result<()> {
writeln!(
log_file,
"[{}] INFO process: Server initialization",
date
)?;
prepare_serve_dir(paths, serve_dir).await?;
println!("\nStarting server at http://127.0.0.1:8000");
println!("Serving content from: {}", serve_dir.display());
warp::serve(warp::fs::dir(serve_dir.clone()))
.run(([127, 0, 0, 1], 8000))
.await;
Ok(())
}
pub async fn prepare_serve_dir(
paths: &Paths,
serve_dir: &PathBuf,
) -> Result<()> {
async_fs::create_dir_all(serve_dir)
.await
.context("Failed to create serve directory")?;
println!("Setting up server...");
println!("Source: {}", paths.site.display());
println!("Serving from: {}", serve_dir.display());
if serve_dir != &paths.site {
verify_and_copy_files_async(&paths.site, serve_dir).await?;
}
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 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();
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)?;
}
for (sub_src, sub_dst) in subdirs {
fs::create_dir_all(&sub_dst)?;
stack.push((sub_src, sub_dst, depth + 1));
}
}
Ok(())
}
#[cfg(feature = "async")]
pub async fn copy_dir_all_async(src: &Path, dst: &Path) -> Result<()> {
internal_copy_dir_async(src, dst).await
}
#[cfg(feature = "async")]
async fn internal_copy_dir_async(src: &Path, dst: &Path) -> Result<()> {
tokio::fs::create_dir_all(dst).await?;
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()
);
let mut entries = tokio::fs::read_dir(&src_path).await?;
while let Some(entry) = entries.next_entry().await? {
let src_entry = entry.path();
let dst_entry = dst_path.join(entry.file_name());
if src_entry.is_dir() {
tokio::fs::create_dir_all(&dst_entry).await?;
stack.push((src_entry, dst_entry, depth + 1));
} else {
verify_file_safety(&src_entry)?;
_ = tokio::fs::copy(&src_entry, &dst_entry).await?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cmd::Cli;
use anyhow::Result;
use std::env;
use std::{
fs::{self, File},
path::PathBuf,
};
use tempfile::{tempdir, TempDir};
#[test]
fn test_create_log_file_success() -> Result<()> {
let temp_dir = tempdir()?;
let log_file_path = temp_dir.path().join("test.log");
let log_file =
create_log_file(log_file_path.to_str().unwrap())?;
assert!(log_file.metadata()?.is_file());
Ok(())
}
#[test]
fn test_log_arguments() -> Result<()> {
let temp_dir = tempdir()?;
let log_file_path = temp_dir.path().join("args_log.log");
let mut log_file = File::create(&log_file_path)?;
let date = DateTime::new();
log_arguments(&mut log_file, &date)?;
let log_content = fs::read_to_string(log_file_path)?;
assert!(log_content.contains("process"));
Ok(())
}
#[test]
fn test_create_directories_success() -> Result<()> {
let temp_dir = tempdir()?;
let base_path = temp_dir.path().to_path_buf();
let paths = Paths {
site: base_path.join("public"),
content: base_path.join("content"),
build: base_path.join("build"),
template: base_path.join("templates"),
};
create_directories(&paths)?;
assert!(paths.site.exists());
assert!(paths.content.exists());
assert!(paths.build.exists());
assert!(paths.template.exists());
Ok(())
}
#[test]
fn test_create_directories_failure() {
let invalid_paths = Paths {
site: PathBuf::from("/invalid/site"),
content: PathBuf::from("/invalid/content"),
build: PathBuf::from("/invalid/build"),
template: PathBuf::from("/invalid/template"),
};
let result = create_directories(&invalid_paths);
assert!(result.is_err());
}
#[test]
fn test_copy_dir_all() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let src_file = src_dir.path().join("test_file.txt");
_ = File::create(&src_file)?;
let result = copy_dir_all(src_dir.path(), dst_dir.path());
assert!(result.is_ok());
assert!(dst_dir.path().join("test_file.txt").exists());
Ok(())
}
#[test]
fn test_verify_and_copy_files_success() -> Result<()> {
let temp_dir = tempdir()?;
let base_path = temp_dir.path().to_path_buf();
let src_dir = base_path.join("src");
fs::create_dir_all(&src_dir)?;
let test_file = src_dir.join("test_file.txt");
fs::write(&test_file, "test content")?;
let dst_dir = base_path.join("dst");
verify_and_copy_files(&src_dir, &dst_dir)?;
assert!(dst_dir.join("test_file.txt").exists());
Ok(())
}
#[test]
fn test_verify_and_copy_files_failure() {
let src_dir = PathBuf::from("/invalid/src");
let dst_dir = PathBuf::from("/invalid/dst");
let result = verify_and_copy_files(&src_dir, &dst_dir);
assert!(result.is_err());
}
#[tokio::test]
async fn test_handle_server_failure() {
let temp_dir = tempdir().unwrap();
let log_file_path = temp_dir.path().join("server_log.log");
let mut log_file = File::create(&log_file_path).unwrap();
let paths = Paths {
site: PathBuf::from("/invalid/site"),
content: PathBuf::from("/invalid/content"),
build: PathBuf::from("/invalid/build"),
template: PathBuf::from("/invalid/template"),
};
let serve_dir = temp_dir.path().join("serve");
let date = DateTime::new();
let result =
handle_server(&mut log_file, &date, &paths, &serve_dir);
assert!(result.await.is_err());
}
#[test]
fn test_is_safe_path_safe() -> Result<()> {
let temp_dir = tempdir()?;
let safe_path = temp_dir.path().to_path_buf().join("safe_path");
fs::create_dir_all(&safe_path)?;
let absolute_safe_path = safe_path.canonicalize()?;
assert!(is_safe_path(&absolute_safe_path)?);
Ok(())
}
#[test]
fn test_create_directories_partial_failure() {
let temp_dir = tempdir().unwrap();
let valid_path = temp_dir.path().join("valid_dir");
let invalid_path = PathBuf::from("/invalid/path");
let paths = Paths {
site: valid_path,
content: invalid_path.clone(),
build: temp_dir.path().join("build"),
template: temp_dir.path().join("template"),
};
let result = create_directories(&paths);
assert!(result.is_err());
}
#[test]
fn test_copy_dir_all_nested() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let nested_dir = src_dir.path().join("nested_dir");
fs::create_dir(&nested_dir)?;
let nested_file = nested_dir.join("nested_file.txt");
_ = File::create(&nested_file)?;
copy_dir_all(src_dir.path(), dst_dir.path())?;
assert!(dst_dir
.path()
.join("nested_dir/nested_file.txt")
.exists());
Ok(())
}
#[test]
fn test_verify_and_copy_files_missing_source() {
let src_path = PathBuf::from("/non_existent_dir");
let dst_dir = tempdir().unwrap();
let result = verify_and_copy_files(&src_path, dst_dir.path());
assert!(result.is_err());
}
#[tokio::test]
async fn test_handle_server_missing_serve_dir() {
let temp_dir = tempdir().unwrap();
let log_file_path = temp_dir.path().join("server_log.log");
let mut log_file = File::create(&log_file_path).unwrap();
let paths = Paths {
site: temp_dir.path().join("site"),
content: temp_dir.path().join("content"),
build: temp_dir.path().join("build"),
template: temp_dir.path().join("template"),
};
let non_existent_serve_dir =
PathBuf::from("/non_existent_serve_dir");
let binding = DateTime::new();
let result = handle_server(
&mut log_file,
&binding,
&paths,
&non_existent_serve_dir,
);
assert!(result.await.is_err());
}
#[test]
fn test_collect_files_recursive_empty() -> Result<()> {
let temp_dir = tempdir()?;
let mut files = Vec::new();
collect_files_recursive(temp_dir.path(), &mut files)?;
assert!(files.is_empty());
Ok(())
}
#[test]
fn test_print_banner() {
Cli::print_banner();
}
#[test]
fn test_collect_files_recursive_with_nested_directories(
) -> Result<()> {
let temp_dir = tempdir()?;
let nested_dir = temp_dir.path().join("nested_dir");
fs::create_dir(&nested_dir)?;
let nested_file = nested_dir.join("nested_file.txt");
_ = File::create(&nested_file)?;
let mut files = Vec::new();
collect_files_recursive(temp_dir.path(), &mut files)?;
assert!(files.contains(&nested_file));
assert_eq!(files.len(), 1);
Ok(())
}
#[tokio::test]
async fn test_handle_server_start_message() -> Result<()> {
let temp_dir = tempdir()?;
let log_file_path = temp_dir.path().join("server_log.log");
let mut log_file = File::create(&log_file_path)?;
let paths = Paths {
site: temp_dir.path().join("site"),
content: temp_dir.path().join("content"),
build: temp_dir.path().join("build"),
template: temp_dir.path().join("template"),
};
let serve_dir = temp_dir.path().join("serve");
fs::create_dir_all(&serve_dir)?;
assert!(
serve_dir.exists(),
"Expected serve directory to be created"
);
let date = DateTime::new();
let result =
handle_server(&mut log_file, &date, &paths, &serve_dir);
assert!(
result.await.is_err(),
"Expected handle_server to fail without valid setup"
);
Ok(())
}
#[cfg(any(unix, windows))]
#[test]
fn test_verify_file_safety_symlink() -> Result<()> {
let temp_dir = tempdir()?;
let file_path = temp_dir.path().join("test.txt");
let symlink_path = temp_dir.path().join("test_link.txt");
fs::write(&file_path, "test content")?;
#[cfg(unix)]
std::os::unix::fs::symlink(&file_path, &symlink_path)?;
#[cfg(windows)]
std::os::windows::fs::symlink_file(&file_path, &symlink_path)?;
println!("File exists: {}", file_path.exists());
println!("Symlink exists: {}", symlink_path.exists());
println!(
"Is symlink: {}",
symlink_path.symlink_metadata()?.file_type().is_symlink()
);
let result = verify_file_safety(&symlink_path);
println!("Result: {:?}", result);
assert!(
result.is_err(),
"Expected error for symlink, got success"
);
let err = result.unwrap_err();
println!("Error message: {}", err);
assert!(
err.to_string().contains("Symlinks are not allowed"),
"Unexpected error message: {}",
err
);
Ok(())
}
#[test]
fn test_verify_file_safety_size() -> Result<()> {
let temp_dir = tempdir()?;
let large_file_path = temp_dir.path().join("large.txt");
let file = File::create(&large_file_path)?;
file.set_len(11 * 1024 * 1024)?;
let result = verify_file_safety(&large_file_path);
assert!(result.is_err(), "Expected error, got: {:?}", result);
Ok(())
}
#[test]
fn test_verify_file_safety_regular() -> Result<()> {
let temp_dir = tempdir()?;
let file_path = temp_dir.path().join("regular.txt");
fs::write(&file_path, "test content")?;
assert!(verify_file_safety(&file_path).is_ok());
Ok(())
}
#[tokio::test]
async fn test_copy_empty_directory_async() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let result =
copy_dir_all_async(src_dir.path(), dst_dir.path()).await;
assert!(result.is_ok());
assert!(dst_dir.path().exists());
Ok(())
}
#[tokio::test]
async fn test_copy_single_file_async() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let test_file = src_dir.path().join("test.txt");
fs::write(&test_file, "test content")?;
copy_dir_all_async(src_dir.path(), dst_dir.path()).await?;
let copied_file = dst_dir.path().join("test.txt");
assert!(copied_file.exists());
assert_eq!(fs::read_to_string(copied_file)?, "test content");
Ok(())
}
#[tokio::test]
async fn test_copy_nested_directories_async() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let nested_dir = src_dir.path().join("nested");
fs::create_dir(&nested_dir)?;
fs::write(src_dir.path().join("root.txt"), "root content")?;
fs::write(nested_dir.join("nested.txt"), "nested content")?;
copy_dir_all_async(src_dir.path(), dst_dir.path()).await?;
assert!(dst_dir.path().join("nested").exists());
assert!(dst_dir.path().join("root.txt").exists());
assert!(dst_dir.path().join("nested/nested.txt").exists());
assert_eq!(
fs::read_to_string(dst_dir.path().join("root.txt"))?,
"root content"
);
assert_eq!(
fs::read_to_string(
dst_dir.path().join("nested/nested.txt")
)?,
"nested content"
);
Ok(())
}
#[tokio::test]
async fn test_copy_with_symlink_async() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let file_path = src_dir.path().join("original.txt");
fs::write(&file_path, "original content")?;
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let symlink_path = src_dir.path().join("link.txt");
symlink(&file_path, &symlink_path)?;
}
#[cfg(windows)]
{
use std::os::windows::fs::symlink_file;
let symlink_path = src_dir.path().join("link.txt");
symlink_file(&file_path, &symlink_path)?;
}
let result =
copy_dir_all_async(src_dir.path(), dst_dir.path()).await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_copy_large_file_async() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let large_file = src_dir.path().join("large.txt");
let file = fs::File::create(&large_file)?;
file.set_len(11 * 1024 * 1024)?;
let result =
copy_dir_all_async(src_dir.path(), dst_dir.path()).await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_copy_invalid_destination_async() -> Result<()> {
let src_dir = tempdir()?;
let invalid_dst = PathBuf::from("/nonexistent/path");
let result =
copy_dir_all_async(src_dir.path(), &invalid_dst).await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_concurrent_copy_async() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
for i in 0..5 {
fs::write(
src_dir.path().join(format!("file{}.txt", i)),
format!("content {}", i),
)?;
}
copy_dir_all_async(src_dir.path(), dst_dir.path()).await?;
for i in 0..5 {
let copied_file =
dst_dir.path().join(format!("file{}.txt", i));
assert!(copied_file.exists());
assert_eq!(
fs::read_to_string(copied_file)?,
format!("content {}", i)
);
}
Ok(())
}
#[tokio::test]
async fn test_max_directory_depth_async() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let max_depth = 5;
let mut current_dir = src_dir.path().to_path_buf();
for i in 0..max_depth {
current_dir = current_dir.join(format!("level{}", i));
fs::create_dir(¤t_dir)?;
fs::write(
current_dir.join("file.txt"),
format!("content level {}", i),
)?;
}
copy_dir_all_async(src_dir.path(), dst_dir.path()).await?;
current_dir = dst_dir.path().to_path_buf();
for i in 0..max_depth {
current_dir = current_dir.join(format!("level{}", i));
assert!(current_dir.exists());
assert!(current_dir.join("file.txt").exists());
assert_eq!(
fs::read_to_string(current_dir.join("file.txt"))?,
format!("content level {}", i)
);
}
Ok(())
}
#[tokio::test]
async fn test_verify_and_copy_files_async_missing_source(
) -> Result<()> {
let temp_dir = tempdir()?;
let src_dir = temp_dir.path().join("nonexistent");
let dst_dir = temp_dir.path().join("dst");
let error = verify_and_copy_files_async(&src_dir, &dst_dir)
.await
.unwrap_err()
.to_string();
assert!(
error.contains("does not exist"),
"Expected error message about non-existent source, got: {}",
error
);
Ok(())
}
#[test]
fn test_paths_builder_default() -> Result<()> {
let paths = Paths::builder().build()?;
assert_eq!(paths.site, PathBuf::from("public"));
assert_eq!(paths.content, PathBuf::from("content"));
assert_eq!(paths.build, PathBuf::from("build"));
assert_eq!(paths.template, PathBuf::from("templates"));
Ok(())
}
#[test]
fn test_resolve_build_and_site_dirs_without_serve_dir() {
let mut config = SsgConfig::default();
config.output_dir = PathBuf::from("docs");
config.serve_dir = None;
let (build_dir, site_dir) =
resolve_build_and_site_dirs(&config);
assert_eq!(site_dir, PathBuf::from("docs"));
assert_eq!(build_dir, PathBuf::from("docs.build-tmp"));
assert_ne!(build_dir, site_dir);
}
#[test]
fn test_resolve_build_and_site_dirs_with_distinct_serve_dir() {
let mut config = SsgConfig::default();
config.output_dir = PathBuf::from("docs");
config.serve_dir = Some(PathBuf::from("public"));
let (build_dir, site_dir) =
resolve_build_and_site_dirs(&config);
assert_eq!(build_dir, PathBuf::from("docs"));
assert_eq!(site_dir, PathBuf::from("public"));
assert_ne!(build_dir, site_dir);
}
#[test]
fn test_resolve_build_and_site_dirs_with_same_serve_and_output_dir()
{
let mut config = SsgConfig::default();
config.output_dir = PathBuf::from("docs");
config.serve_dir = Some(PathBuf::from("docs"));
let (build_dir, site_dir) =
resolve_build_and_site_dirs(&config);
assert_eq!(site_dir, PathBuf::from("docs"));
assert_eq!(build_dir, PathBuf::from("docs.build-tmp"));
assert_ne!(build_dir, site_dir);
}
#[test]
fn test_paths_builder_custom() -> Result<()> {
let temp_dir = tempdir()?;
let paths = Paths::builder()
.site(temp_dir.path().join("custom_public"))
.content(temp_dir.path().join("custom_content"))
.build_dir(temp_dir.path().join("custom_build"))
.template(temp_dir.path().join("custom_templates"))
.build()?;
assert_eq!(paths.site, temp_dir.path().join("custom_public"));
assert_eq!(
paths.content,
temp_dir.path().join("custom_content")
);
assert_eq!(paths.build, temp_dir.path().join("custom_build"));
assert_eq!(
paths.template,
temp_dir.path().join("custom_templates")
);
Ok(())
}
#[test]
fn test_paths_builder_relative() -> Result<()> {
let temp_dir = tempdir()?;
fs::create_dir_all(temp_dir.path().join("public"))?;
fs::create_dir_all(temp_dir.path().join("content"))?;
fs::create_dir_all(temp_dir.path().join("build"))?;
fs::create_dir_all(temp_dir.path().join("templates"))?;
let paths =
Paths::builder().relative_to(temp_dir.path()).build()?;
assert_eq!(paths.site, temp_dir.path().join("public"));
assert_eq!(paths.content, temp_dir.path().join("content"));
assert_eq!(paths.build, temp_dir.path().join("build"));
assert_eq!(paths.template, temp_dir.path().join("templates"));
Ok(())
}
#[test]
fn test_paths_validation() -> Result<()> {
let result = Paths::builder().site("../invalid").build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("directory traversal"),
"Expected error about directory traversal"
);
let result = Paths::builder().site("invalid//path").build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("invalid double slashes"),
"Expected error about invalid double slashes"
);
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let temp_dir = tempdir()?;
let real_path = temp_dir.path().join("real");
let symlink_path = temp_dir.path().join("symlink");
fs::create_dir(&real_path)?;
symlink(&real_path, &symlink_path)?;
let result = Paths::builder().site(symlink_path).build();
assert!(result.is_err());
assert!(
result.unwrap_err().to_string().contains("symlink"),
"Expected error about symlinks"
);
}
Ok(())
}
#[test]
fn test_paths_default_paths() {
let paths = Paths::default_paths();
assert_eq!(paths.site, PathBuf::from("public"));
assert_eq!(paths.content, PathBuf::from("content"));
assert_eq!(paths.build, PathBuf::from("build"));
assert_eq!(paths.template, PathBuf::from("templates"));
}
#[test]
fn test_paths_nonexistent_valid() -> Result<()> {
let temp_dir = tempdir()?;
let valid_path = temp_dir.path().join("new_directory");
let paths =
Paths::builder().site(valid_path.clone()).build()?;
assert_eq!(paths.site, valid_path);
Ok(())
}
#[test]
fn test_initialize_logging_with_custom_level() -> Result<()> {
env::set_var(ENV_LOG_LEVEL, "debug");
assert!(initialize_logging().is_ok());
env::remove_var(ENV_LOG_LEVEL);
Ok(())
}
#[test]
fn test_paths_builder_with_all_invalid_paths() -> Result<()> {
let result = Paths::builder()
.site("../invalid")
.content("content//invalid")
.build_dir("build/../invalid")
.template("template//invalid")
.build();
assert!(result.is_err());
Ok(())
}
#[test]
fn test_paths_builder_clone() {
let builder = PathsBuilder::default();
let cloned = builder.clone();
assert!(cloned.site.is_none());
assert!(cloned.content.is_none());
assert!(cloned.build.is_none());
assert!(cloned.template.is_none());
}
#[test]
fn test_paths_clone() -> Result<()> {
let paths = Paths::default_paths();
let cloned = paths.clone();
assert_eq!(paths.site, cloned.site);
assert_eq!(paths.content, cloned.content);
assert_eq!(paths.build, cloned.build);
assert_eq!(paths.template, cloned.template);
Ok(())
}
#[tokio::test]
async fn test_async_copy_with_empty_source() -> Result<()> {
let temp_dir = tempdir()?;
let src_dir = temp_dir.path().join("empty_src");
let dst_dir = temp_dir.path().join("empty_dst");
fs::create_dir(&src_dir)?;
let result =
verify_and_copy_files_async(&src_dir, &dst_dir).await;
assert!(result.is_ok());
assert!(dst_dir.exists());
Ok(())
}
#[test]
fn test_paths_validation_all_aspects() -> Result<()> {
let temp_dir = tempdir()?;
let result = Paths::builder()
.site(temp_dir.path().join("site"))
.content(temp_dir.path().join("content"))
.build_dir(temp_dir.path().join("build"))
.template(temp_dir.path().join("template"))
.build();
assert!(result.is_ok());
let result = Paths::builder()
.site("../site")
.content("content//test")
.build_dir("build/../../test")
.template("template//test")
.build();
assert!(result.is_err());
Ok(())
}
#[test]
fn test_log_initialization_with_empty_log_file() -> Result<()> {
let temp_dir = tempdir()?;
let log_path = temp_dir.path().join("empty.log");
let mut log_file = File::create(&log_path)?;
let date = DateTime::new();
log_initialization(&mut log_file, &date)?;
let content = fs::read_to_string(&log_path)?;
assert!(!content.is_empty());
assert!(content.contains("process"));
Ok(())
}
#[tokio::test]
async fn test_verify_and_copy_files_async_with_nested_empty_dirs(
) -> Result<()> {
let temp_dir = tempdir()?;
let src_dir = temp_dir.path().join("src");
let dst_dir = temp_dir.path().join("dst");
fs::create_dir_all(src_dir.join("a/b/c"))?;
fs::create_dir_all(src_dir.join("d/e/f"))?;
verify_and_copy_files_async(&src_dir, &dst_dir).await?;
assert!(dst_dir.join("a/b/c").exists());
assert!(dst_dir.join("d/e/f").exists());
Ok(())
}
#[test]
fn test_validate_nonexistent_paths() -> Result<()> {
let paths = Paths {
site: PathBuf::from("nonexistent/site"),
content: PathBuf::from("nonexistent/content"),
build: PathBuf::from("nonexistent/build"),
template: PathBuf::from("nonexistent/template"),
};
assert!(paths.validate().is_ok());
Ok(())
}
#[tokio::test]
async fn test_copy_dir_all_async_with_empty_dirs() -> Result<()> {
let temp_dir = tempdir()?;
let src_dir = temp_dir.path().join("src");
let dst_dir = temp_dir.path().join("dst");
fs::create_dir_all(src_dir.join("empty1"))?;
fs::create_dir_all(src_dir.join("empty2/empty3"))?;
copy_dir_all_async(&src_dir, &dst_dir).await?;
assert!(dst_dir.join("empty1").exists());
assert!(dst_dir.join("empty2/empty3").exists());
Ok(())
}
#[test]
fn test_log_level_from_env() {
let original_value = env::var(ENV_LOG_LEVEL).ok();
fn get_processed_log_level() -> String {
let log_level = env::var(ENV_LOG_LEVEL)
.unwrap_or_else(|_| DEFAULT_LOG_LEVEL.to_string());
match log_level.to_lowercase().as_str() {
"error" => "error",
"warn" => "warn",
"info" => "info",
"debug" => "debug",
"trace" => "trace",
_ => "info", }
.to_string()
}
let test_levels = vec![
("DEBUG", "debug"),
("ERROR", "error"),
("WARN", "warn"),
("INFO", "info"),
("TRACE", "trace"),
("INVALID", "info"), ];
for (input, expected) in test_levels {
env::set_var(ENV_LOG_LEVEL, input);
let processed_level = get_processed_log_level();
assert_eq!(
processed_level, expected,
"Expected log level '{}' for input '{}', but got '{}'",
expected, input, processed_level
);
}
env::remove_var(ENV_LOG_LEVEL);
if let Some(value) = original_value {
env::set_var(ENV_LOG_LEVEL, value);
}
}
#[test]
fn test_default_log_level() {
let original_value = env::var(ENV_LOG_LEVEL).ok();
env::remove_var(ENV_LOG_LEVEL);
let log_level = env::var(ENV_LOG_LEVEL)
.unwrap_or_else(|_| DEFAULT_LOG_LEVEL.to_string())
.to_lowercase();
assert_eq!(log_level, DEFAULT_LOG_LEVEL.to_lowercase());
env::remove_var(ENV_LOG_LEVEL);
if let Some(value) = original_value {
env::set_var(ENV_LOG_LEVEL, value);
}
}
#[test]
fn test_log_level_translation() {
let test_cases = vec![
("error", LevelFilter::Error),
("warn", LevelFilter::Warn),
("info", LevelFilter::Info),
("debug", LevelFilter::Debug),
("trace", LevelFilter::Trace),
("invalid", LevelFilter::Info),
("", LevelFilter::Info),
];
for (input, expected) in test_cases {
let level = match input.to_lowercase().as_str() {
"error" => LevelFilter::Error,
"warn" => LevelFilter::Warn,
"info" => LevelFilter::Info,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
_ => LevelFilter::Info,
};
assert_eq!(
level,
expected,
"Log level mismatch for input: '{}' - expected {:?}, got {:?}",
input,
expected,
level
);
}
}
#[test]
fn test_env_log_level_handling() {
let original_value = env::var(ENV_LOG_LEVEL).ok();
let test_cases = vec![
(Some("DEBUG"), "debug"),
(Some("ERROR"), "error"),
(Some("WARN"), "warn"),
(Some("INFO"), "info"),
(Some("TRACE"), "trace"),
(Some("INVALID"), "info"),
(None, "info"),
];
for (env_value, expected) in test_cases {
env::remove_var(ENV_LOG_LEVEL);
if let Some(value) = env_value {
env::set_var(ENV_LOG_LEVEL, value);
}
let log_level = env::var(ENV_LOG_LEVEL)
.unwrap_or_else(|_| DEFAULT_LOG_LEVEL.to_string())
.to_lowercase();
let actual = match log_level.as_str() {
"error" => "error",
"warn" => "warn",
"info" => "info",
"debug" => "debug",
"trace" => "trace",
_ => "info",
};
assert_eq!(
actual, expected,
"Log level mismatch for env value: {:?}",
env_value
);
}
env::remove_var(ENV_LOG_LEVEL);
if let Some(value) = original_value {
env::set_var(ENV_LOG_LEVEL, value);
}
}
#[test]
fn test_initialize_logging_custom_levels() {
let valid_levels = ["debug", "warn", "error", "trace", "info"];
for level in &valid_levels {
assert!(
["trace", "debug", "info", "warn", "error"]
.contains(level),
"unexpected log level: {level}"
);
}
assert!(
["trace", "debug", "info", "warn", "error"]
.contains(&DEFAULT_LOG_LEVEL),
);
}
#[tokio::test]
async fn test_concurrent_operations() -> Result<()> {
use tokio::fs as async_fs;
let temp_dir = TempDir::new()?;
let src_dir = temp_dir.path().join("src");
let dst_dir = temp_dir.path().join("dst");
async_fs::create_dir_all(&src_dir).await?;
let mut handles = Vec::new();
for i in 0..100 {
let src = src_dir.clone();
handles.push(tokio::spawn(async move {
async_fs::write(
src.join(format!("file_{}.txt", i)),
format!("content {}", i),
)
.await
}));
}
for handle in handles {
handle.await??;
}
tokio::time::sleep(tokio::time::Duration::from_millis(100))
.await;
let mut src_files = Vec::new();
collect_files_recursive(&src_dir, &mut src_files)?;
assert_eq!(src_files.len(), 100);
async_fs::create_dir_all(&dst_dir).await?;
verify_and_copy_files(&src_dir, &dst_dir)?;
tokio::time::sleep(tokio::time::Duration::from_millis(100))
.await;
let mut dst_files = Vec::new();
collect_files_recursive(&dst_dir, &mut dst_files)?;
assert_eq!(dst_files.len(), 100);
for i in 0..100 {
let dst_path = dst_dir.join(format!("file_{}.txt", i));
assert!(
dst_path.exists(),
"File {} does not exist in destination",
dst_path.display()
);
let content = fs::read_to_string(&dst_path)?;
assert_eq!(
content,
format!("content {}", i),
"Content mismatch for file {}",
i
);
}
Ok(())
}
#[test]
fn test_verify_and_copy_files_basic() -> Result<()> {
let temp_dir = TempDir::new()?;
let src_dir = temp_dir.path().join("src");
let dst_dir = temp_dir.path().join("dst");
fs::create_dir_all(&src_dir)?;
fs::write(src_dir.join("test.txt"), "test content")?;
verify_and_copy_files(&src_dir, &dst_dir)?;
assert!(dst_dir.join("test.txt").exists());
assert_eq!(
fs::read_to_string(dst_dir.join("test.txt"))?,
"test content"
);
Ok(())
}
#[test]
fn test_copy_dir_with_progress_empty_source() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
copy_dir_with_progress(src_dir.path(), dst_dir.path())?;
assert!(dst_dir.path().exists());
assert!(fs::read_dir(dst_dir.path())?.next().is_none());
Ok(())
}
#[test]
fn test_copy_dir_with_progress_source_does_not_exist() {
let src_dir = Path::new("/nonexistent");
let dst_dir = tempdir().unwrap();
let result = copy_dir_with_progress(src_dir, dst_dir.path());
assert!(result.is_err());
}
#[test]
fn test_copy_dir_with_progress_single_file() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
fs::write(src_dir.path().join("file1.txt"), "content")?;
copy_dir_with_progress(src_dir.path(), dst_dir.path())?;
let copied_file = dst_dir.path().join("file1.txt");
assert!(copied_file.exists());
assert_eq!(fs::read_to_string(copied_file)?, "content");
Ok(())
}
#[test]
fn test_copy_dir_with_progress_nested_directories() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let nested_dir = src_dir.path().join("nested");
fs::create_dir(&nested_dir)?;
fs::write(nested_dir.join("file.txt"), "nested content")?;
copy_dir_with_progress(src_dir.path(), dst_dir.path())?;
let copied_nested_file = dst_dir.path().join("nested/file.txt");
assert!(copied_nested_file.exists());
assert_eq!(
fs::read_to_string(copied_nested_file)?,
"nested content"
);
Ok(())
}
#[test]
fn test_copy_dir_with_progress_destination_creation_failure() {
let src_dir = tempdir().unwrap();
let dst_dir = Path::new("/invalid_path");
let result = copy_dir_with_progress(src_dir.path(), dst_dir);
assert!(result.is_err());
}
#[test]
fn test_verify_and_copy_files_single_file() -> Result<()> {
let temp_dir = tempdir()?;
let src_file = temp_dir.path().join("single.txt");
fs::write(&src_file, "content")?;
let dst_dir = temp_dir.path().join("dst");
let result = verify_and_copy_files(&src_file, &dst_dir);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_is_safe_path_traversal_nonexistent() -> Result<()> {
assert!(!is_safe_path(Path::new("../../etc/passwd"))?);
Ok(())
}
#[test]
fn test_copy_dir_with_progress_nested() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let sub = src_dir.path().join("sub");
fs::create_dir(&sub)?;
fs::write(src_dir.path().join("root.txt"), "root")?;
fs::write(sub.join("nested.txt"), "nested")?;
copy_dir_with_progress(src_dir.path(), dst_dir.path())?;
assert!(dst_dir.path().join("root.txt").exists());
assert!(dst_dir.path().join("sub/nested.txt").exists());
Ok(())
}
#[test]
fn test_copy_dir_all_parallel_threshold() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
for i in 0..20 {
fs::write(
src_dir.path().join(format!("file{}.txt", i)),
format!("content {}", i),
)?;
}
copy_dir_all(src_dir.path(), dst_dir.path())?;
for i in 0..20 {
assert!(dst_dir
.path()
.join(format!("file{}.txt", i))
.exists());
}
Ok(())
}
#[test]
fn test_collect_files_recursive_depth_exceeded() -> Result<()> {
let temp_dir = tempdir()?;
let mut path = temp_dir.path().to_path_buf();
for i in 0..=MAX_DIR_DEPTH {
path = path.join(format!("d{}", i));
fs::create_dir(&path)?;
}
let mut files = Vec::new();
let result =
collect_files_recursive(temp_dir.path(), &mut files);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("maximum depth"));
Ok(())
}
#[test]
fn test_copy_dir_all_depth_exceeded() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let mut path = src_dir.path().to_path_buf();
for i in 0..=MAX_DIR_DEPTH {
path = path.join(format!("d{}", i));
fs::create_dir(&path)?;
}
let result = copy_dir_all(src_dir.path(), dst_dir.path());
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("maximum depth"));
Ok(())
}
#[tokio::test]
async fn test_verify_and_copy_files_async_depth_exceeded(
) -> Result<()> {
let temp_dir = tempdir()?;
let src = temp_dir.path().join("src");
let dst = temp_dir.path().join("dst");
let mut path = src.clone();
for i in 0..=MAX_DIR_DEPTH {
path = path.join(format!("d{}", i));
fs::create_dir_all(&path)?;
}
let result =
verify_and_copy_files_async(&src, &dst).await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("maximum depth"));
Ok(())
}
#[tokio::test]
async fn test_copy_dir_all_async_depth_exceeded() -> Result<()> {
let temp_dir = tempdir()?;
let src = temp_dir.path().join("src");
let dst = temp_dir.path().join("dst");
let mut path = src.clone();
for i in 0..=MAX_DIR_DEPTH {
path = path.join(format!("d{}", i));
fs::create_dir_all(&path)?;
}
let result = copy_dir_all_async(&src, &dst).await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("maximum depth"));
Ok(())
}
#[test]
fn test_verify_file_safety_nonexistent() {
let result =
verify_file_safety(Path::new("/nonexistent/file.txt"));
assert!(result.is_err());
}
#[test]
fn test_copy_dir_with_progress_nonexistent_source() {
let result = copy_dir_with_progress(
Path::new("/nonexistent/source"),
Path::new("/tmp/dst"),
);
assert!(result.is_err());
}
#[tokio::test]
async fn test_verify_and_copy_files_async_with_files() -> Result<()> {
let temp_dir = tempdir()?;
let src = temp_dir.path().join("src");
let dst = temp_dir.path().join("dst");
fs::create_dir_all(src.join("sub1/sub2"))?;
fs::write(src.join("root.txt"), "root")?;
fs::write(src.join("sub1/a.txt"), "a")?;
fs::write(src.join("sub1/sub2/b.txt"), "b")?;
verify_and_copy_files_async(&src, &dst).await?;
assert_eq!(fs::read_to_string(dst.join("root.txt"))?, "root");
assert_eq!(fs::read_to_string(dst.join("sub1/a.txt"))?, "a");
assert_eq!(
fs::read_to_string(dst.join("sub1/sub2/b.txt"))?,
"b"
);
Ok(())
}
#[test]
fn test_copy_dir_with_progress_with_files() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let sub1 = src_dir.path().join("a");
let sub2 = sub1.join("b");
fs::create_dir_all(&sub2)?;
fs::write(src_dir.path().join("file1.txt"), "f1")?;
fs::write(sub1.join("file2.txt"), "f2")?;
fs::write(sub2.join("file3.txt"), "f3")?;
copy_dir_with_progress(src_dir.path(), dst_dir.path())?;
assert_eq!(
fs::read_to_string(dst_dir.path().join("file1.txt"))?,
"f1"
);
assert_eq!(
fs::read_to_string(dst_dir.path().join("a/file2.txt"))?,
"f2"
);
assert_eq!(
fs::read_to_string(dst_dir.path().join("a/b/file3.txt"))?,
"f3"
);
Ok(())
}
#[cfg(unix)]
#[test]
fn test_is_safe_path_broken_symlink() -> Result<()> {
let temp_dir = tempdir()?;
let target = temp_dir.path().join("nonexistent_target");
let link = temp_dir.path().join("broken_link");
std::os::unix::fs::symlink(&target, &link)?;
let result = is_safe_path(&link)?;
assert!(result);
Ok(())
}
#[cfg(unix)]
#[test]
fn test_paths_validate_symlink() -> Result<()> {
let temp_dir = tempdir()?;
let real = temp_dir.path().join("real");
let link = temp_dir.path().join("link");
fs::create_dir(&real)?;
std::os::unix::fs::symlink(&real, &link)?;
let paths = Paths {
site: link,
content: PathBuf::from("content"),
build: PathBuf::from("build"),
template: PathBuf::from("templates"),
};
let result = paths.validate();
assert!(result.is_err());
assert!(
result.unwrap_err().to_string().contains("symlink")
);
Ok(())
}
#[test]
fn test_copy_dir_with_progress_depth_exceeded() -> Result<()> {
let src_dir = tempdir()?;
let dst_dir = tempdir()?;
let mut path = src_dir.path().to_path_buf();
for i in 0..=MAX_DIR_DEPTH {
path = path.join(format!("d{}", i));
fs::create_dir(&path)?;
}
let result =
copy_dir_with_progress(src_dir.path(), dst_dir.path());
assert!(result.is_err());
assert!(
result.unwrap_err().to_string().contains("maximum depth")
);
Ok(())
}
#[test]
fn test_verify_and_copy_files_source_is_file() -> Result<()> {
let temp_dir = tempdir()?;
let src_file = temp_dir.path().join("source.txt");
let dst_dir = temp_dir.path().join("dst");
fs::write(&src_file, "hello")?;
let result = verify_and_copy_files(&src_file, &dst_dir);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_compile_site_error() -> Result<()> {
let temp_dir = tempdir()?;
let build = temp_dir.path().join("build");
let content = temp_dir.path().join("content");
let site = temp_dir.path().join("site");
let template = temp_dir.path().join("template");
fs::create_dir_all(&build)?;
fs::create_dir_all(&content)?;
fs::create_dir_all(&site)?;
fs::create_dir_all(&template)?;
let result = compile_site(&build, &content, &site, &template);
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn test_prepare_serve_dir_same_as_site() -> Result<()> {
let temp_dir = tempdir()?;
let site_dir = temp_dir.path().join("site");
fs::create_dir_all(&site_dir)?;
fs::write(site_dir.join("index.html"), "<html/>")?;
let paths = Paths {
site: site_dir.clone(),
content: PathBuf::from("content"),
build: PathBuf::from("build"),
template: PathBuf::from("templates"),
};
prepare_serve_dir(&paths, &site_dir).await?;
assert!(site_dir.join("index.html").exists());
Ok(())
}
#[tokio::test]
async fn test_prepare_serve_dir_different() -> Result<()> {
let temp_dir = tempdir()?;
let site_dir = temp_dir.path().join("site");
let serve_dir = temp_dir.path().join("serve");
fs::create_dir_all(&site_dir)?;
fs::write(site_dir.join("index.html"), "<html/>")?;
let paths = Paths {
site: site_dir,
content: PathBuf::from("content"),
build: PathBuf::from("build"),
template: PathBuf::from("templates"),
};
prepare_serve_dir(&paths, &serve_dir).await?;
assert!(serve_dir.join("index.html").exists());
Ok(())
}
#[test]
fn test_create_directories_all_valid() -> Result<()> {
let temp_dir = tempdir()?;
let paths = Paths {
site: temp_dir.path().join("s"),
content: temp_dir.path().join("c"),
build: temp_dir.path().join("b"),
template: temp_dir.path().join("t"),
};
create_directories(&paths)?;
assert!(paths.site.exists());
assert!(paths.build.exists());
Ok(())
}
#[test]
fn test_is_safe_path_existing_valid() -> Result<()> {
let temp_dir = tempdir()?;
let dir = temp_dir.path().join("valid");
fs::create_dir(&dir)?;
let canonical = dir.canonicalize()?;
assert!(is_safe_path(&canonical)?);
Ok(())
}
}