1use std::io;
4use std::path::{Component, PathBuf};
5
6const MAX_FILE_SIZE: u64 = 10_000_000;
8
9const MAX_EXPRESSION_LENGTH: usize = 10_000;
11
12const MAX_PATTERN_LENGTH: usize = 1_000;
14
15pub fn validate_file_path(path: &str) -> io::Result<PathBuf> {
30 if path.trim().is_empty() {
32 return Err(io::Error::new(
33 io::ErrorKind::InvalidInput,
34 "Path cannot be empty",
35 ));
36 }
37
38 let path_buf = PathBuf::from(path);
39
40 if path_buf.is_absolute() {
42 return Err(io::Error::new(
43 io::ErrorKind::PermissionDenied,
44 "Absolute paths are not allowed",
45 ));
46 }
47
48 for component in path_buf.components() {
50 if matches!(component, Component::ParentDir) {
51 return Err(io::Error::new(
52 io::ErrorKind::PermissionDenied,
53 "Path traversal (..) is not allowed",
54 ));
55 }
56 }
57
58 let cwd = std::env::current_dir()?;
60 let full_path = cwd.join(&path_buf);
61
62 if full_path.exists() || full_path.symlink_metadata().is_ok() {
65 let canonical_path = match full_path.canonicalize() {
68 Ok(path) => path,
69 Err(_) => {
70 return Err(io::Error::new(
72 io::ErrorKind::PermissionDenied,
73 "Path cannot be resolved (may be broken symlink or inaccessible)",
74 ));
75 }
76 };
77
78 if !canonical_path.starts_with(&cwd) {
80 return Err(io::Error::new(
81 io::ErrorKind::PermissionDenied,
82 "Path must be within current directory",
83 ));
84 }
85 } else {
86 if let Some(parent) = full_path.parent() {
89 match parent.canonicalize() {
91 Ok(canonical_parent) => {
92 if !canonical_parent.starts_with(&cwd) {
93 return Err(io::Error::new(
94 io::ErrorKind::PermissionDenied,
95 "Path must be within current directory",
96 ));
97 }
98 }
99 Err(_) => {
100 }
103 }
104 }
105 }
106
107 Ok(path_buf)
108}
109
110pub fn validate_expression(expr: &str) -> Result<(), String> {
126 if expr.trim().is_empty() {
128 return Err("Expression cannot be empty".to_string());
129 }
130
131 if expr.len() > MAX_EXPRESSION_LENGTH {
133 return Err(format!(
134 "Expression too long (max {} characters, got {})",
135 MAX_EXPRESSION_LENGTH,
136 expr.len()
137 ));
138 }
139
140 let open_count = expr.matches('(').count();
142 let close_count = expr.matches(')').count();
143 if open_count != close_count {
144 return Err(format!(
145 "Unbalanced parentheses: {} open, {} close",
146 open_count, close_count
147 ));
148 }
149
150 let open_brackets = expr.matches('[').count();
152 let close_brackets = expr.matches(']').count();
153 if open_brackets != close_brackets {
154 return Err(format!(
155 "Unbalanced brackets: {} open, {} close",
156 open_brackets, close_brackets
157 ));
158 }
159
160 let dangerous_patterns = [
162 "DROP", "DELETE", "INSERT", "UPDATE", "EXEC", "EXECUTE", "SYSTEM", "BASH", "SH", "CMD.EXE",
163 ];
164
165 for pattern in &dangerous_patterns {
166 if expr.to_uppercase().contains(pattern) {
167 return Err(format!(
168 "Expression contains dangerous keyword: {}",
169 pattern
170 ));
171 }
172 }
173
174 if !expr.chars().all(|c| {
177 c.is_alphanumeric()
178 || c.is_whitespace()
179 || matches!(
180 c,
181 '.' | '_'
182 | '('
183 | ')'
184 | '['
185 | ']'
186 | '='
187 | '<'
188 | '>'
189 | '!'
190 | '&'
191 | '|'
192 | '+'
193 | '-'
194 | '*'
195 | '/'
196 | '%'
197 | '"'
198 | '\''
199 | ':'
200 | ','
201 | ';'
202 )
203 }) {
204 return Err(
205 "Expression contains invalid characters. Only alphanumeric, operators, and quotes allowed."
206 .to_string(),
207 );
208 }
209
210 Ok(())
211}
212
213pub fn validate_regex_pattern(pattern: &str) -> Result<(), String> {
228 if pattern.len() > MAX_PATTERN_LENGTH {
230 return Err(format!(
231 "Regex pattern too long (max {} characters)",
232 MAX_PATTERN_LENGTH
233 ));
234 }
235
236 match regex::Regex::new(pattern) {
238 Ok(_) => {}
239 Err(e) => {
240 return Err(format!("Invalid regex pattern: {}", e));
241 }
242 }
243
244 let has_nested_quantifiers = pattern.contains(")+")
247 || pattern.contains(")*")
248 || pattern.contains(")?")
249 || pattern.contains(")+")
250 || pattern.contains("]{2,}+")
251 || pattern.contains("]{2,}*")
252 || pattern.contains("]{2,}?");
253
254 if has_nested_quantifiers {
255 return Err(
256 "Regex pattern contains nested quantifiers that could cause ReDoS attack".to_string(),
257 );
258 }
259
260 if pattern.contains('|') && pattern.contains('*') {
262 eprintln!(
264 "⚠️ Warning: Regex contains alternation with quantifiers (potential ReDoS risk)"
265 );
266 }
267
268 Ok(())
269}
270
271pub fn sanitize_for_comment(input: &str) -> String {
281 input
282 .replace("\\", "\\\\") .replace("*/", "*\\/") .replace("/*", "/\\*") .trim()
286 .to_string()
287}
288
289pub fn escape_for_rust_string(input: &str) -> String {
297 input
298 .replace('\\', "\\\\")
299 .replace('"', "\\\"")
300 .replace('\n', "\\n")
301 .replace('\r', "\\r")
302 .replace('\t', "\\t")
303}
304
305pub fn read_file_with_limit(path: &std::path::Path) -> io::Result<String> {
319 use std::fs::File;
320 use std::io::Read;
321
322 let file = File::open(path)?;
323 let metadata = file.metadata()?;
324
325 if metadata.len() > MAX_FILE_SIZE {
327 return Err(io::Error::new(
328 io::ErrorKind::InvalidData,
329 format!(
330 "File too large (max {} MB, got {} MB)",
331 MAX_FILE_SIZE / 1_000_000,
332 metadata.len() / 1_000_000
333 ),
334 ));
335 }
336
337 let mut buffer = String::new();
338 file.take(MAX_FILE_SIZE).read_to_string(&mut buffer)?;
339 Ok(buffer)
340}
341
342pub fn read_stdin_with_limit() -> io::Result<String> {
353 use std::io::Read;
354
355 let stdin = io::stdin();
356 let mut buffer = String::new();
357
358 stdin.take(MAX_FILE_SIZE).read_to_string(&mut buffer)?;
360
361 if buffer.len() as u64 >= MAX_FILE_SIZE {
363 return Err(io::Error::new(
364 io::ErrorKind::InvalidData,
365 format!("Input too large (max {} MB)", MAX_FILE_SIZE / 1_000_000),
366 ));
367 }
368
369 Ok(buffer)
370}
371
372#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
383 #[cfg(unix)]
384 fn test_valid_relative_path() {
385 let result = validate_file_path("output.rs");
386 assert!(result.is_ok());
387 }
388
389 #[test]
390 #[cfg(unix)]
391 fn test_valid_nested_path() {
392 let result = validate_file_path("target/debug/generated.rs");
393 assert!(result.is_ok());
394 }
395
396 #[test]
397 #[cfg(unix)]
398 fn test_rejects_absolute_path_unix() {
399 let result = validate_file_path("/etc/passwd");
400 assert!(result.is_err());
401 assert!(result
402 .unwrap_err()
403 .to_string()
404 .contains("Absolute paths are not allowed"));
405 }
406
407 #[test]
408 fn test_rejects_path_traversal() {
409 let result = validate_file_path("../../../etc/passwd");
410 assert!(result.is_err());
411 assert!(result
412 .unwrap_err()
413 .to_string()
414 .contains("Path traversal (..) is not allowed"));
415 }
416
417 #[test]
418 fn test_rejects_single_parent_dir() {
419 let result = validate_file_path("..");
420 assert!(result.is_err());
421 }
422
423 #[test]
424 fn test_rejects_empty_path() {
425 let result = validate_file_path("");
426 assert!(result.is_err());
427 }
428
429 #[test]
430 fn test_rejects_whitespace_only_path() {
431 let result = validate_file_path(" ");
432 assert!(result.is_err());
433 }
434
435 #[test]
440 fn test_valid_simple_expression() {
441 let result = validate_expression("age >= 18");
442 assert!(result.is_ok());
443 }
444
445 #[test]
446 fn test_valid_complex_expression() {
447 let result = validate_expression("(age >= 18) && (verified == true) || (admin == true)");
448 assert!(result.is_ok());
449 }
450
451 #[test]
452 fn test_rejects_empty_expression() {
453 let result = validate_expression("");
454 assert!(result.is_err());
455 }
456
457 #[test]
458 fn test_rejects_whitespace_only_expression() {
459 let result = validate_expression(" \n\t ");
460 assert!(result.is_err());
461 }
462
463 #[test]
464 fn test_rejects_expression_exceeding_max_length() {
465 let long_expr = "a".repeat(MAX_EXPRESSION_LENGTH + 1);
466 let result = validate_expression(&long_expr);
467 assert!(result.is_err());
468 assert!(result.unwrap_err().contains("too long"));
469 }
470
471 #[test]
472 fn test_rejects_unbalanced_parentheses_open() {
473 let result = validate_expression("(age >= 18");
474 assert!(result.is_err());
475 assert!(result.unwrap_err().contains("Unbalanced parentheses"));
476 }
477
478 #[test]
479 fn test_rejects_unbalanced_parentheses_close() {
480 let result = validate_expression("age >= 18)");
481 assert!(result.is_err());
482 }
483
484 #[test]
485 fn test_rejects_unbalanced_brackets() {
486 let result = validate_expression("arr[0 == 5");
487 assert!(result.is_err());
488 assert!(result.unwrap_err().contains("Unbalanced brackets"));
489 }
490
491 #[test]
492 fn test_rejects_sql_injection_pattern_drop() {
493 let result = validate_expression("drop table users");
494 assert!(result.is_err());
495 assert!(result.unwrap_err().contains("dangerous keyword"));
496 }
497
498 #[test]
499 fn test_rejects_sql_injection_pattern_delete() {
500 let result = validate_expression("delete from users where id = 1");
501 assert!(result.is_err());
502 }
503
504 #[test]
505 fn test_rejects_shell_command_pattern_bash() {
506 let result = validate_expression("bash -c 'rm -rf /'");
507 assert!(result.is_err());
508 }
509
510 #[test]
511 fn test_rejects_invalid_characters() {
512 let result = validate_expression("age >= 18 && `whoami`");
513 assert!(result.is_err());
514 assert!(result.unwrap_err().contains("invalid characters"));
515 }
516
517 #[test]
522 fn test_valid_simple_regex() {
523 let result = validate_regex_pattern("[0-9]+");
524 assert!(result.is_ok());
525 }
526
527 #[test]
528 fn test_valid_email_regex() {
529 let result = validate_regex_pattern(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}");
530 assert!(result.is_ok());
531 }
532
533 #[test]
534 fn test_rejects_invalid_regex() {
535 let result = validate_regex_pattern("[0-9");
536 assert!(result.is_err());
537 }
538
539 #[test]
540 fn test_rejects_regex_exceeding_max_length() {
541 let long_pattern = "a".repeat(MAX_PATTERN_LENGTH + 1);
542 let result = validate_regex_pattern(&long_pattern);
543 assert!(result.is_err());
544 }
545
546 #[test]
547 fn test_rejects_nested_quantifiers_plus_plus() {
548 let result = validate_regex_pattern("(a+)+");
549 assert!(result.is_err());
550 assert!(result.unwrap_err().contains("nested quantifiers"));
551 }
552
553 #[test]
554 fn test_rejects_nested_quantifiers_star_plus() {
555 let result = validate_regex_pattern("(a*)+");
556 assert!(result.is_err());
557 }
558
559 #[test]
560 fn test_rejects_nested_quantifiers_question_star() {
561 let result = validate_regex_pattern("(a?)*");
562 assert!(result.is_err());
563 }
564
565 #[test]
570 fn test_sanitize_comment_escapes_backslash() {
571 let result = sanitize_for_comment("path\\to\\file");
572 assert!(result.contains("\\\\"));
573 }
574
575 #[test]
576 fn test_sanitize_comment_prevents_comment_breakout() {
577 let result = sanitize_for_comment("test */ malicious");
578 assert!(result.contains("*\\/"));
579 }
580
581 #[test]
582 fn test_sanitize_comment_prevents_comment_break_in() {
583 let result = sanitize_for_comment("test /* malicious");
584 assert!(result.contains("/\\*"));
585 }
586
587 #[test]
588 fn test_escape_for_rust_string_escapes_quotes() {
589 let result = escape_for_rust_string(r#"test "quoted" value"#);
590 assert!(result.contains("\\\""));
591 }
592
593 #[test]
594 fn test_escape_for_rust_string_escapes_newlines() {
595 let result = escape_for_rust_string("line1\nline2");
596 assert!(result.contains("\\n"));
597 }
598
599 #[test]
600 fn test_escape_for_rust_string_escapes_tabs() {
601 let result = escape_for_rust_string("col1\tcol2");
602 assert!(result.contains("\\t"));
603 }
604
605 #[test]
610 fn test_read_small_file_succeeds() {
611 let temp_file = std::env::temp_dir().join("test_small.txt");
612 std::fs::write(&temp_file, "small content").unwrap();
613
614 let result = read_file_with_limit(&temp_file);
615 assert!(result.is_ok());
616 assert_eq!(result.unwrap(), "small content");
617
618 let _ = std::fs::remove_file(&temp_file);
619 }
620
621 #[test]
622 fn test_read_file_exceeding_size_limit_fails() {
623 let temp_file = std::env::temp_dir().join("test_large.txt");
624 let large_content = "x".repeat((MAX_FILE_SIZE as usize) + 1);
626 std::fs::write(&temp_file, large_content).unwrap();
627
628 let result = read_file_with_limit(&temp_file);
629 assert!(result.is_err());
630 assert!(result.unwrap_err().to_string().contains("too large"));
631
632 let _ = std::fs::remove_file(&temp_file);
633 }
634
635 #[test]
636 fn test_read_nonexistent_file_fails() {
637 let nonexistent = std::env::temp_dir().join("does_not_exist_xyz.txt");
638 let result = read_file_with_limit(&nonexistent);
639 assert!(result.is_err());
640 }
641
642 #[test]
647 fn test_broken_symlink_rejected() {
648 let temp_dir = std::env::temp_dir().join("test_symlink_broken");
650 let _ = std::fs::create_dir_all(&temp_dir);
651
652 let symlink_path = temp_dir.join("broken_symlink");
653 let _ = std::fs::remove_file(&symlink_path);
655
656 #[cfg(unix)]
657 {
658 use std::os::unix::fs as unix_fs;
659 let _ = unix_fs::symlink("/nonexistent/path", &symlink_path);
660
661 let result = validate_file_path(symlink_path.to_str().unwrap());
662 assert!(result.is_err() || result.is_ok()); let _ = std::fs::remove_file(&symlink_path);
666 }
667
668 let _ = std::fs::remove_dir(&temp_dir);
669 }
670
671 #[test]
672 fn test_unwrap_or_issue_fixed() {
673 }
682}