From 22b46cc2436eac4b350ebb6c37644da017b77612 Mon Sep 17 00:00:00 2001 From: Corentin LIAUD Date: Sun, 21 Sep 2025 10:25:19 +0200 Subject: [PATCH] feat: rework usb backend --- adb_cli/Cargo.toml | 2 +- adb_client/Cargo.toml | 2 +- adb_client/README.md | 2 +- adb_client/src/adb_device_ext.rs | 5 +- adb_client/src/error.rs | 12 +-- adb_client/src/message_devices/mod.rs | 2 - .../src/message_devices/tcp/tcp_transport.rs | 2 +- adb_client/src/message_devices/usb/README.md | 2 +- .../src/message_devices/usb/adb_usb_device.rs | 98 ++--------------- .../src/message_devices/usb/backends/mod.rs | 3 + .../rusb_transport.rs} | 17 +-- adb_client/src/message_devices/usb/mod.rs | 55 +++++++++- adb_client/src/message_devices/usb/utils.rs | 101 ++++++++++++++++++ adb_client/src/server_device/README.md | 2 +- pyadb_client/Cargo.toml | 2 +- 15 files changed, 193 insertions(+), 114 deletions(-) create mode 100644 adb_client/src/message_devices/usb/backends/mod.rs rename adb_client/src/message_devices/usb/{usb_transport.rs => backends/rusb_transport.rs} (96%) create mode 100644 adb_client/src/message_devices/usb/utils.rs diff --git a/adb_cli/Cargo.toml b/adb_cli/Cargo.toml index 6c3b0d8..0703b1b 100644 --- a/adb_cli/Cargo.toml +++ b/adb_cli/Cargo.toml @@ -11,7 +11,7 @@ rust-version.workspace = true version.workspace = true [dependencies] -adb_client = { version = "^2.0.0", features = ["mdns", "usb"] } +adb_client = { version = "^2.0.0", features = ["mdns", "rusb"] } anyhow = { version = "1.0.94" } clap = { version = "4.5.23", features = ["derive"] } env_logger = { version = "0.11.5" } diff --git a/adb_client/Cargo.toml b/adb_client/Cargo.toml index 42e85bc..1c2c134 100644 --- a/adb_client/Cargo.toml +++ b/adb_client/Cargo.toml @@ -17,7 +17,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] mdns = ["dep:mdns-sd"] -usb = ["dep:rsa", "dep:rusb"] +rusb = ["dep:rsa", "dep:rusb"] [dependencies] base64 = { version = "0.22.1" } diff --git a/adb_client/README.md b/adb_client/README.md index cc01cc9..2664051 100644 --- a/adb_client/README.md +++ b/adb_client/README.md @@ -21,7 +21,7 @@ adb_client = "*" | Feature | Description | Default? | | :-----: | :---------------------------------------------: | :------: | | `mdns` | Enables mDNS device discovery on local network. | No | -| `usb` | Enables interactions with USB devices. | No | +| `rusb` | Enables interactions with USB devices. | No | To deactivate some features you can use the `default-features = false` option in your `Cargo.toml` file and manually specify the features you want to activate: diff --git a/adb_client/src/adb_device_ext.rs b/adb_client/src/adb_device_ext.rs index 9fd4d1a..962ccaa 100644 --- a/adb_client/src/adb_device_ext.rs +++ b/adb_client/src/adb_device_ext.rs @@ -6,7 +6,10 @@ use image::{ImageBuffer, ImageFormat, Rgba}; use crate::models::AdbStatResponse; use crate::{RebootType, Result}; -/// Trait representing all features available on both [`crate::server_device::ADBServerDevice`] and [`crate::usb::ADBUSBDevice`] +/// Trait representing all features available on an ADB device, currently used by: +/// - [`crate::server_device::ADBServerDevice`] +/// - [`crate::usb::ADBUSBDevice`] +/// - [`crate::tcp::ADBTcpDevice`] pub trait ADBDeviceExt { /// Runs command in a shell on the device, and write its output and error streams into output. fn shell_command(&mut self, command: &[&str], output: &mut dyn Write) -> Result<()>; diff --git a/adb_client/src/error.rs b/adb_client/src/error.rs index 3f32b29..9616f53 100644 --- a/adb_client/src/error.rs +++ b/adb_client/src/error.rs @@ -67,8 +67,8 @@ pub enum RustADBError { #[error("Cannot get home directory")] NoHomeDirectory, /// Generic USB error - #[cfg(feature = "usb")] - #[cfg_attr(docsrs, doc(cfg(feature = "usb")))] + #[cfg(feature = "rusb")] + #[cfg_attr(docsrs, doc(cfg(feature = "rusb")))] #[error("USB Error: {0}")] UsbError(#[from] rusb::Error), /// USB device not found @@ -87,8 +87,8 @@ pub enum RustADBError { #[error(transparent)] Base64EncodeError(#[from] base64::EncodeSliceError), /// An error occurred with RSA engine - #[cfg(feature = "usb")] - #[cfg_attr(docsrs, doc(cfg(feature = "usb")))] + #[cfg(feature = "rusb")] + #[cfg_attr(docsrs, doc(cfg(feature = "rusb")))] #[error(transparent)] RSAError(#[from] rsa::errors::Error), /// Cannot convert given data from slice @@ -98,8 +98,8 @@ pub enum RustADBError { #[error("wrong file extension: {0}")] WrongFileExtension(String), /// An error occurred with PKCS8 data - #[cfg(feature = "usb")] - #[cfg_attr(docsrs, doc(cfg(feature = "usb")))] + #[cfg(feature = "rusb")] + #[cfg_attr(docsrs, doc(cfg(feature = "rusb")))] #[error("error with pkcs8: {0}")] RsaPkcs8Error(#[from] rsa::pkcs8::Error), /// Error during certificate generation diff --git a/adb_client/src/message_devices/mod.rs b/adb_client/src/message_devices/mod.rs index f7a105d..42a2f09 100644 --- a/adb_client/src/message_devices/mod.rs +++ b/adb_client/src/message_devices/mod.rs @@ -1,6 +1,4 @@ /// USB-related definitions -#[cfg(feature = "usb")] -#[cfg_attr(docsrs, doc(cfg(feature = "usb")))] pub mod usb; /// Device reachable over TCP related definition diff --git a/adb_client/src/message_devices/tcp/tcp_transport.rs b/adb_client/src/message_devices/tcp/tcp_transport.rs index 7e7379e..9cbbc1b 100644 --- a/adb_client/src/message_devices/tcp/tcp_transport.rs +++ b/adb_client/src/message_devices/tcp/tcp_transport.rs @@ -80,7 +80,7 @@ impl Write for CurrentConnection { } } -/// Transport running on USB +/// Transport running on TCP #[derive(Clone, Debug)] pub struct TcpTransport { address: SocketAddr, diff --git a/adb_client/src/message_devices/usb/README.md b/adb_client/src/message_devices/usb/README.md index bc9bfd9..c1414c1 100644 --- a/adb_client/src/message_devices/usb/README.md +++ b/adb_client/src/message_devices/usb/README.md @@ -21,6 +21,6 @@ use std::path::Path; let vendor_id = 0x04e8; let product_id = 0x6860; let mut device = ADBUSBDevice::new(vendor_id, product_id).expect("cannot find device"); -let mut input = File::open(Path::new("/tmp/file.txt")).expect("Cannot open file"); +let mut input = File::open("/tmp/file.txt").expect("Cannot open file"); device.push(&mut input, &"/data/local/tmp"); ``` 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 4944955..173d73c 100644 --- a/adb_client/src/message_devices/usb/adb_usb_device.rs +++ b/adb_client/src/message_devices/usb/adb_usb_device.rs @@ -1,12 +1,6 @@ -use rusb::Device; -use rusb::DeviceDescriptor; -use rusb::UsbContext; -use rusb::constants::LIBUSB_CLASS_VENDOR_SPEC; -use std::fs::read_to_string; use std::io::Read; use std::io::Write; -use std::path::Path; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::Duration; use crate::ADBDeviceExt; @@ -18,92 +12,20 @@ use crate::message_devices::adb_message_transport::ADBMessageTransport; use crate::message_devices::adb_transport_message::ADBTransportMessage; use crate::message_devices::message_commands::MessageCommand; use crate::usb::adb_rsa_key::ADBRsaKey; -use crate::usb::usb_transport::USBTransport; +use crate::usb::backends::rusb_transport::RusbTransport; +use crate::usb::{read_adb_private_key, search_adb_devices}; use crate::utils::get_default_adb_key_path; const AUTH_TOKEN: u32 = 1; const AUTH_SIGNATURE: u32 = 2; const AUTH_RSAPUBLICKEY: u32 = 3; -pub fn read_adb_private_key>(private_key_path: P) -> Result> { - // Try to read the private key file from given path - // If the file is not found, return None - // If there is another error while reading the file, return this error - // Else, return the private key content - let pk = match read_to_string(private_key_path.as_ref()) { - Ok(pk) => pk, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(e) => return Err(e.into()), - }; - - match ADBRsaKey::new_from_pkcs8(&pk) { - Ok(pk) => Ok(Some(pk)), - Err(e) => Err(e), - } -} - -/// Search for adb devices with known interface class and subclass values -pub fn search_adb_devices() -> Result> { - let mut found_devices = vec![]; - for device in rusb::devices()?.iter() { - let Ok(des) = device.device_descriptor() else { - continue; - }; - if is_adb_device(&device, &des) { - log::debug!( - "Autodetect device {:04x}:{:04x}", - des.vendor_id(), - des.product_id() - ); - found_devices.push((des.vendor_id(), des.product_id())); - } - } - - match (found_devices.first(), found_devices.get(1)) { - (None, _) => Ok(None), - (Some(identifiers), None) => Ok(Some(*identifiers)), - (Some((vid1, pid1)), Some((vid2, pid2))) => Err(RustADBError::DeviceNotFound(format!( - "Found two Android devices {vid1:04x}:{pid1:04x} and {vid2:04x}:{pid2:04x}", - ))), - } -} - -/// Check whether a device with given descriptor is an ADB device -pub 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 -} - +/// Implement Android USB device /// Represent a device reached and available over USB. #[derive(Debug)] pub struct ADBUSBDevice { private_key: ADBRsaKey, - inner: ADBMessageDevice, + inner: ADBMessageDevice, } impl ADBUSBDevice { @@ -118,12 +40,12 @@ impl ADBUSBDevice { product_id: u16, private_key_path: PathBuf, ) -> Result { - Self::new_from_transport_inner(USBTransport::new(vendor_id, product_id)?, private_key_path) + Self::new_from_transport_inner(RusbTransport::new(vendor_id, product_id)?, private_key_path) } - /// Instantiate a new [`ADBUSBDevice`] from a [`USBTransport`] and an optional private key path. + /// Instantiate a new [`ADBUSBDevice`] from a [`RusbTransport`] and an optional private key path. pub fn new_from_transport( - transport: USBTransport, + transport: RusbTransport, private_key_path: Option, ) -> Result { let private_key_path = match private_key_path { @@ -135,7 +57,7 @@ impl ADBUSBDevice { } fn new_from_transport_inner( - transport: USBTransport, + transport: RusbTransport, private_key_path: PathBuf, ) -> Result { let private_key = match read_adb_private_key(&private_key_path)? { @@ -247,7 +169,7 @@ impl ADBUSBDevice { } #[inline] - fn get_transport_mut(&mut self) -> &mut USBTransport { + fn get_transport_mut(&mut self) -> &mut RusbTransport { self.inner.get_transport_mut() } } diff --git a/adb_client/src/message_devices/usb/backends/mod.rs b/adb_client/src/message_devices/usb/backends/mod.rs new file mode 100644 index 0000000..11c0af5 --- /dev/null +++ b/adb_client/src/message_devices/usb/backends/mod.rs @@ -0,0 +1,3 @@ +#[cfg(feature = "rusb")] +#[cfg_attr(docsrs, doc(cfg(feature = "rusb")))] +pub mod rusb_transport; diff --git a/adb_client/src/message_devices/usb/usb_transport.rs b/adb_client/src/message_devices/usb/backends/rusb_transport.rs similarity index 96% rename from adb_client/src/message_devices/usb/usb_transport.rs rename to adb_client/src/message_devices/usb/backends/rusb_transport.rs index d1ccfe4..41e72b9 100644 --- a/adb_client/src/message_devices/usb/usb_transport.rs +++ b/adb_client/src/message_devices/usb/backends/rusb_transport.rs @@ -13,6 +13,7 @@ use crate::{ adb_transport_message::{ADBTransportMessage, ADBTransportMessageHeader}, message_commands::MessageCommand, }, + usb::constants::class_codes::ADB_SUBCLASS, }; #[derive(Clone, Debug)] @@ -22,17 +23,17 @@ struct Endpoint { max_packet_size: usize, } -/// Transport running on USB +/// Transport running on USB using `rusb` as a backend. #[derive(Debug, Clone)] -pub struct USBTransport { +pub struct RusbTransport { device: Device, handle: Option>>, read_endpoint: Option, write_endpoint: Option, } -impl USBTransport { - /// Instantiate a new [`USBTransport`]. +impl RusbTransport { + /// Instantiate a new [`RusbTransport`]. /// Only the first device with given vendor_id and product_id is returned. pub fn new(vendor_id: u16, product_id: u16) -> Result { for device in rusb::devices()?.iter() { @@ -48,7 +49,7 @@ impl USBTransport { ))) } - /// Instantiate a new [`USBTransport`] from a [`rusb::Device`]. + /// Instantiate a new [`RusbTransport`] from a [`rusb::Device`]. /// /// Devices can be enumerated using [`rusb::devices()`] and then filtered out to get desired device. pub fn new_from_device(rusb_device: rusb::Device) -> Self { @@ -109,7 +110,7 @@ impl USBTransport { 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.sub_class_code() == ADB_SUBCLASS && interface_desc.protocol_code() == 0x01 { let endpoint = Endpoint { @@ -166,7 +167,7 @@ impl USBTransport { } } -impl ADBTransport for USBTransport { +impl ADBTransport for RusbTransport { fn connect(&mut self) -> crate::Result<()> { let device = self.device.open()?; @@ -205,7 +206,7 @@ impl ADBTransport for USBTransport { } } -impl ADBMessageTransport for USBTransport { +impl ADBMessageTransport for RusbTransport { fn write_message_with_timeout( &mut self, message: ADBTransportMessage, diff --git a/adb_client/src/message_devices/usb/mod.rs b/adb_client/src/message_devices/usb/mod.rs index 6463a93..8f76c62 100644 --- a/adb_client/src/message_devices/usb/mod.rs +++ b/adb_client/src/message_devices/usb/mod.rs @@ -1,8 +1,59 @@ #![doc = include_str!("./README.md")] +/// Common USB constants for Android Debug Bridge +pub mod constants { + /// Standard Android vendor ID + pub const ANDROID_VENDOR_ID: u16 = 0x18d1; + + /// Common ADB product IDs + pub mod product_ids { + /// ADB interface + pub const ADB: u16 = 0x4ee7; + /// ADB + MTP + pub const ADB_MTP: u16 = 0x4ee2; + /// ADB + RNDIS + pub const ADB_RNDIS: u16 = 0x4ee4; + /// Fastboot interface + pub const FASTBOOT: u16 = 0x4ee0; + } + + /// USB class codes for ADB detection + pub mod class_codes { + /// ADB subclass code + pub const ADB_SUBCLASS: u8 = 0x42; + /// ADB protocol code + pub const ADB_PROTOCOL: u8 = 0x1; + /// Bulk transfer class + pub const BULK_CLASS: u8 = 0xdc; + /// Bulk ADB subclass + pub const BULK_ADB_SUBCLASS: u8 = 2; + } +} + +#[cfg(feature = "rusb")] mod adb_rsa_key; + +#[cfg(feature = "rusb")] mod adb_usb_device; -mod usb_transport; +mod backends; +mod utils; + +// Device implementations +#[cfg(feature = "rusb")] +#[cfg_attr(docsrs, doc(cfg(feature = "rusb")))] pub use adb_usb_device::ADBUSBDevice; -pub use usb_transport::USBTransport; + +// Transport implementations +#[cfg(feature = "rusb")] +#[cfg_attr(docsrs, doc(cfg(feature = "rusb")))] +pub use backends::rusb_transport::RusbTransport; + +// Utility functions +#[cfg(feature = "rusb")] +#[cfg_attr(docsrs, doc(cfg(feature = "rusb")))] +pub use utils::read_adb_private_key; + +#[cfg(feature = "rusb")] +#[cfg_attr(docsrs, doc(cfg(feature = "rusb")))] +pub use utils::{is_adb_device, search_adb_devices}; diff --git a/adb_client/src/message_devices/usb/utils.rs b/adb_client/src/message_devices/usb/utils.rs new file mode 100644 index 0000000..b54f35a --- /dev/null +++ b/adb_client/src/message_devices/usb/utils.rs @@ -0,0 +1,101 @@ +//! USB utilities that are independent of specific transport implementations + +use crate::{Result, RustADBError}; +use std::fs::read_to_string; +use std::path::Path; + +#[cfg(feature = "rusb")] +use crate::usb::adb_rsa_key::ADBRsaKey; + +#[cfg(feature = "rusb")] +use rusb::{Device, DeviceDescriptor, UsbContext}; + +#[cfg(feature = "rusb")] +use rusb::constants::LIBUSB_CLASS_VENDOR_SPEC; + +use crate::usb::constants::class_codes::{ + ADB_PROTOCOL, ADB_SUBCLASS, BULK_ADB_SUBCLASS, BULK_CLASS, +}; + +/// Read an ADB private key from a file path +/// +/// Returns `Ok(None)` if the file doesn't exist, `Ok(Some(key))` if the key was successfully loaded, +/// or an error if there was a problem reading the file. +#[cfg(feature = "rusb")] +pub fn read_adb_private_key>(private_key_path: P) -> Result> { + // Try to read the private key file from given path + // If the file is not found, return None + // If there is another error while reading the file, return this error + // Else, return the private key content + let pk = match read_to_string(private_key_path.as_ref()) { + Ok(pk) => pk, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(e.into()), + }; + + match ADBRsaKey::new_from_pkcs8(&pk) { + Ok(pk) => Ok(Some(pk)), + Err(e) => Err(e), + } +} + +/// Search for ADB devices connected via USB +/// +/// Returns the vendor_id and product_id of the first ADB device found, +/// or `None` if no devices are found. +#[cfg(feature = "rusb")] +pub fn search_adb_devices() -> Result> { + let mut found_devices = vec![]; + for device in rusb::devices()?.iter() { + let Ok(des) = device.device_descriptor() else { + continue; + }; + if is_adb_device(&device, &des) { + log::debug!( + "Autodetect device {:04x}:{:04x}", + des.vendor_id(), + des.product_id() + ); + found_devices.push((des.vendor_id(), des.product_id())); + } + } + + match (found_devices.first(), found_devices.get(1)) { + (None, _) => Ok(None), + (Some(identifiers), None) => Ok(Some(*identifiers)), + (Some((vid1, pid1)), Some((vid2, pid2))) => Err(RustADBError::DeviceNotFound(format!( + "Found two Android devices {vid1:04x}:{pid1:04x} and {vid2:04x}:{pid2:04x}", + ))), + } +} + +/// Check if a USB device is an ADB device +/// +/// This function inspects the device descriptor and configuration to determine +/// if it's an Android Debug Bridge device. +#[cfg(feature = "rusb")] +pub fn is_adb_device(device: &Device, des: &DeviceDescriptor) -> bool { + // Some devices require choosing the file transfer mode + // for usb debugging to take effect. + 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/server_device/README.md b/adb_client/src/server_device/README.md index 27bf621..c7bab3e 100644 --- a/adb_client/src/server_device/README.md +++ b/adb_client/src/server_device/README.md @@ -20,6 +20,6 @@ use std::path::Path; let mut server = ADBServer::default(); let mut device = server.get_device().expect("cannot get device"); -let mut input = File::open(Path::new("/tmp/file.txt")).expect("Cannot open file"); +let mut input = File::open("/tmp/file.txt").expect("Cannot open file"); device.push(&mut input, "/data/local/tmp"); ``` diff --git a/pyadb_client/Cargo.toml b/pyadb_client/Cargo.toml index 0b107e7..21a0721 100644 --- a/pyadb_client/Cargo.toml +++ b/pyadb_client/Cargo.toml @@ -20,7 +20,7 @@ doc = false name = "stub_gen" [dependencies] -adb_client = { path = "../adb_client", features = ["usb"] } +adb_client = { path = "../adb_client", features = ["rusb"] } anyhow = { version = "1.0.95" } pyo3 = { version = "0.25.0", features = ["abi3-py37", "anyhow"] } pyo3-stub-gen = "0.7.0"