ssh_commander_core/ssh/
shell.rs1pub fn quote(s: &str) -> String {
12 let mut out = String::with_capacity(s.len() + 2);
13 out.push('\'');
14 for ch in s.chars() {
15 if ch == '\'' {
16 out.push_str("'\\''");
17 } else {
18 out.push(ch);
19 }
20 }
21 out.push('\'');
22 out
23}
24
25pub fn validate_pid(s: &str) -> Result<&str, String> {
28 let trimmed = s.trim();
29 match trimmed.parse::<u32>() {
30 Ok(n) if n >= 1 => Ok(trimmed),
31 _ => Err(format!("Invalid PID: {:?}", s)),
32 }
33}
34
35pub fn validate_signal(s: &str) -> Result<String, String> {
38 const NAMES: &[&str] = &[
39 "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV", "USR2",
40 "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU", "URG",
41 "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS",
42 ];
43 let trimmed = s.trim().to_ascii_uppercase();
44 let trimmed = trimmed.trim_start_matches("SIG").to_string();
45 if let Ok(n) = trimmed.parse::<u32>() {
46 if (1..=64).contains(&n) {
47 return Ok(n.to_string());
48 }
49 return Err(format!("Signal out of range: {}", n));
50 }
51 if NAMES.iter().any(|&n| n == trimmed) {
52 return Ok(trimmed);
53 }
54 Err(format!("Unknown signal: {:?}", s))
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn quote_empty_string() {
63 assert_eq!(quote(""), "''");
64 }
65
66 #[test]
67 fn quote_plain_string() {
68 assert_eq!(quote("foo"), "'foo'");
69 assert_eq!(quote("/var/log/syslog"), "'/var/log/syslog'");
70 }
71
72 #[test]
73 fn quote_defuses_single_quote_injection() {
74 let attack = "foo'; rm -rf /; echo 'x";
76 let quoted = quote(attack);
77 assert_eq!(quoted, r#"'foo'\''; rm -rf /; echo '\''x'"#);
78 assert!(quoted.starts_with('\''));
80 assert!(quoted.ends_with('\''));
81 }
82
83 #[test]
84 fn quote_handles_glob_and_substitution_meta() {
85 assert_eq!(quote("$(whoami)"), "'$(whoami)'");
86 assert_eq!(quote("*.log"), "'*.log'");
87 assert_eq!(quote("a b\tc\nd"), "'a b\tc\nd'");
88 }
89
90 #[test]
91 fn validate_pid_accepts_normal_pids() {
92 assert_eq!(validate_pid("1234").unwrap(), "1234");
93 assert_eq!(validate_pid(" 42 ").unwrap(), "42");
94 }
95
96 #[test]
97 fn validate_pid_rejects_non_numeric_and_zero() {
98 assert!(validate_pid("0").is_err());
99 assert!(validate_pid("-1").is_err());
100 assert!(validate_pid("1; rm -rf /").is_err());
101 assert!(validate_pid("").is_err());
102 assert!(validate_pid("1 2").is_err());
103 }
104
105 #[test]
106 fn validate_signal_accepts_numeric_and_names() {
107 assert_eq!(validate_signal("15").unwrap(), "15");
108 assert_eq!(validate_signal("9").unwrap(), "9");
109 assert_eq!(validate_signal("TERM").unwrap(), "TERM");
110 assert_eq!(validate_signal("sigkill").unwrap(), "KILL");
111 assert_eq!(validate_signal("SIGHUP").unwrap(), "HUP");
112 }
113
114 #[test]
115 fn validate_signal_rejects_garbage() {
116 assert!(validate_signal("0").is_err());
117 assert!(validate_signal("999").is_err());
118 assert!(validate_signal("FOO").is_err());
119 assert!(validate_signal("15; rm -rf /").is_err());
120 }
121}