1use std::fmt;
28use std::path::PathBuf;
29
30pub type CliResult<T> = Result<T, CliError>;
32
33#[derive(Debug, Clone)]
38pub enum CliError {
39 InvalidArgument {
44 arg: String,
45 value: String,
46 reason: String,
47 hint: Option<String>,
48 },
49
50 MissingArgument { arg: String, context: String },
52
53 ConflictingArguments {
55 arg1: String,
56 arg2: String,
57 reason: String,
58 },
59
60 InvalidArgumentCombination { args: Vec<String>, reason: String },
62
63 ConfigIo { path: PathBuf, message: String },
68
69 ConfigParse {
71 path: PathBuf,
72 message: String,
73 line: Option<usize>,
74 },
75
76 ConfigValue {
78 path: PathBuf,
79 key: String,
80 value: String,
81 reason: String,
82 },
83
84 RomLoad { path: PathBuf, message: String },
89
90 RomNotFound { path: PathBuf },
92
93 RomInvalid { path: PathBuf, reason: String },
95
96 SavestateLoad { source: String, message: String },
101
102 SavestateSave {
104 destination: String,
105 message: String,
106 },
107
108 SavestateInvalid { source: String, reason: String },
110
111 InvalidAddress { address: String, reason: String },
116
117 InvalidMemoryRange {
119 range: String,
120 reason: String,
121 hint: Option<String>,
122 },
123
124 MemoryAccess {
126 operation: String,
127 address: u16,
128 message: String,
129 },
130
131 Execution {
136 message: String,
137 cycles: Option<u128>,
138 frames: Option<u64>,
139 },
140
141 InvalidStopCondition {
143 condition: String,
144 reason: String,
145 hint: Option<String>,
146 },
147
148 OutputWrite {
153 destination: String,
154 message: String,
155 },
156
157 InvalidOutputFormat {
159 format: String,
160 valid_formats: Vec<String>,
161 },
162
163 Io { operation: String, message: String },
168
169 Internal { message: String },
171}
172
173impl CliError {
174 pub fn invalid_arg(
180 arg: impl Into<String>,
181 value: impl Into<String>,
182 reason: impl Into<String>,
183 ) -> Self {
184 Self::InvalidArgument {
185 arg: arg.into(),
186 value: value.into(),
187 reason: reason.into(),
188 hint: None,
189 }
190 }
191
192 pub fn invalid_arg_with_hint(
194 arg: impl Into<String>,
195 value: impl Into<String>,
196 reason: impl Into<String>,
197 hint: impl Into<String>,
198 ) -> Self {
199 Self::InvalidArgument {
200 arg: arg.into(),
201 value: value.into(),
202 reason: reason.into(),
203 hint: Some(hint.into()),
204 }
205 }
206
207 pub fn missing_arg(arg: impl Into<String>, context: impl Into<String>) -> Self {
209 Self::MissingArgument {
210 arg: arg.into(),
211 context: context.into(),
212 }
213 }
214
215 pub fn conflicting_args(
217 arg1: impl Into<String>,
218 arg2: impl Into<String>,
219 reason: impl Into<String>,
220 ) -> Self {
221 Self::ConflictingArguments {
222 arg1: arg1.into(),
223 arg2: arg2.into(),
224 reason: reason.into(),
225 }
226 }
227
228 pub fn config_io(path: impl Into<PathBuf>, err: impl fmt::Display) -> Self {
234 Self::ConfigIo {
235 path: path.into(),
236 message: err.to_string(),
237 }
238 }
239
240 pub fn config_parse(path: impl Into<PathBuf>, err: impl fmt::Display) -> Self {
242 Self::ConfigParse {
243 path: path.into(),
244 message: err.to_string(),
245 line: None,
246 }
247 }
248
249 pub fn invalid_memory_range(range: impl Into<String>, reason: impl Into<String>) -> Self {
255 Self::InvalidMemoryRange {
256 range: range.into(),
257 reason: reason.into(),
258 hint: Some(
259 "Use START-END (e.g., 0x0000-0x07FF) or START:LENGTH (e.g., 0x6000:0x100)".into(),
260 ),
261 }
262 }
263
264 pub fn invalid_address(address: impl Into<String>, reason: impl Into<String>) -> Self {
266 Self::InvalidAddress {
267 address: address.into(),
268 reason: reason.into(),
269 }
270 }
271
272 pub fn savestate_load(source: impl Into<String>, err: impl fmt::Display) -> Self {
278 Self::SavestateLoad {
279 source: source.into(),
280 message: err.to_string(),
281 }
282 }
283
284 pub fn savestate_save(destination: impl Into<String>, err: impl fmt::Display) -> Self {
286 Self::SavestateSave {
287 destination: destination.into(),
288 message: err.to_string(),
289 }
290 }
291
292 pub fn execution(message: impl Into<String>) -> Self {
298 Self::Execution {
299 message: message.into(),
300 cycles: None,
301 frames: None,
302 }
303 }
304
305 pub fn invalid_stop_condition(condition: impl Into<String>, reason: impl Into<String>) -> Self {
307 Self::InvalidStopCondition {
308 condition: condition.into(),
309 reason: reason.into(),
310 hint: Some("Valid formats: ADDR==VALUE, ADDR!=VALUE (e.g., 0x6000==0x80)".into()),
311 }
312 }
313
314 pub fn output_write(destination: impl Into<String>, err: impl fmt::Display) -> Self {
320 Self::OutputWrite {
321 destination: destination.into(),
322 message: err.to_string(),
323 }
324 }
325
326 pub fn io(operation: impl Into<String>, err: impl fmt::Display) -> Self {
332 Self::Io {
333 operation: operation.into(),
334 message: err.to_string(),
335 }
336 }
337
338 pub fn internal(message: impl Into<String>) -> Self {
340 Self::Internal {
341 message: message.into(),
342 }
343 }
344
345 pub fn is_user_error(&self) -> bool {
351 matches!(
352 self,
353 Self::InvalidArgument { .. }
354 | Self::MissingArgument { .. }
355 | Self::ConflictingArguments { .. }
356 | Self::InvalidArgumentCombination { .. }
357 | Self::ConfigParse { .. }
358 | Self::ConfigValue { .. }
359 | Self::InvalidAddress { .. }
360 | Self::InvalidMemoryRange { .. }
361 | Self::InvalidStopCondition { .. }
362 | Self::InvalidOutputFormat { .. }
363 )
364 }
365
366 pub fn is_io_error(&self) -> bool {
368 matches!(
369 self,
370 Self::ConfigIo { .. }
371 | Self::RomLoad { .. }
372 | Self::RomNotFound { .. }
373 | Self::SavestateLoad { .. }
374 | Self::SavestateSave { .. }
375 | Self::OutputWrite { .. }
376 | Self::Io { .. }
377 )
378 }
379
380 pub fn exit_code(&self) -> u8 {
382 match self {
383 Self::InvalidArgument {
385 ..
386 }
387 | Self::MissingArgument {
388 ..
389 }
390 | Self::ConflictingArguments {
391 ..
392 }
393 | Self::InvalidArgumentCombination {
394 ..
395 } => 2,
396
397 Self::RomLoad {
399 ..
400 }
401 | Self::RomNotFound {
402 ..
403 }
404 | Self::RomInvalid {
405 ..
406 } => 3,
407
408 Self::SavestateLoad {
410 ..
411 }
412 | Self::SavestateSave {
413 ..
414 }
415 | Self::SavestateInvalid {
416 ..
417 } => 4,
418
419 Self::ConfigIo {
421 ..
422 }
423 | Self::OutputWrite {
424 ..
425 }
426 | Self::Io {
427 ..
428 } => 5,
429
430 _ => 1,
432 }
433 }
434}
435
436impl fmt::Display for CliError {
437 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438 match self {
439 Self::InvalidArgument {
441 arg,
442 value,
443 reason,
444 hint,
445 } => {
446 write!(
447 f,
448 "Invalid value '{}' for argument '{}': {}",
449 value, arg, reason
450 )?;
451 if let Some(h) = hint {
452 write!(f, "\nHint: {}", h)?;
453 }
454 Ok(())
455 }
456 Self::MissingArgument {
457 arg,
458 context,
459 } => {
460 write!(f, "Missing required argument '{}': {}", arg, context)
461 }
462 Self::ConflictingArguments {
463 arg1,
464 arg2,
465 reason,
466 } => {
467 write!(
468 f,
469 "Cannot use '{}' and '{}' together: {}",
470 arg1, arg2, reason
471 )
472 }
473 Self::InvalidArgumentCombination {
474 args,
475 reason,
476 } => {
477 write!(
478 f,
479 "Invalid argument combination [{}]: {}",
480 args.join(", "),
481 reason
482 )
483 }
484
485 Self::ConfigIo {
487 path,
488 message,
489 } => {
490 write!(
491 f,
492 "Failed to read config file '{}': {}",
493 path.display(),
494 message
495 )
496 }
497 Self::ConfigParse {
498 path,
499 message,
500 line,
501 } => {
502 write!(f, "Failed to parse config file '{}'", path.display())?;
503 if let Some(l) = line {
504 write!(f, " at line {}", l)?;
505 }
506 write!(f, ": {}", message)
507 }
508 Self::ConfigValue {
509 path,
510 key,
511 value,
512 reason,
513 } => {
514 write!(
515 f,
516 "Invalid value '{}' for key '{}' in config '{}': {}",
517 value,
518 key,
519 path.display(),
520 reason
521 )
522 }
523
524 Self::RomLoad {
526 path,
527 message,
528 } => {
529 write!(f, "Failed to load ROM '{}': {}", path.display(), message)
530 }
531 Self::RomNotFound {
532 path,
533 } => {
534 write!(f, "ROM file not found: {}", path.display())
535 }
536 Self::RomInvalid {
537 path,
538 reason,
539 } => {
540 write!(f, "Invalid ROM file '{}': {}", path.display(), reason)
541 }
542
543 Self::SavestateLoad {
545 source,
546 message,
547 } => {
548 write!(f, "Failed to load savestate from {}: {}", source, message)
549 }
550 Self::SavestateSave {
551 destination,
552 message,
553 } => {
554 write!(
555 f,
556 "Failed to save savestate to {}: {}",
557 destination, message
558 )
559 }
560 Self::SavestateInvalid {
561 source,
562 reason,
563 } => {
564 write!(f, "Invalid savestate from {}: {}", source, reason)
565 }
566
567 Self::InvalidAddress {
569 address,
570 reason,
571 } => {
572 write!(f, "Invalid address '{}': {}", address, reason)
573 }
574 Self::InvalidMemoryRange {
575 range,
576 reason,
577 hint,
578 } => {
579 write!(f, "Invalid memory range '{}': {}", range, reason)?;
580 if let Some(h) = hint {
581 write!(f, "\nHint: {}", h)?;
582 }
583 Ok(())
584 }
585 Self::MemoryAccess {
586 operation,
587 address,
588 message,
589 } => {
590 write!(
591 f,
592 "Memory {} at 0x{:04X} failed: {}",
593 operation, address, message
594 )
595 }
596
597 Self::Execution {
599 message,
600 cycles,
601 frames,
602 } => {
603 write!(f, "Execution error: {}", message)?;
604 if let Some(c) = cycles {
605 write!(f, " (after {} cycles", c)?;
606 if let Some(fr) = frames {
607 write!(f, ", {} frames", fr)?;
608 }
609 write!(f, ")")?;
610 }
611 Ok(())
612 }
613 Self::InvalidStopCondition {
614 condition,
615 reason,
616 hint,
617 } => {
618 write!(f, "Invalid stop condition '{}': {}", condition, reason)?;
619 if let Some(h) = hint {
620 write!(f, "\nHint: {}", h)?;
621 }
622 Ok(())
623 }
624
625 Self::OutputWrite {
627 destination,
628 message,
629 } => {
630 write!(f, "Failed to write output to {}: {}", destination, message)
631 }
632 Self::InvalidOutputFormat {
633 format,
634 valid_formats,
635 } => {
636 write!(
637 f,
638 "Invalid output format '{}'. Valid formats: {}",
639 format,
640 valid_formats.join(", ")
641 )
642 }
643
644 Self::Io {
646 operation,
647 message,
648 } => {
649 write!(f, "I/O error during {}: {}", operation, message)
650 }
651 Self::Internal {
652 message,
653 } => {
654 write!(f, "Internal error: {}", message)
655 }
656 }
657 }
658}
659
660impl std::error::Error for CliError {}
661
662impl From<std::io::Error> for CliError {
667 fn from(err: std::io::Error) -> Self {
668 Self::Io {
669 operation: "I/O operation".into(),
670 message: err.to_string(),
671 }
672 }
673}
674
675impl From<toml::de::Error> for CliError {
676 fn from(err: toml::de::Error) -> Self {
677 Self::ConfigParse {
678 path: PathBuf::new(),
679 message: err.to_string(),
680 line: None,
683 }
684 }
685}
686
687#[cfg(test)]
692mod tests {
693 use super::*;
694
695 #[test]
696 fn test_error_display() {
697 let err = CliError::invalid_arg("--frames", "-5", "must be positive");
698 assert!(err.to_string().contains("--frames"));
699 assert!(err.to_string().contains("-5"));
700 assert!(err.to_string().contains("positive"));
701 }
702
703 #[test]
704 fn test_error_with_hint() {
705 let err = CliError::invalid_arg_with_hint(
706 "--until-pc",
707 "invalid",
708 "not a valid hex address",
709 "Use format 0xADDR or ADDR",
710 );
711 let msg = err.to_string();
712 assert!(msg.contains("Hint:"));
713 }
714
715 #[test]
716 fn test_conflicting_args() {
717 let err = CliError::conflicting_args(
718 "--state-stdin",
719 "--load-state",
720 "can only use one input source",
721 );
722 assert!(err.to_string().contains("--state-stdin"));
723 assert!(err.to_string().contains("--load-state"));
724 }
725
726 #[test]
727 fn test_exit_codes() {
728 assert_eq!(CliError::invalid_arg("a", "b", "c").exit_code(), 2);
729 assert_eq!(
730 CliError::RomNotFound {
731 path: PathBuf::new()
732 }
733 .exit_code(),
734 3
735 );
736 assert_eq!(CliError::savestate_load("file", "err").exit_code(), 4);
737 assert_eq!(CliError::io("read", "err").exit_code(), 5);
738 }
739
740 #[test]
741 fn test_error_classification() {
742 let user_err = CliError::invalid_arg("a", "b", "c");
743 let io_err = CliError::io("read", "failed");
744
745 assert!(user_err.is_user_error());
746 assert!(!user_err.is_io_error());
747
748 assert!(!io_err.is_user_error());
749 assert!(io_err.is_io_error());
750 }
751
752 #[test]
753 fn test_memory_range_error() {
754 let err = CliError::invalid_memory_range("invalid", "bad format");
755 let msg = err.to_string();
756 assert!(msg.contains("invalid"));
757 assert!(msg.contains("Hint:"));
758 assert!(msg.contains("START-END"));
759 }
760}