const MAX_IDENTIFIER_LENGTH: usize = 63;
#[inline]
#[must_use]
pub fn is_valid_sql_identifier(s: &str) -> bool {
if s.is_empty() || s.len() > MAX_IDENTIFIER_LENGTH {
return false;
}
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {},
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
#[inline]
pub fn assert_valid_sql_identifier(s: &str, context: &str) {
assert!(
is_valid_sql_identifier(s),
"Invalid SQL {context} name '{s}': must start with letter/underscore, \
contain only ASCII alphanumeric/underscore, and be 1-63 chars"
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_sql_identifiers() {
assert!(is_valid_sql_identifier("users"));
assert!(is_valid_sql_identifier("user_id"));
assert!(is_valid_sql_identifier("_private"));
assert!(is_valid_sql_identifier("Table123"));
assert!(is_valid_sql_identifier("a"));
assert!(is_valid_sql_identifier("_"));
assert!(is_valid_sql_identifier("UPPERCASE"));
assert!(is_valid_sql_identifier("mixedCase"));
assert!(is_valid_sql_identifier("with_123_numbers"));
}
#[test]
fn test_invalid_sql_identifiers() {
assert!(!is_valid_sql_identifier(""));
assert!(!is_valid_sql_identifier("123abc"));
assert!(!is_valid_sql_identifier("1"));
assert!(!is_valid_sql_identifier("user-name"));
assert!(!is_valid_sql_identifier("user.id"));
assert!(!is_valid_sql_identifier("user name"));
assert!(!is_valid_sql_identifier("user;drop"));
assert!(!is_valid_sql_identifier("table'"));
assert!(!is_valid_sql_identifier("table\""));
assert!(!is_valid_sql_identifier("table`"));
assert!(!is_valid_sql_identifier("table("));
assert!(!is_valid_sql_identifier("table)"));
assert!(!is_valid_sql_identifier("users; DROP TABLE"));
assert!(!is_valid_sql_identifier("users--"));
assert!(!is_valid_sql_identifier("users/*"));
}
#[test]
fn test_sql_identifier_length_limit() {
let valid_63 = "a".repeat(63);
assert!(is_valid_sql_identifier(&valid_63));
let invalid_64 = "a".repeat(64);
assert!(!is_valid_sql_identifier(&invalid_64));
}
#[test]
fn test_identifier_injection_attempts() {
assert!(!is_valid_sql_identifier("users; DROP TABLE x"));
assert!(!is_valid_sql_identifier("users--"));
assert!(!is_valid_sql_identifier("users/*comment*/"));
assert!(!is_valid_sql_identifier("users'"));
assert!(!is_valid_sql_identifier("users\""));
assert!(!is_valid_sql_identifier("users`"));
assert!(!is_valid_sql_identifier("users;"));
assert!(!is_valid_sql_identifier("(SELECT 1)"));
assert!(!is_valid_sql_identifier("1 OR 1=1"));
assert!(!is_valid_sql_identifier("users\u{0000}")); assert!(!is_valid_sql_identifier("users\u{200B}")); assert!(!is_valid_sql_identifier("usërs")); assert!(!is_valid_sql_identifier("用户"));
assert!(!is_valid_sql_identifier("users")); }
#[test]
#[should_panic(expected = "Invalid SQL table name")]
fn test_assert_valid_identifier_panics() {
assert_valid_sql_identifier("users; DROP TABLE", "table");
}
}