Skip to main content

tursotui_sql/
validation.rs

1//! SQL value validation (pragma sanitization, etc.).
2
3/// Validate and normalize a pragma value to a safe integer string.
4///
5/// Single source of truth for pragma value validation — used by both the UI
6/// layer (`PragmaDashboard`) for fast feedback and the DB layer
7/// (`run_set_pragma_inner`) for defense-in-depth.
8///
9/// Returns the normalized value string on success (trimmed, parsed and
10/// re-formatted as a plain integer).
11pub fn sanitize_pragma_value(name: &str, value: &str) -> Result<String, String> {
12    match name {
13        // Signed integer pragmas (negative cache_size means KB)
14        "cache_size" | "busy_timeout" => {
15            let n: i64 = value
16                .trim()
17                .parse()
18                .map_err(|_| format!("{name} must be an integer"))?;
19            Ok(n.to_string())
20        }
21        // Positive integer pragmas
22        "max_page_count" => {
23            let n: i64 = value
24                .trim()
25                .parse()
26                .map_err(|_| "max_page_count must be a positive integer".to_string())?;
27            if n > 0 {
28                Ok(n.to_string())
29            } else {
30                Err("max_page_count must be a positive integer".to_string())
31            }
32        }
33        // 0/1 boolean pragmas
34        "foreign_keys" | "query_only" => match value.trim() {
35            "0" | "1" => Ok(value.trim().to_string()),
36            _ => Err(format!("{name} must be 0 or 1")),
37        },
38        // Turso only supports OFF (0) and FULL (2) — NORMAL (1) and EXTRA (3)
39        // are not supported and would produce an opaque runtime error.
40        "synchronous" => match value.trim() {
41            "0" | "2" => Ok(value.trim().to_string()),
42            _ => Err("synchronous must be 0 (OFF) or 2 (FULL) on Turso".to_string()),
43        },
44        "temp_store" => match value.trim() {
45            "0" | "1" | "2" => Ok(value.trim().to_string()),
46            _ => Err("temp_store must be 0-2".to_string()),
47        },
48        _ => Err(format!("{name} is not writable")),
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn cache_size_valid_positive() {
58        assert_eq!(sanitize_pragma_value("cache_size", "2000").unwrap(), "2000");
59    }
60
61    #[test]
62    fn cache_size_valid_negative() {
63        assert_eq!(
64            sanitize_pragma_value("cache_size", "-4096").unwrap(),
65            "-4096"
66        );
67    }
68
69    #[test]
70    fn cache_size_trims_whitespace() {
71        assert_eq!(
72            sanitize_pragma_value("cache_size", "  100  ").unwrap(),
73            "100"
74        );
75    }
76
77    #[test]
78    fn cache_size_rejects_non_integer() {
79        assert!(sanitize_pragma_value("cache_size", "abc").is_err());
80    }
81
82    #[test]
83    fn busy_timeout_valid() {
84        assert_eq!(
85            sanitize_pragma_value("busy_timeout", "5000").unwrap(),
86            "5000"
87        );
88    }
89
90    #[test]
91    fn busy_timeout_rejects_float() {
92        assert!(sanitize_pragma_value("busy_timeout", "1.5").is_err());
93    }
94
95    #[test]
96    fn max_page_count_valid() {
97        assert_eq!(
98            sanitize_pragma_value("max_page_count", "1073741823").unwrap(),
99            "1073741823"
100        );
101    }
102
103    #[test]
104    fn max_page_count_rejects_zero() {
105        assert!(sanitize_pragma_value("max_page_count", "0").is_err());
106    }
107
108    #[test]
109    fn max_page_count_rejects_negative() {
110        assert!(sanitize_pragma_value("max_page_count", "-1").is_err());
111    }
112
113    #[test]
114    fn foreign_keys_accepts_zero_and_one() {
115        assert_eq!(sanitize_pragma_value("foreign_keys", "0").unwrap(), "0");
116        assert_eq!(sanitize_pragma_value("foreign_keys", "1").unwrap(), "1");
117    }
118
119    #[test]
120    fn foreign_keys_rejects_other() {
121        assert!(sanitize_pragma_value("foreign_keys", "2").is_err());
122        assert!(sanitize_pragma_value("foreign_keys", "on").is_err());
123    }
124
125    #[test]
126    fn query_only_accepts_zero_and_one() {
127        assert_eq!(sanitize_pragma_value("query_only", "0").unwrap(), "0");
128        assert_eq!(sanitize_pragma_value("query_only", "1").unwrap(), "1");
129    }
130
131    #[test]
132    fn synchronous_accepts_off_and_full() {
133        assert_eq!(sanitize_pragma_value("synchronous", "0").unwrap(), "0");
134        assert_eq!(sanitize_pragma_value("synchronous", "2").unwrap(), "2");
135    }
136
137    #[test]
138    fn synchronous_rejects_normal() {
139        assert!(sanitize_pragma_value("synchronous", "1").is_err());
140    }
141
142    #[test]
143    fn temp_store_accepts_valid_range() {
144        assert_eq!(sanitize_pragma_value("temp_store", "0").unwrap(), "0");
145        assert_eq!(sanitize_pragma_value("temp_store", "1").unwrap(), "1");
146        assert_eq!(sanitize_pragma_value("temp_store", "2").unwrap(), "2");
147    }
148
149    #[test]
150    fn temp_store_rejects_out_of_range() {
151        assert!(sanitize_pragma_value("temp_store", "3").is_err());
152    }
153
154    #[test]
155    fn unknown_pragma_rejected() {
156        assert!(sanitize_pragma_value("page_size", "4096").is_err());
157    }
158}