#![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),
}
}
const ABORT_MARKER_RANDOM_BYTES: usize = 16;
const ABORT_MARKER_PREFIX: &str = "sshcli-job-";
const HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";
#[must_use]
pub fn new_remote_job_marker() -> Option<String> {
let mut raw = [0u8; ABORT_MARKER_RANDOM_BYTES];
if getrandom::fill(&mut raw).is_err() {
return None;
}
let mut marker = String::with_capacity(ABORT_MARKER_PREFIX.len() + raw.len() * 2);
marker.push_str(ABORT_MARKER_PREFIX);
for byte in raw {
marker.push(HEX_DIGITS[usize::from(byte >> 4)] as char);
marker.push(HEX_DIGITS[usize::from(byte & 0x0f)] as char);
}
Some(marker)
}
#[must_use]
pub fn is_remote_job_marker(value: &str) -> bool {
value.strip_prefix(ABORT_MARKER_PREFIX).is_some_and(|hex| {
hex.len() == ABORT_MARKER_RANDOM_BYTES * 2 && hex.bytes().all(|b| b.is_ascii_hexdigit())
})
}
#[must_use]
pub fn wrap_with_abort_marker(command: &str, marker: &str) -> String {
let cmd_esc = escape_shell_single_quotes(command);
let marker_esc = escape_shell_single_quotes(marker);
format!("sh -c {cmd_esc} {marker_esc}")
}
#[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());
}
#[test]
fn job_markers_are_unique_per_invocation() {
let a = new_remote_job_marker().expect("csprng available");
let b = new_remote_job_marker().expect("csprng available");
assert_ne!(a, b);
assert!(is_remote_job_marker(&a) && is_remote_job_marker(&b));
let kill_a = pack_abort_pkill(&a);
let kill_b = pack_abort_pkill(&b);
assert!(!kill_a.contains(&b));
assert!(!kill_b.contains(&a));
let cmd_a = wrap_with_abort_marker("sleep 999", &a);
let cmd_b = wrap_with_abort_marker("sleep 999", &b);
assert!(cmd_a.contains(&a) && !cmd_a.contains(&b));
assert!(cmd_b.contains(&b) && !cmd_b.contains(&a));
}
#[test]
fn abort_marker_never_matches_third_party_sudo() {
let password = SecretString::from("s3cr3t".to_string());
let pack = pack_sudo("systemctl restart nginx", Some(&password));
let marker = new_remote_job_marker().expect("csprng available");
let kill = pack_abort_pkill(&marker);
assert!(!kill.contains("sudo"));
assert!(kill.contains(&marker));
let wrapped = wrap_with_abort_marker(&pack.command, &marker);
assert!(!wrapped.contains("s3cr3t"));
assert!(wrapped.contains(&marker));
}
#[test]
fn marker_shape_is_validated() {
assert!(!is_remote_job_marker("sudo -S -p"));
assert!(!is_remote_job_marker("sshcli-job-"));
assert!(!is_remote_job_marker("sshcli-job-zz"));
}
}