use anyhow::Result;
use std::path::Path;
pub fn normalize_project_name(name: &str) -> String {
name.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else {
'-'
}
})
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<&str>>()
.join("-")
}
pub fn validate_project_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(anyhow::anyhow!("Project name cannot be empty"));
}
if name.len() > 50 {
return Err(anyhow::anyhow!(
"Project name cannot be longer than 50 characters (current: {} characters)",
name.len()
));
}
for c in name.chars() {
if !c.is_alphanumeric() && c != '-' {
return Err(anyhow::anyhow!(
"Project name can only contain lowercase letters, numbers, and hyphens (kebab-case format)"
));
}
if c.is_alphabetic() && c.is_uppercase() {
return Err(anyhow::anyhow!(
"Project name must be in kebab-case format (lowercase letters, numbers, and hyphens only)"
));
}
}
if name.contains("--") {
return Err(anyhow::anyhow!(
"Project name cannot contain consecutive hyphens (e.g., 'my--project' is invalid)"
));
}
let first_char_valid = name
.chars()
.next()
.map(|c| c.is_alphanumeric())
.unwrap_or(false);
let last_char_valid = name
.chars()
.last()
.map(|c| c.is_alphanumeric())
.unwrap_or(false);
if !first_char_valid || !last_char_valid {
return Err(anyhow::anyhow!(
"Project name must start and end with alphanumeric characters (e.g., '-my-project' and 'my-project-' are invalid)"
));
}
Ok(())
}
pub fn validate_feature_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(anyhow::anyhow!("Feature name cannot be empty"));
}
if name.len() > 50 {
return Err(anyhow::anyhow!(
"Feature name cannot be longer than 50 characters"
));
}
for c in name.chars() {
if !c.is_alphanumeric() && c != '_' {
return Err(anyhow::anyhow!(
"Feature name can only contain alphanumeric characters and underscores"
));
}
if c.is_alphabetic() && c.is_uppercase() {
return Err(anyhow::anyhow!(
"Feature name must be in snake_case (lowercase letters, numbers, and underscores only)"
));
}
}
if name.starts_with('_') || name.ends_with('_') {
return Err(anyhow::anyhow!(
"Feature name cannot start or end with an underscore"
));
}
if name.contains("__") {
return Err(anyhow::anyhow!(
"Feature name cannot contain consecutive underscores"
));
}
Ok(())
}
pub fn relative_to_foundry(path: &Path) -> Result<String> {
let foundry_dir = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?
.join(".foundry");
let relative_path = path
.strip_prefix(&foundry_dir)
.map_err(|_| anyhow::anyhow!("Path is not within foundry directory"))?;
Ok(relative_path.to_string_lossy().to_string())
}
pub fn ensure_safe_path(path: &Path) -> Result<()> {
let foundry_dir = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?
.join(".foundry");
if !path.starts_with(&foundry_dir) {
return Err(anyhow::anyhow!("Path is outside of foundry directory"));
}
let path_str = path.to_string_lossy();
if path_str.contains("../") || path_str.contains("..\\") {
return Err(anyhow::anyhow!("Path contains directory traversal"));
}
Ok(())
}