use crate::DbError;
pub fn validate_identifier(name: &str, kind: &str) -> Result<(), DbError> {
if name.is_empty() || name.len() > 63 {
return Err(DbError::InvalidInput(format!(
"invalid {}: empty or too long (max 63 chars): {:?}",
kind, name
)));
}
let mut chars = name.chars();
let first = chars.next().ok_or_else(|| {
DbError::InvalidInput(format!(
"invalid {}: empty string after length check: {:?}",
kind, name
))
})?;
if !first.is_ascii_alphabetic() && first != '_' {
return Err(DbError::InvalidInput(format!(
"invalid {}: must start with ASCII letter or underscore, got {:?}",
kind, name
)));
}
if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(DbError::InvalidInput(format!(
"invalid {}: only ASCII alphanumeric and underscore allowed, got {:?}",
kind, name
)));
}
Ok(())
}
pub fn validate_fk_action(action: &str) -> Result<(), DbError> {
const ALLOWED: &[&str] = &[
"CASCADE",
"SET NULL",
"SET DEFAULT",
"RESTRICT",
"NO ACTION",
];
let upper = action.trim().to_uppercase();
if !ALLOWED.contains(&upper.as_str()) {
return Err(DbError::InvalidInput(format!(
"invalid foreign key action: {:?}, allowed: {:?}",
action, ALLOWED
)));
}
Ok(())
}
pub fn validate_id_value(id: &str) -> Result<(), DbError> {
if id.is_empty() || id.len() > 128 {
return Err(DbError::InvalidInput(format!(
"invalid id value: empty or too long (max 128 chars): {:?}",
id
)));
}
if id.contains("--") {
return Err(DbError::InvalidInput(format!(
"invalid id value: SQL comment sequence '--' not allowed, got {:?}",
id
)));
}
if !id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
return Err(DbError::InvalidInput(format!(
"invalid id value: only ASCII alphanumeric, underscore and hyphen allowed, got {:?}",
id
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_identifier_valid() {
assert!(validate_identifier("users", "table").is_ok());
assert!(validate_identifier("_idx", "index").is_ok());
assert!(validate_identifier("geom_2026", "column").is_ok());
assert!(validate_identifier("a", "column").is_ok());
assert!(validate_identifier(&"a".repeat(63), "table").is_ok());
}
#[test]
fn test_validate_identifier_injection_attempts() {
assert!(validate_identifier("users; DROP TABLE users", "table").is_err());
assert!(validate_identifier("col'--", "column").is_err());
assert!(validate_identifier("col\"x", "column").is_err());
assert!(validate_identifier("col`x", "column").is_err());
assert!(validate_identifier("col--", "column").is_err());
assert!(validate_identifier("col/*x*/", "column").is_err());
assert!(validate_identifier("col OR 1=1", "column").is_err());
assert!(validate_identifier("1col", "column").is_err());
assert!(validate_identifier("", "table").is_err());
let long_name = "a".repeat(64);
assert!(validate_identifier(&long_name, "table").is_err());
assert!(validate_identifier("col name", "column").is_err());
assert!(validate_identifier("col$name", "column").is_err());
assert!(validate_identifier("col%name", "column").is_err());
assert!(validate_identifier("col@name", "column").is_err());
}
#[test]
fn test_validate_fk_action_valid() {
assert!(validate_fk_action("CASCADE").is_ok());
assert!(validate_fk_action("cascade").is_ok()); assert!(validate_fk_action("Cascade").is_ok());
assert!(validate_fk_action("SET NULL").is_ok());
assert!(validate_fk_action("set null").is_ok());
assert!(validate_fk_action("SET DEFAULT").is_ok());
assert!(validate_fk_action("RESTRICT").is_ok());
assert!(validate_fk_action("NO ACTION").is_ok());
assert!(validate_fk_action(" NO ACTION ").is_ok()); }
#[test]
fn test_validate_fk_action_injection_attempts() {
assert!(validate_fk_action("CASCADE; DROP TABLE users").is_err());
assert!(validate_fk_action("CASCADE--").is_err());
assert!(validate_fk_action("CASCADE OR 1=1").is_err());
assert!(validate_fk_action("EVIL").is_err());
assert!(validate_fk_action("' OR '1'='1").is_err());
assert!(validate_fk_action("").is_err());
}
#[test]
fn test_validate_id_value_valid() {
assert!(validate_id_value("1").is_ok());
assert!(validate_id_value("100").is_ok());
assert!(validate_id_value("abc").is_ok());
assert!(validate_id_value("user_123").is_ok());
assert!(validate_id_value("uuid-abc-123").is_ok());
assert!(validate_id_value(&"a".repeat(128)).is_ok());
}
#[test]
fn test_validate_id_value_injection_attempts() {
assert!(validate_id_value("1; DROP TABLE users").is_err());
assert!(validate_id_value("1) OR 1=1").is_err());
assert!(validate_id_value("' OR '1'='1").is_err());
assert!(validate_id_value("1--").is_err());
assert!(validate_id_value("1/*comment*/").is_err());
assert!(validate_id_value("1;").is_err());
assert!(validate_id_value("1'").is_err());
assert!(validate_id_value("1\"").is_err());
assert!(validate_id_value("").is_err());
let long_id = "a".repeat(129);
assert!(validate_id_value(&long_id).is_err());
assert!(validate_id_value("1 2").is_err());
assert!(validate_id_value("users.id").is_err());
}
}