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