Skip to main content

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}