database_replicator/jsonb/
mod.rs

1// ABOUTME: JSONB utilities for storing non-PostgreSQL database data
2// ABOUTME: Provides schema creation and validation for SQLite, MongoDB, and MySQL data storage
3
4pub mod writer;
5
6use anyhow::{bail, Result};
7
8/// Validate a table name to prevent SQL injection
9///
10/// Table names must contain only:
11/// - Lowercase letters (a-z)
12/// - Uppercase letters (A-Z)
13/// - Digits (0-9)
14/// - Underscores (_)
15///
16/// This prevents SQL injection attacks through table names.
17///
18/// # Arguments
19///
20/// * `table_name` - The table name to validate
21///
22/// # Returns
23///
24/// Ok(()) if valid, Err with message if invalid
25///
26/// # Examples
27///
28/// ```
29/// # use database_replicator::jsonb::validate_table_name;
30/// assert!(validate_table_name("users").is_ok());
31/// assert!(validate_table_name("user_events_2024").is_ok());
32/// assert!(validate_table_name("users; DROP TABLE users;").is_err());
33/// assert!(validate_table_name("users'--").is_err());
34/// ```
35pub fn validate_table_name(table_name: &str) -> Result<()> {
36    if table_name.is_empty() {
37        bail!("Table name cannot be empty");
38    }
39
40    if table_name.len() > 63 {
41        bail!("Table name too long (max 63 characters): {}", table_name);
42    }
43
44    // Check that all characters are alphanumeric or underscore
45    for ch in table_name.chars() {
46        if !ch.is_ascii_alphanumeric() && ch != '_' {
47            bail!(
48                "Invalid table name '{}': contains invalid character '{}'. \
49                Only alphanumeric characters and underscores are allowed.",
50                table_name,
51                ch
52            );
53        }
54    }
55
56    // Prevent reserved SQL keywords (case-insensitive)
57    let lower = table_name.to_lowercase();
58    let reserved_keywords = [
59        "select",
60        "insert",
61        "update",
62        "delete",
63        "drop",
64        "create",
65        "alter",
66        "table",
67        "database",
68        "index",
69        "view",
70        "function",
71        "procedure",
72        "trigger",
73        "user",
74        "role",
75        "grant",
76        "revoke",
77    ];
78
79    if reserved_keywords.contains(&lower.as_str()) {
80        bail!(
81            "Invalid table name '{}': cannot use SQL reserved keyword",
82            table_name
83        );
84    }
85
86    Ok(())
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_valid_table_names() {
95        assert!(validate_table_name("users").is_ok());
96        assert!(validate_table_name("user_events").is_ok());
97        assert!(validate_table_name("UserEvents2024").is_ok());
98        assert!(validate_table_name("_private").is_ok());
99        assert!(validate_table_name("table123").is_ok());
100    }
101
102    #[test]
103    fn test_invalid_table_names() {
104        // SQL injection attempts
105        assert!(validate_table_name("users; DROP TABLE users;").is_err());
106        assert!(validate_table_name("users'--").is_err());
107        assert!(validate_table_name("users OR 1=1").is_err());
108        assert!(validate_table_name("users/**/").is_err());
109
110        // Special characters
111        assert!(validate_table_name("users-events").is_err());
112        assert!(validate_table_name("users.events").is_err());
113        assert!(validate_table_name("users@host").is_err());
114        assert!(validate_table_name("users$var").is_err());
115
116        // Empty or too long
117        assert!(validate_table_name("").is_err());
118        assert!(validate_table_name(&"a".repeat(64)).is_err());
119
120        // Reserved keywords
121        assert!(validate_table_name("select").is_err());
122        assert!(validate_table_name("SELECT").is_err());
123        assert!(validate_table_name("table").is_err());
124        assert!(validate_table_name("drop").is_err());
125    }
126}