use regex::Regex;
use std::sync::LazyLock;
use crate::error::AppError;
static ENV_VAR_NAME_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap());
const MAX_NAME_LEN: usize = 256;
const MAX_VALUE_LEN: usize = 65_536;
pub fn validate_env_var_name(name: &str) -> Result<(), AppError> {
if name.is_empty() {
return Err(AppError::BadRequest(
"Environment variable name cannot be empty".to_string(),
));
}
if name.len() > MAX_NAME_LEN {
return Err(AppError::BadRequest(format!(
"Environment variable name exceeds {} characters",
MAX_NAME_LEN
)));
}
if !ENV_VAR_NAME_RE.is_match(name) {
return Err(AppError::BadRequest(format!(
"Invalid environment variable name '{}': must match [A-Za-z_][A-Za-z0-9_]*",
name
)));
}
Ok(())
}
pub fn validate_env_var_value(value: &str) -> Result<(), AppError> {
if value.len() > MAX_VALUE_LEN {
return Err(AppError::BadRequest(format!(
"Environment variable value exceeds {} characters",
MAX_VALUE_LEN
)));
}
Ok(())
}
pub fn check_duplicate_names(names: &[&str]) -> Result<(), AppError> {
let mut seen = std::collections::HashSet::new();
for name in names {
if !seen.insert(*name) {
return Err(AppError::BadRequest(format!(
"Duplicate environment variable name: '{}'",
name
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn name_accepts_simple_identifiers() {
assert!(validate_env_var_name("HOME").is_ok());
assert!(validate_env_var_name("_PRIVATE").is_ok());
assert!(validate_env_var_name("GITHUB_TOKEN").is_ok());
assert!(validate_env_var_name("a").is_ok());
assert!(validate_env_var_name("_").is_ok());
assert!(validate_env_var_name("A1B2C3").is_ok());
}
#[test]
fn name_rejects_empty() {
let err = validate_env_var_name("").unwrap_err();
assert!(matches!(err, AppError::BadRequest(msg) if msg.contains("cannot be empty")));
}
#[test]
fn name_rejects_starting_with_digit() {
assert!(validate_env_var_name("1ABC").is_err());
assert!(validate_env_var_name("9_VAR").is_err());
}
#[test]
fn name_rejects_special_characters() {
assert!(validate_env_var_name("MY-VAR").is_err());
assert!(validate_env_var_name("MY.VAR").is_err());
assert!(validate_env_var_name("MY VAR").is_err());
assert!(validate_env_var_name("MY@VAR").is_err());
assert!(validate_env_var_name("MY=VAR").is_err());
assert!(validate_env_var_name("MY$VAR").is_err());
assert!(validate_env_var_name("path/var").is_err());
}
#[test]
fn name_rejects_unicode() {
assert!(validate_env_var_name("变量").is_err());
assert!(validate_env_var_name("café").is_err());
}
#[test]
fn name_rejects_too_long() {
let long = "A".repeat(257);
assert!(validate_env_var_name(&long).is_err());
}
#[test]
fn name_accepts_max_length() {
let exactly_256 = "A".repeat(256);
assert!(validate_env_var_name(&exactly_256).is_ok());
}
#[test]
fn value_accepts_normal_strings() {
assert!(validate_env_var_value("hello").is_ok());
assert!(validate_env_var_value("").is_ok()); assert!(validate_env_var_value("sk-proj-1234567890").is_ok());
}
#[test]
fn value_accepts_max_length() {
let val = "x".repeat(65_536);
assert!(validate_env_var_value(&val).is_ok());
}
#[test]
fn value_rejects_too_long() {
let val = "x".repeat(65_537);
assert!(validate_env_var_value(&val).is_err());
}
#[test]
fn duplicates_allows_unique_names() {
assert!(check_duplicate_names(&["A", "B", "C"]).is_ok());
}
#[test]
fn duplicates_detects_same_name() {
let err = check_duplicate_names(&["TOKEN", "SECRET", "TOKEN"]).unwrap_err();
assert!(matches!(err, AppError::BadRequest(msg) if msg.contains("TOKEN")));
}
#[test]
fn duplicates_allows_empty_list() {
assert!(check_duplicate_names(&[]).is_ok());
}
#[test]
fn duplicates_single_entry() {
assert!(check_duplicate_names(&["ONLY_ONE"]).is_ok());
}
#[test]
fn duplicates_case_sensitive() {
assert!(check_duplicate_names(&["token", "TOKEN"]).is_ok());
}
}