use anyhow::{bail, Result};
const RESERVED_NAMES: &[&str] = &[
"backup",
"temp",
".git",
"node_modules",
"target",
"build",
"common", ];
const MAX_NAME_LENGTH: usize = 50;
pub fn validate_profile_name(name: &str, existing_profiles: &[String]) -> Result<()> {
let trimmed = name.trim();
if trimmed.is_empty() {
bail!("Profile name cannot be empty");
}
if trimmed.len() > MAX_NAME_LENGTH {
bail!(
"Profile name must be {} characters or less (got {})",
MAX_NAME_LENGTH,
trimmed.len()
);
}
if !trimmed
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
{
bail!("Profile name can only contain letters, numbers, hyphens, and underscores");
}
if trimmed.starts_with('.') {
bail!("Profile name cannot start with a dot");
}
let lower_name = trimmed.to_lowercase();
if RESERVED_NAMES.contains(&lower_name.as_str()) {
bail!("'{trimmed}' is a reserved name and cannot be used");
}
if existing_profiles
.iter()
.any(|p| p.eq_ignore_ascii_case(trimmed))
{
bail!("A profile with the name '{trimmed}' already exists");
}
Ok(())
}
#[must_use]
pub fn sanitize_profile_name(name: &str) -> String {
name.trim()
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else if c.is_whitespace() {
'-'
} else {
'_'
}
})
.collect::<String>()
.chars()
.take(MAX_NAME_LENGTH)
.collect()
}
#[allow(dead_code)] #[must_use]
pub fn is_safe_profile_name(name: &str) -> bool {
let trimmed = name.trim();
!trimmed.is_empty()
&& trimmed.len() <= MAX_NAME_LENGTH
&& !trimmed.starts_with('.')
&& trimmed
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
&& !RESERVED_NAMES.contains(&trimmed.to_lowercase().as_str())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_names() {
let existing = vec![];
assert!(validate_profile_name("Personal-Mac", &existing).is_ok());
assert!(validate_profile_name("Work_Linux", &existing).is_ok());
assert!(validate_profile_name("Home-Server-2024", &existing).is_ok());
assert!(validate_profile_name("test123", &existing).is_ok());
}
#[test]
fn test_invalid_names() {
let existing = vec![];
assert!(validate_profile_name("", &existing).is_err());
assert!(validate_profile_name(" ", &existing).is_err());
assert!(validate_profile_name("Profile Name", &existing).is_err());
assert!(validate_profile_name("Profile@Home", &existing).is_err());
assert!(validate_profile_name("Test/Profile", &existing).is_err());
assert!(validate_profile_name("backup", &existing).is_err());
assert!(validate_profile_name("temp", &existing).is_err());
assert!(validate_profile_name(".git", &existing).is_err());
assert!(validate_profile_name("common", &existing).is_err());
assert!(validate_profile_name("Common", &existing).is_err());
assert!(validate_profile_name(".hidden", &existing).is_err());
let long_name = "a".repeat(51);
assert!(validate_profile_name(&long_name, &existing).is_err());
}
#[test]
fn test_duplicate_names() {
let existing = vec!["Personal".to_string(), "Work".to_string()];
assert!(validate_profile_name("Personal", &existing).is_err());
assert!(validate_profile_name("personal", &existing).is_err());
assert!(validate_profile_name("PERSONAL", &existing).is_err());
assert!(validate_profile_name("work", &existing).is_err());
assert!(validate_profile_name("Home", &existing).is_ok());
}
#[test]
fn test_sanitize_name() {
assert_eq!(sanitize_profile_name("Personal Mac"), "Personal-Mac");
assert_eq!(sanitize_profile_name("Work @ Office"), "Work-_-Office");
assert_eq!(sanitize_profile_name(" test "), "test");
assert_eq!(sanitize_profile_name("Test/Profile"), "Test_Profile");
let long_name = "a".repeat(60);
assert_eq!(sanitize_profile_name(&long_name).len(), MAX_NAME_LENGTH);
}
#[test]
fn test_is_safe_name() {
assert!(is_safe_profile_name("Personal-Mac"));
assert!(is_safe_profile_name("Work_Linux"));
assert!(is_safe_profile_name("test123"));
assert!(!is_safe_profile_name(""));
assert!(!is_safe_profile_name("Profile Name"));
assert!(!is_safe_profile_name(".hidden"));
assert!(!is_safe_profile_name("backup"));
assert!(!is_safe_profile_name("common"));
let long_name = "a".repeat(51);
assert!(!is_safe_profile_name(&long_name));
}
}