Skip to main content

mtp_rs/
error.rs

1//! Error types for mtp-rs.
2
3use crate::ptp::ObjectHandle;
4use thiserror::Error;
5
6/// The main error type for mtp-rs operations.
7#[derive(Debug, Error)]
8pub enum Error {
9    /// USB communication error
10    #[error("USB error: {0}")]
11    Usb(#[from] nusb::Error),
12
13    /// Protocol-level error from device
14    #[error("Protocol error: {code:?} during {operation:?}")]
15    Protocol {
16        /// The response code returned by the device.
17        code: crate::ptp::ResponseCode,
18        /// The operation that triggered the error.
19        operation: crate::ptp::OperationCode,
20    },
21
22    /// Invalid data received from device
23    #[error("Invalid data: {message}")]
24    InvalidData {
25        /// Description of what was invalid.
26        message: String,
27    },
28
29    /// I/O error
30    #[error("I/O error: {0}")]
31    Io(std::io::Error),
32
33    /// Operation timed out
34    #[error("Operation timed out")]
35    Timeout,
36
37    /// Device was disconnected
38    #[error("Device disconnected")]
39    Disconnected,
40
41    /// Session not open
42    #[error("Session not open")]
43    SessionNotOpen,
44
45    /// No device found
46    #[error("No MTP device found")]
47    NoDevice,
48
49    /// Operation cancelled
50    #[error("Operation cancelled")]
51    Cancelled,
52}
53
54/// Error from an upload, carrying the handle of the object the device created
55/// during `SendObjectInfo` before the data phase failed.
56///
57/// PTP uploads are two-phase: `SendObjectInfo` creates the object on the device
58/// (returning a handle), then `SendObject` streams the bytes. If the data phase
59/// fails or is cancelled, the device is left holding a partial (often empty or
60/// truncated) object. This error surfaces that handle so the caller owns the
61/// cleanup-or-resume decision, rather than the library guessing.
62///
63/// The library does **not** auto-delete the partial object: deleting it would
64/// issue hidden USB I/O to a possibly-disconnected device, the leave-vs-delete
65/// behavior is device-dependent, and PTP's two-phase model is designed so a
66/// failed `SendObject` can be retried against the same handle (resume).
67///
68/// [`From<UploadError> for Error`] keeps `?` ergonomic for callers working in an
69/// [`enum@Error`] context; they drop the [`partial`](Self::partial) handle unless
70/// they match on `UploadError` explicitly.
71#[derive(Debug, Error)]
72#[error("{source}")]
73pub struct UploadError {
74    /// The underlying failure (I/O, protocol, cancellation, timeout, …).
75    #[source]
76    pub source: Error,
77    /// The handle of the partially-written object the device may still hold.
78    ///
79    /// `Some` iff `SendObjectInfo` succeeded but the data phase did not complete
80    /// (genuine error OR cancellation). The object may be empty or truncated. The
81    /// caller decides: delete it (for example, [`Storage::delete`]) to discard the
82    /// corrupt artifact, or retry the data phase to resume.
83    ///
84    /// `None` iff no object was created (for example, `SendObjectInfo` itself
85    /// failed because the storage is read-only or the parent is invalid).
86    ///
87    /// [`Storage::delete`]: crate::mtp::Storage::delete
88    pub partial: Option<ObjectHandle>,
89}
90
91impl From<UploadError> for Error {
92    fn from(e: UploadError) -> Self {
93        e.source
94    }
95}
96
97impl Error {
98    /// Create an invalid data error with a message.
99    #[must_use]
100    pub fn invalid_data(message: impl Into<String>) -> Self {
101        Error::InvalidData {
102            message: message.into(),
103        }
104    }
105
106    /// Check if this is a retryable error.
107    ///
108    /// Retryable errors are transient and the operation may succeed if retried:
109    /// - `DeviceBusy`: Device is temporarily busy
110    /// - `Timeout`: Operation timed out but device may still be responsive
111    #[must_use]
112    pub fn is_retryable(&self) -> bool {
113        matches!(
114            self,
115            Error::Protocol {
116                code: crate::ptp::ResponseCode::DeviceBusy,
117                ..
118            } | Error::Timeout
119        )
120    }
121
122    /// Get the response code if this is a protocol error.
123    #[must_use]
124    pub fn response_code(&self) -> Option<crate::ptp::ResponseCode> {
125        match self {
126            Error::Protocol { code, .. } => Some(*code),
127            _ => None,
128        }
129    }
130
131    /// Check if this error indicates another process has exclusive access to the device.
132    ///
133    /// This typically happens on macOS when `ptpcamerad` or another application
134    /// has already claimed the USB interface. Applications can use this to provide
135    /// platform-specific guidance to users.
136    ///
137    /// # Example
138    ///
139    /// ```ignore
140    /// match device.open().await {
141    ///     Err(e) if e.is_exclusive_access() => {
142    ///         // On macOS, likely ptpcamerad interference
143    ///         // App can query IORegistry for UsbExclusiveOwner to get details
144    ///         show_exclusive_access_help();
145    ///     }
146    ///     Err(e) => handle_other_error(e),
147    ///     Ok(dev) => use_device(dev),
148    /// }
149    /// ```
150    #[must_use]
151    pub fn is_exclusive_access(&self) -> bool {
152        match self {
153            Error::Usb(io_err) => {
154                let msg = io_err.to_string().to_lowercase();
155                // macOS: "could not be opened for exclusive access"
156                // Linux: typically EBUSY, but message varies
157                // Windows: "access denied" or similar
158                msg.contains("exclusive access")
159                    || msg.contains("device or resource busy")
160                    || (msg.contains("access") && msg.contains("denied"))
161            }
162            Error::Io(io_err) => {
163                let msg = io_err.to_string().to_lowercase();
164                msg.contains("exclusive access")
165                    || msg.contains("device or resource busy")
166                    || (msg.contains("access") && msg.contains("denied"))
167            }
168            _ => false,
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use std::io::{Error as IoError, ErrorKind};
177
178    #[test]
179    fn test_is_exclusive_access_macos_message() {
180        // macOS nusb error message (tested via Io variant; same logic as Usb variant)
181        let io_err = IoError::other("could not be opened for exclusive access");
182        let err = Error::Io(io_err);
183        assert!(err.is_exclusive_access());
184    }
185
186    #[test]
187    fn test_is_exclusive_access_linux_busy() {
188        // Linux EBUSY style message (tested via Io variant; same logic as Usb variant)
189        let io_err = IoError::other("Device or resource busy");
190        let err = Error::Io(io_err);
191        assert!(err.is_exclusive_access());
192    }
193
194    #[test]
195    fn test_is_exclusive_access_windows_denied() {
196        // Windows access denied style message (tested via Io variant; same logic as Usb variant)
197        let io_err = IoError::new(ErrorKind::PermissionDenied, "Access is denied");
198        let err = Error::Io(io_err);
199        assert!(err.is_exclusive_access());
200    }
201
202    #[test]
203    fn test_is_exclusive_access_io_error() {
204        // Also works for Io variant
205        let io_err = IoError::other("could not be opened for exclusive access");
206        let err = Error::Io(io_err);
207        assert!(err.is_exclusive_access());
208    }
209
210    #[test]
211    fn test_is_exclusive_access_false_for_other_errors() {
212        assert!(!Error::Timeout.is_exclusive_access());
213        assert!(!Error::Disconnected.is_exclusive_access());
214        assert!(!Error::NoDevice.is_exclusive_access());
215        assert!(!Error::invalid_data("some error").is_exclusive_access());
216
217        let io_err = IoError::new(ErrorKind::NotFound, "device not found");
218        assert!(!Error::Io(io_err).is_exclusive_access());
219    }
220}