use crate::error::{Result, SshMcpError};
pub fn sanitize_command(command: &str, max_chars: Option<usize>) -> Result<String> {
let trimmed = command.trim();
if trimmed.is_empty() {
return Err(SshMcpError::invalid_params("Command cannot be empty"));
}
if let Some(max) = max_chars
&& trimmed.len() > max
{
return Err(SshMcpError::invalid_params(format!(
"Command is too long (max {} characters, got {})",
max,
trimmed.len()
)));
}
Ok(trimmed.to_string())
}
pub fn escape_command_for_shell(command: &str) -> String {
crate::shell_escape::escape_for_shell(
&command
.replace('\\', "\\\\")
.replace('$', "\\$")
.replace('`', "\\`")
.replace('(', "\\(")
.replace(')', "\\)")
.replace('|', "\\|"),
)
}
pub fn wrap_in_posix_shell(command: &str, login: bool) -> String {
let escaped = escape_for_timeout_wrapper(command);
if login {
format!("sh -lc '{escaped}'")
} else {
format!("sh -c '{escaped}'")
}
}
pub fn escape_for_timeout_wrapper(command: &str) -> String {
crate::shell_escape::escape_for_shell(command)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_command_valid() {
let result = sanitize_command("ls -la", Some(1000));
assert!(result.is_ok());
assert_eq!(result.unwrap(), "ls -la");
}
#[test]
fn test_sanitize_command_trims_whitespace() {
let result = sanitize_command(" ls -la ", Some(1000));
assert!(result.is_ok());
assert_eq!(result.unwrap(), "ls -la");
}
#[test]
fn test_sanitize_command_empty() {
let result = sanitize_command("", Some(1000));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot be empty"));
}
#[test]
fn test_sanitize_command_whitespace_only() {
let result = sanitize_command(" ", Some(1000));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("cannot be empty"));
}
#[test]
fn test_sanitize_command_too_long() {
let long_cmd = "a".repeat(100);
let result = sanitize_command(&long_cmd, Some(50));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("too long"));
}
#[test]
fn test_sanitize_command_exactly_at_limit() {
let cmd = "a".repeat(50);
let result = sanitize_command(&cmd, Some(50));
assert!(result.is_ok());
}
#[test]
fn test_sanitize_command_unlimited() {
let long_cmd = "a".repeat(10000);
let result = sanitize_command(&long_cmd, None);
assert!(result.is_ok());
}
#[test]
fn test_escape_command_for_shell_no_quotes() {
let escaped = escape_command_for_shell("ls -la");
assert_eq!(escaped, "ls -la");
}
#[test]
fn test_escape_command_for_shell_with_quotes() {
let escaped = escape_command_for_shell("echo 'hello'");
assert_eq!(escaped, "echo '\"'\"'hello'\"'\"'");
}
#[test]
fn test_escape_command_for_shell_dollar_sign() {
let escaped = escape_command_for_shell("echo $HOME");
assert_eq!(escaped, "echo \\$HOME");
}
#[test]
fn test_escape_command_for_shell_backtick() {
let escaped = escape_command_for_shell("echo `date`");
assert_eq!(escaped, "echo \\`date\\`");
}
#[test]
fn test_escape_command_for_shell_backslash() {
let escaped = escape_command_for_shell("echo \\n");
assert_eq!(escaped, "echo \\\\n");
}
#[test]
fn test_escape_command_for_shell_parentheses() {
let escaped = escape_command_for_shell("echo (test)");
assert_eq!(escaped, "echo \\(test\\)");
}
#[test]
fn test_escape_command_for_shell_pipe() {
let escaped = escape_command_for_shell("cat file | grep test");
assert_eq!(escaped, "cat file \\| grep test");
}
#[test]
fn test_escape_command_for_shell_combined_special_chars() {
let escaped = escape_command_for_shell("echo '$HOME' | cat");
assert_eq!(escaped, "echo '\"'\"'\\$HOME'\"'\"' \\| cat");
}
#[test]
fn test_escape_command_for_shell_multiple_quotes() {
let escaped = escape_command_for_shell("echo 'a' 'b'");
assert_eq!(escaped, "echo '\"'\"'a'\"'\"' '\"'\"'b'\"'\"'");
}
#[test]
fn test_escape_command_for_shell_empty() {
let escaped = escape_command_for_shell("");
assert_eq!(escaped, "");
}
#[test]
fn test_escape_for_timeout_wrapper_no_special_chars() {
let escaped = escape_for_timeout_wrapper("sleep 10");
assert_eq!(escaped, "sleep 10");
}
#[test]
fn test_escape_for_timeout_wrapper_with_single_quotes() {
let escaped = escape_for_timeout_wrapper("echo 'hello'");
assert_eq!(escaped, "echo '\"'\"'hello'\"'\"'");
}
#[test]
fn test_escape_for_timeout_wrapper_with_backslashes() {
let escaped = escape_for_timeout_wrapper("echo \\$HOME");
assert_eq!(escaped, "echo \\$HOME");
}
#[test]
fn test_escape_for_timeout_wrapper_with_both_quotes_and_backslashes() {
let escaped = escape_for_timeout_wrapper("echo '$HOME'");
assert_eq!(escaped, "echo '\"'\"'$HOME'\"'\"'");
}
#[test]
fn test_escape_for_timeout_wrapper_empty() {
let escaped = escape_for_timeout_wrapper("");
assert_eq!(escaped, "");
}
#[test]
fn test_escape_for_timeout_wrapper_multiple_quotes() {
let escaped = escape_for_timeout_wrapper("echo 'a' 'b'");
assert_eq!(escaped, "echo '\"'\"'a'\"'\"' '\"'\"'b'\"'\"'");
}
#[test]
fn test_wrap_in_posix_shell_non_login() {
let wrapped = wrap_in_posix_shell("ls -la", false);
assert_eq!(wrapped, "sh -c 'ls -la'");
}
#[test]
fn test_wrap_in_posix_shell_login() {
let wrapped = wrap_in_posix_shell("ls -la", true);
assert_eq!(wrapped, "sh -lc 'ls -la'");
}
#[test]
fn test_wrap_in_posix_shell_with_embedded_single_quotes() {
let wrapped = wrap_in_posix_shell("echo 'hello'", false);
assert_eq!(wrapped, "sh -c 'echo '\"'\"'hello'\"'\"''");
}
#[test]
fn test_wrap_in_posix_shell_empty_command() {
let wrapped = wrap_in_posix_shell("", false);
assert_eq!(wrapped, "sh -c ''");
}
#[test]
fn test_wrap_in_posix_shell_preserves_dollar_syntax() {
let wrapped = wrap_in_posix_shell("echo $HOME", false);
assert_eq!(wrapped, "sh -c 'echo $HOME'");
}
#[test]
fn test_wrap_in_posix_shell_preserves_pipe_syntax() {
let wrapped = wrap_in_posix_shell("printf test | wc -c", false);
assert_eq!(wrapped, "sh -c 'printf test | wc -c'");
}
#[test]
fn test_wrap_in_posix_shell_preserves_command_substitution_syntax() {
let wrapped = wrap_in_posix_shell("echo `whoami` $(pwd)", false);
assert_eq!(wrapped, "sh -c 'echo `whoami` $(pwd)'");
}
}