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