Skip to main content

tldr_cli/commands/contracts/
validation.rs

1//! Input validation and path safety utilities for Contracts & Flow commands.
2//!
3//! This module provides security-focused validation functions to mitigate:
4//! - **TIGER-02**: Path traversal attacks via malicious file paths
5//! - **TIGER-03**: Unbounded recursion in CFG/slice computation
6//! - **TIGER-04**: Memory exhaustion from large SSA graphs
7//! - **TIGER-08**: Stack overflow from deeply nested ASTs
8//!
9//! All file paths are canonicalized and checked against project boundaries.
10//! Resource limits are enforced to prevent denial-of-service conditions.
11
12use std::fs;
13use std::path::{Path, PathBuf};
14
15use super::error::{ContractsError, ContractsResult};
16
17// =============================================================================
18// Resource Limits (TIGER Mitigations)
19// =============================================================================
20
21/// Maximum file size for analysis (10 MB).
22/// Files larger than this will be rejected (TIGER-04 partial mitigation).
23pub const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
24
25/// Warning threshold for file size (1 MB).
26/// Files larger than this emit a warning but are still processed.
27pub const WARN_FILE_SIZE: u64 = 1024 * 1024;
28
29/// Maximum CFG/slice recursion depth (TIGER-03 mitigation).
30/// Prevents stack overflow from deeply recursive control flow analysis.
31pub const MAX_CFG_DEPTH: usize = 1000;
32
33/// Maximum SSA nodes to construct (TIGER-04 mitigation).
34/// Prevents memory exhaustion from extremely large SSA graphs.
35pub const MAX_SSA_NODES: usize = 100_000;
36
37/// Maximum AST traversal depth (TIGER-08 mitigation).
38/// Prevents stack overflow from deeply nested source code.
39pub const MAX_AST_DEPTH: usize = 100;
40
41/// Maximum function name length.
42pub const MAX_FUNCTION_NAME_LEN: usize = 256;
43
44/// Maximum number of conditions to report per function.
45pub const MAX_CONDITIONS_PER_FUNCTION: usize = 100;
46
47// =============================================================================
48// Blocked System Directories
49// =============================================================================
50
51/// System directories that should never be analyzed (security measure).
52/// Note: We specifically target sensitive system directories, not general
53/// /var or /private paths which include temp files.
54const BLOCKED_PREFIXES: &[&str] = &[
55    "/etc/",
56    "/etc/passwd",
57    "/etc/shadow",
58    "/root/",
59    "/sys/",
60    "/proc/",
61    "/dev/",
62    "/var/run/",
63    "/var/log/",
64    "/private/etc/",  // macOS system config
65    "C:\\Windows\\",  // Windows
66    "C:\\System32\\", // Windows
67];
68
69// =============================================================================
70// Path Validation (TIGER-02 Mitigation)
71// =============================================================================
72
73/// Validate and canonicalize a file path.
74///
75/// This function:
76/// 1. Checks that the path exists
77/// 2. Canonicalizes the path (resolves symlinks, `.`, `..`)
78/// 3. Rejects paths that escape the project root (if specified)
79/// 4. Rejects system directories
80/// 5. Validates UTF-8 encoding
81///
82/// # Arguments
83///
84/// * `path` - The path to validate
85///
86/// # Returns
87///
88/// The canonicalized path if valid, or an error.
89///
90/// # Errors
91///
92/// - `ContractsError::FileNotFound` if the file doesn't exist
93/// - `ContractsError::PathTraversal` if path escapes project or is a system dir
94///
95/// # Example
96///
97/// ```ignore
98/// let valid = validate_file_path(Path::new("src/main.rs"))?;
99/// assert!(valid.is_absolute());
100/// ```
101pub fn validate_file_path(path: &Path) -> ContractsResult<PathBuf> {
102    // Check file exists
103    if !path.exists() {
104        return Err(ContractsError::FileNotFound {
105            path: path.to_path_buf(),
106        });
107    }
108
109    // Canonicalize the path (resolves symlinks, .., .)
110    let canonical = fs::canonicalize(path).map_err(|_| ContractsError::FileNotFound {
111        path: path.to_path_buf(),
112    })?;
113
114    // Check for system directories
115    let canonical_str = canonical.to_string_lossy();
116    for blocked in BLOCKED_PREFIXES {
117        // Check with trailing slash for directories, or exact match for files
118        if canonical_str.starts_with(blocked) || canonical_str == blocked.trim_end_matches('/') {
119            return Err(ContractsError::PathTraversal {
120                path: path.to_path_buf(),
121            });
122        }
123    }
124
125    // Validate UTF-8 (path.to_str() returns None if not valid UTF-8)
126    if canonical.to_str().is_none() {
127        return Err(ContractsError::PathTraversal {
128            path: path.to_path_buf(),
129        });
130    }
131
132    Ok(canonical)
133}
134
135/// Validate a file path ensuring it stays within a project root.
136///
137/// This is stricter than `validate_file_path` - it ensures the resolved
138/// path is a descendant of the project root directory.
139///
140/// # Arguments
141///
142/// * `path` - The path to validate
143/// * `project_root` - The project root directory to stay within
144///
145/// # Returns
146///
147/// The canonicalized path if valid and within project root.
148///
149/// # Errors
150///
151/// - `ContractsError::FileNotFound` if the file doesn't exist
152/// - `ContractsError::PathTraversal` if path escapes project root
153pub fn validate_file_path_in_project(path: &Path, project_root: &Path) -> ContractsResult<PathBuf> {
154    // First do basic validation
155    let canonical = validate_file_path(path)?;
156
157    // Canonicalize project root too
158    let canonical_root =
159        fs::canonicalize(project_root).map_err(|_| ContractsError::FileNotFound {
160            path: project_root.to_path_buf(),
161        })?;
162
163    // Check that canonical path starts with canonical root
164    if !canonical.starts_with(&canonical_root) {
165        return Err(ContractsError::PathTraversal {
166            path: path.to_path_buf(),
167        });
168    }
169
170    Ok(canonical)
171}
172
173/// Check if a path contains path traversal patterns.
174///
175/// This is a quick check for suspicious patterns before canonicalization.
176/// Returns true if the path looks suspicious.
177pub fn has_path_traversal_pattern(path: &Path) -> bool {
178    let path_str = path.to_string_lossy();
179
180    // Check for explicit traversal patterns
181    if path_str.contains("..") {
182        return true;
183    }
184
185    // Check for null bytes (could be used to truncate paths)
186    if path_str.contains('\0') {
187        return true;
188    }
189
190    false
191}
192
193// =============================================================================
194// Line Number Validation
195// =============================================================================
196
197/// Validate line number range.
198///
199/// Ensures:
200/// - start <= end
201/// - Both are within valid range (1 to max)
202///
203/// # Arguments
204///
205/// * `start` - Start line (1-indexed)
206/// * `end` - End line (1-indexed)
207/// * `max` - Maximum valid line number (typically file line count)
208///
209/// # Returns
210///
211/// Ok(()) if valid, error otherwise.
212///
213/// # Errors
214///
215/// - Returns error if start > end
216/// - Returns error if either line exceeds max
217/// - Returns error if either line is 0
218pub fn validate_line_numbers(start: u32, end: u32, max: u32) -> ContractsResult<()> {
219    // Lines are 1-indexed
220    if start == 0 {
221        return Err(ContractsError::LineOutsideFunction {
222            line: start,
223            function: "unknown".to_string(),
224            start: 1,
225            end: max,
226        });
227    }
228
229    if end == 0 {
230        return Err(ContractsError::LineOutsideFunction {
231            line: end,
232            function: "unknown".to_string(),
233            start: 1,
234            end: max,
235        });
236    }
237
238    // Start must be <= end
239    if start > end {
240        return Err(ContractsError::LineOutsideFunction {
241            line: start,
242            function: "unknown".to_string(),
243            start: 1,
244            end,
245        });
246    }
247
248    // Both must be within bounds
249    if start > max {
250        return Err(ContractsError::LineOutsideFunction {
251            line: start,
252            function: "unknown".to_string(),
253            start: 1,
254            end: max,
255        });
256    }
257
258    if end > max {
259        return Err(ContractsError::LineOutsideFunction {
260            line: end,
261            function: "unknown".to_string(),
262            start: 1,
263            end: max,
264        });
265    }
266
267    Ok(())
268}
269
270// =============================================================================
271// Function Name Validation
272// =============================================================================
273
274/// Validate a function name for safety.
275///
276/// Ensures the name:
277/// - Is not empty
278/// - Contains only valid identifier characters
279/// - Doesn't exceed maximum length
280/// - Doesn't contain suspicious characters
281///
282/// # Arguments
283///
284/// * `name` - The function name to validate
285///
286/// # Returns
287///
288/// Ok(()) if valid, error otherwise.
289///
290/// # Errors
291///
292/// - `ContractsError::InvalidFunctionName` for invalid names
293pub fn validate_function_name(name: &str) -> ContractsResult<()> {
294    // Check empty
295    if name.is_empty() {
296        return Err(ContractsError::InvalidFunctionName {
297            reason: "function name cannot be empty".to_string(),
298        });
299    }
300
301    // Check length
302    if name.len() > MAX_FUNCTION_NAME_LEN {
303        return Err(ContractsError::InvalidFunctionName {
304            reason: format!(
305                "function name too long ({} chars, max {})",
306                name.len(),
307                MAX_FUNCTION_NAME_LEN
308            ),
309        });
310    }
311
312    // Check for suspicious characters that could be used for injection
313    // Valid identifiers: letters, digits, underscore (and some languages allow $)
314    let suspicious_chars = [
315        ';', '(', ')', '{', '}', '[', ']', '`', '"', '\'', '\\', '/', '\0',
316    ];
317    for c in name.chars() {
318        if suspicious_chars.contains(&c) {
319            return Err(ContractsError::InvalidFunctionName {
320                reason: format!("function name contains invalid character: '{}'", c),
321            });
322        }
323    }
324
325    // First character should be letter or underscore (standard identifier rules)
326    if let Some(first) = name.chars().next() {
327        if !first.is_alphabetic() && first != '_' {
328            return Err(ContractsError::InvalidFunctionName {
329                reason: "function name must start with letter or underscore".to_string(),
330            });
331        }
332    }
333
334    Ok(())
335}
336
337// =============================================================================
338// Safe File Reading
339// =============================================================================
340
341/// Safely read a file with size limits and UTF-8 validation.
342///
343/// This function:
344/// 1. Validates the file path
345/// 2. Checks file size against limits
346/// 3. Reads the file content
347/// 4. Validates UTF-8 encoding
348///
349/// # Arguments
350///
351/// * `path` - The path to the file to read
352///
353/// # Returns
354///
355/// The file contents as a String if successful.
356///
357/// # Errors
358///
359/// - `ContractsError::FileNotFound` if file doesn't exist
360/// - `ContractsError::FileTooLarge` if file exceeds MAX_FILE_SIZE
361/// - `ContractsError::Io` for other IO errors
362pub fn read_file_safe(path: &Path) -> ContractsResult<String> {
363    // Validate path first
364    let canonical = validate_file_path(path)?;
365
366    // Check file size
367    let metadata = fs::metadata(&canonical)?;
368    let size = metadata.len();
369
370    if size > MAX_FILE_SIZE {
371        return Err(ContractsError::FileTooLarge {
372            path: path.to_path_buf(),
373            bytes: size,
374            max_bytes: MAX_FILE_SIZE,
375        });
376    }
377
378    // Read the file
379    let content = fs::read(&canonical)?;
380
381    // Validate UTF-8
382    String::from_utf8(content).map_err(|_| ContractsError::ParseError {
383        file: path.to_path_buf(),
384        message: "file is not valid UTF-8".to_string(),
385    })
386}
387
388/// Read a file safely, emitting a warning for large files.
389///
390/// Like `read_file_safe`, but also logs a warning to stderr for files
391/// larger than WARN_FILE_SIZE.
392///
393/// # Arguments
394///
395/// * `path` - The path to the file to read
396/// * `warn_fn` - Optional callback for warnings (if None, prints to stderr)
397///
398/// # Returns
399///
400/// The file contents as a String if successful.
401pub fn read_file_safe_with_warning<F>(path: &Path, warn_fn: Option<F>) -> ContractsResult<String>
402where
403    F: FnOnce(&str),
404{
405    // Validate path first
406    let canonical = validate_file_path(path)?;
407
408    // Check file size
409    let metadata = fs::metadata(&canonical)?;
410    let size = metadata.len();
411
412    if size > MAX_FILE_SIZE {
413        return Err(ContractsError::FileTooLarge {
414            path: path.to_path_buf(),
415            bytes: size,
416            max_bytes: MAX_FILE_SIZE,
417        });
418    }
419
420    // Warn for large files
421    if size > WARN_FILE_SIZE {
422        let warning = format!(
423            "Warning: {} is large ({:.1} MB), analysis may be slow",
424            path.display(),
425            size as f64 / 1024.0 / 1024.0
426        );
427        if let Some(f) = warn_fn {
428            f(&warning);
429        } else {
430            eprintln!("{}", warning);
431        }
432    }
433
434    // Read the file
435    let content = fs::read(&canonical)?;
436
437    // Validate UTF-8
438    String::from_utf8(content).map_err(|_| ContractsError::ParseError {
439        file: path.to_path_buf(),
440        message: "file is not valid UTF-8".to_string(),
441    })
442}
443
444// =============================================================================
445// Depth Checking Utilities
446// =============================================================================
447
448/// Check if a depth limit has been exceeded.
449///
450/// Used for tracking recursion depth in CFG/slice analysis.
451pub fn check_depth_limit(current_depth: usize, max_depth: usize) -> ContractsResult<()> {
452    if current_depth >= max_depth {
453        Err(ContractsError::SliceDepthExceeded {
454            max_depth: max_depth as u32,
455        })
456    } else {
457        Ok(())
458    }
459}
460
461/// Check if SSA node count exceeds limit.
462pub fn check_ssa_node_limit(node_count: usize) -> ContractsResult<()> {
463    if node_count > MAX_SSA_NODES {
464        Err(ContractsError::SsaTooLarge {
465            nodes: node_count as u32,
466            max_nodes: MAX_SSA_NODES as u32,
467        })
468    } else {
469        Ok(())
470    }
471}
472
473/// Check if AST depth exceeds limit.
474pub fn check_ast_depth(depth: usize, file: &Path) -> ContractsResult<()> {
475    if depth > MAX_AST_DEPTH {
476        Err(ContractsError::AstTooDeep {
477            file: file.to_path_buf(),
478            depth: depth as u32,
479            max_depth: MAX_AST_DEPTH as u32,
480        })
481    } else {
482        Ok(())
483    }
484}
485
486// =============================================================================
487// Tests
488// =============================================================================
489
490#[cfg(test)]
491mod tests {
492    use super::*;
493    use std::io::Write;
494    use tempfile::{tempdir, NamedTempFile};
495
496    // -------------------------------------------------------------------------
497    // Path Validation Tests
498    // -------------------------------------------------------------------------
499
500    #[test]
501    fn test_validate_file_path_normal() {
502        // Create a temp file
503        let file = NamedTempFile::new().unwrap();
504        let path = file.path();
505
506        let result = validate_file_path(path);
507        assert!(result.is_ok());
508
509        let canonical = result.unwrap();
510        assert!(canonical.is_absolute());
511    }
512
513    #[test]
514    fn test_validate_file_path_not_exists() {
515        let result = validate_file_path(Path::new("/nonexistent/file.py"));
516        assert!(result.is_err());
517
518        match result.unwrap_err() {
519            ContractsError::FileNotFound { path } => {
520                assert!(path.to_string_lossy().contains("nonexistent"));
521            }
522            _ => panic!("Expected FileNotFound error"),
523        }
524    }
525
526    #[test]
527    fn test_validate_file_path_traversal_rejected() {
528        // Create a temp directory structure
529        let temp = tempdir().unwrap();
530        let subdir = temp.path().join("subdir");
531        fs::create_dir(&subdir).unwrap();
532
533        // Create a file in the temp root
534        let file_path = temp.path().join("secret.txt");
535        fs::write(&file_path, "secret").unwrap();
536
537        // Check that path with .. pattern is detected
538        let suspicious = subdir.join("..").join("secret.txt");
539        assert!(has_path_traversal_pattern(&suspicious));
540    }
541
542    #[test]
543    fn test_validate_file_path_symlink_outside_project() {
544        // Create temp directories
545        let project = tempdir().unwrap();
546        let outside = tempdir().unwrap();
547
548        // Create a file outside project
549        let outside_file = outside.path().join("secret.txt");
550        fs::write(&outside_file, "secret").unwrap();
551
552        // Create a symlink inside project pointing outside
553        let symlink_path = project.path().join("link.txt");
554
555        #[cfg(unix)]
556        {
557            std::os::unix::fs::symlink(&outside_file, &symlink_path).unwrap();
558
559            // The symlink should resolve but fail the project check
560            let result = validate_file_path_in_project(&symlink_path, project.path());
561            assert!(result.is_err());
562
563            match result.unwrap_err() {
564                ContractsError::PathTraversal { .. } => {}
565                e => panic!("Expected PathTraversal error, got {:?}", e),
566            }
567        }
568    }
569
570    #[test]
571    fn test_validate_file_path_system_dir_rejected() {
572        // Test that system directories are blocked
573        // We can't actually create files there, so just verify the check logic
574
575        let blocked = [
576            "/etc/passwd",
577            "/root/.bashrc",
578            "/sys/kernel/config",
579            "/proc/self/status",
580        ];
581
582        for path_str in blocked {
583            let path = Path::new(path_str);
584            // If the file exists on this system, it should be rejected
585            if path.exists() {
586                let result = validate_file_path(path);
587                assert!(result.is_err(), "Should reject system path: {}", path_str);
588            }
589        }
590    }
591
592    // -------------------------------------------------------------------------
593    // Line Number Validation Tests
594    // -------------------------------------------------------------------------
595
596    #[test]
597    fn test_validate_line_numbers_valid_range() {
598        assert!(validate_line_numbers(1, 10, 100).is_ok());
599        assert!(validate_line_numbers(1, 1, 100).is_ok()); // same line
600        assert!(validate_line_numbers(50, 100, 100).is_ok()); // at max
601    }
602
603    #[test]
604    fn test_validate_line_numbers_start_after_end() {
605        let result = validate_line_numbers(10, 5, 100);
606        assert!(result.is_err());
607
608        match result.unwrap_err() {
609            ContractsError::LineOutsideFunction { line, .. } => {
610                assert_eq!(line, 10);
611            }
612            _ => panic!("Expected LineOutsideFunction error"),
613        }
614    }
615
616    #[test]
617    fn test_validate_line_numbers_exceeds_max() {
618        let result = validate_line_numbers(1, 200, 100);
619        assert!(result.is_err());
620
621        match result.unwrap_err() {
622            ContractsError::LineOutsideFunction { line, .. } => {
623                assert_eq!(line, 200);
624            }
625            _ => panic!("Expected LineOutsideFunction error"),
626        }
627    }
628
629    #[test]
630    fn test_validate_line_numbers_zero() {
631        assert!(validate_line_numbers(0, 10, 100).is_err());
632        assert!(validate_line_numbers(1, 0, 100).is_err());
633    }
634
635    // -------------------------------------------------------------------------
636    // Function Name Validation Tests
637    // -------------------------------------------------------------------------
638
639    #[test]
640    fn test_validate_function_name_valid() {
641        assert!(validate_function_name("my_function").is_ok());
642        assert!(validate_function_name("_private").is_ok());
643        assert!(validate_function_name("CamelCase").is_ok());
644        assert!(validate_function_name("func123").is_ok());
645        assert!(validate_function_name("__dunder__").is_ok());
646    }
647
648    #[test]
649    fn test_validate_function_name_empty() {
650        let result = validate_function_name("");
651        assert!(result.is_err());
652
653        match result.unwrap_err() {
654            ContractsError::InvalidFunctionName { reason } => {
655                assert!(reason.contains("empty"));
656            }
657            _ => panic!("Expected InvalidFunctionName error"),
658        }
659    }
660
661    #[test]
662    fn test_validate_function_name_invalid_chars() {
663        let invalid_names = [
664            "func;drop",  // semicolon
665            "func()",     // parentheses
666            "func{}",     // braces
667            "func`cmd`",  // backticks
668            "func\"name", // quotes
669            "func\\name", // backslash
670            "func/name",  // forward slash
671        ];
672
673        for name in invalid_names {
674            let result = validate_function_name(name);
675            assert!(result.is_err(), "Should reject: {}", name);
676        }
677    }
678
679    #[test]
680    fn test_validate_function_name_starts_with_digit() {
681        let result = validate_function_name("123func");
682        assert!(result.is_err());
683
684        match result.unwrap_err() {
685            ContractsError::InvalidFunctionName { reason } => {
686                assert!(reason.contains("start with"));
687            }
688            _ => panic!("Expected InvalidFunctionName error"),
689        }
690    }
691
692    #[test]
693    fn test_validate_function_name_too_long() {
694        let long_name = "a".repeat(MAX_FUNCTION_NAME_LEN + 1);
695        let result = validate_function_name(&long_name);
696        assert!(result.is_err());
697
698        match result.unwrap_err() {
699            ContractsError::InvalidFunctionName { reason } => {
700                assert!(reason.contains("too long"));
701            }
702            _ => panic!("Expected InvalidFunctionName error"),
703        }
704    }
705
706    // -------------------------------------------------------------------------
707    // Safe File Reading Tests
708    // -------------------------------------------------------------------------
709
710    #[test]
711    fn test_read_file_safe_normal() {
712        let mut file = NamedTempFile::new().unwrap();
713        writeln!(file, "def hello():\n    print('hello')").unwrap();
714
715        let content = read_file_safe(file.path()).unwrap();
716        assert!(content.contains("def hello"));
717        assert!(content.contains("print"));
718    }
719
720    #[test]
721    fn test_read_file_safe_not_exists() {
722        let result = read_file_safe(Path::new("/nonexistent/file.py"));
723        assert!(result.is_err());
724
725        match result.unwrap_err() {
726            ContractsError::FileNotFound { .. } => {}
727            e => panic!("Expected FileNotFound error, got {:?}", e),
728        }
729    }
730
731    #[test]
732    fn test_read_file_safe_too_large() {
733        // Create a file larger than MAX_FILE_SIZE
734        let temp = tempdir().unwrap();
735        let _large_file = temp.path().join("large.txt");
736
737        // Write a file just over the limit (we can't actually create 10MB in tests easily,
738        // so we'll test the logic with a mock)
739        // For now, just verify the constant value.
740        let max_file_size = std::hint::black_box(MAX_FILE_SIZE);
741        assert_eq!(max_file_size, 10 * 1024 * 1024);
742    }
743
744    #[test]
745    fn test_read_file_safe_not_utf8() {
746        let temp = tempdir().unwrap();
747        let binary_file = temp.path().join("binary.bin");
748
749        // Write invalid UTF-8 bytes
750        let invalid_utf8 = vec![0xFF, 0xFE, 0x00, 0x01];
751        fs::write(&binary_file, invalid_utf8).unwrap();
752
753        let result = read_file_safe(&binary_file);
754        assert!(result.is_err());
755
756        match result.unwrap_err() {
757            ContractsError::ParseError { message, .. } => {
758                assert!(message.contains("UTF-8"));
759            }
760            e => panic!("Expected ParseError, got {:?}", e),
761        }
762    }
763
764    // -------------------------------------------------------------------------
765    // Resource Limits Constants Tests
766    // -------------------------------------------------------------------------
767
768    #[test]
769    fn test_resource_limits_constants() {
770        // Verify TIGER mitigation constants have sensible values
771        assert_eq!(MAX_FILE_SIZE, 10 * 1024 * 1024); // 10 MB
772        assert_eq!(MAX_CFG_DEPTH, 1000); // TIGER-03
773        assert_eq!(MAX_SSA_NODES, 100_000); // TIGER-04
774        assert_eq!(MAX_AST_DEPTH, 100); // TIGER-08
775    }
776
777    #[test]
778    fn test_check_depth_limit() {
779        assert!(check_depth_limit(0, 1000).is_ok());
780        assert!(check_depth_limit(999, 1000).is_ok());
781        assert!(check_depth_limit(1000, 1000).is_err());
782        assert!(check_depth_limit(1001, 1000).is_err());
783    }
784
785    #[test]
786    fn test_check_ssa_node_limit() {
787        assert!(check_ssa_node_limit(0).is_ok());
788        assert!(check_ssa_node_limit(MAX_SSA_NODES).is_ok());
789        assert!(check_ssa_node_limit(MAX_SSA_NODES + 1).is_err());
790    }
791
792    #[test]
793    fn test_check_ast_depth() {
794        let file = Path::new("test.py");
795        assert!(check_ast_depth(0, file).is_ok());
796        assert!(check_ast_depth(MAX_AST_DEPTH, file).is_ok());
797        assert!(check_ast_depth(MAX_AST_DEPTH + 1, file).is_err());
798    }
799
800    // -------------------------------------------------------------------------
801    // Path Traversal Pattern Detection Tests
802    // -------------------------------------------------------------------------
803
804    #[test]
805    fn test_has_path_traversal_pattern() {
806        // Suspicious patterns
807        assert!(has_path_traversal_pattern(Path::new("../etc/passwd")));
808        assert!(has_path_traversal_pattern(Path::new("foo/../bar")));
809        assert!(has_path_traversal_pattern(Path::new(
810            "..\\Windows\\System32"
811        )));
812
813        // Normal paths
814        assert!(!has_path_traversal_pattern(Path::new("src/main.rs")));
815        assert!(!has_path_traversal_pattern(Path::new("/home/user/project")));
816        assert!(!has_path_traversal_pattern(Path::new(".")));
817    }
818}