1use crate::error::{Result, SshMcpError};
6
7pub fn sanitize_command(command: &str, max_chars: Option<usize>) -> Result<String> {
34 let trimmed = command.trim();
35
36 if trimmed.is_empty() {
37 return Err(SshMcpError::invalid_params("Command cannot be empty"));
38 }
39
40 if let Some(max) = max_chars
42 && trimmed.len() > max
43 {
44 return Err(SshMcpError::invalid_params(format!(
45 "Command is too long (max {} characters, got {})",
46 max,
47 trimmed.len()
48 )));
49 }
50
51 Ok(trimmed.to_string())
52}
53
54pub fn escape_command_for_shell(command: &str) -> String {
75 crate::shell_escape::escape_for_shell(
78 &command
79 .replace('\\', "\\\\")
80 .replace('$', "\\$")
81 .replace('`', "\\`")
82 .replace('(', "\\(")
83 .replace(')', "\\)")
84 .replace('|', "\\|"),
85 )
86}
87
88pub fn wrap_in_posix_shell(command: &str, login: bool) -> String {
105 let escaped = escape_for_timeout_wrapper(command);
106 if login {
107 format!("sh -lc '{escaped}'")
108 } else {
109 format!("sh -c '{escaped}'")
110 }
111}
112
113pub fn escape_for_timeout_wrapper(command: &str) -> String {
138 crate::shell_escape::escape_for_shell(command)
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_sanitize_command_valid() {
149 let result = sanitize_command("ls -la", Some(1000));
150 assert!(result.is_ok());
151 assert_eq!(result.unwrap(), "ls -la");
152 }
153
154 #[test]
155 fn test_sanitize_command_trims_whitespace() {
156 let result = sanitize_command(" ls -la ", Some(1000));
157 assert!(result.is_ok());
158 assert_eq!(result.unwrap(), "ls -la");
159 }
160
161 #[test]
162 fn test_sanitize_command_empty() {
163 let result = sanitize_command("", Some(1000));
164 assert!(result.is_err());
165 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
166 }
167
168 #[test]
169 fn test_sanitize_command_whitespace_only() {
170 let result = sanitize_command(" ", Some(1000));
171 assert!(result.is_err());
172 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
173 }
174
175 #[test]
176 fn test_sanitize_command_too_long() {
177 let long_cmd = "a".repeat(100);
178 let result = sanitize_command(&long_cmd, Some(50));
179 assert!(result.is_err());
180 assert!(result.unwrap_err().to_string().contains("too long"));
181 }
182
183 #[test]
184 fn test_sanitize_command_exactly_at_limit() {
185 let cmd = "a".repeat(50);
186 let result = sanitize_command(&cmd, Some(50));
187 assert!(result.is_ok());
188 }
189
190 #[test]
191 fn test_sanitize_command_unlimited() {
192 let long_cmd = "a".repeat(10000);
193 let result = sanitize_command(&long_cmd, None);
194 assert!(result.is_ok());
195 }
196
197 #[test]
198 fn test_escape_command_for_shell_no_quotes() {
199 let escaped = escape_command_for_shell("ls -la");
200 assert_eq!(escaped, "ls -la");
201 }
202
203 #[test]
204 fn test_escape_command_for_shell_with_quotes() {
205 let escaped = escape_command_for_shell("echo 'hello'");
206 assert_eq!(escaped, "echo '\"'\"'hello'\"'\"'");
207 }
208
209 #[test]
210 fn test_escape_command_for_shell_dollar_sign() {
211 let escaped = escape_command_for_shell("echo $HOME");
212 assert_eq!(escaped, "echo \\$HOME");
213 }
214
215 #[test]
216 fn test_escape_command_for_shell_backtick() {
217 let escaped = escape_command_for_shell("echo `date`");
218 assert_eq!(escaped, "echo \\`date\\`");
219 }
220
221 #[test]
222 fn test_escape_command_for_shell_backslash() {
223 let escaped = escape_command_for_shell("echo \\n");
224 assert_eq!(escaped, "echo \\\\n");
225 }
226
227 #[test]
228 fn test_escape_command_for_shell_parentheses() {
229 let escaped = escape_command_for_shell("echo (test)");
230 assert_eq!(escaped, "echo \\(test\\)");
231 }
232
233 #[test]
234 fn test_escape_command_for_shell_pipe() {
235 let escaped = escape_command_for_shell("cat file | grep test");
236 assert_eq!(escaped, "cat file \\| grep test");
237 }
238
239 #[test]
240 fn test_escape_command_for_shell_combined_special_chars() {
241 let escaped = escape_command_for_shell("echo '$HOME' | cat");
242 assert_eq!(escaped, "echo '\"'\"'\\$HOME'\"'\"' \\| cat");
243 }
244
245 #[test]
246 fn test_escape_command_for_shell_multiple_quotes() {
247 let escaped = escape_command_for_shell("echo 'a' 'b'");
248 assert_eq!(escaped, "echo '\"'\"'a'\"'\"' '\"'\"'b'\"'\"'");
249 }
250
251 #[test]
252 fn test_escape_command_for_shell_empty() {
253 let escaped = escape_command_for_shell("");
254 assert_eq!(escaped, "");
255 }
256
257 #[test]
258 fn test_escape_for_timeout_wrapper_no_special_chars() {
259 let escaped = escape_for_timeout_wrapper("sleep 10");
260 assert_eq!(escaped, "sleep 10");
261 }
262
263 #[test]
264 fn test_escape_for_timeout_wrapper_with_single_quotes() {
265 let escaped = escape_for_timeout_wrapper("echo 'hello'");
266 assert_eq!(escaped, "echo '\"'\"'hello'\"'\"'");
267 }
268
269 #[test]
270 fn test_escape_for_timeout_wrapper_with_backslashes() {
271 let escaped = escape_for_timeout_wrapper("echo \\$HOME");
272 assert_eq!(escaped, "echo \\$HOME");
273 }
274
275 #[test]
276 fn test_escape_for_timeout_wrapper_with_both_quotes_and_backslashes() {
277 let escaped = escape_for_timeout_wrapper("echo '$HOME'");
278 assert_eq!(escaped, "echo '\"'\"'$HOME'\"'\"'");
279 }
280
281 #[test]
282 fn test_escape_for_timeout_wrapper_empty() {
283 let escaped = escape_for_timeout_wrapper("");
284 assert_eq!(escaped, "");
285 }
286
287 #[test]
288 fn test_escape_for_timeout_wrapper_multiple_quotes() {
289 let escaped = escape_for_timeout_wrapper("echo 'a' 'b'");
290 assert_eq!(escaped, "echo '\"'\"'a'\"'\"' '\"'\"'b'\"'\"'");
291 }
292
293 #[test]
294 fn test_wrap_in_posix_shell_non_login() {
295 let wrapped = wrap_in_posix_shell("ls -la", false);
296 assert_eq!(wrapped, "sh -c 'ls -la'");
297 }
298
299 #[test]
300 fn test_wrap_in_posix_shell_login() {
301 let wrapped = wrap_in_posix_shell("ls -la", true);
302 assert_eq!(wrapped, "sh -lc 'ls -la'");
303 }
304
305 #[test]
306 fn test_wrap_in_posix_shell_with_embedded_single_quotes() {
307 let wrapped = wrap_in_posix_shell("echo 'hello'", false);
308 assert_eq!(wrapped, "sh -c 'echo '\"'\"'hello'\"'\"''");
309 }
310
311 #[test]
312 fn test_wrap_in_posix_shell_empty_command() {
313 let wrapped = wrap_in_posix_shell("", false);
314 assert_eq!(wrapped, "sh -c ''");
315 }
316
317 #[test]
318 fn test_wrap_in_posix_shell_preserves_dollar_syntax() {
319 let wrapped = wrap_in_posix_shell("echo $HOME", false);
320 assert_eq!(wrapped, "sh -c 'echo $HOME'");
321 }
322
323 #[test]
324 fn test_wrap_in_posix_shell_preserves_pipe_syntax() {
325 let wrapped = wrap_in_posix_shell("printf test | wc -c", false);
326 assert_eq!(wrapped, "sh -c 'printf test | wc -c'");
327 }
328
329 #[test]
330 fn test_wrap_in_posix_shell_preserves_command_substitution_syntax() {
331 let wrapped = wrap_in_posix_shell("echo `whoami` $(pwd)", false);
332 assert_eq!(wrapped, "sh -c 'echo `whoami` $(pwd)'");
333 }
334}