use crate::error::{Result, WasmrunError};
use std::fs;
use std::path::Path;
pub struct PathResolver;
impl PathResolver {
pub fn resolve_input_path(positional: Option<String>, flag: Option<String>) -> String {
positional.unwrap_or_else(|| flag.unwrap_or_else(|| String::from("./")))
}
pub fn has_extension(path: &str, expected_ext: &str) -> bool {
Path::new(path)
.extension()
.is_some_and(|ext| ext.to_string_lossy().to_lowercase() == expected_ext.to_lowercase())
}
#[allow(dead_code)]
pub fn get_extension(path: &str) -> Option<String> {
Path::new(path)
.extension()
.map(|ext| ext.to_string_lossy().to_lowercase().to_string())
}
pub fn validate_file_exists(path: &str) -> Result<()> {
let path_obj = Path::new(path);
if !path_obj.exists() {
return Err(WasmrunError::file_not_found(path));
}
if !path_obj.is_file() {
return Err(WasmrunError::path(format!("Path is not a file: {path}")));
}
Ok(())
}
pub fn validate_directory_exists(path: &str) -> Result<()> {
let path_obj = Path::new(path);
if !path_obj.exists() {
return Err(WasmrunError::directory_not_found(path));
}
if !path_obj.is_dir() {
return Err(WasmrunError::path(format!(
"Path is not a directory: {path}"
)));
}
Ok(())
}
pub fn validate_wasm_file(path: &str) -> Result<()> {
Self::validate_file_exists(path)?;
if !Self::has_extension(path, "wasm") {
return Err(WasmrunError::invalid_file_format(
path,
"File does not have .wasm extension",
));
}
Ok(())
}
#[allow(dead_code)]
pub fn get_absolute_path(path: &str) -> Result<String> {
fs::canonicalize(path)
.map(|p| p.to_string_lossy().to_string())
.map_err(|e| WasmrunError::add_context(format!("Getting absolute path for {path}"), e))
}
pub fn get_filename(path: &str) -> Result<String> {
Path::new(path)
.file_name()
.ok_or_else(|| WasmrunError::path(format!("Invalid path: {path}")))?
.to_string_lossy()
.to_string()
.pipe(Ok)
}
#[allow(dead_code)]
pub fn get_file_stem(path: &str) -> Result<String> {
Path::new(path)
.file_stem()
.ok_or_else(|| WasmrunError::path(format!("Invalid path: {path}")))?
.to_string_lossy()
.to_string()
.pipe(Ok)
}
pub fn join_paths(base: &str, additional: &str) -> String {
Path::new(base)
.join(additional)
.to_string_lossy()
.to_string()
}
pub fn ensure_output_directory(output_dir: &str) -> Result<()> {
let output_path = Path::new(output_dir);
if !output_path.exists() {
fs::create_dir_all(output_path).map_err(|e| {
WasmrunError::add_context(format!("Creating output directory {output_dir}"), e)
})?;
}
Ok(())
}
pub fn find_files_with_extension(dir_path: &str, extension: &str) -> Result<Vec<String>> {
let mut files = Vec::new();
let path = Path::new(dir_path);
if !path.is_dir() {
return Err(WasmrunError::path(format!(
"Path is not a directory: {dir_path}"
)));
}
let entries = fs::read_dir(path)
.map_err(|e| WasmrunError::add_context(format!("Reading directory {dir_path}"), e))?;
for entry in entries.flatten() {
let entry_path = entry.path();
if entry_path.is_file() && Self::has_extension(&entry_path.to_string_lossy(), extension)
{
files.push(entry_path.to_string_lossy().to_string());
}
}
Ok(files)
}
#[allow(dead_code)]
pub fn find_entry_file(project_path: &str, candidates: &[&str]) -> Option<String> {
for candidate in candidates {
let entry_path = Self::join_paths(project_path, candidate);
if Path::new(&entry_path).exists() {
return Some(entry_path);
}
}
None
}
#[allow(dead_code)]
pub fn is_safe_path(path: &str) -> bool {
let path = Path::new(path);
for component in path.components() {
match component {
std::path::Component::ParentDir => return false,
std::path::Component::Normal(name) => {
let name_str = name.to_string_lossy();
if name_str.starts_with('.') && name_str.len() > 1 {
continue;
}
}
_ => {}
}
}
true
}
pub fn get_file_size_human(path: &str) -> Result<String> {
use crate::utils::CommandExecutor;
let metadata = fs::metadata(path).map_err(|e| {
WasmrunError::add_context(format!("Getting file metadata for {path}"), e)
})?;
Ok(CommandExecutor::format_file_size(metadata.len()))
}
pub fn create_temp_directory(name: &str) -> Result<String> {
let temp_dir = std::env::temp_dir().join(name);
if !temp_dir.exists() {
std::fs::create_dir_all(&temp_dir).map_err(|e| {
WasmrunError::add_context(format!("Creating temporary directory {name}"), e)
})?;
}
Ok(temp_dir.to_str().unwrap_or("/tmp").to_string())
}
pub fn cleanup_temp_directory(name: &str) -> Result<()> {
let temp_dir = std::env::temp_dir().join(name);
if temp_dir.exists() {
println!("🧹 Cleaning temporary directory: {}", temp_dir.display());
let entries = fs::read_dir(&temp_dir).map_err(|e| {
WasmrunError::add_context(
format!("Reading temporary directory {}", temp_dir.display()),
e,
)
})?;
let mut cleaned_files = 0;
for entry in entries.flatten() {
let entry_path = entry.path();
if entry_path.is_file() {
if let Err(e) = fs::remove_file(&entry_path) {
println!(
"⚠️ Warning: Failed to remove {}: {}",
entry_path.display(),
e
);
} else {
cleaned_files += 1;
}
}
}
if cleaned_files > 0 {
println!("✅ Cleaned {cleaned_files} stale files from temporary directory");
} else {
println!("✅ Temporary directory was already clean");
}
} else {
println!("ℹ️ Temporary directory does not exist, nothing to clean");
}
Ok(())
}
pub fn cleanup_all_temp_directories() -> Result<()> {
println!("🧹 Starting cleanup of all wasmrun temporary directories...");
Self::cleanup_temp_directory("wasmrun_temp")?;
let temp_base = std::env::temp_dir();
if let Ok(entries) = fs::read_dir(&temp_base) {
let mut additional_cleaned = 0;
for entry in entries.flatten() {
let entry_path = entry.path();
if entry_path.is_dir() {
if let Some(dir_name) = entry_path.file_name() {
let dir_name_str = dir_name.to_string_lossy();
if dir_name_str.starts_with("wasmrun_") && dir_name_str != "wasmrun_temp" {
if let Err(e) = fs::remove_dir_all(&entry_path) {
println!(
"⚠️ Warning: Failed to remove {}: {e}",
entry_path.display()
);
} else {
println!(
"🗑️ Removed old temp directory: {}",
entry_path.display()
);
additional_cleaned += 1;
}
}
}
}
}
if additional_cleaned > 0 {
println!("✅ Cleaned {additional_cleaned} additional temporary directories");
}
}
let pid_file = "/tmp/wasmrun_server.pid";
if Path::new(pid_file).exists() {
if let Err(e) = fs::remove_file(pid_file) {
println!("⚠️ Warning: Failed to remove PID file: {e}");
} else {
println!("🗑️ Removed stale PID file");
}
}
println!("✅ Cleanup completed successfully!");
Ok(())
}
pub fn remove_file(path: &str) -> Result<()> {
fs::remove_file(path)
.map_err(|e| WasmrunError::add_context(format!("Removing file {path}"), e))?;
Ok(())
}
pub fn remove_dir_all(path: &str) -> Result<()> {
fs::remove_dir_all(path)
.map_err(|e| WasmrunError::add_context(format!("Removing directory {path}"), e))?;
Ok(())
}
}
trait Pipe<T> {
fn pipe<U, F>(self, f: F) -> U
where
F: FnOnce(T) -> U;
}
impl<T> Pipe<T> for T {
fn pipe<U, F>(self, f: F) -> U
where
F: FnOnce(T) -> U,
{
f(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use tempfile::{tempdir, NamedTempFile};
#[test]
fn test_resolve_input_path_with_positional() {
let result = PathResolver::resolve_input_path(Some("test.wasm".to_string()), None);
assert_eq!(result, "test.wasm");
}
#[test]
fn test_resolve_input_path_with_flag() {
let result = PathResolver::resolve_input_path(None, Some("flag.wasm".to_string()));
assert_eq!(result, "flag.wasm");
}
#[test]
fn test_resolve_input_path_with_both() {
let result = PathResolver::resolve_input_path(
Some("positional.wasm".to_string()),
Some("flag.wasm".to_string()),
);
assert_eq!(result, "positional.wasm");
}
#[test]
fn test_resolve_input_path_with_neither() {
let result = PathResolver::resolve_input_path(None, None);
assert_eq!(result, "./");
}
#[test]
fn test_has_extension_wasm() {
assert!(PathResolver::has_extension("test.wasm", "wasm"));
assert!(PathResolver::has_extension("test.WASM", "wasm"));
assert!(!PathResolver::has_extension("test.js", "wasm"));
assert!(!PathResolver::has_extension("test", "wasm"));
}
#[test]
fn test_get_extension() {
assert_eq!(
PathResolver::get_extension("test.wasm"),
Some("wasm".to_string())
);
assert_eq!(
PathResolver::get_extension("test.WASM"),
Some("wasm".to_string())
);
assert_eq!(PathResolver::get_extension("test"), None);
}
#[test]
fn test_validate_file_exists_with_valid_file() {
let temp_file = NamedTempFile::new().unwrap();
let result = PathResolver::validate_file_exists(temp_file.path().to_str().unwrap());
assert!(result.is_ok());
}
#[test]
fn test_validate_file_exists_with_nonexistent_file() {
let result = PathResolver::validate_file_exists("/nonexistent/file.wasm");
assert!(result.is_err());
match result {
Err(WasmrunError::FileNotFound { path }) => {
assert_eq!(path, "/nonexistent/file.wasm");
}
_ => panic!("Expected FileNotFound error"),
}
}
#[test]
fn test_validate_directory_exists_with_valid_dir() {
let temp_dir = tempdir().unwrap();
let result = PathResolver::validate_directory_exists(temp_dir.path().to_str().unwrap());
assert!(result.is_ok());
}
#[test]
fn test_validate_directory_exists_with_nonexistent_dir() {
let result = PathResolver::validate_directory_exists("/nonexistent/dir");
assert!(result.is_err());
match result {
Err(WasmrunError::DirectoryNotFound { path }) => {
assert_eq!(path, "/nonexistent/dir");
}
_ => panic!("Expected DirectoryNotFound error"),
}
}
#[test]
fn test_validate_wasm_file_with_valid_wasm() {
let temp_file = NamedTempFile::new().unwrap();
let new_path = temp_file.path().with_extension("wasm");
std::fs::rename(temp_file.path(), &new_path).unwrap();
let result = PathResolver::validate_wasm_file(new_path.to_str().unwrap());
assert!(result.is_ok());
}
#[test]
fn test_validate_wasm_file_with_wrong_extension() {
let temp_file = NamedTempFile::new().unwrap();
let result = PathResolver::validate_wasm_file(temp_file.path().to_str().unwrap());
assert!(result.is_err());
}
#[test]
fn test_get_filename() {
let result = PathResolver::get_filename("/path/to/file.wasm");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "file.wasm");
}
#[test]
fn test_get_filename_invalid_path() {
let result = PathResolver::get_filename("");
assert!(result.is_err());
}
#[test]
fn test_join_paths() {
let result = PathResolver::join_paths("/base/path", "file.wasm");
assert_eq!(result, "/base/path/file.wasm");
}
#[test]
fn test_ensure_output_directory() {
let temp_dir = tempdir().unwrap();
let new_dir = temp_dir.path().join("new_output_dir");
let result = PathResolver::ensure_output_directory(new_dir.to_str().unwrap());
assert!(result.is_ok());
assert!(new_dir.exists());
}
#[test]
fn test_find_files_with_extension() {
let temp_dir = tempdir().unwrap();
File::create(temp_dir.path().join("test1.wasm")).unwrap();
File::create(temp_dir.path().join("test2.wasm")).unwrap();
File::create(temp_dir.path().join("test3.js")).unwrap();
let result =
PathResolver::find_files_with_extension(temp_dir.path().to_str().unwrap(), "wasm");
assert!(result.is_ok());
let files = result.unwrap();
assert_eq!(files.len(), 2);
assert!(files.iter().all(|f| f.ends_with(".wasm")));
}
#[test]
fn test_find_entry_file() {
let temp_dir = tempdir().unwrap();
File::create(temp_dir.path().join("main.rs")).unwrap();
let candidates = ["lib.rs", "main.rs", "src/main.rs"];
let result = PathResolver::find_entry_file(temp_dir.path().to_str().unwrap(), &candidates);
assert!(result.is_some());
assert!(result.unwrap().ends_with("main.rs"));
}
#[test]
fn test_is_safe_path() {
assert!(PathResolver::is_safe_path("safe/path/file.wasm"));
assert!(PathResolver::is_safe_path("./file.wasm"));
assert!(!PathResolver::is_safe_path("../dangerous/path"));
assert!(!PathResolver::is_safe_path("/path/../dangerous"));
}
#[test]
fn test_format_file_size() {
use crate::utils::CommandExecutor;
assert_eq!(CommandExecutor::format_file_size(500), "500 bytes");
assert_eq!(CommandExecutor::format_file_size(1536), "1.50 KB");
assert_eq!(CommandExecutor::format_file_size(1536 * 1024), "1.50 MB");
assert_eq!(
CommandExecutor::format_file_size(1536 * 1024 * 1024),
"1.50 GB"
);
}
#[test]
fn test_create_temp_directory() {
let result = PathResolver::create_temp_directory("wasmrun_test");
assert!(result.is_ok());
let path = result.unwrap();
assert!(Path::new(&path).exists());
}
}