1use secrecy::{ExposeSecret, SecretString};
7
8#[must_use]
13pub fn escape_shell_single_quotes(value: &str) -> String {
14 let mut result = String::with_capacity(value.len() + 2);
15 result.push('\'');
16 for ch in value.chars() {
17 if ch == '\'' {
18 result.push_str("'\\''");
19 } else {
20 result.push(ch);
21 }
22 }
23 result.push('\'');
24 result
25}
26
27#[must_use]
29pub fn append_description(command: &str, description: Option<&str>) -> String {
30 match description {
31 Some(d) if !d.trim().is_empty() => {
32 let limpo = d.replace(['\n', '\r'], " ");
33 format!("{command} # {limpo}")
34 }
35 _ => command.to_string(),
36 }
37}
38
39#[derive(Debug, Clone)]
42pub struct PackedCommand {
43 pub command: String,
45 pub stdin: Option<Vec<u8>>,
47}
48
49#[must_use]
54pub fn pack_sudo(command: &str, sudo_password: Option<&SecretString>) -> PackedCommand {
55 let cmd_esc = escape_shell_single_quotes(command);
56 match sudo_password {
57 Some(password) => {
58 let mut stdin = password.expose_secret().as_bytes().to_vec();
59 stdin.push(b'\n');
60 PackedCommand {
61 command: format!("sudo -S -p '' sh -c {cmd_esc}"),
62 stdin: Some(stdin),
63 }
64 }
65 None => PackedCommand {
66 command: format!("sudo -n sh -c {cmd_esc}"),
67 stdin: None,
68 },
69 }
70}
71
72#[must_use]
74pub fn pack_su(command: &str, su_password: &SecretString) -> PackedCommand {
75 let cmd_esc = escape_shell_single_quotes(command);
76 let mut stdin = su_password.expose_secret().as_bytes().to_vec();
77 stdin.push(b'\n');
78 PackedCommand {
79 command: format!("su - -c {cmd_esc}"),
80 stdin: Some(stdin),
81 }
82}
83
84#[must_use]
89pub fn remote_abort_pattern(command: &str) -> Option<String> {
90 let mut limpo = String::with_capacity(command.len().min(128));
91 for ch in command.chars().take(128) {
92 if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | '/' | ' ' | ':' | '=') {
93 limpo.push(ch);
94 } else {
95 break;
96 }
97 }
98 let t = limpo.trim();
99 if t.len() < 3 {
100 None
101 } else {
102 Some(t.to_string())
103 }
104}
105
106#[must_use]
110pub fn pack_abort_pkill(pattern: &str) -> String {
111 let esc = escape_shell_single_quotes(pattern);
112 format!(
113 "(pkill -TERM -f {esc} 2>/dev/null || true); sleep 0.2; (pkill -KILL -f {esc} 2>/dev/null || true)"
114 )
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn escape_single_quote() {
123 assert_eq!(escape_shell_single_quotes("ab'cd"), "'ab'\\''cd'");
124 assert_eq!(escape_shell_single_quotes("abc"), "'abc'");
125 }
126
127 #[test]
128 fn sudo_with_password_uses_sh_c_no_secret_in_argv() {
129 let password = SecretString::from("s3cr3t".to_string());
130 let pack = pack_sudo("echo hi | tee /tmp/x", Some(&password));
131 assert!(pack.command.contains("sudo -S -p '' sh -c"));
132 assert!(!pack.command.contains("s3cr3t"));
133 assert!(!pack.command.contains("printf"));
134 let stdin = pack.stdin.expect("stdin com senha");
135 assert_eq!(stdin, b"s3cr3t\n");
136 }
137
138 #[test]
139 fn sudo_without_password_uses_n() {
140 let pack = pack_sudo("id", None);
141 assert_eq!(pack.command, "sudo -n sh -c 'id'");
142 assert!(pack.stdin.is_none());
143 }
144
145 #[test]
146 fn su_pack_no_secret_in_argv() {
147 let password = SecretString::from("rootpw".to_string());
148 let pack = pack_su("whoami", &password);
149 assert!(pack.command.contains("su - -c"));
150 assert!(!pack.command.contains("rootpw"));
151 assert_eq!(pack.stdin.as_deref(), Some(b"rootpw\n".as_slice()));
152 }
153
154 #[test]
155 fn description_appends_comment() {
156 assert_eq!(
157 append_description("ls", Some("lista arquivos")),
158 "ls # lista arquivos"
159 );
160 assert_eq!(append_description("ls", None), "ls");
161 }
162
163 #[test]
164 fn abort_pattern_sanitizes() {
165 assert_eq!(
166 remote_abort_pattern("sleep 999"),
167 Some("sleep 999".to_string())
168 );
169 assert_eq!(remote_abort_pattern("$(rm -rf)"), None);
171 assert!(remote_abort_pattern("ab").is_none());
172 }
173}