use crate::error::{ArchToolkitError, Result};
use std::sync::LazyLock;
static DEFAULT_VALIDATION_CONFIG: LazyLock<ValidationConfig> =
LazyLock::new(ValidationConfig::default);
#[derive(Debug, Clone)]
pub struct ValidationConfig {
pub strict_empty: bool,
pub max_query_length: usize,
pub max_package_name_length: usize,
}
impl Default for ValidationConfig {
fn default() -> Self {
Self {
strict_empty: true,
max_query_length: 256,
max_package_name_length: 127,
}
}
}
pub fn validate_package_name<'a>(
name: &'a str,
config: Option<&ValidationConfig>,
) -> Result<&'a str> {
let config = config.unwrap_or(&DEFAULT_VALIDATION_CONFIG);
if name.is_empty() {
if config.strict_empty {
return Err(ArchToolkitError::EmptyInput {
field: "package name".to_string(),
message: "package name cannot be empty".to_string(),
});
}
return Ok(name); }
if name.len() > config.max_package_name_length {
return Err(ArchToolkitError::InputTooLong {
field: "package name".to_string(),
max_length: config.max_package_name_length,
actual_length: name.len(),
});
}
if name.starts_with('-') {
return Err(ArchToolkitError::InvalidPackageName {
name: name.to_string(),
reason: "package name cannot start with a hyphen (-)".to_string(),
});
}
if name.starts_with('.') {
return Err(ArchToolkitError::InvalidPackageName {
name: name.to_string(),
reason: "package name cannot start with a period (.)".to_string(),
});
}
for (idx, ch) in name.char_indices() {
let is_valid = matches!(ch,
'a'..='z' | '0'..='9' | '@' | '.' | '_' | '+' | '-'
);
if !is_valid {
return Err(ArchToolkitError::InvalidPackageName {
name: name.to_string(),
reason: format!(
"package name contains invalid character '{ch}' at position {idx} (allowed: lowercase letters, digits, @, ., _, +, -)"
),
});
}
}
Ok(name)
}
pub fn validate_package_names(names: &[&str], config: Option<&ValidationConfig>) -> Result<()> {
let config = config.unwrap_or(&DEFAULT_VALIDATION_CONFIG);
if names.is_empty() {
if config.strict_empty {
return Err(ArchToolkitError::EmptyInput {
field: "package names".to_string(),
message: "at least one package name is required".to_string(),
});
}
return Ok(()); }
for name in names {
validate_package_name(name, Some(config))?;
}
Ok(())
}
pub fn validate_search_query<'a>(
query: &'a str,
config: Option<&ValidationConfig>,
) -> Result<&'a str> {
let config = config.unwrap_or(&DEFAULT_VALIDATION_CONFIG);
let trimmed = query.trim();
if trimmed.is_empty() {
if config.strict_empty {
return Err(ArchToolkitError::EmptyInput {
field: "search query".to_string(),
message: "search query cannot be empty".to_string(),
});
}
return Ok(trimmed); }
if trimmed.len() > config.max_query_length {
return Err(ArchToolkitError::InputTooLong {
field: "search query".to_string(),
max_length: config.max_query_length,
actual_length: trimmed.len(),
});
}
Ok(trimmed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_package_name_valid() {
let valid_names = [
"yay",
"paru",
"linux-zen",
"lib32-mesa",
"python-numpy",
"gcc@12",
"package_name",
"pkg+plus",
"123package",
];
for name in &valid_names {
assert!(
validate_package_name(name, None).is_ok(),
"Package name '{name}' should be valid"
);
}
}
#[test]
fn test_validate_package_name_empty() {
let result = validate_package_name("", None);
assert!(result.is_err());
match result.expect_err("Expected validation error") {
ArchToolkitError::EmptyInput { field, .. } => {
assert_eq!(field, "package name");
}
_ => panic!("Expected EmptyInput error"),
}
let config = ValidationConfig {
strict_empty: false,
..Default::default()
};
assert!(validate_package_name("", Some(&config)).is_ok());
}
#[test]
fn test_validate_package_name_starts_with_hyphen() {
let result = validate_package_name("-invalid", None);
assert!(result.is_err());
match result.expect_err("Expected validation error") {
ArchToolkitError::InvalidPackageName { name, reason } => {
assert_eq!(name, "-invalid");
assert!(reason.contains("hyphen"));
}
_ => panic!("Expected InvalidPackageName error"),
}
}
#[test]
fn test_validate_package_name_starts_with_period() {
let result = validate_package_name(".invalid", None);
assert!(result.is_err());
match result.expect_err("Expected validation error") {
ArchToolkitError::InvalidPackageName { name, reason } => {
assert_eq!(name, ".invalid");
assert!(reason.contains("period"));
}
_ => panic!("Expected InvalidPackageName error"),
}
}
#[test]
fn test_validate_package_name_uppercase() {
let result = validate_package_name("Invalid", None);
assert!(result.is_err());
match result.expect_err("Expected validation error") {
ArchToolkitError::InvalidPackageName { name, reason } => {
assert_eq!(name, "Invalid");
assert!(reason.contains("invalid character"));
}
_ => panic!("Expected InvalidPackageName error"),
}
}
#[test]
fn test_validate_package_name_special_chars() {
let invalid = ["package#name", "package name", "package!"];
for name in &invalid {
let result = validate_package_name(name, None);
assert!(result.is_err(), "Package name '{name}' should be invalid");
match result.expect_err("Expected validation error") {
ArchToolkitError::InvalidPackageName { .. } => {}
_ => panic!("Expected InvalidPackageName error for '{name}'"),
}
}
}
#[test]
fn test_validate_package_name_too_long() {
let long_name = "a".repeat(128); let result = validate_package_name(&long_name, None);
assert!(result.is_err());
match result.expect_err("Expected validation error") {
ArchToolkitError::InputTooLong {
field,
max_length,
actual_length,
} => {
assert_eq!(field, "package name");
assert_eq!(max_length, 127);
assert_eq!(actual_length, 128);
}
_ => panic!("Expected InputTooLong error"),
}
}
#[test]
fn test_validate_package_name_custom_max_length() {
let config = ValidationConfig {
max_package_name_length: 10,
..Default::default()
};
let name = "a".repeat(11);
let result = validate_package_name(&name, Some(&config));
assert!(result.is_err());
match result.expect_err("Expected validation error") {
ArchToolkitError::InputTooLong { max_length, .. } => {
assert_eq!(max_length, 10);
}
_ => panic!("Expected InputTooLong error"),
}
}
#[test]
fn test_validate_package_names_valid() {
let names = &["yay", "paru", "linux-zen"];
assert!(validate_package_names(names, None).is_ok());
}
#[test]
fn test_validate_package_names_empty() {
let result = validate_package_names(&[], None);
assert!(result.is_err());
match result.expect_err("Expected validation error") {
ArchToolkitError::EmptyInput { field, .. } => {
assert_eq!(field, "package names");
}
_ => panic!("Expected EmptyInput error"),
}
let config = ValidationConfig {
strict_empty: false,
..Default::default()
};
assert!(validate_package_names(&[], Some(&config)).is_ok());
}
#[test]
fn test_validate_package_names_invalid() {
let names = &["yay", "-invalid", "paru"];
let result = validate_package_names(names, None);
assert!(result.is_err());
match result.expect_err("Expected validation error") {
ArchToolkitError::InvalidPackageName { name, .. } => {
assert_eq!(name, "-invalid");
}
_ => panic!("Expected InvalidPackageName error"),
}
}
#[test]
fn test_validate_search_query_valid() {
let queries = ["yay", "paru helper", "linux", " trimmed "];
for query in &queries {
let result = validate_search_query(query, None);
assert!(result.is_ok(), "Query '{query}' should be valid");
if let Ok(trimmed) = result {
assert_eq!(trimmed, query.trim());
}
}
}
#[test]
fn test_validate_search_query_empty() {
let result = validate_search_query("", None);
assert!(result.is_err());
match result.expect_err("Expected validation error") {
ArchToolkitError::EmptyInput { field, .. } => {
assert_eq!(field, "search query");
}
_ => panic!("Expected EmptyInput error"),
}
let result = validate_search_query(" ", None);
assert!(result.is_err());
let config = ValidationConfig {
strict_empty: false,
..Default::default()
};
assert!(validate_search_query("", Some(&config)).is_ok());
assert!(validate_search_query(" ", Some(&config)).is_ok());
}
#[test]
fn test_validate_search_query_too_long() {
let long_query = "a".repeat(257); let result = validate_search_query(&long_query, None);
assert!(result.is_err());
match result.expect_err("Expected validation error") {
ArchToolkitError::InputTooLong {
field,
max_length,
actual_length,
} => {
assert_eq!(field, "search query");
assert_eq!(max_length, 256);
assert_eq!(actual_length, 257);
}
_ => panic!("Expected InputTooLong error"),
}
}
#[test]
fn test_validate_search_query_custom_max_length() {
let config = ValidationConfig {
max_query_length: 10,
..Default::default()
};
let query = "a".repeat(11);
let result = validate_search_query(&query, Some(&config));
assert!(result.is_err());
match result.expect_err("Expected validation error") {
ArchToolkitError::InputTooLong { max_length, .. } => {
assert_eq!(max_length, 10);
}
_ => panic!("Expected InputTooLong error"),
}
}
#[test]
fn test_validation_config_default() {
let config = ValidationConfig::default();
assert!(config.strict_empty);
assert_eq!(config.max_query_length, 256);
assert_eq!(config.max_package_name_length, 127);
}
}