Skip to main content

geode_client/
validate.rs

1//! Input validation utilities for the Geode client.
2//!
3//! This module provides validation functions for user-supplied inputs
4//! to prevent injection attacks and ensure data integrity.
5
6use crate::error::{Error, Result};
7
8/// Maximum allowed query length in bytes
9pub const MAX_QUERY_LENGTH: usize = 1_000_000; // 1 MB
10
11/// Maximum allowed parameter name length
12pub const MAX_PARAM_NAME_LENGTH: usize = 128;
13
14/// Maximum allowed hostname length
15pub const MAX_HOSTNAME_LENGTH: usize = 253;
16
17/// Validate a GQL query string.
18///
19/// # Validation Rules
20/// - Query must not be empty
21/// - Query must not exceed MAX_QUERY_LENGTH bytes
22/// - Query must be valid UTF-8 (enforced by Rust's String type)
23///
24/// # Examples
25///
26/// ```
27/// use geode_client::validate;
28///
29/// assert!(validate::query("MATCH (n) RETURN n").is_ok());
30/// assert!(validate::query("").is_err());
31/// ```
32pub fn query(q: &str) -> Result<()> {
33    if q.is_empty() {
34        return Err(Error::validation("Query cannot be empty"));
35    }
36
37    if q.len() > MAX_QUERY_LENGTH {
38        return Err(Error::validation(format!(
39            "Query exceeds maximum length of {} bytes",
40            MAX_QUERY_LENGTH
41        )));
42    }
43
44    // Check for null bytes which could cause issues with C-based libraries
45    if q.contains('\0') {
46        return Err(Error::validation("Query contains invalid null character"));
47    }
48
49    Ok(())
50}
51
52/// Validate a parameter name.
53///
54/// # Validation Rules
55/// - Name must not be empty
56/// - Name must not exceed MAX_PARAM_NAME_LENGTH
57/// - Name must start with a letter or underscore
58/// - Name can only contain letters, digits, and underscores
59///
60/// # Examples
61///
62/// ```
63/// use geode_client::validate;
64///
65/// assert!(validate::param_name("user_id").is_ok());
66/// assert!(validate::param_name("_private").is_ok());
67/// assert!(validate::param_name("123invalid").is_err());
68/// assert!(validate::param_name("").is_err());
69/// ```
70pub fn param_name(name: &str) -> Result<()> {
71    if name.is_empty() {
72        return Err(Error::validation("Parameter name cannot be empty"));
73    }
74
75    if name.len() > MAX_PARAM_NAME_LENGTH {
76        return Err(Error::validation(format!(
77            "Parameter name exceeds maximum length of {} characters",
78            MAX_PARAM_NAME_LENGTH
79        )));
80    }
81
82    let mut chars = name.chars();
83
84    // First character must be letter or underscore
85    match chars.next() {
86        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
87        Some(c) => {
88            return Err(Error::validation(format!(
89                "Parameter name must start with a letter or underscore, found '{}'",
90                c
91            )));
92        }
93        None => unreachable!(), // Already checked for empty
94    }
95
96    // Rest must be alphanumeric or underscore
97    for c in chars {
98        if !c.is_ascii_alphanumeric() && c != '_' {
99            return Err(Error::validation(format!(
100                "Parameter name contains invalid character '{}'",
101                c
102            )));
103        }
104    }
105
106    Ok(())
107}
108
109/// Validate a hostname.
110///
111/// # Validation Rules
112/// - Hostname must not be empty
113/// - Hostname must not exceed MAX_HOSTNAME_LENGTH (253 characters per RFC 1035)
114/// - Each label (between dots) must be 1-63 characters
115/// - Labels can only contain letters, digits, and hyphens
116/// - Labels cannot start or end with a hyphen
117/// - IP addresses (v4 and v6) are allowed
118///
119/// # Examples
120///
121/// ```
122/// use geode_client::validate;
123///
124/// assert!(validate::hostname("localhost").is_ok());
125/// assert!(validate::hostname("geode.example.com").is_ok());
126/// assert!(validate::hostname("192.168.1.1").is_ok());
127/// assert!(validate::hostname("::1").is_ok());
128/// assert!(validate::hostname("").is_err());
129/// ```
130pub fn hostname(host: &str) -> Result<()> {
131    if host.is_empty() {
132        return Err(Error::validation("Hostname cannot be empty"));
133    }
134
135    if host.len() > MAX_HOSTNAME_LENGTH {
136        return Err(Error::validation(format!(
137            "Hostname exceeds maximum length of {} characters",
138            MAX_HOSTNAME_LENGTH
139        )));
140    }
141
142    // Allow IPv6 addresses in brackets
143    if host.starts_with('[') && host.ends_with(']') {
144        let ipv6 = &host[1..host.len() - 1];
145        return validate_ipv6(ipv6);
146    }
147
148    // Allow bare IPv6 addresses
149    if host.contains(':') && !host.contains('.') {
150        return validate_ipv6(host);
151    }
152
153    // Check if it's an IPv4 address
154    if host.chars().all(|c| c.is_ascii_digit() || c == '.') {
155        return validate_ipv4(host);
156    }
157
158    // Validate as hostname
159    validate_hostname_labels(host)
160}
161
162fn validate_ipv4(addr: &str) -> Result<()> {
163    let parts: Vec<&str> = addr.split('.').collect();
164    if parts.len() != 4 {
165        return Err(Error::validation("Invalid IPv4 address format"));
166    }
167
168    for part in parts {
169        match part.parse::<u8>() {
170            Ok(_) => {}
171            Err(_) => {
172                return Err(Error::validation(format!("Invalid IPv4 octet: {}", part)));
173            }
174        }
175    }
176
177    Ok(())
178}
179
180fn validate_ipv6(addr: &str) -> Result<()> {
181    // Basic IPv6 validation - check for valid characters
182    for c in addr.chars() {
183        if !c.is_ascii_hexdigit() && c != ':' {
184            return Err(Error::validation(format!(
185                "Invalid character in IPv6 address: {}",
186                c
187            )));
188        }
189    }
190
191    // Check for at least one colon
192    if !addr.contains(':') {
193        return Err(Error::validation("Invalid IPv6 address format"));
194    }
195
196    Ok(())
197}
198
199fn validate_hostname_labels(host: &str) -> Result<()> {
200    let labels: Vec<&str> = host.split('.').collect();
201
202    for label in labels {
203        if label.is_empty() {
204            return Err(Error::validation("Hostname contains empty label"));
205        }
206
207        if label.len() > 63 {
208            return Err(Error::validation(format!(
209                "Hostname label '{}' exceeds 63 characters",
210                label
211            )));
212        }
213
214        if label.starts_with('-') || label.ends_with('-') {
215            return Err(Error::validation(format!(
216                "Hostname label '{}' cannot start or end with hyphen",
217                label
218            )));
219        }
220
221        for c in label.chars() {
222            if !c.is_ascii_alphanumeric() && c != '-' {
223                return Err(Error::validation(format!(
224                    "Hostname contains invalid character '{}'",
225                    c
226                )));
227            }
228        }
229    }
230
231    Ok(())
232}
233
234/// Validate a port number.
235///
236/// # Validation Rules
237/// - Port must be in the range 1-65535
238/// - Port 0 is not allowed (reserved)
239///
240/// # Examples
241///
242/// ```
243/// use geode_client::validate;
244///
245/// assert!(validate::port(3141).is_ok());
246/// assert!(validate::port(443).is_ok());
247/// assert!(validate::port(0).is_err());
248/// ```
249pub fn port(p: u16) -> Result<()> {
250    if p == 0 {
251        return Err(Error::validation("Port 0 is reserved and cannot be used"));
252    }
253    Ok(())
254}
255
256/// Validate a page size.
257///
258/// # Validation Rules
259/// - Page size must be at least 1
260/// - Page size must not exceed 100,000
261///
262/// # Examples
263///
264/// ```
265/// use geode_client::validate;
266///
267/// assert!(validate::page_size(100).is_ok());
268/// assert!(validate::page_size(0).is_err());
269/// assert!(validate::page_size(200_000).is_err());
270/// ```
271pub fn page_size(size: usize) -> Result<()> {
272    if size == 0 {
273        return Err(Error::validation("Page size must be at least 1"));
274    }
275
276    if size > 100_000 {
277        return Err(Error::validation("Page size cannot exceed 100,000 rows"));
278    }
279
280    Ok(())
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    // ==================== Query Validation Tests ====================
288
289    #[test]
290    fn test_query_valid() {
291        assert!(query("MATCH (n) RETURN n").is_ok());
292        assert!(query("RETURN 1").is_ok());
293        assert!(query("CREATE (n:Person {name: 'Alice'})").is_ok());
294    }
295
296    #[test]
297    fn test_query_empty() {
298        let result = query("");
299        assert!(result.is_err());
300        assert!(result.unwrap_err().to_string().contains("empty"));
301    }
302
303    #[test]
304    fn test_query_too_long() {
305        let long_query = "x".repeat(MAX_QUERY_LENGTH + 1);
306        let result = query(&long_query);
307        assert!(result.is_err());
308        assert!(result.unwrap_err().to_string().contains("maximum length"));
309    }
310
311    #[test]
312    fn test_query_with_null() {
313        let result = query("RETURN \0 AS x");
314        assert!(result.is_err());
315        assert!(result.unwrap_err().to_string().contains("null"));
316    }
317
318    #[test]
319    fn test_query_unicode() {
320        assert!(query("RETURN '日本語' AS text").is_ok());
321        assert!(query("CREATE (n {emoji: '🚀'})").is_ok());
322    }
323
324    #[test]
325    fn test_query_whitespace_only() {
326        // Whitespace-only is technically valid (will fail at server)
327        assert!(query("   ").is_ok());
328    }
329
330    // ==================== Parameter Name Tests ====================
331
332    #[test]
333    fn test_param_name_valid() {
334        assert!(param_name("user_id").is_ok());
335        assert!(param_name("_private").is_ok());
336        assert!(param_name("x").is_ok());
337        assert!(param_name("userName123").is_ok());
338        assert!(param_name("_").is_ok());
339        assert!(param_name("__double__").is_ok());
340    }
341
342    #[test]
343    fn test_param_name_empty() {
344        let result = param_name("");
345        assert!(result.is_err());
346        assert!(result.unwrap_err().to_string().contains("empty"));
347    }
348
349    #[test]
350    fn test_param_name_starts_with_digit() {
351        let result = param_name("123invalid");
352        assert!(result.is_err());
353        assert!(result.unwrap_err().to_string().contains("start with"));
354    }
355
356    #[test]
357    fn test_param_name_invalid_chars() {
358        assert!(param_name("user-id").is_err()); // hyphen
359        assert!(param_name("user.id").is_err()); // dot
360        assert!(param_name("user id").is_err()); // space
361        assert!(param_name("user@id").is_err()); // special char
362    }
363
364    #[test]
365    fn test_param_name_too_long() {
366        let long_name = "a".repeat(MAX_PARAM_NAME_LENGTH + 1);
367        let result = param_name(&long_name);
368        assert!(result.is_err());
369        assert!(result.unwrap_err().to_string().contains("maximum length"));
370    }
371
372    // ==================== Hostname Tests ====================
373
374    #[test]
375    fn test_hostname_valid() {
376        assert!(hostname("localhost").is_ok());
377        assert!(hostname("geode.example.com").is_ok());
378        assert!(hostname("my-server").is_ok());
379        assert!(hostname("server1").is_ok());
380        assert!(hostname("a.b.c.d.e").is_ok());
381    }
382
383    #[test]
384    fn test_hostname_empty() {
385        let result = hostname("");
386        assert!(result.is_err());
387        assert!(result.unwrap_err().to_string().contains("empty"));
388    }
389
390    #[test]
391    fn test_hostname_ipv4() {
392        assert!(hostname("192.168.1.1").is_ok());
393        assert!(hostname("127.0.0.1").is_ok());
394        assert!(hostname("0.0.0.0").is_ok());
395        assert!(hostname("255.255.255.255").is_ok());
396    }
397
398    #[test]
399    fn test_hostname_ipv4_invalid() {
400        assert!(hostname("256.1.1.1").is_err()); // octet > 255
401        assert!(hostname("1.2.3").is_err()); // too few octets
402        assert!(hostname("1.2.3.4.5").is_err()); // too many octets
403    }
404
405    #[test]
406    fn test_hostname_ipv6() {
407        assert!(hostname("::1").is_ok());
408        assert!(hostname("fe80::1").is_ok());
409        assert!(hostname("[::1]").is_ok());
410        assert!(hostname("[fe80::1]").is_ok());
411    }
412
413    #[test]
414    fn test_hostname_label_hyphen() {
415        assert!(hostname("-invalid").is_err());
416        assert!(hostname("invalid-").is_err());
417        assert!(hostname("valid-host").is_ok());
418    }
419
420    #[test]
421    fn test_hostname_label_too_long() {
422        let long_label = "a".repeat(64);
423        let result = hostname(&long_label);
424        assert!(result.is_err());
425        assert!(result.unwrap_err().to_string().contains("63"));
426    }
427
428    #[test]
429    fn test_hostname_too_long() {
430        let long_host = format!("{}.example.com", "a".repeat(250));
431        let result = hostname(&long_host);
432        assert!(result.is_err());
433    }
434
435    #[test]
436    fn test_hostname_invalid_chars() {
437        assert!(hostname("invalid_host").is_err()); // underscore
438        assert!(hostname("invalid host").is_err()); // space
439        assert!(hostname("invalid@host").is_err()); // special char
440    }
441
442    // ==================== Port Tests ====================
443
444    #[test]
445    fn test_port_valid() {
446        assert!(port(1).is_ok());
447        assert!(port(80).is_ok());
448        assert!(port(443).is_ok());
449        assert!(port(3141).is_ok());
450        assert!(port(8443).is_ok());
451        assert!(port(65535).is_ok());
452    }
453
454    #[test]
455    fn test_port_zero() {
456        let result = port(0);
457        assert!(result.is_err());
458        assert!(result.unwrap_err().to_string().contains("reserved"));
459    }
460
461    // ==================== Page Size Tests ====================
462
463    #[test]
464    fn test_page_size_valid() {
465        assert!(page_size(1).is_ok());
466        assert!(page_size(100).is_ok());
467        assert!(page_size(1000).is_ok());
468        assert!(page_size(100_000).is_ok());
469    }
470
471    #[test]
472    fn test_page_size_zero() {
473        let result = page_size(0);
474        assert!(result.is_err());
475        assert!(result.unwrap_err().to_string().contains("at least 1"));
476    }
477
478    #[test]
479    fn test_page_size_too_large() {
480        let result = page_size(100_001);
481        assert!(result.is_err());
482        assert!(result.unwrap_err().to_string().contains("100,000"));
483    }
484}