Skip to main content

oxide_cli/utils/
validate.rs

1use 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}