Skip to main content

mtp_rs/
error.rs

1//! Error types for mtp-rs.
2
3use thiserror::Error;
4
5/// The main error type for mtp-rs operations.
6#[derive(Debug, Error)]
7pub enum Error {
8    /// USB communication error
9    #[error("USB error: {0}")]
10    Usb(#[from] nusb::Error),
11
12    /// Protocol-level error from device
13    #[error("Protocol error: {code:?} during {operation:?}")]
14    Protocol {
15        /// The response code returned by the device.
16        code: crate::ptp::ResponseCode,
17        /// The operation that triggered the error.
18        operation: crate::ptp::OperationCode,
19    },
20
21    /// Invalid data received from device
22    #[error("Invalid data: {message}")]
23    InvalidData {
24        /// Description of what was invalid.
25        message: String,
26    },
27
28    /// I/O error
29    #[error("I/O error: {0}")]
30    Io(std::io::Error),
31
32    /// Operation timed out
33    #[error("Operation timed out")]
34    Timeout,
35
36    /// Device was disconnected
37    #[error("Device disconnected")]
38    Disconnected,
39
40    /// Session not open
41    #[error("Session not open")]
42    SessionNotOpen,
43
44    /// No device found
45    #[error("No MTP device found")]
46    NoDevice,
47
48    /// Operation cancelled
49    #[error("Operation cancelled")]
50    Cancelled,
51}
52
53impl Error {
54    /// Create an invalid data error with a message.
55    #[must_use]
56    pub fn invalid_data(message: impl Into<String>) -> Self {
57        Error::InvalidData {
58            message: message.into(),
59        }
60    }
61
62    /// Check if this is a retryable error.
63    ///
64    /// Retryable errors are transient and the operation may succeed if retried:
65    /// - `DeviceBusy`: Device is temporarily busy
66    /// - `Timeout`: Operation timed out but device may still be responsive
67    #[must_use]
68    pub fn is_retryable(&self) -> bool {
69        matches!(
70            self,
71            Error::Protocol {
72                code: crate::ptp::ResponseCode::DeviceBusy,
73                ..
74            } | Error::Timeout
75        )
76    }
77
78    /// Get the response code if this is a protocol error.
79    #[must_use]
80    pub fn response_code(&self) -> Option<crate::ptp::ResponseCode> {
81        match self {
82            Error::Protocol { code, .. } => Some(*code),
83            _ => None,
84        }
85    }
86
87    /// Check if this error indicates another process has exclusive access to the device.
88    ///
89    /// This typically happens on macOS when `ptpcamerad` or another application
90    /// has already claimed the USB interface. Applications can use this to provide
91    /// platform-specific guidance to users.
92    ///
93    /// # Example
94    ///
95    /// ```ignore
96    /// match device.open().await {
97    ///     Err(e) if e.is_exclusive_access() => {
98    ///         // On macOS, likely ptpcamerad interference
99    ///         // App can query IORegistry for UsbExclusiveOwner to get details
100    ///         show_exclusive_access_help();
101    ///     }
102    ///     Err(e) => handle_other_error(e),
103    ///     Ok(dev) => use_device(dev),
104    /// }
105    /// ```
106    #[must_use]
107    pub fn is_exclusive_access(&self) -> bool {
108        match self {
109            Error::Usb(io_err) => {
110                let msg = io_err.to_string().to_lowercase();
111                // macOS: "could not be opened for exclusive access"
112                // Linux: typically EBUSY, but message varies
113                // Windows: "access denied" or similar
114                msg.contains("exclusive access")
115                    || msg.contains("device or resource busy")
116                    || (msg.contains("access") && msg.contains("denied"))
117            }
118            Error::Io(io_err) => {
119                let msg = io_err.to_string().to_lowercase();
120                msg.contains("exclusive access")
121                    || msg.contains("device or resource busy")
122                    || (msg.contains("access") && msg.contains("denied"))
123            }
124            _ => false,
125        }
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use std::io::{Error as IoError, ErrorKind};
133
134    #[test]
135    fn test_is_exclusive_access_macos_message() {
136        // macOS nusb error message
137        let io_err = IoError::other("could not be opened for exclusive access");
138        let err = Error::Usb(io_err);
139        assert!(err.is_exclusive_access());
140    }
141
142    #[test]
143    fn test_is_exclusive_access_linux_busy() {
144        // Linux EBUSY style message
145        let io_err = IoError::other("Device or resource busy");
146        let err = Error::Usb(io_err);
147        assert!(err.is_exclusive_access());
148    }
149
150    #[test]
151    fn test_is_exclusive_access_windows_denied() {
152        // Windows access denied style message
153        let io_err = IoError::new(ErrorKind::PermissionDenied, "Access is denied");
154        let err = Error::Usb(io_err);
155        assert!(err.is_exclusive_access());
156    }
157
158    #[test]
159    fn test_is_exclusive_access_io_error() {
160        // Also works for Io variant
161        let io_err = IoError::other("could not be opened for exclusive access");
162        let err = Error::Io(io_err);
163        assert!(err.is_exclusive_access());
164    }
165
166    #[test]
167    fn test_is_exclusive_access_false_for_other_errors() {
168        assert!(!Error::Timeout.is_exclusive_access());
169        assert!(!Error::Disconnected.is_exclusive_access());
170        assert!(!Error::NoDevice.is_exclusive_access());
171        assert!(!Error::invalid_data("some error").is_exclusive_access());
172
173        let io_err = IoError::new(ErrorKind::NotFound, "device not found");
174        assert!(!Error::Usb(io_err).is_exclusive_access());
175    }
176}