#![forbid(unsafe_code)]
use secrecy::{ExposeSecret, SecretString};
use zeroize::Zeroize;
#[must_use]
pub fn escape_shell_single_quotes(value: &str) -> String {
let mut result = String::with_capacity(value.len() + 2);
result.push('\'');
for ch in value.chars() {
if ch == '\'' {
result.push_str("'\\''");
} else {
result.push(ch);
}
}
result.push('\'');
result
}
#[must_use]
pub fn append_description(command: &str, description: Option<&str>) -> String {
match description {
Some(d) if !d.trim().is_empty() => {
let cleaned = d.replace(['\n', '\r'], " ");
format!("{command} # {cleaned}")
}
_ => command.to_string(),
}
}
#[derive(Clone)]
pub struct PackedCommand {
pub command: String,
pub stdin: Option<Vec<u8>>,
}
impl std::fmt::Debug for PackedCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PackedCommand")
.field("command", &self.command)
.field(
"stdin",
&self.stdin.as_ref().map(|_| "<redacted bytes>"),
)
.finish()
}
}
impl Drop for PackedCommand {
fn drop(&mut self) {
if let Some(ref mut bytes) = self.stdin {
bytes.zeroize();
}
}
}
impl PackedCommand {
#[must_use]
pub fn take_stdin(&mut self) -> Option<Vec<u8>> {
self.stdin.take()
}
}
#[must_use]
pub fn pack_sudo(command: &str, sudo_password: Option<&SecretString>) -> PackedCommand {
let cmd_esc = escape_shell_single_quotes(command);
match sudo_password {
Some(password) => {
let mut stdin = password.expose_secret().as_bytes().to_vec();
stdin.push(b'\n');
PackedCommand {
command: format!("sudo -S -p '' sh -c {cmd_esc}"),
stdin: Some(stdin),
}
}
None => PackedCommand {
command: format!("sudo -n sh -c {cmd_esc}"),
stdin: None,
},
}
}
#[must_use]
pub fn pack_su(command: &str, su_password: &SecretString) -> PackedCommand {
let cmd_esc = escape_shell_single_quotes(command);
let mut stdin = su_password.expose_secret().as_bytes().to_vec();
stdin.push(b'\n');
PackedCommand {
command: format!("su - -c {cmd_esc}"),
stdin: Some(stdin),
}
}
#[must_use]
pub fn remote_abort_pattern(command: &str) -> Option<String> {
let mut cleaned = String::with_capacity(command.len().min(128));
for ch in command.chars().take(128) {
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | '/' | ' ' | ':' | '=') {
cleaned.push(ch);
} else {
break;
}
}
let trimmed = cleaned.trim();
if trimmed.len() < 3 {
None
} else if trimmed.len() == cleaned.len() {
Some(cleaned)
} else {
Some(trimmed.to_string())
}
}
#[must_use]
pub fn pack_abort_pkill(pattern: &str) -> String {
let esc = escape_shell_single_quotes(pattern);
format!(
"(pkill -TERM -f {esc} 2>/dev/null || true); sleep 0.2; (pkill -KILL -f {esc} 2>/dev/null || true)"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escape_single_quote() {
assert_eq!(escape_shell_single_quotes("ab'cd"), "'ab'\\''cd'");
assert_eq!(escape_shell_single_quotes("abc"), "'abc'");
}
#[test]
fn sudo_with_password_uses_sh_c_no_secret_in_argv() {
let password = SecretString::from("s3cr3t".to_string());
let pack = pack_sudo("echo hi | tee /tmp/x", Some(&password));
assert!(pack.command.contains("sudo -S -p '' sh -c"));
assert!(!pack.command.contains("s3cr3t"));
assert!(!pack.command.contains("printf"));
let mut pack = pack;
let stdin = pack.take_stdin().expect("stdin with password");
assert_eq!(stdin, b"s3cr3t\n");
}
#[test]
fn sudo_without_password_uses_n() {
let pack = pack_sudo("id", None);
assert_eq!(pack.command, "sudo -n sh -c 'id'");
assert!(pack.stdin.is_none());
}
#[test]
fn su_pack_no_secret_in_argv() {
let password = SecretString::from("rootpw".to_string());
let pack = pack_su("whoami", &password);
assert!(pack.command.contains("su - -c"));
assert!(!pack.command.contains("rootpw"));
assert_eq!(pack.stdin.as_deref(), Some(b"rootpw\n".as_slice()));
}
#[test]
fn description_appends_comment() {
assert_eq!(
append_description("ls", Some("lista arquivos")),
"ls # lista arquivos"
);
assert_eq!(append_description("ls", None), "ls");
}
#[test]
fn debug_redacts_stdin() {
let password = SecretString::from("s3cr3t".to_string());
let pack = pack_sudo("id", Some(&password));
let dbg = format!("{pack:?}");
assert!(!dbg.contains("s3cr3t"));
assert!(dbg.contains("<redacted bytes>"));
}
#[test]
fn abort_pattern_sanitizes() {
assert_eq!(
remote_abort_pattern("sleep 999"),
Some("sleep 999".to_string())
);
assert_eq!(remote_abort_pattern("$(rm -rf)"), None);
assert!(remote_abort_pattern("ab").is_none());
}
}