use super::{BulkStream, Transport};
use async_trait::async_trait;
use futures::lock::Mutex;
use futures_timer::Delay;
use nusb::descriptors::{InterfaceDescriptor, TransferType};
use nusb::transfer::{
Buffer, Bulk, ControlOut, ControlType, Direction, In, Interrupt, Out, Recipient, TransferError,
};
use nusb::MaybeFuture;
use std::time::Duration;
const MTP_CLASS_IMAGE: u8 = 0x06;
const MTP_CLASS_VENDOR: u8 = 0xFF;
const MTP_SUBCLASS: u8 = 0x01;
const MTP_PROTOCOL: u8 = 0x01;
const SIC_CANCEL_REQUEST: u8 = 0x64;
const SIC_CANCEL_EVENT_CODE: u16 = 0x4001;
const SIC_DEVICE_RESET_REQUEST: u8 = 0x66;
const SIC_GET_DEVICE_STATUS_REQUEST: u8 = 0x67;
const SIC_STATUS_DEVICE_BUSY: u16 = 0x2019;
fn parse_device_status(data: &[u8]) -> Option<(u16, Vec<u8>)> {
if data.len() < 4 {
return None;
}
let wlength = u16::from_le_bytes([data[0], data[1]]) as usize;
let code = u16::from_le_bytes([data[2], data[3]]);
let end = data.len().min(wlength);
let mut endpoints = Vec::new();
let mut offset = 4;
while offset + 4 <= end {
let param = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]);
endpoints.push(param as u8);
offset += 4;
}
Some((code, endpoints))
}
fn clear_halt_if_stalled<T, D>(ep: &mut nusb::Endpoint<T, D>, err: TransferError) -> crate::PtpError
where
T: nusb::transfer::BulkOrInterrupt,
D: nusb::transfer::EndpointDirection,
{
if matches!(err, TransferError::Stall) {
let _ = ep.clear_halt().wait();
}
NusbTransport::convert_transfer_error(err)
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum UsbSpeed {
Low,
Full,
High,
Super,
SuperPlus,
}
impl UsbSpeed {
fn from_nusb(s: nusb::Speed) -> Option<Self> {
match s {
nusb::Speed::Low => Some(UsbSpeed::Low),
nusb::Speed::Full => Some(UsbSpeed::Full),
nusb::Speed::High => Some(UsbSpeed::High),
nusb::Speed::Super => Some(UsbSpeed::Super),
nusb::Speed::SuperPlus => Some(UsbSpeed::SuperPlus),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub enum MtpMatchReason {
StandardClass,
InterfaceString,
KnownVidPid,
OpenedDescriptorScan,
}
impl MtpMatchReason {
pub fn as_str(self) -> &'static str {
match self {
Self::StandardClass => "standard_class",
Self::InterfaceString => "interface_string",
Self::KnownVidPid => "known_vid_pid",
Self::OpenedDescriptorScan => "opened_descriptor_scan",
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct UsbDeviceInfo {
pub vendor_id: u16,
pub product_id: u16,
pub manufacturer: Option<String>,
pub product: Option<String>,
pub serial_number: Option<String>,
pub location_id: u64,
pub speed: Option<UsbSpeed>,
pub match_reason: MtpMatchReason,
nusb_info: nusb::DeviceInfo,
}
impl UsbDeviceInfo {
pub fn open(&self) -> Result<nusb::Device, nusb::Error> {
self.nusb_info.open().wait()
}
}
pub struct NusbTransport {
interface: nusb::Interface,
interface_number: u8,
bulk_in: Mutex<nusb::Endpoint<Bulk, In>>,
bulk_out: Mutex<nusb::Endpoint<Bulk, Out>>,
interrupt_in: Mutex<nusb::Endpoint<Interrupt, In>>,
timeout: Duration,
}
impl NusbTransport {
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
const INTERRUPT_BUFFER_SIZE: usize = 64;
pub fn list_mtp_devices() -> Result<Vec<UsbDeviceInfo>, crate::PtpError> {
Self::list_mtp_devices_with_known(&[])
}
pub fn list_mtp_devices_with_known(
known: &[(u16, u16)],
) -> Result<Vec<UsbDeviceInfo>, crate::PtpError> {
let devices = nusb::list_devices()
.wait()
.map_err(crate::PtpError::Usb)?
.filter_map(|dev| {
let match_reason = Self::mtp_match_reason(&dev, known)?;
let location_id = location_id_from_topology(&dev);
let speed = dev.speed().and_then(UsbSpeed::from_nusb);
Some(UsbDeviceInfo {
vendor_id: dev.vendor_id(),
product_id: dev.product_id(),
manufacturer: dev.manufacturer_string().map(String::from),
product: dev.product_string().map(String::from),
serial_number: dev.serial_number().map(String::from),
location_id,
speed,
match_reason,
nusb_info: dev,
})
})
.collect();
Ok(devices)
}
fn mtp_match_reason(dev: &nusb::DeviceInfo, known: &[(u16, u16)]) -> Option<MtpMatchReason> {
if known
.iter()
.any(|&(v, p)| v == dev.vendor_id() && p == dev.product_id())
{
return Some(MtpMatchReason::KnownVidPid);
}
if Self::is_mtp_class(dev.class(), dev.subclass(), dev.protocol()) {
return Some(MtpMatchReason::StandardClass);
}
if dev.class() != 0 && dev.class() != MTP_CLASS_VENDOR {
return None;
}
for intf in dev.interfaces() {
if let Some(reason) = Self::mtp_summary_match_reason(
intf.class(),
intf.subclass(),
intf.protocol(),
intf.interface_string(),
) {
return Some(reason);
}
}
if let Ok(device) = dev.open().wait() {
if let Ok(config) = device.active_configuration() {
for interface in config.interfaces() {
if let Some(alt) = interface.alt_settings().next() {
if Self::is_mtp_interface(&alt) {
return Some(MtpMatchReason::OpenedDescriptorScan);
}
}
}
}
}
None
}
fn mtp_summary_match_reason(
class: u8,
subclass: u8,
protocol: u8,
interface_string: Option<&str>,
) -> Option<MtpMatchReason> {
if Self::is_mtp_class(class, subclass, protocol) {
return Some(MtpMatchReason::StandardClass);
}
if interface_string.is_some_and(|s| s.trim().eq_ignore_ascii_case("MTP")) {
return Some(MtpMatchReason::InterfaceString);
}
None
}
fn is_mtp_class(class: u8, subclass: u8, protocol: u8) -> bool {
(class == MTP_CLASS_IMAGE || class == MTP_CLASS_VENDOR)
&& subclass == MTP_SUBCLASS
&& protocol == MTP_PROTOCOL
}
fn is_mtp_interface(alt: &InterfaceDescriptor) -> bool {
if Self::is_mtp_class(alt.class(), alt.subclass(), alt.protocol()) {
return true;
}
alt.class() == MTP_CLASS_VENDOR && Self::has_mtp_endpoint_layout(alt)
}
fn has_mtp_endpoint_layout(alt: &InterfaceDescriptor) -> bool {
let mut bulk_in = false;
let mut bulk_out = false;
let mut interrupt_in = false;
for ep in alt.endpoints() {
match (ep.direction(), ep.transfer_type()) {
(Direction::In, TransferType::Bulk) => bulk_in = true,
(Direction::Out, TransferType::Bulk) => bulk_out = true,
(Direction::In, TransferType::Interrupt) => interrupt_in = true,
_ => {}
}
}
bulk_in && bulk_out && interrupt_in
}
#[cfg(target_os = "macos")]
fn is_interface_unpublished(e: &nusb::Error) -> bool {
matches!(e.kind(), nusb::ErrorKind::NotFound)
}
pub async fn open(device: nusb::Device) -> Result<Self, crate::PtpError> {
Self::open_with_timeout(device, Self::DEFAULT_TIMEOUT).await
}
pub async fn open_with_timeout(
device: nusb::Device,
timeout: Duration,
) -> Result<Self, crate::PtpError> {
let config = device.active_configuration().map_err(|e| {
crate::PtpError::invalid_data(format!("Failed to get configuration: {}", e))
})?;
let mut mtp_interface_number = None;
let mut bulk_in_addr = None;
let mut bulk_out_addr = None;
let mut interrupt_in_addr = None;
let pick = |strict: bool| {
for interface in config.interfaces() {
let Some(alt_setting) = interface.alt_settings().next() else {
continue;
};
let matches = if strict {
Self::is_mtp_interface(&alt_setting)
} else {
Self::has_mtp_endpoint_layout(&alt_setting)
};
if matches {
let mut bin = None;
let mut bout = None;
let mut iin = None;
for endpoint in alt_setting.endpoints() {
match (endpoint.direction(), endpoint.transfer_type()) {
(Direction::Out, TransferType::Bulk) => bout = Some(endpoint.address()),
(Direction::In, TransferType::Bulk) => bin = Some(endpoint.address()),
(Direction::In, TransferType::Interrupt) => {
iin = Some(endpoint.address())
}
_ => {}
}
}
return Some((interface.interface_number(), bin, bout, iin));
}
}
None
};
if let Some((n, bin, bout, iin)) = pick(true).or_else(|| pick(false)) {
mtp_interface_number = Some(n);
bulk_in_addr = bin;
bulk_out_addr = bout;
interrupt_in_addr = iin;
}
let interface_number = mtp_interface_number
.ok_or_else(|| crate::PtpError::invalid_data("No MTP interface found on device"))?;
let bulk_in_addr = bulk_in_addr
.ok_or_else(|| crate::PtpError::invalid_data("No bulk IN endpoint found"))?;
let bulk_out_addr = bulk_out_addr
.ok_or_else(|| crate::PtpError::invalid_data("No bulk OUT endpoint found"))?;
let interrupt_in_addr = interrupt_in_addr
.ok_or_else(|| crate::PtpError::invalid_data("No interrupt IN endpoint found"))?;
let interface = match device.claim_interface(interface_number).wait() {
Ok(iface) => iface,
#[cfg(target_os = "macos")]
Err(e) if Self::is_interface_unpublished(&e) => {
device
.set_configuration(1)
.wait()
.map_err(crate::PtpError::Usb)?;
device
.claim_interface(interface_number)
.wait()
.map_err(crate::PtpError::Usb)?
}
Err(e) => return Err(crate::PtpError::Usb(e)),
};
let bulk_in = interface
.endpoint::<Bulk, In>(bulk_in_addr)
.map_err(crate::PtpError::Usb)?;
let bulk_out = interface
.endpoint::<Bulk, Out>(bulk_out_addr)
.map_err(crate::PtpError::Usb)?;
let interrupt_in = interface
.endpoint::<Interrupt, In>(interrupt_in_addr)
.map_err(crate::PtpError::Usb)?;
Ok(Self {
interface,
interface_number,
bulk_in: Mutex::new(bulk_in),
bulk_out: Mutex::new(bulk_out),
interrupt_in: Mutex::new(interrupt_in),
timeout,
})
}
#[must_use]
pub fn timeout(&self) -> Duration {
self.timeout
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.timeout = timeout;
}
fn convert_transfer_error(err: TransferError) -> crate::PtpError {
match err {
TransferError::Cancelled => crate::PtpError::Timeout,
TransferError::Disconnected => crate::PtpError::Disconnected,
TransferError::Stall
| TransferError::Fault
| TransferError::InvalidArgument
| TransferError::Unknown(_) => {
crate::PtpError::Io(std::io::Error::other(err.to_string()))
}
}
}
async fn settle_after_cancel(&self) {
const POLL_INTERVAL: Duration = Duration::from_millis(50);
const MAX_POLLS: u32 = 100;
for _ in 0..MAX_POLLS {
let result = self
.interface
.control_in(
nusb::transfer::ControlIn {
control_type: ControlType::Class,
recipient: Recipient::Interface,
request: SIC_GET_DEVICE_STATUS_REQUEST,
value: 0,
index: self.interface_number as u16,
length: 64,
},
Duration::from_millis(300),
)
.await;
let Ok(data) = result else {
return; };
let Some((code, stalled_endpoints)) = parse_device_status(&data) else {
return; };
for address in stalled_endpoints {
self.clear_bulk_halt_by_address(address).await;
}
if code != SIC_STATUS_DEVICE_BUSY {
return;
}
Delay::new(POLL_INTERVAL).await;
}
}
async fn clear_bulk_halt_by_address(&self, address: u8) {
{
let mut ep = self.bulk_in.lock().await;
if ep.endpoint_address() == address {
if ep.pending() > 0 {
ep.cancel_all();
while ep.pending() > 0 {
let _ = ep.next_complete().await;
}
}
let _ = ep.clear_halt().wait();
return;
}
}
let mut ep = self.bulk_out.lock().await;
if ep.endpoint_address() == address {
if ep.pending() > 0 {
ep.cancel_all();
while ep.pending() > 0 {
let _ = ep.next_complete().await;
}
}
let _ = ep.clear_halt().wait();
}
}
}
#[async_trait]
impl Transport for NusbTransport {
async fn send_bulk(&self, data: &[u8]) -> Result<(), crate::PtpError> {
let mut ep = self.bulk_out.lock().await;
let buf: Buffer = data.to_vec().into();
let completion = ep.transfer_blocking(buf, self.timeout);
if let Err(e) = completion.status {
return Err(clear_halt_if_stalled(&mut ep, e));
}
Ok(())
}
async fn send_bulk_streaming(&self, chunks: BulkStream<'_>) -> Result<(), crate::PtpError> {
use futures::StreamExt;
let mut ep = self.bulk_out.lock().await;
let max_packet_size = ep.max_packet_size();
let transfer_size: usize = 256 * 1024; let transfer_size = (transfer_size.div_ceil(max_packet_size)).max(1) * max_packet_size;
let mut current_buf = ep.allocate(transfer_size);
let mut total_sent = 0usize;
let mut stream = chunks;
while let Some(chunk_result) = stream.next().await {
let chunk = chunk_result.map_err(crate::PtpError::Io)?;
let mut remaining = chunk.as_ref();
while !remaining.is_empty() {
let space = current_buf.remaining_capacity();
let to_copy = remaining.len().min(space);
current_buf.extend_from_slice(&remaining[..to_copy]);
remaining = &remaining[to_copy..];
if current_buf.remaining_capacity() == 0 {
ep.submit(current_buf);
let completion = ep
.wait_next_complete(self.timeout)
.ok_or(crate::PtpError::Timeout)?;
if let Err(e) = completion.status {
return Err(clear_halt_if_stalled(&mut ep, e));
}
total_sent += transfer_size;
current_buf = ep.allocate(transfer_size);
}
}
}
let final_len = current_buf.len();
if final_len > 0 {
ep.submit(current_buf);
let completion = ep
.wait_next_complete(self.timeout)
.ok_or(crate::PtpError::Timeout)?;
if let Err(e) = completion.status {
return Err(clear_halt_if_stalled(&mut ep, e));
}
if final_len % max_packet_size == 0 {
ep.submit(Buffer::new(0));
let completion = ep
.wait_next_complete(self.timeout)
.ok_or(crate::PtpError::Timeout)?;
if let Err(e) = completion.status {
return Err(clear_halt_if_stalled(&mut ep, e));
}
}
} else if total_sent > 0 && total_sent % max_packet_size == 0 {
ep.submit(Buffer::new(0));
let completion = ep
.wait_next_complete(self.timeout)
.ok_or(crate::PtpError::Timeout)?;
if let Err(e) = completion.status {
return Err(clear_halt_if_stalled(&mut ep, e));
}
}
Ok(())
}
async fn receive_bulk(&self, max_size: usize) -> Result<Vec<u8>, crate::PtpError> {
let mut ep = self.bulk_in.lock().await;
if ep.pending() == 0 {
let max_packet_size = ep.max_packet_size();
let aligned_size = align_to_packet_size(max_size, max_packet_size);
ep.submit(Buffer::new(aligned_size));
}
let completed = {
let selected = futures::future::select(
Box::pin(ep.next_complete()),
Box::pin(Delay::new(self.timeout)),
)
.await;
match selected {
futures::future::Either::Left((comp, _)) => Some(comp),
futures::future::Either::Right((_, _)) => None,
}
};
match completed {
Some(comp) => {
if let Err(e) = comp.status {
return Err(clear_halt_if_stalled(&mut ep, e));
}
Ok(comp.buffer[..comp.actual_len].to_vec())
}
None => {
Err(crate::PtpError::Timeout)
}
}
}
async fn receive_interrupt(&self) -> Result<Vec<u8>, crate::PtpError> {
let mut ep = self.interrupt_in.lock().await;
if ep.pending() == 0 {
let max_packet_size = ep.max_packet_size();
let aligned_size = align_to_packet_size(Self::INTERRUPT_BUFFER_SIZE, max_packet_size);
ep.submit(Buffer::new(aligned_size));
}
let completion = ep.next_complete().await;
if let Err(e) = completion.status {
return Err(clear_halt_if_stalled(&mut ep, e));
}
Ok(completion.buffer[..completion.actual_len].to_vec())
}
async fn cancel_transfer(
&self,
transaction_id: u32,
idle_timeout: Duration,
) -> Result<(), crate::PtpError> {
let mut payload = [0u8; 6];
payload[0..2].copy_from_slice(&SIC_CANCEL_EVENT_CODE.to_le_bytes());
payload[2..6].copy_from_slice(&transaction_id.to_le_bytes());
self.interface
.control_out(
ControlOut {
control_type: ControlType::Class,
recipient: Recipient::Interface,
request: SIC_CANCEL_REQUEST,
value: 0,
index: self.interface_number as u16,
data: &payload,
},
Duration::from_millis(300),
)
.await
.map_err(Self::convert_transfer_error)?;
{
let mut ep = self.bulk_in.lock().await;
let max_packet_size = ep.max_packet_size();
loop {
if ep.pending() == 0 {
let aligned_size = align_to_packet_size(max_packet_size, max_packet_size);
ep.submit(Buffer::new(aligned_size));
}
let drain_result = {
let complete_fut = ep.next_complete();
let timeout_fut = Delay::new(idle_timeout);
futures::pin_mut!(complete_fut, timeout_fut);
match futures::future::select(complete_fut, timeout_fut).await {
futures::future::Either::Left((completion, _)) => {
match completion.status {
Ok(()) => {
if completion.actual_len >= 6 {
let type_code = u16::from_le_bytes([
completion.buffer[4],
completion.buffer[5],
]);
if type_code == 3 {
Ok(true) } else {
Ok(false) }
} else {
Ok(false) }
}
Err(TransferError::Cancelled) => Ok(true),
Err(TransferError::Disconnected) => {
Err(crate::PtpError::Disconnected)
}
Err(_) => Ok(true),
}
}
futures::future::Either::Right((_, _)) => {
Ok(true)
}
}
};
match drain_result {
Ok(true) => {
ep.cancel_all();
while ep.pending() > 0 {
let _ = ep.next_complete().await;
}
break;
}
Ok(false) => continue,
Err(e) => return Err(e),
}
}
}
{
let mut ep = self.interrupt_in.lock().await;
if ep.pending() == 0 {
let max_packet_size = ep.max_packet_size();
let aligned_size =
align_to_packet_size(Self::INTERRUPT_BUFFER_SIZE, max_packet_size);
ep.submit(Buffer::new(aligned_size));
}
let timed_out = {
let complete_fut = ep.next_complete();
let timeout_fut = Delay::new(idle_timeout);
futures::pin_mut!(complete_fut, timeout_fut);
matches!(
futures::future::select(complete_fut, timeout_fut).await,
futures::future::Either::Right(_)
)
};
if timed_out {
ep.cancel_all();
while ep.pending() > 0 {
let _ = ep.next_complete().await;
}
}
}
self.settle_after_cancel().await;
Ok(())
}
async fn reset_device(&self) -> Result<(), crate::PtpError> {
self.interface
.control_out(
ControlOut {
control_type: ControlType::Class,
recipient: Recipient::Interface,
request: SIC_DEVICE_RESET_REQUEST,
value: 0,
index: self.interface_number as u16,
data: &[],
},
Duration::from_secs(1),
)
.await
.map_err(Self::convert_transfer_error)?;
{
let mut ep = self.bulk_out.lock().await;
if ep.pending() > 0 {
ep.cancel_all();
while ep.pending() > 0 {
let _ = ep.next_complete().await;
}
}
let _ = ep.clear_halt().wait();
}
{
let mut ep = self.bulk_in.lock().await;
if ep.pending() > 0 {
ep.cancel_all();
while ep.pending() > 0 {
let _ = ep.next_complete().await;
}
}
let _ = ep.clear_halt().wait();
let max_packet_size = ep.max_packet_size();
loop {
if ep.pending() == 0 {
ep.submit(Buffer::new(align_to_packet_size(
max_packet_size,
max_packet_size,
)));
}
let got_data = {
let complete_fut = ep.next_complete();
let timeout_fut = Delay::new(Duration::from_millis(300));
futures::pin_mut!(complete_fut, timeout_fut);
match futures::future::select(complete_fut, timeout_fut).await {
futures::future::Either::Left((completion, _)) => completion.status.is_ok(),
futures::future::Either::Right((_, _)) => false,
}
};
if !got_data {
ep.cancel_all();
while ep.pending() > 0 {
let _ = ep.next_complete().await;
}
break;
}
}
}
Ok(())
}
}
fn align_to_packet_size(size: usize, packet_size: usize) -> usize {
if packet_size == 0 {
return size.max(1);
}
if size == 0 {
return packet_size;
}
if size % packet_size == 0 {
size
} else {
((size / packet_size) + 1) * packet_size
}
}
fn location_id_from_topology(dev: &nusb::DeviceInfo) -> u64 {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0100_0000_01b3;
let mut hash = FNV_OFFSET;
for byte in dev.bus_id().as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash ^= 0xFF;
hash = hash.wrapping_mul(FNV_PRIME);
for byte in dev.port_chain() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_device_status_ok() {
let data = [0x04, 0x00, 0x01, 0x20];
assert_eq!(parse_device_status(&data), Some((0x2001, vec![])));
}
#[test]
fn parse_device_status_busy() {
let data = [0x04, 0x00, 0x19, 0x20];
assert_eq!(parse_device_status(&data), Some((0x2019, vec![])));
}
#[test]
fn parse_device_status_with_stalled_endpoints() {
let data = [
0x0C, 0x00, 0x1F, 0x20, 0x81, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, ];
assert_eq!(parse_device_status(&data), Some((0x201F, vec![0x81, 0x02])));
}
#[test]
fn parse_device_status_respects_wlength_over_buffer_padding() {
let data = [
0x08, 0x00, 0x01, 0x20, 0x81, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, ];
assert_eq!(parse_device_status(&data), Some((0x2001, vec![0x81])));
}
#[test]
fn parse_device_status_too_short() {
assert_eq!(parse_device_status(&[]), None);
assert_eq!(parse_device_status(&[0x04, 0x00, 0x01]), None);
}
#[test]
fn usb_speed_from_nusb_maps_every_documented_variant() {
assert_eq!(UsbSpeed::from_nusb(nusb::Speed::Low), Some(UsbSpeed::Low));
assert_eq!(UsbSpeed::from_nusb(nusb::Speed::Full), Some(UsbSpeed::Full));
assert_eq!(UsbSpeed::from_nusb(nusb::Speed::High), Some(UsbSpeed::High));
assert_eq!(
UsbSpeed::from_nusb(nusb::Speed::Super),
Some(UsbSpeed::Super)
);
assert_eq!(
UsbSpeed::from_nusb(nusb::Speed::SuperPlus),
Some(UsbSpeed::SuperPlus)
);
}
#[test]
fn usb_speed_is_round_trip_safe() {
let tiers = [
UsbSpeed::Low,
UsbSpeed::Full,
UsbSpeed::High,
UsbSpeed::Super,
UsbSpeed::SuperPlus,
];
for (a, b) in tiers.iter().zip(tiers.iter().skip(1)) {
assert_ne!(a, b);
}
}
#[test]
fn summary_match_accepts_garmin_style_mtp_interface_string() {
assert_eq!(
NusbTransport::mtp_summary_match_reason(0xff, 0xff, 0x00, Some("MTP")),
Some(MtpMatchReason::InterfaceString)
);
}
#[test]
fn summary_match_rejects_vendor_specific_without_mtp_string() {
assert_eq!(
NusbTransport::mtp_summary_match_reason(0xff, 0xff, 0x00, None),
None
);
assert_eq!(
NusbTransport::mtp_summary_match_reason(0xff, 0xff, 0x00, Some("Vendor")),
None
);
}
#[test]
fn summary_match_accepts_standard_class_before_interface_string() {
assert_eq!(
NusbTransport::mtp_summary_match_reason(0x06, 0x01, 0x01, Some("Camera")),
Some(MtpMatchReason::StandardClass)
);
}
#[test]
fn match_reason_as_str_uses_stable_snake_case_values() {
assert_eq!(MtpMatchReason::StandardClass.as_str(), "standard_class");
assert_eq!(MtpMatchReason::InterfaceString.as_str(), "interface_string");
assert_eq!(MtpMatchReason::KnownVidPid.as_str(), "known_vid_pid");
assert_eq!(
MtpMatchReason::OpenedDescriptorScan.as_str(),
"opened_descriptor_scan"
);
}
#[test]
#[ignore] fn test_list_devices() {
let devices = NusbTransport::list_mtp_devices().unwrap();
println!("Found {} MTP devices", devices.len());
for dev in &devices {
println!(
" {:04x}:{:04x} serial={:?} location={:08x}",
dev.vendor_id, dev.product_id, dev.serial_number, dev.location_id,
);
}
}
#[tokio::test]
#[ignore] async fn test_open_device() {
let devices = NusbTransport::list_mtp_devices().unwrap();
assert!(!devices.is_empty(), "No MTP device found");
let device = devices[0].open().unwrap();
let transport = NusbTransport::open(device).await.unwrap();
assert_eq!(transport.timeout(), NusbTransport::DEFAULT_TIMEOUT);
}
#[tokio::test]
#[ignore] async fn test_timeout_configuration() {
let devices = NusbTransport::list_mtp_devices().unwrap();
assert!(!devices.is_empty(), "No MTP device found");
let device = devices[0].open().unwrap();
let custom_timeout = Duration::from_secs(60);
let mut transport = NusbTransport::open_with_timeout(device, custom_timeout)
.await
.unwrap();
assert_eq!(transport.timeout(), custom_timeout);
let new_timeout = Duration::from_secs(10);
transport.set_timeout(new_timeout);
assert_eq!(transport.timeout(), new_timeout);
}
#[test]
fn test_align_to_packet_size() {
assert_eq!(align_to_packet_size(0, 512), 512);
assert_eq!(align_to_packet_size(1, 512), 512);
assert_eq!(align_to_packet_size(512, 512), 512);
assert_eq!(align_to_packet_size(1024, 512), 1024);
assert_eq!(align_to_packet_size(513, 512), 1024);
assert_eq!(align_to_packet_size(100, 64), 128);
assert_eq!(align_to_packet_size(0, 0), 1);
assert_eq!(align_to_packet_size(100, 0), 100);
}
#[test]
fn test_mtp_class_detection() {
assert!(NusbTransport::is_mtp_class(0x06, 0x01, 0x01));
assert!(NusbTransport::is_mtp_class(0xFF, 0x01, 0x01));
assert!(!NusbTransport::is_mtp_class(0x08, 0x01, 0x01));
assert!(!NusbTransport::is_mtp_class(0x06, 0x00, 0x01));
assert!(!NusbTransport::is_mtp_class(0x06, 0x01, 0x00));
assert!(!NusbTransport::is_mtp_class(0xFF, 0xFF, 0x00));
}
}