use crate::error::{Error, Result};
pub fn sanitize_graph_name(name: &str) -> String {
let mut out = String::with_capacity(name.len());
for c in name.chars() {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' {
out.push(c);
}
}
out
}
pub fn quote_ident(name: &str) -> Result<String> {
if name.is_empty() {
return Err(Error::validation(
"identifier contains forbidden characters",
));
}
let mut first = true;
for c in name.chars() {
match c {
'a'..='z' | 'A'..='Z' | '_' => {
}
'0'..='9' => {
}
'-' => {
if first {
return Err(Error::validation(
"identifier contains forbidden characters",
));
}
}
_ => {
return Err(Error::validation(
"identifier contains forbidden characters",
));
}
}
first = false;
}
Ok(name.to_string())
}
pub fn quote_string(s: &str) -> Result<String> {
for c in s.chars() {
if (c as u32) < 0x20 || c == '\u{7f}' {
return Err(Error::validation(
"string literal contains forbidden characters",
));
}
}
let escaped = s.replace('\\', "\\\\").replace('\'', "''");
Ok(format!("'{}'", escaped))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_graph_name() {
assert_eq!(sanitize_graph_name("mygraph"), "mygraph");
assert_eq!(sanitize_graph_name("my graph!"), "mygraph");
assert_eq!(sanitize_graph_name("prod-db_01"), "prod-db_01");
assert_eq!(sanitize_graph_name("a;DROP GRAPH x"), "aDROPGRAPHx");
assert_eq!(sanitize_graph_name(""), "");
assert_eq!(sanitize_graph_name("café"), "caf");
}
#[test]
fn test_quote_ident_valid() {
assert_eq!(quote_ident("user").unwrap(), "user");
assert_eq!(quote_ident("User_01").unwrap(), "User_01");
assert_eq!(quote_ident("a-b-c").unwrap(), "a-b-c");
assert_eq!(quote_ident("_private").unwrap(), "_private");
}
#[test]
fn test_quote_ident_digit_prefix_allowed() {
assert_eq!(quote_ident("123abc").unwrap(), "123abc");
assert_eq!(quote_ident("005_add_index").unwrap(), "005_add_index");
assert_eq!(quote_ident("9").unwrap(), "9");
}
#[test]
fn test_quote_ident_leading_dash_rejected() {
assert!(quote_ident("-bad").is_err());
assert!(quote_ident("-").is_err());
}
#[test]
fn test_quote_ident_empty_rejected() {
assert!(quote_ident("").is_err());
}
#[test]
fn test_quote_ident_invalid_chars_rejected() {
assert!(quote_ident("has space").is_err());
assert!(quote_ident("semi;colon").is_err());
assert!(quote_ident("quote'here").is_err());
assert!(quote_ident("bad\u{0}").is_err());
assert!(quote_ident("café").is_err());
}
#[test]
fn test_quote_string_basic() {
assert_eq!(quote_string("hello").unwrap(), "'hello'");
assert_eq!(quote_string("").unwrap(), "''");
}
#[test]
fn test_quote_string_escapes_single_quote() {
assert_eq!(quote_string("O'Brien").unwrap(), "'O''Brien'");
assert_eq!(quote_string("''").unwrap(), "''''''");
}
#[test]
fn test_quote_string_escapes_backslash() {
assert_eq!(quote_string("a\\b").unwrap(), "'a\\\\b'");
assert_eq!(quote_string("\\'").unwrap(), "'\\\\'''");
}
#[test]
fn test_quote_string_rejects_control_chars() {
assert!(quote_string("bad\nline").is_err());
assert!(quote_string("tab\there").is_err());
assert!(quote_string("null\u{0}byte").is_err());
assert!(quote_string("del\u{7f}").is_err());
}
}