From c1b5ea0cd8dd80f7200b5d41f9b4e42c364ba256 Mon Sep 17 00:00:00 2001 From: Corentin LIAUD Date: Sat, 2 May 2026 13:47:21 +0200 Subject: [PATCH 1/2] feat(usb): improve ADBUSBDevice layering Preparing work for webusb integration. - ADBUSBDevice does not depend anymore on rusb code, but now holds an USBTransport implementor as a TRANSPORT generic. - This trait wraps ADBTransport + ADBMessageTransport to "hide" concrete dependencies and is currently implemented by WiredUSBTransport. --- adb_cli/src/main.rs | 4 +- adb_client/Cargo.toml | 4 + .../message_devices/usb/adb_device_info.rs | 10 + .../src/message_devices/usb/adb_usb_device.rs | 88 +++-- adb_client/src/message_devices/usb/mod.rs | 8 +- .../src/message_devices/usb/usb_transport.rs | 289 +------------- adb_client/src/message_devices/usb/utils.rs | 108 ----- .../usb/wired_usb_transport.rs | 371 ++++++++++++++++++ pyadb_client/src/adb_usb_device.rs | 11 +- 9 files changed, 469 insertions(+), 424 deletions(-) create mode 100644 adb_client/src/message_devices/usb/adb_device_info.rs delete mode 100644 adb_client/src/message_devices/usb/utils.rs create mode 100644 adb_client/src/message_devices/usb/wired_usb_transport.rs diff --git a/adb_cli/src/main.rs b/adb_cli/src/main.rs index de219cd..950a6e6 100644 --- a/adb_cli/src/main.rs +++ b/adb_cli/src/main.rs @@ -12,7 +12,7 @@ use adb_client::mdns::MDNSDiscoveryService; use adb_client::server::ADBServer; use adb_client::server_device::ADBServerDevice; use adb_client::tcp::ADBTcpDevice; -use adb_client::usb::{ADBDeviceInfo, ADBUSBDevice, find_all_connected_adb_devices}; +use adb_client::usb::{ADBDeviceInfo, ADBUSBDevice, USBTransport, WiredUSBTransport}; #[cfg(any(target_os = "linux", target_os = "macos"))] use adb_termios::ADBTermios; @@ -153,7 +153,7 @@ fn inner_main() -> ADBCliResult<()> { } MainCommand::Usb(usb_command) => { if usb_command.list_devices { - let devices = find_all_connected_adb_devices()?; + let devices = WiredUSBTransport::find_all_connected_adb_devices()?; let mut writer = TabWriter::new(stdout()).alignment(tabwriter::Alignment::Center); writeln!(writer, "Index\tVendor ID\tProduct ID\tDevice Description")?; diff --git a/adb_client/Cargo.toml b/adb_client/Cargo.toml index 79f22e5..161282a 100644 --- a/adb_client/Cargo.toml +++ b/adb_client/Cargo.toml @@ -21,6 +21,7 @@ rustdoc-args = ["--cfg", "docsrs"] default = [] mdns = ["dep:mdns-sd"] usb = ["dep:rusb"] +# webusb = ["dep:webusb-web"] [dependencies] base64 = { version = "0.22.1" } @@ -54,6 +55,9 @@ mdns-sd = { version = "0.19.1", default-features = false, features = [ rusb = { version = "0.9.4", features = ["vendored"], optional = true } num_enum = { version = "0.7.6" } ######### +# `webusb` feature-specific dependencies +# webusb-web = { version = "0.5.1", optional = true } +######### [dev-dependencies] anyhow = { version = "1.0.102" } diff --git a/adb_client/src/message_devices/usb/adb_device_info.rs b/adb_client/src/message_devices/usb/adb_device_info.rs new file mode 100644 index 0000000..9e48c82 --- /dev/null +++ b/adb_client/src/message_devices/usb/adb_device_info.rs @@ -0,0 +1,10 @@ +/// Represents an Android device connected via USB +#[derive(Clone, Debug)] +pub struct ADBDeviceInfo { + /// Vendor ID of the device + pub vendor_id: u16, + /// Product ID of the device + pub product_id: u16, + /// Textual description of the device + pub device_description: String, +} diff --git a/adb_client/src/message_devices/usb/adb_usb_device.rs b/adb_client/src/message_devices/usb/adb_usb_device.rs index a2790c0..5ef1523 100644 --- a/adb_client/src/message_devices/usb/adb_usb_device.rs +++ b/adb_client/src/message_devices/usb/adb_usb_device.rs @@ -9,36 +9,59 @@ use crate::Result; use crate::RustADBError; use crate::message_devices::adb_message_device::ADBMessageDevice; use crate::models::RemountInfo; +use crate::usb::ADBDeviceInfo; use crate::usb::usb_transport::USBTransport; -use crate::usb::utils; +use crate::usb::wired_usb_transport::WiredUSBTransport; use crate::utils::get_default_adb_key_path; -/// Represent a device reached and available over USB. +/// Represent a device reached and available over a USB transport. #[derive(Debug)] -pub struct ADBUSBDevice { - inner: ADBMessageDevice, +pub struct ADBUSBDevice { + inner: ADBMessageDevice, vendor_id: u16, product_id: u16, } -impl ADBUSBDevice { - /// Instantiate a new [`ADBUSBDevice`] - pub fn new(vendor_id: u16, product_id: u16) -> Result { - Self::new_with_custom_private_key(vendor_id, product_id, get_default_adb_key_path()?) +impl ADBUSBDevice { + /// Returns the vendor ID of the device + #[must_use] + pub const fn vendor_id(&self) -> u16 { + self.vendor_id } - /// Instantiate a new [`ADBUSBDevice`] using a custom private key path - pub fn new_with_custom_private_key>( - vendor_id: u16, - product_id: u16, - private_key_path: P, - ) -> Result { - Self::new_from_transport_inner(USBTransport::new(vendor_id, product_id)?, private_key_path) + /// Returns the product ID of the device + #[must_use] + pub const fn product_id(&self) -> u16 { + self.product_id + } + + /// Find and return an USB-connected Android device with known interface class and subclass values. + /// + /// Returns the first device found or None if no device is found. + /// If multiple devices are found, an error is returned. + pub fn get_single_connected_adb_device() -> Result> { + let found_devices = TRANSPORT::find_all_connected_adb_devices()?; + match (found_devices.first(), found_devices.get(1)) { + (None, _) => Ok(None), + (Some(device_info), None) => { + log::debug!( + "Autodetect device {:04x}:{:04x} - {}", + device_info.vendor_id, + device_info.product_id, + device_info.device_description + ); + Ok(Some(device_info.clone())) + } + (Some(device_1), Some(device_2)) => Err(RustADBError::DeviceNotFound(format!( + "Found two Android devices {:04x}:{:04x} and {:04x}:{:04x}", + device_1.vendor_id, device_1.product_id, device_2.vendor_id, device_2.product_id + ))), + } } /// Instantiate a new [`ADBUSBDevice`] from a [`USBTransport`] and an optional private key path. pub fn new_from_transport( - transport: USBTransport, + transport: TRANSPORT, private_key_path: Option, ) -> Result { let private_key_path = match private_key_path { @@ -50,11 +73,11 @@ impl ADBUSBDevice { } fn new_from_transport_inner>( - transport: USBTransport, + transport: TRANSPORT, private_key_path: P, ) -> Result { - let vendor_id = transport.vendor_id()?; - let product_id = transport.product_id()?; + let vendor_id = transport.vendor_id(); + let product_id = transport.product_id(); Ok(Self { inner: ADBMessageDevice::new(transport, private_key_path)?, @@ -62,17 +85,24 @@ impl ADBUSBDevice { product_id, }) } +} - /// Returns the vendor ID of the device - #[must_use] - pub const fn vendor_id(&self) -> u16 { - self.vendor_id +impl ADBUSBDevice { + /// Instantiate a new [`ADBUSBDevice`] + pub fn new(vendor_id: u16, product_id: u16) -> Result { + Self::new_with_custom_private_key(vendor_id, product_id, get_default_adb_key_path()?) } - /// Returns the product ID of the device - #[must_use] - pub const fn product_id(&self) -> u16 { - self.product_id + /// Instantiate a new [`ADBUSBDevice`] using a custom private key path + pub fn new_with_custom_private_key>( + vendor_id: u16, + product_id: u16, + private_key_path: P, + ) -> Result { + Self::new_from_transport_inner( + WiredUSBTransport::new(vendor_id, product_id)?, + private_key_path, + ) } /// autodetect connected ADB devices and establish a connection with the first device found @@ -82,7 +112,7 @@ impl ADBUSBDevice { /// autodetect connected ADB devices and establish a connection with the first device found using a custom private key path pub fn autodetect_with_custom_private_key(private_key_path: PathBuf) -> Result { - match utils::get_single_connected_adb_device()? { + match Self::get_single_connected_adb_device()? { Some(device_info) => Self::new_with_custom_private_key( device_info.vendor_id, device_info.product_id, @@ -95,7 +125,7 @@ impl ADBUSBDevice { } } -impl ADBDeviceExt for ADBUSBDevice { +impl ADBDeviceExt for ADBUSBDevice { #[inline] fn shell_command( &mut self, diff --git a/adb_client/src/message_devices/usb/mod.rs b/adb_client/src/message_devices/usb/mod.rs index 3677f5f..6417558 100644 --- a/adb_client/src/message_devices/usb/mod.rs +++ b/adb_client/src/message_devices/usb/mod.rs @@ -1,7 +1,11 @@ +#![doc = include_str!("./README.md")] + +mod adb_device_info; mod adb_usb_device; mod usb_transport; -mod utils; +mod wired_usb_transport; +pub use adb_device_info::ADBDeviceInfo; pub use adb_usb_device::ADBUSBDevice; pub use usb_transport::USBTransport; -pub use utils::{ADBDeviceInfo, find_all_connected_adb_devices}; +pub use wired_usb_transport::WiredUSBTransport; diff --git a/adb_client/src/message_devices/usb/usb_transport.rs b/adb_client/src/message_devices/usb/usb_transport.rs index 35fafef..22b6c80 100644 --- a/adb_client/src/message_devices/usb/usb_transport.rs +++ b/adb_client/src/message_devices/usb/usb_transport.rs @@ -1,285 +1,16 @@ -use std::{sync::Arc, time::Duration}; - -use rusb::{ - Context, Device, DeviceHandle, Direction, TransferType, UsbContext, - constants::LIBUSB_CLASS_VENDOR_SPEC, -}; - use crate::{ - Result, RustADBError, - adb_transport::ADBTransport, - message_devices::{ - adb_message_transport::ADBMessageTransport, - adb_transport_message::{ADBTransportMessage, ADBTransportMessageHeader}, - message_commands::MessageCommand, - }, + Result, adb_transport::ADBTransport, + message_devices::adb_message_transport::ADBMessageTransport, usb::ADBDeviceInfo, }; -#[derive(Clone, Debug)] -struct Endpoint { - iface: u8, - address: u8, - max_packet_size: usize, -} - -/// Transport running on USB -#[derive(Debug, Clone)] -pub struct USBTransport { - device: Device, - handle: Option>>, - read_endpoint: Option, - write_endpoint: Option, -} - -impl USBTransport { - /// Instantiate a new [`USBTransport`]. - /// Only the first device with given `vendor_id` and `product_id` is returned. - pub fn new(vendor_id: u16, product_id: u16) -> Result { - let context = Context::new()?; - for device in context.devices()?.iter() { - if let Ok(descriptor) = device.device_descriptor() - && descriptor.vendor_id() == vendor_id - && descriptor.product_id() == product_id - { - return Ok(Self::new_from_device(device)); - } - } - - Err(RustADBError::DeviceNotFound(format!( - "cannot find USB device with vendor_id={vendor_id} and product_id={product_id}", - ))) - } - - /// Instantiate a new [`USBTransport`] from a [`rusb::Device`]. - /// - /// Devices can be enumerated using [`rusb::Context::devices()`] and then filtered out to get desired device. - #[must_use] - pub const fn new_from_device(rusb_device: rusb::Device) -> Self { - Self { - device: rusb_device, - handle: None, - read_endpoint: None, - write_endpoint: None, - } - } - - pub(crate) fn vendor_id(&self) -> Result { - Ok(self.device.device_descriptor().map(|d| d.vendor_id())?) - } - - pub(crate) fn product_id(&self) -> Result { - Ok(self.device.device_descriptor().map(|d| d.product_id())?) - } - - pub(crate) fn get_raw_connection(&self) -> Result>> { - self.handle - .as_ref() - .ok_or(RustADBError::IOError(std::io::Error::new( - std::io::ErrorKind::NotConnected, - "not connected", - ))) - .cloned() - } - - fn get_read_endpoint(&self) -> Result { - self.read_endpoint - .as_ref() - .ok_or(RustADBError::IOError(std::io::Error::new( - std::io::ErrorKind::NotConnected, - "no read endpoint setup", - ))) - .cloned() - } - - fn get_write_endpoint(&self) -> Result<&Endpoint> { - self.write_endpoint - .as_ref() - .ok_or(RustADBError::IOError(std::io::Error::new( - std::io::ErrorKind::NotConnected, - "no write endpoint setup", - ))) - } - - fn configure_endpoint(handle: &DeviceHandle, endpoint: &Endpoint) -> Result<()> { - match handle.claim_interface(endpoint.iface) { - Ok(()) => Ok(()), - // busy state likely indicates an ADB server is running and has taken the lock over the device - Err(rusb::Error::Busy) => Err(RustADBError::DeviceBusy), - Err(err) => Err(err.into()), - } - } - - fn find_endpoints(handle: &DeviceHandle) -> Result<(Endpoint, Endpoint)> { - let mut read_endpoint: Option = None; - let mut write_endpoint: Option = None; - - for n in 0..handle.device().device_descriptor()?.num_configurations() { - let Ok(config_desc) = handle.device().config_descriptor(n) else { - continue; - }; - - for interface in config_desc.interfaces() { - for interface_desc in interface.descriptors() { - for endpoint_desc in interface_desc.endpoint_descriptors() { - if endpoint_desc.transfer_type() == TransferType::Bulk - && interface_desc.class_code() == LIBUSB_CLASS_VENDOR_SPEC - && interface_desc.sub_class_code() == 0x42 - && interface_desc.protocol_code() == 0x01 - { - let endpoint = Endpoint { - iface: interface_desc.interface_number(), - address: endpoint_desc.address(), - max_packet_size: endpoint_desc.max_packet_size() as usize, - }; - match endpoint_desc.direction() { - Direction::In => { - if let Some(write_endpoint) = write_endpoint { - return Ok((endpoint, write_endpoint)); - } - read_endpoint = Some(endpoint); - } - Direction::Out => { - if let Some(read_endpoint) = read_endpoint { - return Ok((read_endpoint, endpoint)); - } - write_endpoint = Some(endpoint); - } - } - } - } - } - } - } - - Err(RustADBError::USBNoDescriptorFound) - } - - fn write_bulk_data(&self, data: &[u8], timeout: Duration) -> Result<()> { - let endpoint = self.get_write_endpoint()?; - let handle = self.get_raw_connection()?; - let max_packet_size = endpoint.max_packet_size; - - let mut offset = 0; - let data_len = data.len(); - while offset < data_len { - let end = (offset + max_packet_size).min(data_len); - let write_amount = handle.write_bulk(endpoint.address, &data[offset..end], timeout)?; - offset += write_amount; - - log::trace!("wrote chunk of size {write_amount} - {offset}/{data_len}"); - } - - if offset % max_packet_size == 0 { - log::trace!("must send final zero-length packet"); - handle.write_bulk(endpoint.address, &[], timeout)?; - } - - Ok(()) - } -} - -impl ADBTransport for USBTransport { - fn connect(&mut self) -> crate::Result<()> { - let device = self.device.open()?; - - let (read_endpoint, write_endpoint) = Self::find_endpoints(&device)?; - - Self::configure_endpoint(&device, &read_endpoint)?; - log::debug!("got read endpoint: {read_endpoint:?}"); - self.read_endpoint = Some(read_endpoint); - - Self::configure_endpoint(&device, &write_endpoint)?; - log::debug!("got write endpoint: {write_endpoint:?}"); - self.write_endpoint = Some(write_endpoint); - - self.handle = Some(Arc::new(device)); - - Ok(()) - } - - fn disconnect(&mut self) -> crate::Result<()> { - if self.handle.is_none() { - // device has not been initialized, nothing to do - return Ok(()); - } - - let message = ADBTransportMessage::try_new(MessageCommand::Clse, 0, 0, &[])?; - if let Err(e) = self.write_message(message) { - log::error!("error while sending CLSE message: {e}"); - } - - if let Some(handle) = &self.handle { - let endpoint = self.read_endpoint.as_ref().or(self.write_endpoint.as_ref()); - if let Some(endpoint) = &endpoint { - match handle.release_interface(endpoint.iface) { - Ok(()) => log::debug!("succesfully released interface"), - Err(e) => log::error!("error while release interface: {e}"), - } - } - } - - Ok(()) - } -} - -impl ADBMessageTransport for USBTransport { - fn write_message_with_timeout( - &mut self, - message: ADBTransportMessage, - timeout: Duration, - ) -> Result<()> { - let message_bytes = message.header().as_bytes(); - self.write_bulk_data(&message_bytes, timeout)?; - - log::trace!("successfully write header: {} bytes", message_bytes.len()); - - let payload = message.into_payload(); - if !payload.is_empty() { - self.write_bulk_data(&payload, timeout)?; - log::trace!("successfully write payload: {} bytes", payload.len()); - } - - Ok(()) - } - - fn read_message_with_timeout(&mut self, timeout: Duration) -> Result { - let endpoint = self.get_read_endpoint()?; - let handle = self.get_raw_connection()?; - let max_packet_size = endpoint.max_packet_size; - - let mut data = [0u8; 24]; - let mut offset = 0; - while offset < data.len() { - let end = (offset + max_packet_size).min(data.len()); - let chunk = &mut data[offset..end]; - offset += handle.read_bulk(endpoint.address, chunk, timeout)?; - } - - let header = ADBTransportMessageHeader::try_from(data)?; - log::trace!("received header {header:?}"); - - if header.data_length() != 0 { - let mut msg_data = vec![0_u8; header.data_length() as usize]; - let mut offset = 0; - while offset < msg_data.len() { - let end = (offset + max_packet_size).min(msg_data.len()); - let chunk = &mut msg_data[offset..end]; - offset += handle.read_bulk(endpoint.address, chunk, timeout)?; - } - - let message = ADBTransportMessage::from_header_and_payload(header, msg_data); - - // Check message integrity - if !message.check_message_integrity() { - return Err(RustADBError::InvalidIntegrity( - ADBTransportMessageHeader::compute_crc32(message.payload()), - message.header().data_crc32(), - )); - } +/// Trait representing a USB transport layer for ADB devices. +pub trait USBTransport: ADBTransport + ADBMessageTransport { + /// Find and return a list of all connected Android devices with known interface class and subclass values + fn find_all_connected_adb_devices() -> Result>; - return Ok(message); - } + /// Return the vendor ID of the underlying USB device + fn vendor_id(&self) -> u16; - Ok(ADBTransportMessage::from_header_and_payload(header, vec![])) - } + /// Return the product ID of the underlying USB device + fn product_id(&self) -> u16; } diff --git a/adb_client/src/message_devices/usb/utils.rs b/adb_client/src/message_devices/usb/utils.rs deleted file mode 100644 index ac03324..0000000 --- a/adb_client/src/message_devices/usb/utils.rs +++ /dev/null @@ -1,108 +0,0 @@ -use rusb::{Context, Device, DeviceDescriptor, UsbContext, constants::LIBUSB_CLASS_VENDOR_SPEC}; - -use crate::{Result, RustADBError}; - -/// Represents an Android device connected via USB -#[derive(Clone, Debug)] -pub struct ADBDeviceInfo { - /// Vendor ID of the device - pub vendor_id: u16, - /// Product ID of the device - pub product_id: u16, - /// Textual description of the device - pub device_description: String, -} - -/// Find and return a list of all connected Android devices with known interface class and subclass values -pub fn find_all_connected_adb_devices() -> Result> { - let mut found_devices = vec![]; - - let context = Context::new()?; - for device in context.devices()?.iter() { - let Ok(des) = device.device_descriptor() else { - continue; - }; - - if is_adb_device(&device, &des) { - let Ok(device_handle) = device.open() else { - found_devices.push(ADBDeviceInfo { - vendor_id: des.vendor_id(), - product_id: des.product_id(), - device_description: "Unknown device".to_string(), - }); - continue; - }; - - let manufacturer = device_handle - .read_manufacturer_string_ascii(&des) - .unwrap_or_else(|_| "Unknown".to_string()); - - let product = device_handle - .read_product_string_ascii(&des) - .unwrap_or_else(|_| "Unknown".to_string()); - - found_devices.push(ADBDeviceInfo { - vendor_id: des.vendor_id(), - product_id: des.product_id(), - device_description: format!("{manufacturer} {product}"), - }); - } - } - - Ok(found_devices) -} - -/// Find and return an USB-connected Android device with known interface class and subclass values. -/// -/// Returns the first device found or None if no device is found. -/// If multiple devices are found, an error is returned. -pub fn get_single_connected_adb_device() -> Result> { - let found_devices = find_all_connected_adb_devices()?; - match (found_devices.first(), found_devices.get(1)) { - (None, _) => Ok(None), - (Some(device_info), None) => { - log::debug!( - "Autodetect device {:04x}:{:04x} - {}", - device_info.vendor_id, - device_info.product_id, - device_info.device_description - ); - Ok(Some(device_info.clone())) - } - (Some(device_1), Some(device_2)) => Err(RustADBError::DeviceNotFound(format!( - "Found two Android devices {:04x}:{:04x} and {:04x}:{:04x}", - device_1.vendor_id, device_1.product_id, device_2.vendor_id, device_2.product_id - ))), - } -} - -/// Check whether a device with given descriptor is an ADB device -fn is_adb_device(device: &Device, des: &DeviceDescriptor) -> bool { - const ADB_SUBCLASS: u8 = 0x42; - const ADB_PROTOCOL: u8 = 0x1; - - // Some devices require choosing the file transfer mode - // for usb debugging to take effect. - const BULK_CLASS: u8 = 0xdc; - const BULK_ADB_SUBCLASS: u8 = 2; - - for n in 0..des.num_configurations() { - let Ok(config_des) = device.config_descriptor(n) else { - continue; - }; - for interface in config_des.interfaces() { - for interface_des in interface.descriptors() { - let proto = interface_des.protocol_code(); - let class = interface_des.class_code(); - let subcl = interface_des.sub_class_code(); - if proto == ADB_PROTOCOL - && ((class == LIBUSB_CLASS_VENDOR_SPEC && subcl == ADB_SUBCLASS) - || (class == BULK_CLASS && subcl == BULK_ADB_SUBCLASS)) - { - return true; - } - } - } - } - false -} diff --git a/adb_client/src/message_devices/usb/wired_usb_transport.rs b/adb_client/src/message_devices/usb/wired_usb_transport.rs new file mode 100644 index 0000000..bfbc7fa --- /dev/null +++ b/adb_client/src/message_devices/usb/wired_usb_transport.rs @@ -0,0 +1,371 @@ +use std::{sync::Arc, time::Duration}; + +use rusb::{ + Context, Device, DeviceDescriptor, DeviceHandle, Direction, TransferType, UsbContext, + constants::LIBUSB_CLASS_VENDOR_SPEC, +}; + +use crate::{ + Result, RustADBError, + adb_transport::ADBTransport, + message_devices::{ + adb_message_transport::ADBMessageTransport, + adb_transport_message::{ADBTransportMessage, ADBTransportMessageHeader}, + message_commands::MessageCommand, + }, + usb::{ADBDeviceInfo, usb_transport::USBTransport}, +}; + +#[derive(Clone, Debug)] +struct WiredUSBEndpoint { + iface: u8, + address: u8, + max_packet_size: usize, +} + +/// Transport running on wired USB +#[derive(Debug, Clone)] +pub struct WiredUSBTransport { + device: Device, + vendor_id: u16, + product_id: u16, + handle: Option>>, + read_endpoint: Option, + write_endpoint: Option, +} + +impl WiredUSBTransport { + /// Instantiate a new [`WiredUSBTransport`]. + /// Only the first device with given `vendor_id` and `product_id` is returned. + pub fn new(vendor_id: u16, product_id: u16) -> Result { + let context = Context::new()?; + for device in context.devices()?.iter() { + if let Ok(descriptor) = device.device_descriptor() + && descriptor.vendor_id() == vendor_id + && descriptor.product_id() == product_id + { + return Self::new_from_device(device); + } + } + + Err(RustADBError::DeviceNotFound(format!( + "cannot find USB device with vendor_id={vendor_id} and product_id={product_id}", + ))) + } + + /// Instantiate a new [`WiredUSBTransport`] from a [`rusb::Device`]. + /// + /// Devices can be enumerated using [`rusb::Context::devices()`] and then filtered out to get desired device. + pub fn new_from_device(rusb_device: rusb::Device) -> Result { + let vendor_id = rusb_device.device_descriptor().map(|d| d.vendor_id())?; + let product_id = rusb_device.device_descriptor().map(|d| d.product_id())?; + + Ok(Self { + device: rusb_device, + vendor_id, + product_id, + handle: None, + read_endpoint: None, + write_endpoint: None, + }) + } + + pub(crate) fn get_raw_connection(&self) -> Result>> { + self.handle + .as_ref() + .ok_or(RustADBError::IOError(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "not connected", + ))) + .cloned() + } + + fn get_read_endpoint(&self) -> Result { + self.read_endpoint + .as_ref() + .ok_or(RustADBError::IOError(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "no read endpoint setup", + ))) + .cloned() + } + + fn get_write_endpoint(&self) -> Result<&WiredUSBEndpoint> { + self.write_endpoint + .as_ref() + .ok_or(RustADBError::IOError(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "no write endpoint setup", + ))) + } + + fn configure_endpoint( + handle: &DeviceHandle, + endpoint: &WiredUSBEndpoint, + ) -> Result<()> { + match handle.claim_interface(endpoint.iface) { + Ok(()) => Ok(()), + // busy state likely indicates an ADB server is running and has taken the lock over the device + Err(rusb::Error::Busy) => Err(RustADBError::DeviceBusy), + Err(err) => Err(err.into()), + } + } + + fn find_endpoints( + handle: &DeviceHandle, + ) -> Result<(WiredUSBEndpoint, WiredUSBEndpoint)> { + let mut read_endpoint: Option = None; + let mut write_endpoint: Option = None; + + for n in 0..handle.device().device_descriptor()?.num_configurations() { + let Ok(config_desc) = handle.device().config_descriptor(n) else { + continue; + }; + + for interface in config_desc.interfaces() { + for interface_desc in interface.descriptors() { + for endpoint_desc in interface_desc.endpoint_descriptors() { + if endpoint_desc.transfer_type() == TransferType::Bulk + && interface_desc.class_code() == LIBUSB_CLASS_VENDOR_SPEC + && interface_desc.sub_class_code() == 0x42 + && interface_desc.protocol_code() == 0x01 + { + let endpoint = WiredUSBEndpoint { + iface: interface_desc.interface_number(), + address: endpoint_desc.address(), + max_packet_size: endpoint_desc.max_packet_size() as usize, + }; + match endpoint_desc.direction() { + Direction::In => { + if let Some(write_endpoint) = write_endpoint { + return Ok((endpoint, write_endpoint)); + } + read_endpoint = Some(endpoint); + } + Direction::Out => { + if let Some(read_endpoint) = read_endpoint { + return Ok((read_endpoint, endpoint)); + } + write_endpoint = Some(endpoint); + } + } + } + } + } + } + } + + Err(RustADBError::USBNoDescriptorFound) + } + + fn write_bulk_data(&self, data: &[u8], timeout: Duration) -> Result<()> { + let endpoint = self.get_write_endpoint()?; + let handle = self.get_raw_connection()?; + let max_packet_size = endpoint.max_packet_size; + + let mut offset = 0; + let data_len = data.len(); + while offset < data_len { + let end = (offset + max_packet_size).min(data_len); + let write_amount = handle.write_bulk(endpoint.address, &data[offset..end], timeout)?; + offset += write_amount; + + log::trace!("wrote chunk of size {write_amount} - {offset}/{data_len}"); + } + + if offset % max_packet_size == 0 { + log::trace!("must send final zero-length packet"); + handle.write_bulk(endpoint.address, &[], timeout)?; + } + + Ok(()) + } +} + +impl ADBTransport for WiredUSBTransport { + fn connect(&mut self) -> crate::Result<()> { + let device = self.device.open()?; + + let (read_endpoint, write_endpoint) = Self::find_endpoints(&device)?; + + Self::configure_endpoint(&device, &read_endpoint)?; + log::debug!("got read endpoint: {read_endpoint:?}"); + self.read_endpoint = Some(read_endpoint); + + Self::configure_endpoint(&device, &write_endpoint)?; + log::debug!("got write endpoint: {write_endpoint:?}"); + self.write_endpoint = Some(write_endpoint); + + self.handle = Some(Arc::new(device)); + + Ok(()) + } + + fn disconnect(&mut self) -> crate::Result<()> { + if self.handle.is_none() { + // device has not been initialized, nothing to do + return Ok(()); + } + + let message = ADBTransportMessage::try_new(MessageCommand::Clse, 0, 0, &[])?; + if let Err(e) = self.write_message(message) { + log::error!("error while sending CLSE message: {e}"); + } + + if let Some(handle) = &self.handle { + let endpoint = self.read_endpoint.as_ref().or(self.write_endpoint.as_ref()); + if let Some(endpoint) = &endpoint { + match handle.release_interface(endpoint.iface) { + Ok(()) => log::debug!("succesfully released interface"), + Err(e) => log::error!("error while release interface: {e}"), + } + } + } + + Ok(()) + } +} + +impl ADBMessageTransport for WiredUSBTransport { + fn write_message_with_timeout( + &mut self, + message: ADBTransportMessage, + timeout: Duration, + ) -> Result<()> { + let message_bytes = message.header().as_bytes(); + self.write_bulk_data(&message_bytes, timeout)?; + + log::trace!("successfully write header: {} bytes", message_bytes.len()); + + let payload = message.into_payload(); + if !payload.is_empty() { + self.write_bulk_data(&payload, timeout)?; + log::trace!("successfully write payload: {} bytes", payload.len()); + } + + Ok(()) + } + + fn read_message_with_timeout(&mut self, timeout: Duration) -> Result { + let endpoint = self.get_read_endpoint()?; + let handle = self.get_raw_connection()?; + let max_packet_size = endpoint.max_packet_size; + + let mut data = [0u8; 24]; + let mut offset = 0; + while offset < data.len() { + let end = (offset + max_packet_size).min(data.len()); + let chunk = &mut data[offset..end]; + offset += handle.read_bulk(endpoint.address, chunk, timeout)?; + } + + let header = ADBTransportMessageHeader::try_from(data)?; + log::trace!("received header {header:?}"); + + if header.data_length() != 0 { + let mut msg_data = vec![0_u8; header.data_length() as usize]; + let mut offset = 0; + while offset < msg_data.len() { + let end = (offset + max_packet_size).min(msg_data.len()); + let chunk = &mut msg_data[offset..end]; + offset += handle.read_bulk(endpoint.address, chunk, timeout)?; + } + + let message = ADBTransportMessage::from_header_and_payload(header, msg_data); + + // Check message integrity + if !message.check_message_integrity() { + return Err(RustADBError::InvalidIntegrity( + ADBTransportMessageHeader::compute_crc32(message.payload()), + message.header().data_crc32(), + )); + } + + return Ok(message); + } + + Ok(ADBTransportMessage::from_header_and_payload(header, vec![])) + } +} + +/// Check whether a device with given descriptor is an ADB device +fn is_adb_device(device: &Device, des: &DeviceDescriptor) -> bool { + const ADB_SUBCLASS: u8 = 0x42; + const ADB_PROTOCOL: u8 = 0x1; + + // Some devices require choosing the file transfer mode + // for usb debugging to take effect. + const BULK_CLASS: u8 = 0xdc; + const BULK_ADB_SUBCLASS: u8 = 2; + + for n in 0..des.num_configurations() { + let Ok(config_des) = device.config_descriptor(n) else { + continue; + }; + for interface in config_des.interfaces() { + for interface_des in interface.descriptors() { + let proto = interface_des.protocol_code(); + let class = interface_des.class_code(); + let subcl = interface_des.sub_class_code(); + if proto == ADB_PROTOCOL + && ((class == LIBUSB_CLASS_VENDOR_SPEC && subcl == ADB_SUBCLASS) + || (class == BULK_CLASS && subcl == BULK_ADB_SUBCLASS)) + { + return true; + } + } + } + } + false +} + +impl USBTransport for WiredUSBTransport { + /// Find and return a list of all connected Android devices with known interface class and subclass values + fn find_all_connected_adb_devices() -> Result> { + let mut found_devices = vec![]; + + let context = Context::new()?; + for device in context.devices()?.iter() { + let Ok(des) = device.device_descriptor() else { + continue; + }; + + if is_adb_device(&device, &des) { + let Ok(device_handle) = device.open() else { + found_devices.push(ADBDeviceInfo { + vendor_id: des.vendor_id(), + product_id: des.product_id(), + device_description: "Unknown device".to_string(), + }); + continue; + }; + + let manufacturer = device_handle + .read_manufacturer_string_ascii(&des) + .unwrap_or_else(|_| "Unknown".to_string()); + + let product = device_handle + .read_product_string_ascii(&des) + .unwrap_or_else(|_| "Unknown".to_string()); + + found_devices.push(ADBDeviceInfo { + vendor_id: des.vendor_id(), + product_id: des.product_id(), + device_description: format!("{manufacturer} {product}"), + }); + } + } + + Ok(found_devices) + } + + #[inline] + fn vendor_id(&self) -> u16 { + self.vendor_id + } + + #[inline] + fn product_id(&self) -> u16 { + self.product_id + } +} diff --git a/pyadb_client/src/adb_usb_device.rs b/pyadb_client/src/adb_usb_device.rs index 65a21f0..ff2fc2f 100644 --- a/pyadb_client/src/adb_usb_device.rs +++ b/pyadb_client/src/adb_usb_device.rs @@ -1,6 +1,9 @@ use std::{fs::File, path::PathBuf}; -use adb_client::{ADBDeviceExt, usb::ADBUSBDevice}; +use adb_client::{ + ADBDeviceExt, + usb::{ADBUSBDevice, WiredUSBTransport}, +}; use anyhow::Result; use pyo3::{Bound, Python, pyclass, pymethods, types::PyBytes}; use pyo3_stub_gen_derive::{gen_stub_pyclass, gen_stub_pymethods}; @@ -8,7 +11,7 @@ use pyo3_stub_gen_derive::{gen_stub_pyclass, gen_stub_pymethods}; #[gen_stub_pyclass] #[pyclass] /// Represent a device directly reachable over USB. -pub struct PyADBUSBDevice(ADBUSBDevice); +pub struct PyADBUSBDevice(ADBUSBDevice); #[gen_stub_pymethods] #[pymethods] @@ -102,8 +105,8 @@ impl PyADBUSBDevice { } } -impl From for PyADBUSBDevice { - fn from(value: ADBUSBDevice) -> Self { +impl From> for PyADBUSBDevice { + fn from(value: ADBUSBDevice) -> Self { Self(value) } } From 682b8f8bf8f87058515f5e325d5f2e6e8f6770ef Mon Sep 17 00:00:00 2001 From: Corentin LIAUD Date: Sat, 2 May 2026 15:54:59 +0200 Subject: [PATCH 2/2] feat: build ok for wasm targets --- adb_client/Cargo.toml | 14 ++++- adb_client/README.md | 13 +++-- adb_client/src/message_devices/mod.rs | 5 +- .../src/message_devices/usb/adb_usb_device.rs | 12 +++- adb_client/src/message_devices/usb/mod.rs | 18 +++++- .../src/message_devices/usb/webusb/mod.rs | 3 + .../message_devices/usb/webusb/transport.rs | 56 +++++++++++++++++++ .../src/message_devices/usb/wired/mod.rs | 3 + .../transport.rs} | 0 9 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 adb_client/src/message_devices/usb/webusb/mod.rs create mode 100644 adb_client/src/message_devices/usb/webusb/transport.rs create mode 100644 adb_client/src/message_devices/usb/wired/mod.rs rename adb_client/src/message_devices/usb/{wired_usb_transport.rs => wired/transport.rs} (100%) diff --git a/adb_client/Cargo.toml b/adb_client/Cargo.toml index 161282a..66c2070 100644 --- a/adb_client/Cargo.toml +++ b/adb_client/Cargo.toml @@ -21,7 +21,7 @@ rustdoc-args = ["--cfg", "docsrs"] default = [] mdns = ["dep:mdns-sd"] usb = ["dep:rusb"] -# webusb = ["dep:webusb-web"] +webusb = ["dep:webusb-web"] [dependencies] base64 = { version = "0.22.1" } @@ -50,13 +50,23 @@ mdns-sd = { version = "0.19.1", default-features = false, features = [ "logging", ], optional = true } ######### + ######### # USB-only dependencies rusb = { version = "0.9.4", features = ["vendored"], optional = true } num_enum = { version = "0.7.6" } +######### + ######### # `webusb` feature-specific dependencies -# webusb-web = { version = "0.5.1", optional = true } +# we need to enable wasm-specific dependency features +# getrandom is quite problematic here as we are internally depending on two different versions +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +getrandom_02 = { package = "getrandom", version = "0.2.17", features = ["js"] } +getrandom_03 = { package = "getrandom", version = "0.4.2", features = ["wasm_js"] } +ring = { version = "0.17.14", features = ["wasm32_unknown_unknown_js"] } +rustls-pki-types = { version = "1.14.1", features = ["web"] } +webusb-web = { version = "0.5.1", optional = true } ######### [dev-dependencies] diff --git a/adb_client/README.md b/adb_client/README.md index a875716..755194b 100644 --- a/adb_client/README.md +++ b/adb_client/README.md @@ -18,10 +18,11 @@ adb_client = "*" ## Crate features -| Feature | Description | Default? | -| :-----: | :---------------------------------------------: | :------: | -| `mdns` | Enables mDNS device discovery on local network. | No | -| `usb` | Enables interactions with USB devices. | No | +| Feature | Description | Default? | +| :------: | :---------------------------------------------: | :------: | +| `mdns` | Enables mDNS device discovery on local network. | No | +| `usb` | Enables interactions with USB devices. | No | +| `webusb` | Enables webusb support | No | To deactivate some default features you can use the `default-features = false` option in your `Cargo.toml` file and manually specify the features you want to activate: @@ -30,6 +31,10 @@ To deactivate some default features you can use the `default-features = false` o adb_client = { version = "*", default-features = false, features = ["mdns", "usb"] } ``` +## WASM support + +TODO: RUSTFLAGS="--cfg=web_sys_unstable_apis" + ## Examples Usage examples can be found in the `examples/` directory of this repository. diff --git a/adb_client/src/message_devices/mod.rs b/adb_client/src/message_devices/mod.rs index eb41e22..0fdf768 100644 --- a/adb_client/src/message_devices/mod.rs +++ b/adb_client/src/message_devices/mod.rs @@ -1,6 +1,7 @@ /// USB-related definitions -#[cfg(feature = "usb")] -#[cfg_attr(docsrs, doc(cfg(feature = "usb")))] +/// Enabled for both `usb` and `webusb` features +#[cfg(any(feature = "usb", feature = "webusb"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "usb", feature = "webusb"))))] pub mod usb; /// Device reachable over TCP related definition diff --git a/adb_client/src/message_devices/usb/adb_usb_device.rs b/adb_client/src/message_devices/usb/adb_usb_device.rs index 5ef1523..570811f 100644 --- a/adb_client/src/message_devices/usb/adb_usb_device.rs +++ b/adb_client/src/message_devices/usb/adb_usb_device.rs @@ -10,8 +10,14 @@ use crate::RustADBError; use crate::message_devices::adb_message_device::ADBMessageDevice; use crate::models::RemountInfo; use crate::usb::ADBDeviceInfo; + +#[cfg(feature = "usb")] +use crate::usb::wired::WiredUSBTransport; + +#[cfg(feature = "webusb")] +use crate::usb::webusb::WebUSBTransport; + use crate::usb::usb_transport::USBTransport; -use crate::usb::wired_usb_transport::WiredUSBTransport; use crate::utils::get_default_adb_key_path; /// Represent a device reached and available over a USB transport. @@ -87,6 +93,7 @@ impl ADBUSBDevice { } } +#[cfg(feature = "usb")] impl ADBUSBDevice { /// Instantiate a new [`ADBUSBDevice`] pub fn new(vendor_id: u16, product_id: u16) -> Result { @@ -125,6 +132,9 @@ impl ADBUSBDevice { } } +#[cfg(feature = "webusb")] +impl ADBUSBDevice {} + impl ADBDeviceExt for ADBUSBDevice { #[inline] fn shell_command( diff --git a/adb_client/src/message_devices/usb/mod.rs b/adb_client/src/message_devices/usb/mod.rs index 6417558..29c4bee 100644 --- a/adb_client/src/message_devices/usb/mod.rs +++ b/adb_client/src/message_devices/usb/mod.rs @@ -3,9 +3,23 @@ mod adb_device_info; mod adb_usb_device; mod usb_transport; -mod wired_usb_transport; + +#[cfg(feature = "webusb")] +#[cfg_attr(docsrs, doc(cfg(feature = "webusb")))] +mod webusb; + +#[cfg(feature = "usb")] +#[cfg_attr(docsrs, doc(cfg(feature = "usb")))] +mod wired; pub use adb_device_info::ADBDeviceInfo; pub use adb_usb_device::ADBUSBDevice; pub use usb_transport::USBTransport; -pub use wired_usb_transport::WiredUSBTransport; + +#[cfg(feature = "webusb")] +#[cfg_attr(docsrs, doc(cfg(feature = "webusb")))] +pub use webusb::WebUSBTransport; + +#[cfg(feature = "usb")] +#[cfg_attr(docsrs, doc(cfg(feature = "usb")))] +pub use wired::WiredUSBTransport; diff --git a/adb_client/src/message_devices/usb/webusb/mod.rs b/adb_client/src/message_devices/usb/webusb/mod.rs new file mode 100644 index 0000000..1a31833 --- /dev/null +++ b/adb_client/src/message_devices/usb/webusb/mod.rs @@ -0,0 +1,3 @@ +mod transport; + +pub use transport::WebUSBTransport; diff --git a/adb_client/src/message_devices/usb/webusb/transport.rs b/adb_client/src/message_devices/usb/webusb/transport.rs new file mode 100644 index 0000000..569e5bb --- /dev/null +++ b/adb_client/src/message_devices/usb/webusb/transport.rs @@ -0,0 +1,56 @@ +use crate::{ + adb_transport::ADBTransport, message_devices::adb_message_transport::ADBMessageTransport, + usb::ADBDeviceInfo, usb::USBTransport, +}; + +/// A transport implementation for WebUSB. +#[derive(Clone, Debug)] +pub struct WebUSBTransport {} + +impl WebUSBTransport { + /// Creates a new [`WebUSBTransport`] instance. + pub fn new() -> Self { + todo!() + } +} + +impl ADBTransport for WebUSBTransport { + fn connect(&mut self) -> crate::Result<()> { + todo!() + } + + fn disconnect(&mut self) -> crate::Result<()> { + todo!() + } +} + +impl ADBMessageTransport for WebUSBTransport { + fn read_message_with_timeout( + &mut self, + read_timeout: std::time::Duration, + ) -> crate::Result { + todo!() + } + + fn write_message_with_timeout( + &mut self, + message: crate::message_devices::adb_transport_message::ADBTransportMessage, + write_timeout: std::time::Duration, + ) -> crate::Result<()> { + todo!() + } +} + +impl USBTransport for WebUSBTransport { + fn find_all_connected_adb_devices() -> crate::Result> { + todo!() + } + + fn vendor_id(&self) -> u16 { + todo!() + } + + fn product_id(&self) -> u16 { + todo!() + } +} diff --git a/adb_client/src/message_devices/usb/wired/mod.rs b/adb_client/src/message_devices/usb/wired/mod.rs new file mode 100644 index 0000000..07fe594 --- /dev/null +++ b/adb_client/src/message_devices/usb/wired/mod.rs @@ -0,0 +1,3 @@ +mod transport; + +pub use transport::WiredUSBTransport; diff --git a/adb_client/src/message_devices/usb/wired_usb_transport.rs b/adb_client/src/message_devices/usb/wired/transport.rs similarity index 100% rename from adb_client/src/message_devices/usb/wired_usb_transport.rs rename to adb_client/src/message_devices/usb/wired/transport.rs