1use crate::error::{ExecutionError, ExecutionResult};
4use crate::models::{ExecutionPlan, ExecutionStep, StepAction};
5
6pub struct ExecutionValidator;
8
9impl ExecutionValidator {
10 pub fn validate_plan(plan: &ExecutionPlan) -> ExecutionResult<()> {
19 Self::validate_plan_name(&plan.name)?;
21
22 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 for step in &plan.steps {
31 Self::validate_step(step)?;
32 }
33
34 Self::validate_dependencies(plan)?;
36
37 Ok(())
38 }
39
40 pub fn validate_step(step: &ExecutionStep) -> ExecutionResult<()> {
49 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 Self::validate_step_action(&step.action)?;
64
65 Ok(())
66 }
67
68 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 pub fn validate_file_path(path: &str) -> ExecutionResult<()> {
112 if path.is_empty() {
114 return Err(ExecutionError::ValidationError(
115 "File path cannot be empty".to_string(),
116 ));
117 }
118
119 if path.len() > 4096 {
121 return Err(ExecutionError::ValidationError(
122 "File path cannot exceed 4096 characters".to_string(),
123 ));
124 }
125
126 if path.contains('\0') {
128 return Err(ExecutionError::ValidationError(
129 "File path cannot contain null bytes".to_string(),
130 ));
131 }
132
133 if path.starts_with('/') && !path.starts_with("./") && !path.starts_with("../") {
136 }
139
140 Ok(())
141 }
142
143 pub fn validate_file_content(content: &str) -> ExecutionResult<()> {
152 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 pub fn validate_diff(diff: &str) -> ExecutionResult<()> {
172 if diff.is_empty() {
174 return Err(ExecutionError::ValidationError(
175 "Diff cannot be empty".to_string(),
176 ));
177 }
178
179 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 pub fn validate_command(command: &str) -> ExecutionResult<()> {
198 if command.is_empty() {
200 return Err(ExecutionError::ValidationError(
201 "Command cannot be empty".to_string(),
202 ));
203 }
204
205 if command.len() > 4096 {
207 return Err(ExecutionError::ValidationError(
208 "Command cannot exceed 4096 characters".to_string(),
209 ));
210 }
211
212 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 pub fn validate_command_args(args: &[String]) -> ExecutionResult<()> {
231 if args.len() > 1000 {
233 return Err(ExecutionError::ValidationError(
234 "Command arguments cannot exceed 1000 items".to_string(),
235 ));
236 }
237
238 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 pub fn validate_test_pattern(pattern: &str) -> ExecutionResult<()> {
265 if pattern.is_empty() {
267 return Err(ExecutionError::ValidationError(
268 "Test pattern cannot be empty".to_string(),
269 ));
270 }
271
272 if pattern.len() > 1024 {
274 return Err(ExecutionError::ValidationError(
275 "Test pattern cannot exceed 1024 characters".to_string(),
276 ));
277 }
278
279 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 pub fn validate_plan_name(name: &str) -> ExecutionResult<()> {
298 if name.is_empty() {
300 return Err(ExecutionError::ValidationError(
301 "Plan name cannot be empty".to_string(),
302 ));
303 }
304
305 if name.len() > 256 {
307 return Err(ExecutionError::ValidationError(
308 "Plan name cannot exceed 256 characters".to_string(),
309 ));
310 }
311
312 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 fn validate_dependencies(plan: &ExecutionPlan) -> ExecutionResult<()> {
331 let valid_ids: std::collections::HashSet<_> =
333 plan.steps.iter().map(|s| s.id.as_str()).collect();
334
335 for step in &plan.steps {
337 for dep_id in &step.dependencies {
338 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 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 Self::check_circular_dependencies(plan)?;
358
359 Ok(())
360 }
361
362 fn check_circular_dependencies(plan: &ExecutionPlan) -> ExecutionResult<()> {
371 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 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 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 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 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 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}