use std::path::{Path, PathBuf};
use crate::cli::generator::error::{GeneratorError, GeneratorResult};
pub(crate) fn validate_project_name(name: &str) -> GeneratorResult<()> {
if name.is_empty() {
return Err(GeneratorError::EmptyProjectName);
}
if name.len() > crate::cli::generator::error::MAX_PROJECT_NAME_LENGTH {
return Err(GeneratorError::ProjectNameTooLong);
}
if !name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
{
return Err(GeneratorError::InvalidProjectNameCharacters);
}
if name.contains("..") || name.starts_with('.') || name.ends_with('.') {
return Err(GeneratorError::InvalidProjectNamePattern);
}
if name.contains('/') || name.contains('\\') {
return Err(GeneratorError::ProjectNamePathSeparator);
}
Ok(())
}
pub(crate) fn validate_output_path(path: &Path) -> GeneratorResult<PathBuf> {
let canonical_path = std::fs::canonicalize(path)
.map_err(|e| GeneratorError::InvalidOutputPath(e.to_string()))?;
let current_dir = std::env::current_dir()
.map_err(|e| GeneratorError::CurrentDirError(e.to_string()))?
.canonicalize()
.map_err(|e| GeneratorError::CurrentDirError(e.to_string()))?;
if !canonical_path.starts_with(¤t_dir) {
return Err(GeneratorError::PathTraversal(canonical_path));
}
Ok(canonical_path)
}
const DANGEROUS_PATTERNS: &[&str] = &[
"include::", "import::", "crate::", "super::", "self::", "std::", "env!", "file!", "line!", "column!", ];
pub(crate) fn validate_template_content(template_name: &str, content: &str) -> GeneratorResult<()> {
for pattern in DANGEROUS_PATTERNS {
if content.contains(pattern) {
return Err(GeneratorError::DangerousTemplate(
template_name.to_string(),
pattern.to_string(),
));
}
}
Ok(())
}
pub(crate) fn validate_template_directory(template_dir: &Path) -> GeneratorResult<()> {
if !template_dir.exists() {
return Err(GeneratorError::TemplateNotFound(template_dir.to_path_buf()));
}
Ok(())
}
pub(crate) fn validate_project_directory_does_not_exist(project_dir: &Path) -> GeneratorResult<()> {
if project_dir.exists() {
return Err(GeneratorError::DirectoryExists(
project_dir.to_string_lossy().to_string(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_project_name_valid() {
assert!(validate_project_name("my-project").is_ok());
assert!(validate_project_name("my_project_v2").is_ok());
assert!(validate_project_name("project.123").is_ok());
assert!(validate_project_name("a").is_ok());
}
#[test]
fn test_validate_project_name_empty() {
assert!(validate_project_name("").is_err());
}
#[test]
fn test_validate_project_name_too_long() {
let long_name = "a".repeat(65);
assert!(validate_project_name(&long_name).is_err());
}
#[test]
fn test_validate_project_name_invalid_chars() {
assert!(validate_project_name("project/name").is_err());
assert!(validate_project_name("project\\name").is_err());
assert!(validate_project_name("project name").is_err());
assert!(validate_project_name("project@name").is_err());
}
#[test]
fn test_validate_project_name_invalid_patterns() {
assert!(validate_project_name("..").is_err());
assert!(validate_project_name("../malicious").is_err());
assert!(validate_project_name(".hidden").is_err());
assert!(validate_project_name("hidden.").is_err());
}
#[test]
fn test_validate_template_content_safe() {
let safe_content = r#"
fn main() {
println!("Hello, {}!", name);
}
"#;
assert!(validate_template_content("test.rs", safe_content).is_ok());
}
#[test]
fn test_validate_template_content_dangerous() {
let dangerous_content = r#"
fn main() {
let path = std::env::current_dir();
}
"#;
assert!(validate_template_content("test.rs", dangerous_content).is_err());
}
#[test]
fn test_validate_template_content_dangerous_patterns() {
let patterns = [
"include::",
"import::",
"crate::",
"super::",
"self::",
"std::",
"env!",
"file!",
"line!",
"column!",
];
for pattern in patterns {
let content = format!("let x = {};", pattern);
assert!(
validate_template_content("test.rs", &content).is_err(),
"Pattern '{}' should be detected as dangerous",
pattern
);
}
}
}