oxide_cli/utils/
validate.rs1use std::{path::Path, sync::LazyLock};
2
3use anyhow::{Result, anyhow};
4use regex::Regex;
5use url::Url;
6
7static VALID_NAME_CHARS: LazyLock<Regex> =
8 LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_\-\.]+$").unwrap());
9
10static VALID_TEMPLATE_NAME: LazyLock<Regex> =
11 LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap());
12
13pub fn validate_project_name(name: &str) -> Result<()> {
14 if name == "." {
15 return Ok(());
16 }
17
18 if name.is_empty() {
19 return Err(anyhow!("Project name cannot be empty"));
20 }
21
22 if name.len() > 255 {
23 return Err(anyhow!("Project name is too long (max 255 characters)"));
24 }
25
26 if Path::new(name).exists() {
27 return Err(anyhow!("Directory '{}' already exists!", name));
28 }
29
30 let valid_chars = &*VALID_NAME_CHARS;
31 if !valid_chars.is_match(name) {
32 return Err(anyhow!(
33 "Project name can only contain letters, numbers, hyphens, underscores, and dots"
34 ));
35 }
36
37 if name.starts_with('.') {
38 return Err(anyhow!("Project name cannot start with a dot"));
39 }
40
41 if name.ends_with('.') || name.ends_with(' ') {
42 return Err(anyhow!("Project name cannot end with a dot or space"));
43 }
44
45 let reserved_windows = [
46 "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
47 "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
48 ];
49
50 let uppercase_name = name.to_uppercase();
51 if reserved_windows.contains(&uppercase_name.as_str()) {
52 return Err(anyhow!("'{}' is a reserved name in Windows", name));
53 }
54
55 Ok(())
56}
57
58pub fn is_valid_github_repo_url(input: &str) -> Result<()> {
59 let Ok(url) = Url::parse(input) else {
60 return Err(anyhow!("Invalid URL format"));
61 };
62
63 if url.host_str() != Some("github.com") {
64 return Err(anyhow!("URL is not a GitHub domain"));
65 }
66
67 let segments: Vec<_> = match url.path_segments() {
68 Some(s) => s.collect(),
69 None => {
70 return Err(anyhow!("Failed to extract path segments from URL"));
71 }
72 };
73
74 if segments.len() < 2 {
75 return Err(anyhow!("URL does not point to a GitHub repository"));
76 }
77
78 Ok(())
79}
80
81pub fn validate_template_name(template_name: &str) -> Result<()> {
82 if !VALID_TEMPLATE_NAME.is_match(template_name) {
83 anyhow::bail!(
84 "Invalid template name '{}'. Allowed characters: a-z, A-Z, 0-9, '-' and '_'",
85 template_name
86 );
87 }
88
89 Ok(())
90}