ggen_utils/safe_command.rs
1//! Safe command execution with validation to prevent command injection attacks
2//!
3//! This module provides the `SafeCommand` type that wraps shell commands with validation
4//! to prevent common command injection vulnerabilities. All commands are validated on
5//! construction to ensure they:
6//!
7//! - Are in the whitelist of allowed commands
8//! - Do not contain shell metacharacters in arguments (;|&><$`\n)
9//! - Do not exceed maximum command length of 4096 characters
10//! - Use validated paths for path arguments
11//!
12//! ## Security Model
13//!
14//! This module uses a multi-layered defense approach:
15//!
16//! 1. **Command Whitelist**: Only explicitly allowed commands can be executed
17//! 2. **Argument Sanitization**: Shell metacharacters are blocked in arguments
18//! 3. **Type-State Pattern**: Commands must be validated before execution
19//! 4. **Path Integration**: Path arguments use SafePath validation
20//! 5. **Length Limits**: Maximum 4096 characters to prevent buffer overflow
21//!
22//! ## Examples
23//!
24//! ```rust
25//! use ggen_utils::safe_command::{SafeCommand, CommandName, CommandArg};
26//!
27//! // Valid command
28//! let cmd = SafeCommand::new("cargo")
29//! .unwrap()
30//! .arg("build")
31//! .unwrap()
32//! .arg("--release")
33//! .unwrap()
34//! .validate()
35//! .unwrap();
36//!
37//! // Invalid command (not in whitelist)
38//! assert!(SafeCommand::new("rm").is_err());
39//!
40//! // Invalid argument (shell metacharacter)
41//! let result = SafeCommand::new("cargo")
42//! .unwrap()
43//! .arg("build; rm -rf /")
44//! .unwrap_or_else(|e| panic!("Should fail: {}", e));
45//! ```
46
47use crate::error::{Error, Result};
48use crate::safe_path::SafePath;
49use std::convert::TryFrom;
50use std::fmt;
51use std::process::Command;
52
53/// Maximum allowed command length (including all arguments)
54const MAX_COMMAND_LENGTH: usize = 4096;
55
56/// Shell metacharacters that are blocked in command arguments
57const SHELL_METACHARACTERS: &[char] = &[';', '|', '&', '>', '<', '$', '`', '\n', '\r'];
58
59/// Whitelist of allowed commands
60const ALLOWED_COMMANDS: &[&str] = &[
61 "cargo",
62 "git",
63 "npm",
64 "rustc",
65 "rustfmt",
66 "clippy-driver",
67 "timeout",
68 "make",
69 "cmake",
70 "sh", // Only when explicitly needed with validated scripts
71 "bash", // Only when explicitly needed with validated scripts
72 "ggen", // Our own CLI
73];
74
75/// A validated command name from the whitelist
76///
77/// This newtype ensures that only whitelisted commands can be constructed.
78#[derive(Debug, Clone, PartialEq, Eq, Hash)]
79pub struct CommandName {
80 /// Inner command name - private to prevent direct mutation
81 inner: String,
82}
83
84impl CommandName {
85 /// Create a new CommandName from a string
86 ///
87 /// # Validation Rules
88 ///
89 /// - Command must be in the whitelist
90 /// - Command name cannot be empty
91 /// - Command name cannot contain whitespace
92 ///
93 /// # Examples
94 ///
95 /// ```rust
96 /// use ggen_utils::safe_command::CommandName;
97 ///
98 /// // Valid commands
99 /// assert!(CommandName::new("cargo").is_ok());
100 /// assert!(CommandName::new("git").is_ok());
101 ///
102 /// // Invalid commands
103 /// assert!(CommandName::new("rm").is_err());
104 /// assert!(CommandName::new("").is_err());
105 /// ```
106 pub fn new<S: AsRef<str>>(name: S) -> Result<Self> {
107 let name = name.as_ref();
108
109 // Check for empty
110 if name.is_empty() {
111 return Err(Error::invalid_input("Command name cannot be empty"));
112 }
113
114 // Check for whitespace
115 if name.contains(char::is_whitespace) {
116 return Err(Error::invalid_input(
117 "Command name cannot contain whitespace",
118 ));
119 }
120
121 // Check whitelist
122 if !ALLOWED_COMMANDS.contains(&name) {
123 return Err(Error::invalid_input(format!(
124 "Command '{}' is not in whitelist. Allowed commands: {:?}",
125 name, ALLOWED_COMMANDS
126 )));
127 }
128
129 Ok(Self {
130 inner: name.to_string(),
131 })
132 }
133
134 /// Get the command name as a string slice
135 #[must_use]
136 pub fn as_str(&self) -> &str {
137 &self.inner
138 }
139
140 /// Convert into the inner String
141 #[must_use]
142 pub fn into_string(self) -> String {
143 self.inner
144 }
145}
146
147impl TryFrom<&str> for CommandName {
148 type Error = Error;
149
150 fn try_from(value: &str) -> Result<Self> {
151 Self::new(value)
152 }
153}
154
155impl TryFrom<String> for CommandName {
156 type Error = Error;
157
158 fn try_from(value: String) -> Result<Self> {
159 Self::new(value)
160 }
161}
162
163impl fmt::Display for CommandName {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 write!(f, "{}", self.inner)
166 }
167}
168
169/// A validated command argument with sanitization
170///
171/// This newtype ensures that command arguments do not contain shell metacharacters
172/// that could lead to command injection attacks.
173#[derive(Debug, Clone, PartialEq, Eq, Hash)]
174pub struct CommandArg {
175 /// Inner argument - private to prevent direct mutation
176 inner: String,
177}
178
179impl CommandArg {
180 /// Create a new CommandArg from a string
181 ///
182 /// # Validation Rules
183 ///
184 /// - Argument cannot contain shell metacharacters (;|&><$`\n\r)
185 /// - Argument can be empty (for flags like --release)
186 ///
187 /// # Examples
188 ///
189 /// ```rust
190 /// use ggen_utils::safe_command::CommandArg;
191 ///
192 /// // Valid arguments
193 /// assert!(CommandArg::new("build").is_ok());
194 /// assert!(CommandArg::new("--release").is_ok());
195 /// assert!(CommandArg::new("").is_ok());
196 ///
197 /// // Invalid arguments (shell metacharacters)
198 /// assert!(CommandArg::new("build; rm -rf /").is_err());
199 /// assert!(CommandArg::new("build | tee").is_err());
200 /// assert!(CommandArg::new("$(whoami)").is_err());
201 /// ```
202 pub fn new<S: AsRef<str>>(arg: S) -> Result<Self> {
203 let arg = arg.as_ref();
204
205 // Check for shell metacharacters
206 for ch in SHELL_METACHARACTERS {
207 if arg.contains(*ch) {
208 return Err(Error::invalid_input(format!(
209 "Argument contains shell metacharacter '{}': {}",
210 ch, arg
211 )));
212 }
213 }
214
215 Ok(Self {
216 inner: arg.to_string(),
217 })
218 }
219
220 /// Create a CommandArg from a SafePath
221 ///
222 /// This ensures that path arguments are validated through SafePath.
223 ///
224 /// # Examples
225 ///
226 /// ```rust
227 /// use ggen_utils::safe_command::CommandArg;
228 /// use ggen_utils::safe_path::SafePath;
229 ///
230 /// let path = SafePath::new("src/generated").unwrap();
231 /// let arg = CommandArg::from_path(&path);
232 /// assert_eq!(arg.as_str(), "src/generated");
233 /// ```
234 #[must_use]
235 pub fn from_path(path: &SafePath) -> Self {
236 // SafePath already validated, so we can safely convert
237 // We know it doesn't contain shell metacharacters
238 Self {
239 inner: path.as_path().display().to_string(),
240 }
241 }
242
243 /// Get the argument as a string slice
244 #[must_use]
245 pub fn as_str(&self) -> &str {
246 &self.inner
247 }
248
249 /// Convert into the inner String
250 #[must_use]
251 pub fn into_string(self) -> String {
252 self.inner
253 }
254}
255
256impl TryFrom<&str> for CommandArg {
257 type Error = Error;
258
259 fn try_from(value: &str) -> Result<Self> {
260 Self::new(value)
261 }
262}
263
264impl TryFrom<String> for CommandArg {
265 type Error = Error;
266
267 fn try_from(value: String) -> Result<Self> {
268 Self::new(value)
269 }
270}
271
272impl fmt::Display for CommandArg {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 write!(f, "{}", self.inner)
275 }
276}
277
278/// Type-state marker for unvalidated commands
279#[derive(Debug)]
280pub struct Unvalidated;
281
282/// Type-state marker for validated commands
283#[derive(Debug, Clone)]
284pub struct Validated;
285
286/// A safe command builder with type-state pattern
287///
288/// Commands must be validated before they can be executed. The type-state pattern
289/// ensures this at compile time.
290///
291/// # Type States
292///
293/// - `SafeCommand<Unvalidated>`: Command is being built, not yet validated
294/// - `SafeCommand<Validated>`: Command has been validated and can be executed
295///
296/// # Examples
297///
298/// ```rust
299/// use ggen_utils::safe_command::SafeCommand;
300///
301/// // Build and validate command
302/// let cmd = SafeCommand::new("cargo")
303/// .unwrap()
304/// .arg("build")
305/// .unwrap()
306/// .arg("--release")
307/// .unwrap()
308/// .validate()
309/// .unwrap();
310///
311/// // Now cmd can be converted to std::process::Command
312/// let process_cmd = cmd.into_command();
313/// ```
314#[derive(Debug, Clone)]
315pub struct SafeCommand<State = Unvalidated> {
316 /// Validated command name
317 command: CommandName,
318 /// List of validated arguments
319 args: Vec<CommandArg>,
320 /// Type-state marker
321 _state: std::marker::PhantomData<State>,
322}
323
324impl SafeCommand<Unvalidated> {
325 /// Create a new SafeCommand with a validated command name
326 ///
327 /// # Examples
328 ///
329 /// ```rust
330 /// use ggen_utils::safe_command::SafeCommand;
331 ///
332 /// let cmd = SafeCommand::new("cargo").unwrap();
333 /// assert!(SafeCommand::new("rm").is_err());
334 /// ```
335 pub fn new<S: AsRef<str>>(command: S) -> Result<Self> {
336 let command = CommandName::new(command)?;
337 Ok(Self {
338 command,
339 args: Vec::new(),
340 _state: std::marker::PhantomData,
341 })
342 }
343
344 /// Add an argument to the command
345 ///
346 /// # Examples
347 ///
348 /// ```rust
349 /// use ggen_utils::safe_command::SafeCommand;
350 ///
351 /// let cmd = SafeCommand::new("cargo")
352 /// .unwrap()
353 /// .arg("build")
354 /// .unwrap()
355 /// .arg("--release")
356 /// .unwrap();
357 /// ```
358 pub fn arg<S: AsRef<str>>(mut self, arg: S) -> Result<Self> {
359 let arg = CommandArg::new(arg)?;
360 self.args.push(arg);
361 Ok(self)
362 }
363
364 /// Add a path argument to the command
365 ///
366 /// This ensures path arguments are validated through SafePath.
367 ///
368 /// # Examples
369 ///
370 /// ```rust
371 /// use ggen_utils::safe_command::SafeCommand;
372 /// use ggen_utils::safe_path::SafePath;
373 ///
374 /// let path = SafePath::new("src/generated").unwrap();
375 /// let cmd = SafeCommand::new("cargo")
376 /// .unwrap()
377 /// .arg("build")
378 /// .unwrap()
379 /// .arg_path(&path);
380 /// ```
381 #[must_use]
382 pub fn arg_path(mut self, path: &SafePath) -> Self {
383 let arg = CommandArg::from_path(path);
384 self.args.push(arg);
385 self
386 }
387
388 /// Add multiple arguments at once
389 ///
390 /// # Examples
391 ///
392 /// ```rust
393 /// use ggen_utils::safe_command::SafeCommand;
394 ///
395 /// let cmd = SafeCommand::new("cargo")
396 /// .unwrap()
397 /// .args(&["build", "--release"])
398 /// .unwrap();
399 /// ```
400 pub fn args<I, S>(mut self, args: I) -> Result<Self>
401 where
402 I: IntoIterator<Item = S>,
403 S: AsRef<str>,
404 {
405 for arg in args {
406 let validated_arg = CommandArg::new(arg)?;
407 self.args.push(validated_arg);
408 }
409 Ok(self)
410 }
411
412 /// Validate the command and transition to Validated state
413 ///
414 /// This performs final validation including total command length check.
415 ///
416 /// # Examples
417 ///
418 /// ```rust
419 /// use ggen_utils::safe_command::SafeCommand;
420 ///
421 /// let cmd = SafeCommand::new("cargo")
422 /// .unwrap()
423 /// .arg("build")
424 /// .unwrap()
425 /// .validate()
426 /// .unwrap();
427 /// ```
428 pub fn validate(self) -> Result<SafeCommand<Validated>> {
429 // Calculate total command length
430 let total_length = self.total_length();
431
432 if total_length > MAX_COMMAND_LENGTH {
433 return Err(Error::invalid_input(format!(
434 "Command length {} exceeds maximum allowed length of {}",
435 total_length, MAX_COMMAND_LENGTH
436 )));
437 }
438
439 Ok(SafeCommand {
440 command: self.command,
441 args: self.args,
442 _state: std::marker::PhantomData,
443 })
444 }
445
446 /// Calculate total command length (command + args + spaces)
447 fn total_length(&self) -> usize {
448 let mut length = self.command.as_str().len();
449
450 for arg in &self.args {
451 length += 1; // space
452 length += arg.as_str().len();
453 }
454
455 length
456 }
457}
458
459impl SafeCommand<Validated> {
460 /// Convert this SafeCommand into a std::process::Command
461 ///
462 /// This is only available for validated commands.
463 ///
464 /// # Examples
465 ///
466 /// ```rust
467 /// use ggen_utils::safe_command::SafeCommand;
468 ///
469 /// let safe_cmd = SafeCommand::new("cargo")
470 /// .unwrap()
471 /// .arg("--version")
472 /// .unwrap()
473 /// .validate()
474 /// .unwrap();
475 ///
476 /// let mut cmd = safe_cmd.into_command();
477 /// // Now you can execute cmd with std::process::Command
478 /// ```
479 #[must_use]
480 pub fn into_command(self) -> Command {
481 let mut cmd = Command::new(self.command.as_str());
482
483 for arg in self.args {
484 cmd.arg(arg.as_str());
485 }
486
487 cmd
488 }
489
490 /// Get a reference to the command name
491 #[must_use]
492 pub fn command(&self) -> &CommandName {
493 &self.command
494 }
495
496 /// Get a reference to the arguments
497 #[must_use]
498 pub fn args(&self) -> &[CommandArg] {
499 &self.args
500 }
501
502 /// Get the command as a string for display purposes
503 ///
504 /// This does NOT execute the command, it just formats it as a string.
505 #[must_use]
506 pub fn to_string_debug(&self) -> String {
507 let mut result = self.command.as_str().to_string();
508
509 for arg in &self.args {
510 result.push(' ');
511 result.push_str(arg.as_str());
512 }
513
514 result
515 }
516}
517
518impl fmt::Display for SafeCommand<Validated> {
519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
520 write!(f, "{}", self.to_string_debug())
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 // ============================================================================
529 // CommandName Tests
530 // ============================================================================
531
532 #[test]
533 fn test_command_name_valid() {
534 // Arrange
535 let name = "cargo";
536
537 // Act
538 let result = CommandName::new(name);
539
540 // Assert
541 assert!(result.is_ok());
542 assert_eq!(result.unwrap().as_str(), "cargo");
543 }
544
545 #[test]
546 fn test_command_name_all_whitelisted() {
547 // Arrange & Act & Assert
548 for &cmd in ALLOWED_COMMANDS {
549 let result = CommandName::new(cmd);
550 assert!(result.is_ok(), "Should allow whitelisted command: {}", cmd);
551 }
552 }
553
554 #[test]
555 fn test_command_name_not_whitelisted() {
556 // Arrange
557 let dangerous_commands = vec!["rm", "mv", "dd", "mkfs", "kill"];
558
559 // Act & Assert
560 for cmd in dangerous_commands {
561 let result = CommandName::new(cmd);
562 assert!(
563 result.is_err(),
564 "Should block non-whitelisted command: {}",
565 cmd
566 );
567 assert!(
568 result.unwrap_err().to_string().contains("not in whitelist"),
569 "Error should mention whitelist"
570 );
571 }
572 }
573
574 #[test]
575 fn test_command_name_empty() {
576 // Arrange
577 let name = "";
578
579 // Act
580 let result = CommandName::new(name);
581
582 // Assert
583 assert!(result.is_err());
584 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
585 }
586
587 #[test]
588 fn test_command_name_with_whitespace() {
589 // Arrange
590 let name = "cargo build";
591
592 // Act
593 let result = CommandName::new(name);
594
595 // Assert
596 assert!(result.is_err());
597 assert!(result.unwrap_err().to_string().contains("whitespace"));
598 }
599
600 #[test]
601 fn test_command_name_try_from_str() {
602 // Arrange
603 let name = "git";
604
605 // Act
606 let result = CommandName::try_from(name);
607
608 // Assert
609 assert!(result.is_ok());
610 assert_eq!(result.unwrap().as_str(), "git");
611 }
612
613 #[test]
614 fn test_command_name_try_from_string() {
615 // Arrange
616 let name = String::from("npm");
617
618 // Act
619 let result = CommandName::try_from(name);
620
621 // Assert
622 assert!(result.is_ok());
623 assert_eq!(result.unwrap().as_str(), "npm");
624 }
625
626 #[test]
627 fn test_command_name_display() {
628 // Arrange
629 let name = CommandName::new("cargo").unwrap();
630
631 // Act
632 let display = format!("{}", name);
633
634 // Assert
635 assert_eq!(display, "cargo");
636 }
637
638 // ============================================================================
639 // CommandArg Tests
640 // ============================================================================
641
642 #[test]
643 fn test_command_arg_valid() {
644 // Arrange
645 let arg = "build";
646
647 // Act
648 let result = CommandArg::new(arg);
649
650 // Assert
651 assert!(result.is_ok());
652 assert_eq!(result.unwrap().as_str(), "build");
653 }
654
655 #[test]
656 fn test_command_arg_with_dashes() {
657 // Arrange
658 let args = vec!["--release", "-v", "--all-features"];
659
660 // Act & Assert
661 for arg in args {
662 let result = CommandArg::new(arg);
663 assert!(result.is_ok(), "Should allow arg with dashes: {}", arg);
664 }
665 }
666
667 #[test]
668 fn test_command_arg_empty() {
669 // Arrange
670 let arg = "";
671
672 // Act
673 let result = CommandArg::new(arg);
674
675 // Assert - empty args are allowed (for compatibility)
676 assert!(result.is_ok());
677 }
678
679 #[test]
680 fn test_command_arg_shell_metacharacters() {
681 // Arrange
682 let attacks = vec![
683 ("build; rm -rf /", ';'),
684 ("build | tee output", '|'),
685 ("build && rm -rf /", '&'),
686 ("build > /dev/null", '>'),
687 ("build < input", '<'),
688 ("$(whoami)", '$'),
689 ("`whoami`", '`'),
690 ("build\nrm -rf /", '\n'),
691 ("build\rrm -rf /", '\r'),
692 ];
693
694 // Act & Assert
695 for (attack, metachar) in attacks {
696 let result = CommandArg::new(attack);
697 assert!(
698 result.is_err(),
699 "Should block shell metacharacter: {}",
700 metachar
701 );
702 assert!(
703 result.unwrap_err().to_string().contains("metacharacter"),
704 "Error should mention metacharacter"
705 );
706 }
707 }
708
709 #[test]
710 fn test_command_arg_from_path() {
711 // Arrange
712 let path = SafePath::new("src/generated").unwrap();
713
714 // Act
715 let arg = CommandArg::from_path(&path);
716
717 // Assert
718 assert_eq!(arg.as_str(), "src/generated");
719 }
720
721 #[test]
722 fn test_command_arg_try_from() {
723 // Arrange
724 let arg_str = "--release";
725
726 // Act
727 let result = CommandArg::try_from(arg_str);
728
729 // Assert
730 assert!(result.is_ok());
731 assert_eq!(result.unwrap().as_str(), "--release");
732 }
733
734 // ============================================================================
735 // SafeCommand Basic Tests
736 // ============================================================================
737
738 #[test]
739 fn test_safe_command_new() {
740 // Arrange
741 let cmd = "cargo";
742
743 // Act
744 let result = SafeCommand::new(cmd);
745
746 // Assert
747 assert!(result.is_ok());
748 }
749
750 #[test]
751 fn test_safe_command_new_invalid() {
752 // Arrange
753 let cmd = "rm";
754
755 // Act
756 let result = SafeCommand::new(cmd);
757
758 // Assert
759 assert!(result.is_err());
760 assert!(result.unwrap_err().to_string().contains("not in whitelist"));
761 }
762
763 #[test]
764 fn test_safe_command_single_arg() {
765 // Arrange & Act
766 let result = SafeCommand::new("cargo").unwrap().arg("build");
767
768 // Assert
769 assert!(result.is_ok());
770 }
771
772 #[test]
773 fn test_safe_command_multiple_args() {
774 // Arrange & Act
775 let result = SafeCommand::new("cargo")
776 .unwrap()
777 .arg("build")
778 .unwrap()
779 .arg("--release")
780 .unwrap()
781 .arg("--all-features");
782
783 // Assert
784 assert!(result.is_ok());
785 }
786
787 #[test]
788 fn test_safe_command_args_bulk() {
789 // Arrange & Act
790 let result =
791 SafeCommand::new("cargo")
792 .unwrap()
793 .args(["build", "--release", "--all-features"]);
794
795 // Assert
796 assert!(result.is_ok());
797 }
798
799 #[test]
800 fn test_safe_command_arg_path() {
801 // Arrange
802 let path = SafePath::new("src/generated").unwrap();
803
804 // Act
805 let cmd = SafeCommand::new("cargo")
806 .unwrap()
807 .arg("build")
808 .unwrap()
809 .arg_path(&path);
810
811 // Assert - arg_path is infallible (uses already-validated SafePath)
812 let validated = cmd.validate();
813 assert!(validated.is_ok());
814 }
815
816 #[test]
817 fn test_safe_command_validate_success() {
818 // Arrange
819 let cmd = SafeCommand::new("cargo")
820 .unwrap()
821 .arg("build")
822 .unwrap()
823 .arg("--release")
824 .unwrap();
825
826 // Act
827 let result = cmd.validate();
828
829 // Assert
830 assert!(result.is_ok());
831 }
832
833 #[test]
834 fn test_safe_command_into_command() {
835 // Arrange
836 let safe_cmd = SafeCommand::new("cargo")
837 .unwrap()
838 .arg("--version")
839 .unwrap()
840 .validate()
841 .unwrap();
842
843 // Act
844 let process_cmd = safe_cmd.into_command();
845
846 // Assert - verify it creates a valid Command
847 // We can't easily test execution here, but we can verify it compiles
848 let _ = process_cmd;
849 }
850
851 #[test]
852 fn test_safe_command_to_string_debug() {
853 // Arrange
854 let cmd = SafeCommand::new("cargo")
855 .unwrap()
856 .arg("build")
857 .unwrap()
858 .arg("--release")
859 .unwrap()
860 .validate()
861 .unwrap();
862
863 // Act
864 let debug_string = cmd.to_string_debug();
865
866 // Assert
867 assert_eq!(debug_string, "cargo build --release");
868 }
869
870 #[test]
871 fn test_safe_command_display() {
872 // Arrange
873 let cmd = SafeCommand::new("git")
874 .unwrap()
875 .arg("status")
876 .unwrap()
877 .validate()
878 .unwrap();
879
880 // Act
881 let display = format!("{}", cmd);
882
883 // Assert
884 assert_eq!(display, "git status");
885 }
886
887 // ============================================================================
888 // SafeCommand Security Tests
889 // ============================================================================
890
891 #[test]
892 fn test_safe_command_injection_in_arg() {
893 // Arrange & Act
894 let result = SafeCommand::new("cargo").unwrap().arg("build; rm -rf /");
895
896 // Assert
897 assert!(result.is_err());
898 assert!(result.unwrap_err().to_string().contains("metacharacter"));
899 }
900
901 #[test]
902 fn test_safe_command_injection_in_multiple_args() {
903 // Arrange & Act
904 let result = SafeCommand::new("cargo")
905 .unwrap()
906 .arg("build")
907 .unwrap()
908 .arg("--release && rm -rf /");
909
910 // Assert
911 assert!(result.is_err());
912 }
913
914 #[test]
915 fn test_safe_command_max_length() {
916 // Arrange - create a command that exceeds MAX_COMMAND_LENGTH
917 let long_arg = "a".repeat(MAX_COMMAND_LENGTH);
918
919 // Act
920 let result = SafeCommand::new("cargo")
921 .unwrap()
922 .arg(&long_arg)
923 .unwrap()
924 .validate();
925
926 // Assert
927 assert!(result.is_err());
928 assert!(result.unwrap_err().to_string().contains("exceeds maximum"));
929 }
930
931 #[test]
932 fn test_safe_command_max_length_boundary() {
933 // Arrange - create a command at exactly MAX_COMMAND_LENGTH
934 // "cargo" = 5 chars
935 // " " = 1 char
936 // So we need MAX_COMMAND_LENGTH - 6 chars for the arg
937 let arg_length = MAX_COMMAND_LENGTH - 6;
938 let exact_arg = "a".repeat(arg_length);
939
940 // Act
941 let result = SafeCommand::new("cargo")
942 .unwrap()
943 .arg(&exact_arg)
944 .unwrap()
945 .validate();
946
947 // Assert - should succeed at exact boundary
948 assert!(result.is_ok());
949 }
950
951 #[test]
952 fn test_safe_command_command_and_args_accessors() {
953 // Arrange
954 let cmd = SafeCommand::new("cargo")
955 .unwrap()
956 .arg("build")
957 .unwrap()
958 .arg("--release")
959 .unwrap()
960 .validate()
961 .unwrap();
962
963 // Act
964 let command = cmd.command();
965 let args = cmd.args();
966
967 // Assert
968 assert_eq!(command.as_str(), "cargo");
969 assert_eq!(args.len(), 2);
970 assert_eq!(args[0].as_str(), "build");
971 assert_eq!(args[1].as_str(), "--release");
972 }
973
974 // ============================================================================
975 // Integration with SafePath Tests
976 // ============================================================================
977
978 #[test]
979 fn test_safe_command_with_safe_path() {
980 // Arrange
981 let path = SafePath::new("src/generated/output.rs").unwrap();
982
983 // Act
984 let cmd = SafeCommand::new("rustfmt")
985 .unwrap()
986 .arg_path(&path)
987 .validate()
988 .unwrap();
989
990 // Assert
991 let debug_string = cmd.to_string_debug();
992 assert!(debug_string.contains("src/generated/output.rs"));
993 }
994
995 #[test]
996 fn test_safe_command_multiple_paths() {
997 // Arrange
998 let path1 = SafePath::new("src/main.rs").unwrap();
999 let path2 = SafePath::new("src/lib.rs").unwrap();
1000
1001 // Act
1002 let cmd = SafeCommand::new("rustfmt")
1003 .unwrap()
1004 .arg_path(&path1)
1005 .arg_path(&path2)
1006 .validate()
1007 .unwrap();
1008
1009 // Assert
1010 let debug_string = cmd.to_string_debug();
1011 assert!(debug_string.contains("src/main.rs"));
1012 assert!(debug_string.contains("src/lib.rs"));
1013 }
1014
1015 // ============================================================================
1016 // Edge Cases
1017 // ============================================================================
1018
1019 #[test]
1020 fn test_safe_command_no_args() {
1021 // Arrange & Act
1022 let result = SafeCommand::new("git").unwrap().validate();
1023
1024 // Assert - command with no args should be valid
1025 assert!(result.is_ok());
1026 }
1027
1028 #[test]
1029 fn test_safe_command_many_small_args() {
1030 // Arrange - create many small args
1031 let mut cmd = SafeCommand::new("cargo").unwrap();
1032
1033 for i in 0..100 {
1034 cmd = cmd.arg(format!("arg{}", i)).unwrap();
1035 }
1036
1037 // Act
1038 let result = cmd.validate();
1039
1040 // Assert - should succeed if under max length
1041 assert!(result.is_ok());
1042 }
1043
1044 #[test]
1045 fn test_command_name_clone() {
1046 // Arrange
1047 let name = CommandName::new("cargo").unwrap();
1048
1049 // Act
1050 let cloned = name.clone();
1051
1052 // Assert
1053 assert_eq!(name, cloned);
1054 }
1055
1056 #[test]
1057 fn test_command_arg_clone() {
1058 // Arrange
1059 let arg = CommandArg::new("build").unwrap();
1060
1061 // Act
1062 let cloned = arg.clone();
1063
1064 // Assert
1065 assert_eq!(arg, cloned);
1066 }
1067
1068 #[test]
1069 fn test_safe_command_clone() {
1070 // Arrange
1071 let cmd = SafeCommand::new("cargo")
1072 .unwrap()
1073 .arg("build")
1074 .unwrap()
1075 .validate()
1076 .unwrap();
1077
1078 // Act
1079 let cloned = cmd.clone();
1080
1081 // Assert
1082 assert_eq!(cmd.to_string_debug(), cloned.to_string_debug());
1083 }
1084}