cd_da_reader/errors.rs
1use std::fmt;
2
3use crate::SectorReadFormat;
4
5/// SCSI command groups issued by this library.
6#[derive(Debug, Clone, Copy)]
7pub enum ScsiOp {
8 /// `READ TOC/PMA/ATIP` command (opcode `0x43`) for TOC/session metadata.
9 ReadToc,
10 /// `READ CD` command (opcode `0xBE`) for audio or data sectors.
11 ReadCd,
12 /// `READ TRACK INFORMATION` command (opcode `0x52`) for track metadata.
13 ReadTrackInformation,
14}
15
16/// Structured SCSI failure context captured at the call site.
17///
18/// This keeps transport/protocol details (status + sense) separate from plain I/O failures,
19/// which allows retry logic and application diagnostics to branch on SCSI metadata.
20#[derive(Debug, Clone)]
21pub struct ScsiError {
22 /// Operation that failed.
23 pub op: ScsiOp,
24 /// Starting logical block address used by the failed command, when applicable.
25 pub lba: Option<u32>,
26 /// Sector count requested by the failed command, when applicable.
27 pub sectors: Option<u32>,
28 /// SCSI status byte reported by the device (for example `0x02` for CHECK CONDITION).
29 pub scsi_status: u8,
30 /// Sense key nibble from fixed-format sense data (if sense data was returned).
31 pub sense_key: Option<u8>,
32 /// Additional Sense Code from sense data (if available).
33 pub asc: Option<u8>,
34 /// Additional Sense Code Qualifier paired with `asc` (if available).
35 pub ascq: Option<u8>,
36}
37
38/// Top-level error type returned by `cd-da-reader`.
39#[derive(Debug)]
40pub enum CdReaderError {
41 /// OS/transport I/O error (open/ioctl/DeviceIoControl/FFI command failure, etc.).
42 Io(std::io::Error),
43 /// Device reported a SCSI command failure with status/sense context.
44 Scsi(ScsiError),
45 /// Parsing failure for command response payloads.
46 Parse(String),
47 /// The requested sector format is incompatible with the track type.
48 TrackFormatMismatch {
49 /// Track number from the TOC.
50 track_number: u8,
51 /// Whether the TOC identifies the track as audio.
52 track_is_audio: bool,
53 /// Format requested by the caller.
54 requested_format: SectorReadFormat,
55 },
56 /// The track's sector format could not be determined.
57 CannotDetectTrackFormat {
58 /// Track number from the TOC.
59 track_number: u8,
60 /// MMC Data Mode value that was reported, when available.
61 data_mode: Option<u8>,
62 },
63 /// Drive enumeration completed without finding a usable audio CD.
64 NoUsableDrive,
65 /// A pluggable [`AudioSectorReader`](crate::AudioSectorReader) backing failed
66 /// while reading sectors. The backing's own error is boxed and preserved as
67 /// this error's [`source`](std::error::Error::source), so it can be displayed
68 /// or downcast without being flattened into this SCSI-oriented enum.
69 Backend(Box<dyn std::error::Error + Send + Sync + 'static>),
70}
71
72impl fmt::Display for CdReaderError {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 match self {
75 Self::Io(err) => write!(f, "io error: {err}"),
76 Self::Scsi(err) => write!(
77 f,
78 "SCSI {:?} failed (status=0x{:02x}, lba={:?}, sectors={:?}, sense_key={:?}, asc={:?}, ascq={:?})",
79 err.op, err.scsi_status, err.lba, err.sectors, err.sense_key, err.asc, err.ascq
80 ),
81 Self::Parse(msg) => write!(f, "parse error: {msg}"),
82 Self::TrackFormatMismatch {
83 track_number,
84 track_is_audio,
85 requested_format,
86 } => {
87 let track_type = if *track_is_audio { "audio" } else { "data" };
88 write!(
89 f,
90 "cannot read {track_type} track {track_number} using {requested_format:?}"
91 )
92 }
93 Self::CannotDetectTrackFormat {
94 track_number,
95 data_mode: Some(data_mode),
96 } => write!(
97 f,
98 "could not detect sector format for track {track_number} from MMC data mode 0x{data_mode:02x}"
99 ),
100 Self::CannotDetectTrackFormat {
101 track_number,
102 data_mode: None,
103 } => write!(f, "could not detect sector format for track {track_number}"),
104 Self::NoUsableDrive => write!(f, "no usable audio CD drive found"),
105 Self::Backend(err) => write!(f, "backend reader error: {err}"),
106 }
107 }
108}
109
110impl std::error::Error for CdReaderError {
111 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
112 match self {
113 Self::Io(error) => Some(error),
114 Self::Backend(error) => Some(&**error),
115 Self::Scsi(_)
116 | Self::Parse(_)
117 | Self::TrackFormatMismatch { .. }
118 | Self::CannotDetectTrackFormat { .. }
119 | Self::NoUsableDrive => None,
120 }
121 }
122}
123
124impl From<std::io::Error> for CdReaderError {
125 fn from(value: std::io::Error) -> Self {
126 Self::Io(value)
127 }
128}