Skip to main content

nord_usb/
error.rs

1use thiserror::Error as ThisError;
2
3use crate::wire::{Location, ObjectClass};
4
5pub type Result<T> = std::result::Result<T, Error>;
6
7#[derive(ThisError, Debug)]
8#[non_exhaustive]
9pub enum Error {
10    #[error("message truncated: got {got} bytes, need at least {need}")]
11    Truncated { got: usize, need: usize },
12
13    #[error("length field says {declared} bytes but the message is {actual}")]
14    LengthMismatch { declared: usize, actual: usize },
15
16    #[error("crc mismatch: message carries {expected:#06x}, computed {actual:#06x}")]
17    BadCrc { expected: u16, actual: u16 },
18
19    #[error("device reported status {0:#x}")]
20    DeviceStatus(u32),
21
22    /// The device refused `SESSION_OPEN` for this class: it does not serve the class.
23    /// A refusal of any other command stays a [`Error::DeviceStatus`].
24    #[error("the device refused a session for {class:?} with status {status:#x}")]
25    ClassRefused { class: ObjectClass, status: u32 },
26
27    #[error("expected a response to command {expected:#x}, got {got:#x}")]
28    UnexpectedResponse { expected: u32, got: u32 },
29
30    #[error("device reported location {reported:?} for the requested location {requested:?}")]
31    UnexpectedLocation {
32        requested: Location,
33        reported: Location,
34    },
35
36    #[error("device reported partition {reported} for the requested partition {requested}")]
37    UnexpectedPartition { requested: u32, reported: u32 },
38
39    /// An inventory walk contradicted the geometry declared by the instrument.
40    #[error(
41        "walking bank {bank}, which declares {slots} slots, became inconsistent at \
42         {answered:?}"
43    )]
44    Enumeration {
45        bank: u32,
46        answered: Location,
47        slots: u32,
48    },
49
50    #[error("bank {bank} cannot be scanned completely within {limit} slots")]
51    ScanLimit { bank: u32, limit: u32 },
52
53    /// The byte pipe itself failed — a USB transfer error, a missing device, a claim
54    /// refusal. Nothing about message *content* belongs here.
55    #[error("transport: {0}")]
56    Transport(String),
57
58    /// The `CBIN` header around an entity body is wrong: bad magic, a checksum that
59    /// does not match the body, a malformed format tag.
60    #[error("envelope: {0}")]
61    Envelope(String),
62
63    /// A replay script that could not be parsed or was contradicted by the code under
64    /// test. Only produced by the `replay` feature's transport.
65    #[error("replay: {0}")]
66    Replay(String),
67
68    #[error("invalid argument: {0}")]
69    InvalidArgument(String),
70
71    #[error(transparent)]
72    Io(#[from] std::io::Error),
73}
74
75impl Error {
76    /// This failure's kind in a replay script's `expect: err <kind>` header.
77    ///
78    /// The vocabulary is short on purpose — it exists to tell one *expected* refusal
79    /// from another — so a failure it does not name is reported as the nearest kind
80    /// rather than left out, where the script would claim the operation succeeded. A
81    /// script that names the wrong kind fails the sweep, which is the report; a script
82    /// that names none passes silently, which is not.
83    ///
84    /// The match is exhaustive so that a new variant is given a kind rather than
85    /// defaulting into one, and [`ErrKind::matches`] is its inverse.
86    pub fn expect_kind(&self) -> ErrKind {
87        match self {
88            Error::DeviceStatus(code) => ErrKind::DeviceStatus(*code),
89            Error::ClassRefused { status, .. } => ErrKind::ClassRefused(*status),
90            Error::UnexpectedResponse { .. } => ErrKind::UnexpectedResponse,
91            Error::UnexpectedLocation { .. } => ErrKind::UnexpectedLocation,
92            Error::UnexpectedPartition { .. } => ErrKind::UnexpectedPartition,
93            Error::Enumeration { .. } | Error::ScanLimit { .. } => ErrKind::Enumeration,
94            Error::Replay(_) => ErrKind::Replay,
95            Error::Truncated { .. }
96            | Error::LengthMismatch { .. }
97            | Error::BadCrc { .. }
98            | Error::Transport(_)
99            | Error::Envelope(_)
100            | Error::InvalidArgument(_)
101            | Error::Io(_) => ErrKind::Transport,
102        }
103    }
104}
105
106/// The failures a replay script may name, spelled in kebab-case after the [`Error`]
107/// variant.
108///
109/// Deliberately a short list: it exists to tell one *expected* refusal from another, not
110/// to mirror the error type. A device refusal carries its status code, because the code
111/// is the finding — `0x15` (the library classes refusing a rename) and `0x1` (nothing
112/// loaded) are different results, not two spellings of one.
113///
114/// [`Error::expect_kind`] is the one table: what a recorder writes, what a script
115/// parses, and what the sweep judges are the same value.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum ErrKind {
118    DeviceStatus(u32),
119    ClassRefused(u32),
120    UnexpectedResponse,
121    UnexpectedLocation,
122    UnexpectedPartition,
123    Enumeration,
124    Transport,
125    Replay,
126}
127
128impl ErrKind {
129    /// Whether an error is the one this names.
130    pub fn matches(&self, e: &Error) -> bool {
131        *self == e.expect_kind()
132    }
133
134    /// The inverse of [`Display`](std::fmt::Display): read a kind as a script spells it.
135    #[cfg(feature = "replay")]
136    pub(crate) fn parse(value: &str) -> std::result::Result<Self, String> {
137        let (kind, arg) = match value.split_once(char::is_whitespace) {
138            Some((kind, arg)) => (kind, arg.trim()),
139            None => (value, ""),
140        };
141        match (kind, arg) {
142            ("device-status", "") => Err("device-status needs its code, e.g. \
143                                          'err device-status 0x15'"
144                .into()),
145            ("device-status", code) => parse_u32(code)
146                .map(ErrKind::DeviceStatus)
147                .ok_or_else(|| format!("bad device status {code:?}")),
148            ("class-refused", "") => Err("class-refused needs its code, e.g. \
149                                         'err class-refused 0x5'"
150                .into()),
151            ("class-refused", code) => parse_u32(code)
152                .map(ErrKind::ClassRefused)
153                .ok_or_else(|| format!("bad class refusal status {code:?}")),
154            ("unexpected-response", "") => Ok(ErrKind::UnexpectedResponse),
155            ("unexpected-location", "") => Ok(ErrKind::UnexpectedLocation),
156            ("unexpected-partition", "") => Ok(ErrKind::UnexpectedPartition),
157            ("enumeration", "") => Ok(ErrKind::Enumeration),
158            ("transport", "") => Ok(ErrKind::Transport),
159            ("replay", "") => Ok(ErrKind::Replay),
160            (kind, _) => Err(format!(
161                "unknown failure {kind:?}; the vocabulary is device-status <code>, \
162                class-refused <code>, unexpected-response, unexpected-location, \
163                unexpected-partition, enumeration, \
164                transport, replay"
165            )),
166        }
167    }
168}
169
170impl std::fmt::Display for ErrKind {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        match self {
173            ErrKind::DeviceStatus(code) => write!(f, "device-status {code:#x}"),
174            ErrKind::ClassRefused(code) => write!(f, "class-refused {code:#x}"),
175            ErrKind::UnexpectedResponse => f.write_str("unexpected-response"),
176            ErrKind::UnexpectedLocation => f.write_str("unexpected-location"),
177            ErrKind::UnexpectedPartition => f.write_str("unexpected-partition"),
178            ErrKind::Enumeration => f.write_str("enumeration"),
179            ErrKind::Transport => f.write_str("transport"),
180            ErrKind::Replay => f.write_str("replay"),
181        }
182    }
183}
184
185/// `0x`-prefixed hex or decimal — status codes are quoted both ways.
186#[cfg(feature = "replay")]
187fn parse_u32(s: &str) -> Option<u32> {
188    match s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
189        Some(hex) => u32::from_str_radix(hex, 16).ok(),
190        None => s.parse().ok(),
191    }
192}