1use crate::error::{Error, Result};
7
8pub const MAX_QUERY_LENGTH: usize = 1_000_000; pub const MAX_PARAM_NAME_LENGTH: usize = 128;
13
14pub const MAX_HOSTNAME_LENGTH: usize = 253;
16
17pub 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 if q.contains('\0') {
46 return Err(Error::validation("Query contains invalid null character"));
47 }
48
49 Ok(())
50}
51
52pub 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 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!(), }
95
96 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
109pub 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 if host.starts_with('[') && host.ends_with(']') {
144 let ipv6 = &host[1..host.len() - 1];
145 return validate_ipv6(ipv6);
146 }
147
148 if host.contains(':') && !host.contains('.') {
150 return validate_ipv6(host);
151 }
152
153 if host.chars().all(|c| c.is_ascii_digit() || c == '.') {
155 return validate_ipv4(host);
156 }
157
158 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 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 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
234pub 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
256pub 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 #[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 assert!(query(" ").is_ok());
328 }
329
330 #[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()); assert!(param_name("user.id").is_err()); assert!(param_name("user id").is_err()); assert!(param_name("user@id").is_err()); }
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 #[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()); assert!(hostname("1.2.3").is_err()); assert!(hostname("1.2.3.4.5").is_err()); }
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()); assert!(hostname("invalid host").is_err()); assert!(hostname("invalid@host").is_err()); }
441
442 #[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 #[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}