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