use glob::Pattern;
use regex::Regex;
use std::sync::LazyLock;
static TEAM_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^@[a-zA-Z0-9-]+/[a-zA-Z0-9-]+$").unwrap());
static USER_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^@[a-zA-Z0-9-]+$").unwrap());
static EMAIL_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").unwrap());
pub fn validate_owner(owner: &str) -> Option<String> {
if TEAM_RE.is_match(owner) || USER_RE.is_match(owner) || EMAIL_RE.is_match(owner) {
None
} else {
Some(format!(
"Invalid owner format '{}'. Expected @user, @org/team, or email@domain.com",
owner
))
}
}
pub fn validate_pattern(pattern: &str) -> Option<String> {
let pattern_for_glob = pattern.trim_start_matches('/');
if pattern_for_glob.is_empty() {
return Some("Empty pattern".to_string());
}
if pattern.starts_with('!') {
return Some(
"CODEOWNERS does not support '!' negation; the rule will never match".to_string(),
);
}
if pattern.contains('[') {
return Some(
"CODEOWNERS does not support '[...]' character ranges; the rule will never match"
.to_string(),
);
}
if let Err(e) = Pattern::new(pattern_for_glob) {
return Some(format!("Invalid glob pattern: {}", e));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_user() {
assert!(validate_owner("@username").is_none());
assert!(validate_owner("@user-name").is_none());
assert!(validate_owner("@user123").is_none());
}
#[test]
fn test_valid_team() {
assert!(validate_owner("@org/team").is_none());
assert!(validate_owner("@my-org/my-team").is_none());
}
#[test]
fn test_invalid_owner_with_underscore() {
assert!(validate_owner("@user_name").is_some());
assert!(validate_owner("@org_name/team_name").is_some());
}
#[test]
fn test_valid_email() {
assert!(validate_owner("user@example.com").is_none());
assert!(validate_owner("user.name@domain.co.uk").is_none());
assert!(validate_owner("user+tag@example.org").is_none());
}
#[test]
fn test_invalid_owner_with_period() {
assert!(validate_owner("@user.name").is_some());
assert!(validate_owner("@org.name/team").is_some());
}
#[test]
fn test_invalid_owner_no_at() {
assert!(validate_owner("username").is_some());
}
#[test]
fn test_invalid_owner_empty() {
assert!(validate_owner("").is_some());
assert!(validate_owner("@").is_some());
}
#[test]
fn test_invalid_owner_spaces() {
assert!(validate_owner("@user name").is_some());
assert!(validate_owner("user @name").is_some());
}
#[test]
fn test_invalid_team_format() {
assert!(validate_owner("@org/").is_some());
assert!(validate_owner("@/team").is_some());
assert!(validate_owner("@org//team").is_some());
}
#[test]
fn test_valid_patterns() {
assert!(validate_pattern("*").is_none());
assert!(validate_pattern("**").is_none());
assert!(validate_pattern("*.rs").is_none());
assert!(validate_pattern("/src/").is_none());
assert!(validate_pattern("/src/**").is_none());
assert!(validate_pattern("docs/*.md").is_none());
}
#[test]
fn test_invalid_empty_pattern() {
assert!(validate_pattern("").is_some());
assert!(validate_pattern("/").is_some());
}
#[test]
fn test_invalid_glob_syntax() {
assert!(validate_pattern("[invalid").is_some());
}
#[test]
fn test_rejects_syntax_codeowners_does_not_support() {
assert!(validate_pattern("[abc].txt").is_some());
assert!(validate_pattern("src/[a-z]*.rs").is_some());
assert!(validate_pattern("!docs/").is_some());
assert!(validate_pattern("!/src/main.rs").is_some());
assert!(validate_pattern("docs/hello!.md").is_none());
}
}