ricecoder_execution/
validation.rs

1//! Input validation for execution plans, steps, and configurations
2
3use crate::error::{ExecutionError, ExecutionResult};
4use crate::models::{ExecutionPlan, ExecutionStep, StepAction};
5
6/// Validator for execution plans and their components
7pub struct ExecutionValidator;
8
9impl ExecutionValidator {
10    /// Validate an entire execution plan
11    ///
12    /// # Arguments
13    /// * `plan` - The execution plan to validate
14    ///
15    /// # Returns
16    /// * `Ok(())` if the plan is valid
17    /// * `Err(ExecutionError)` if validation fails
18    pub fn validate_plan(plan: &ExecutionPlan) -> ExecutionResult<()> {
19        // Validate plan name
20        Self::validate_plan_name(&plan.name)?;
21
22        // Validate plan has at least one step
23        if plan.steps.is_empty() {
24            return Err(ExecutionError::ValidationError(
25                "Execution plan must contain at least one step".to_string(),
26            ));
27        }
28
29        // Validate each step
30        for step in &plan.steps {
31            Self::validate_step(step)?;
32        }
33
34        // Validate step dependencies
35        Self::validate_dependencies(plan)?;
36
37        Ok(())
38    }
39
40    /// Validate a single execution step
41    ///
42    /// # Arguments
43    /// * `step` - The execution step to validate
44    ///
45    /// # Returns
46    /// * `Ok(())` if the step is valid
47    /// * `Err(ExecutionError)` if validation fails
48    pub fn validate_step(step: &ExecutionStep) -> ExecutionResult<()> {
49        // Validate step description
50        if step.description.is_empty() {
51            return Err(ExecutionError::ValidationError(
52                "Step description cannot be empty".to_string(),
53            ));
54        }
55
56        if step.description.len() > 1000 {
57            return Err(ExecutionError::ValidationError(
58                "Step description cannot exceed 1000 characters".to_string(),
59            ));
60        }
61
62        // Validate step action
63        Self::validate_step_action(&step.action)?;
64
65        Ok(())
66    }
67
68    /// Validate a step action
69    ///
70    /// # Arguments
71    /// * `action` - The step action to validate
72    ///
73    /// # Returns
74    /// * `Ok(())` if the action is valid
75    /// * `Err(ExecutionError)` if validation fails
76    pub fn validate_step_action(action: &StepAction) -> ExecutionResult<()> {
77        match action {
78            StepAction::CreateFile { path, content } => {
79                Self::validate_file_path(path)?;
80                Self::validate_file_content(content)?;
81            }
82            StepAction::ModifyFile { path, diff } => {
83                Self::validate_file_path(path)?;
84                Self::validate_diff(diff)?;
85            }
86            StepAction::DeleteFile { path } => {
87                Self::validate_file_path(path)?;
88            }
89            StepAction::RunCommand { command, args } => {
90                Self::validate_command(command)?;
91                Self::validate_command_args(args)?;
92            }
93            StepAction::RunTests { pattern } => {
94                if let Some(p) = pattern {
95                    Self::validate_test_pattern(p)?;
96                }
97            }
98        }
99
100        Ok(())
101    }
102
103    /// Validate a file path
104    ///
105    /// # Arguments
106    /// * `path` - The file path to validate
107    ///
108    /// # Returns
109    /// * `Ok(())` if the path is valid
110    /// * `Err(ExecutionError)` if validation fails
111    pub fn validate_file_path(path: &str) -> ExecutionResult<()> {
112        // Path cannot be empty
113        if path.is_empty() {
114            return Err(ExecutionError::ValidationError(
115                "File path cannot be empty".to_string(),
116            ));
117        }
118
119        // Path cannot exceed reasonable length
120        if path.len() > 4096 {
121            return Err(ExecutionError::ValidationError(
122                "File path cannot exceed 4096 characters".to_string(),
123            ));
124        }
125
126        // Path cannot contain null bytes
127        if path.contains('\0') {
128            return Err(ExecutionError::ValidationError(
129                "File path cannot contain null bytes".to_string(),
130            ));
131        }
132
133        // Path should not start with absolute path indicators (security check)
134        // Allow relative paths and paths resolved by PathResolver
135        if path.starts_with('/') && !path.starts_with("./") && !path.starts_with("../") {
136            // Absolute paths are allowed but should be validated by PathResolver
137            // This is a warning-level check
138        }
139
140        Ok(())
141    }
142
143    /// Validate file content
144    ///
145    /// # Arguments
146    /// * `content` - The file content to validate
147    ///
148    /// # Returns
149    /// * `Ok(())` if the content is valid
150    /// * `Err(ExecutionError)` if validation fails
151    pub fn validate_file_content(content: &str) -> ExecutionResult<()> {
152        // Content can be empty (empty files are valid)
153        // Content cannot exceed reasonable size (100MB)
154        if content.len() > 100 * 1024 * 1024 {
155            return Err(ExecutionError::ValidationError(
156                "File content cannot exceed 100MB".to_string(),
157            ));
158        }
159
160        Ok(())
161    }
162
163    /// Validate a diff
164    ///
165    /// # Arguments
166    /// * `diff` - The diff to validate
167    ///
168    /// # Returns
169    /// * `Ok(())` if the diff is valid
170    /// * `Err(ExecutionError)` if validation fails
171    pub fn validate_diff(diff: &str) -> ExecutionResult<()> {
172        // Diff cannot be empty
173        if diff.is_empty() {
174            return Err(ExecutionError::ValidationError(
175                "Diff cannot be empty".to_string(),
176            ));
177        }
178
179        // Diff cannot exceed reasonable size (10MB)
180        if diff.len() > 10 * 1024 * 1024 {
181            return Err(ExecutionError::ValidationError(
182                "Diff cannot exceed 10MB".to_string(),
183            ));
184        }
185
186        Ok(())
187    }
188
189    /// Validate a shell command
190    ///
191    /// # Arguments
192    /// * `command` - The command to validate
193    ///
194    /// # Returns
195    /// * `Ok(())` if the command is valid
196    /// * `Err(ExecutionError)` if validation fails
197    pub fn validate_command(command: &str) -> ExecutionResult<()> {
198        // Command cannot be empty
199        if command.is_empty() {
200            return Err(ExecutionError::ValidationError(
201                "Command cannot be empty".to_string(),
202            ));
203        }
204
205        // Command cannot exceed reasonable length
206        if command.len() > 4096 {
207            return Err(ExecutionError::ValidationError(
208                "Command cannot exceed 4096 characters".to_string(),
209            ));
210        }
211
212        // Command cannot contain null bytes
213        if command.contains('\0') {
214            return Err(ExecutionError::ValidationError(
215                "Command cannot contain null bytes".to_string(),
216            ));
217        }
218
219        Ok(())
220    }
221
222    /// Validate command arguments
223    ///
224    /// # Arguments
225    /// * `args` - The command arguments to validate
226    ///
227    /// # Returns
228    /// * `Ok(())` if the arguments are valid
229    /// * `Err(ExecutionError)` if validation fails
230    pub fn validate_command_args(args: &[String]) -> ExecutionResult<()> {
231        // Arguments list cannot exceed reasonable size
232        if args.len() > 1000 {
233            return Err(ExecutionError::ValidationError(
234                "Command arguments cannot exceed 1000 items".to_string(),
235            ));
236        }
237
238        // Each argument must be valid
239        for arg in args {
240            if arg.len() > 4096 {
241                return Err(ExecutionError::ValidationError(
242                    "Command argument cannot exceed 4096 characters".to_string(),
243                ));
244            }
245
246            if arg.contains('\0') {
247                return Err(ExecutionError::ValidationError(
248                    "Command argument cannot contain null bytes".to_string(),
249                ));
250            }
251        }
252
253        Ok(())
254    }
255
256    /// Validate a test pattern
257    ///
258    /// # Arguments
259    /// * `pattern` - The test pattern to validate
260    ///
261    /// # Returns
262    /// * `Ok(())` if the pattern is valid
263    /// * `Err(ExecutionError)` if validation fails
264    pub fn validate_test_pattern(pattern: &str) -> ExecutionResult<()> {
265        // Pattern cannot be empty
266        if pattern.is_empty() {
267            return Err(ExecutionError::ValidationError(
268                "Test pattern cannot be empty".to_string(),
269            ));
270        }
271
272        // Pattern cannot exceed reasonable length
273        if pattern.len() > 1024 {
274            return Err(ExecutionError::ValidationError(
275                "Test pattern cannot exceed 1024 characters".to_string(),
276            ));
277        }
278
279        // Pattern cannot contain null bytes
280        if pattern.contains('\0') {
281            return Err(ExecutionError::ValidationError(
282                "Test pattern cannot contain null bytes".to_string(),
283            ));
284        }
285
286        Ok(())
287    }
288
289    /// Validate plan name
290    ///
291    /// # Arguments
292    /// * `name` - The plan name to validate
293    ///
294    /// # Returns
295    /// * `Ok(())` if the name is valid
296    /// * `Err(ExecutionError)` if validation fails
297    pub fn validate_plan_name(name: &str) -> ExecutionResult<()> {
298        // Name cannot be empty
299        if name.is_empty() {
300            return Err(ExecutionError::ValidationError(
301                "Plan name cannot be empty".to_string(),
302            ));
303        }
304
305        // Name cannot exceed reasonable length
306        if name.len() > 256 {
307            return Err(ExecutionError::ValidationError(
308                "Plan name cannot exceed 256 characters".to_string(),
309            ));
310        }
311
312        // Name cannot contain null bytes
313        if name.contains('\0') {
314            return Err(ExecutionError::ValidationError(
315                "Plan name cannot contain null bytes".to_string(),
316            ));
317        }
318
319        Ok(())
320    }
321
322    /// Validate step dependencies
323    ///
324    /// # Arguments
325    /// * `plan` - The execution plan to validate dependencies for
326    ///
327    /// # Returns
328    /// * `Ok(())` if dependencies are valid
329    /// * `Err(ExecutionError)` if validation fails
330    fn validate_dependencies(plan: &ExecutionPlan) -> ExecutionResult<()> {
331        // Create a set of valid step IDs
332        let valid_ids: std::collections::HashSet<_> =
333            plan.steps.iter().map(|s| s.id.as_str()).collect();
334
335        // Check each step's dependencies
336        for step in &plan.steps {
337            for dep_id in &step.dependencies {
338                // Dependency must reference an existing step
339                if !valid_ids.contains(dep_id.as_str()) {
340                    return Err(ExecutionError::ValidationError(format!(
341                        "Step {} references non-existent dependency: {}",
342                        step.id, dep_id
343                    )));
344                }
345
346                // Dependency cannot be self-referential
347                if dep_id == &step.id {
348                    return Err(ExecutionError::ValidationError(format!(
349                        "Step {} has self-referential dependency",
350                        step.id
351                    )));
352                }
353            }
354        }
355
356        // Check for circular dependencies
357        Self::check_circular_dependencies(plan)?;
358
359        Ok(())
360    }
361
362    /// Check for circular dependencies in the plan
363    ///
364    /// # Arguments
365    /// * `plan` - The execution plan to check
366    ///
367    /// # Returns
368    /// * `Ok(())` if no circular dependencies exist
369    /// * `Err(ExecutionError)` if circular dependencies are found
370    fn check_circular_dependencies(plan: &ExecutionPlan) -> ExecutionResult<()> {
371        // Build a map of step ID to dependencies
372        let mut dep_map: std::collections::HashMap<&str, Vec<&str>> =
373            std::collections::HashMap::new();
374
375        for step in &plan.steps {
376            dep_map.insert(
377                &step.id,
378                step.dependencies.iter().map(|s| s.as_str()).collect(),
379            );
380        }
381
382        // Check each step for cycles
383        for step in &plan.steps {
384            let mut visited = std::collections::HashSet::new();
385            let mut rec_stack = std::collections::HashSet::new();
386
387            if Self::has_cycle(&step.id, &dep_map, &mut visited, &mut rec_stack) {
388                return Err(ExecutionError::ValidationError(format!(
389                    "Circular dependency detected involving step: {}",
390                    step.id
391                )));
392            }
393        }
394
395        Ok(())
396    }
397
398    /// Helper function to detect cycles using DFS
399    fn has_cycle(
400        node: &str,
401        dep_map: &std::collections::HashMap<&str, Vec<&str>>,
402        visited: &mut std::collections::HashSet<String>,
403        rec_stack: &mut std::collections::HashSet<String>,
404    ) -> bool {
405        let node_str = node.to_string();
406
407        visited.insert(node_str.clone());
408        rec_stack.insert(node_str.clone());
409
410        if let Some(deps) = dep_map.get(node) {
411            for dep in deps {
412                let dep_str = dep.to_string();
413
414                if !visited.contains(&dep_str) {
415                    if Self::has_cycle(dep, dep_map, visited, rec_stack) {
416                        return true;
417                    }
418                } else if rec_stack.contains(&dep_str) {
419                    return true;
420                }
421            }
422        }
423
424        rec_stack.remove(&node_str);
425        false
426    }
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    #[test]
434    fn test_validate_plan_name_empty() {
435        let result = ExecutionValidator::validate_plan_name("");
436        assert!(result.is_err());
437        assert!(result.unwrap_err().to_string().contains("empty"));
438    }
439
440    #[test]
441    fn test_validate_plan_name_valid() {
442        let result = ExecutionValidator::validate_plan_name("My Execution Plan");
443        assert!(result.is_ok());
444    }
445
446    #[test]
447    fn test_validate_plan_name_too_long() {
448        let long_name = "a".repeat(257);
449        let result = ExecutionValidator::validate_plan_name(&long_name);
450        assert!(result.is_err());
451    }
452
453    #[test]
454    fn test_validate_file_path_empty() {
455        let result = ExecutionValidator::validate_file_path("");
456        assert!(result.is_err());
457    }
458
459    #[test]
460    fn test_validate_file_path_valid() {
461        let result = ExecutionValidator::validate_file_path("src/main.rs");
462        assert!(result.is_ok());
463    }
464
465    #[test]
466    fn test_validate_file_path_with_null_byte() {
467        let result = ExecutionValidator::validate_file_path("src/main\0.rs");
468        assert!(result.is_err());
469    }
470
471    #[test]
472    fn test_validate_file_content_empty() {
473        let result = ExecutionValidator::validate_file_content("");
474        assert!(result.is_ok());
475    }
476
477    #[test]
478    fn test_validate_file_content_valid() {
479        let result = ExecutionValidator::validate_file_content("fn main() {}");
480        assert!(result.is_ok());
481    }
482
483    #[test]
484    fn test_validate_command_empty() {
485        let result = ExecutionValidator::validate_command("");
486        assert!(result.is_err());
487    }
488
489    #[test]
490    fn test_validate_command_valid() {
491        let result = ExecutionValidator::validate_command("cargo build");
492        assert!(result.is_ok());
493    }
494
495    #[test]
496    fn test_validate_command_args_valid() {
497        let args = vec!["--release".to_string(), "--verbose".to_string()];
498        let result = ExecutionValidator::validate_command_args(&args);
499        assert!(result.is_ok());
500    }
501
502    #[test]
503    fn test_validate_test_pattern_empty() {
504        let result = ExecutionValidator::validate_test_pattern("");
505        assert!(result.is_err());
506    }
507
508    #[test]
509    fn test_validate_test_pattern_valid() {
510        let result = ExecutionValidator::validate_test_pattern("test_*");
511        assert!(result.is_ok());
512    }
513
514    #[test]
515    fn test_validate_step_action_create_file() {
516        let action = StepAction::CreateFile {
517            path: "src/lib.rs".to_string(),
518            content: "pub fn hello() {}".to_string(),
519        };
520        let result = ExecutionValidator::validate_step_action(&action);
521        assert!(result.is_ok());
522    }
523
524    #[test]
525    fn test_validate_step_action_create_file_empty_path() {
526        let action = StepAction::CreateFile {
527            path: "".to_string(),
528            content: "pub fn hello() {}".to_string(),
529        };
530        let result = ExecutionValidator::validate_step_action(&action);
531        assert!(result.is_err());
532    }
533
534    #[test]
535    fn test_validate_step_action_modify_file() {
536        let action = StepAction::ModifyFile {
537            path: "src/lib.rs".to_string(),
538            diff: "--- a/src/lib.rs\n+++ b/src/lib.rs\n@@ -1 +1 @@\n-old\n+new".to_string(),
539        };
540        let result = ExecutionValidator::validate_step_action(&action);
541        assert!(result.is_ok());
542    }
543
544    #[test]
545    fn test_validate_step_action_delete_file() {
546        let action = StepAction::DeleteFile {
547            path: "src/old.rs".to_string(),
548        };
549        let result = ExecutionValidator::validate_step_action(&action);
550        assert!(result.is_ok());
551    }
552
553    #[test]
554    fn test_validate_step_action_run_command() {
555        let action = StepAction::RunCommand {
556            command: "cargo".to_string(),
557            args: vec!["test".to_string()],
558        };
559        let result = ExecutionValidator::validate_step_action(&action);
560        assert!(result.is_ok());
561    }
562
563    #[test]
564    fn test_validate_step_action_run_tests() {
565        let action = StepAction::RunTests {
566            pattern: Some("test_*".to_string()),
567        };
568        let result = ExecutionValidator::validate_step_action(&action);
569        assert!(result.is_ok());
570    }
571
572    #[test]
573    fn test_validate_step_valid() {
574        let step = ExecutionStep::new(
575            "Create a new file".to_string(),
576            StepAction::CreateFile {
577                path: "src/lib.rs".to_string(),
578                content: "pub fn hello() {}".to_string(),
579            },
580        );
581        let result = ExecutionValidator::validate_step(&step);
582        assert!(result.is_ok());
583    }
584
585    #[test]
586    fn test_validate_step_empty_description() {
587        let mut step = ExecutionStep::new(
588            "".to_string(),
589            StepAction::CreateFile {
590                path: "src/lib.rs".to_string(),
591                content: "pub fn hello() {}".to_string(),
592            },
593        );
594        step.description = "".to_string();
595        let result = ExecutionValidator::validate_step(&step);
596        assert!(result.is_err());
597    }
598
599    #[test]
600    fn test_validate_plan_empty_steps() {
601        let plan = ExecutionPlan::new("Test Plan".to_string(), vec![]);
602        let result = ExecutionValidator::validate_plan(&plan);
603        assert!(result.is_err());
604        assert!(result
605            .unwrap_err()
606            .to_string()
607            .contains("at least one step"));
608    }
609
610    #[test]
611    fn test_validate_plan_valid() {
612        let step = ExecutionStep::new(
613            "Create a new file".to_string(),
614            StepAction::CreateFile {
615                path: "src/lib.rs".to_string(),
616                content: "pub fn hello() {}".to_string(),
617            },
618        );
619        let plan = ExecutionPlan::new("Test Plan".to_string(), vec![step]);
620        let result = ExecutionValidator::validate_plan(&plan);
621        assert!(result.is_ok());
622    }
623
624    #[test]
625    fn test_validate_plan_invalid_dependency() {
626        let mut step1 = ExecutionStep::new(
627            "Create a new file".to_string(),
628            StepAction::CreateFile {
629                path: "src/lib.rs".to_string(),
630                content: "pub fn hello() {}".to_string(),
631            },
632        );
633
634        let step2 = ExecutionStep::new(
635            "Modify the file".to_string(),
636            StepAction::ModifyFile {
637                path: "src/lib.rs".to_string(),
638                diff: "--- a/src/lib.rs\n+++ b/src/lib.rs\n@@ -1 +1 @@\n-old\n+new".to_string(),
639            },
640        );
641
642        // Add invalid dependency
643        step1.dependencies.push("non-existent-id".to_string());
644
645        let plan = ExecutionPlan::new("Test Plan".to_string(), vec![step1, step2]);
646        let result = ExecutionValidator::validate_plan(&plan);
647        assert!(result.is_err());
648        assert!(result
649            .unwrap_err()
650            .to_string()
651            .contains("non-existent dependency"));
652    }
653
654    #[test]
655    fn test_validate_plan_self_referential_dependency() {
656        let mut step = ExecutionStep::new(
657            "Create a new file".to_string(),
658            StepAction::CreateFile {
659                path: "src/lib.rs".to_string(),
660                content: "pub fn hello() {}".to_string(),
661            },
662        );
663
664        // Add self-referential dependency
665        step.dependencies.push(step.id.clone());
666
667        let plan = ExecutionPlan::new("Test Plan".to_string(), vec![step]);
668        let result = ExecutionValidator::validate_plan(&plan);
669        assert!(result.is_err());
670        assert!(result.unwrap_err().to_string().contains("self-referential"));
671    }
672
673    #[test]
674    fn test_validate_plan_circular_dependency() {
675        let mut step1 = ExecutionStep::new(
676            "Step 1".to_string(),
677            StepAction::CreateFile {
678                path: "src/lib.rs".to_string(),
679                content: "pub fn hello() {}".to_string(),
680            },
681        );
682
683        let mut step2 = ExecutionStep::new(
684            "Step 2".to_string(),
685            StepAction::CreateFile {
686                path: "src/main.rs".to_string(),
687                content: "fn main() {}".to_string(),
688            },
689        );
690
691        let step1_id = step1.id.clone();
692        let step2_id = step2.id.clone();
693
694        // Create circular dependency: step1 -> step2 -> step1
695        step1.dependencies.push(step2_id.clone());
696        step2.dependencies.push(step1_id);
697
698        let plan = ExecutionPlan::new("Test Plan".to_string(), vec![step1, step2]);
699        let result = ExecutionValidator::validate_plan(&plan);
700        assert!(result.is_err());
701        assert!(result
702            .unwrap_err()
703            .to_string()
704            .contains("Circular dependency"));
705    }
706}