diff --git a/Cargo.toml b/Cargo.toml index 16aa275..fcbecf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "adb_client" -version = "0.1.0" +version = "0.1.1" description = "Rust ADB (Android Debug Bridge) client library" keywords = ["adb"] readme = "README.md" @@ -21,7 +21,8 @@ required-features = ["adbclient"] thiserror = { version = "1.0.30", default_features = false } # Features needed by adb_client binary -clap = { version = "3.0.1", default_features = false, features = ["std", "derive"], optional = true } +clap = { version = "3.0.4", default_features = false, features = ["std", "derive"], optional = true } +anyhow = { version = "1.0.52", default_features = false, features = ["std"], optional = true } [features] -adbclient = ["clap"] \ No newline at end of file +adbclient = ["clap", "anyhow"] \ No newline at end of file diff --git a/README.md b/README.md index 3019723..7197343 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,23 @@ +# adb_client - Rust ADB (Android Debug Bridge) client library + +This crate is not affiliated with Android development core team. + +## Rust crate + +Simply add this to your `Cargo.toml`: +```toml +[dependencies] +adb_client = "*" +``` + +## Rust binary + +You can install the lightweight adb binary by running the following command : +``` +cargo install adb_client --features adbclient +``` + + https://developer.android.com/studio/command-line/adb + https://github.com/cstyan/adbDocumentation diff --git a/bin/adb_client.rs b/bin/adb_client.rs index 5f14c59..418e78a 100644 --- a/bin/adb_client.rs +++ b/bin/adb_client.rs @@ -1,22 +1,15 @@ -use adb_client::{AdbCommandProvider, AdbTcpConnexion}; +use adb_client::{AdbCommandProvider, AdbTcpConnexion, Device}; +use anyhow::Result; use clap::Parser; #[derive(Parser, Debug)] #[clap(about, version, author)] struct Args { - #[clap( - short = 'a', - long = "address", - help = "Sets the listening address of ADB server", - default_value = "127.0.0.1" - )] + /// Sets the listening address of ADB server + #[clap(short = 'a', long = "address", default_value = "127.0.0.1")] pub address: String, - #[clap( - short = 'p', - long = "port", - help = "Sets the listening port of ADB server", - default_value = "5037" - )] + /// Sets the listening port of ADB server + #[clap(short = 'p', long = "port", default_value = "5037")] pub port: u16, #[clap(subcommand)] pub command: Command, @@ -24,33 +17,54 @@ struct Args { #[derive(Parser, Debug)] enum Command { + /// Prints current ADB version Version, + /// Asks ADB server to quit immediately + Kill, + /// List connected devices Devices { #[clap(short = 'l', long = "long")] long: bool, }, + /// Tracks new devices showing up + TrackDevices, } -fn main() { +fn main() -> Result<()> { let opt = Args::parse(); - let connexion = AdbTcpConnexion::new() - .address(opt.address) - .unwrap() - .port(opt.port); + let connexion = AdbTcpConnexion::new().address(opt.address)?.port(opt.port); match opt.command { Command::Version => { - connexion.version().unwrap(); + let version = connexion.version()?; + + println!("Android Debug Bridge version {}", version); + println!("Package version {}-rust", std::env!("CARGO_PKG_VERSION")); + } + Command::Kill => { + connexion.kill()?; } Command::Devices { long } => { if long { - connexion.devices_long().unwrap(); + println!("List of devices attached (long)"); + connexion.devices_long()?; } else { - for device in connexion.devices().unwrap() { + println!("List of devices attached"); + for device in connexion.devices()? { println!("{}\t{}", device.identifier, device.state); } } } + Command::TrackDevices => { + let callback = |device: Device| { + println!("{}", device); + Ok(()) + }; + println!("Live list of devices attached"); + connexion.track_devices(callback)?; + } } + + Ok(()) } diff --git a/src/adb_tcp_connexion.rs b/src/adb_tcp_connexion.rs index 79f772a..3afaca3 100644 --- a/src/adb_tcp_connexion.rs +++ b/src/adb_tcp_connexion.rs @@ -1,11 +1,12 @@ use std::{ io::{Read, Write}, net::{Ipv4Addr, SocketAddrV4, TcpStream}, + str, str::FromStr, }; use crate::{ - models::{AdbCommand, AdbRequestStatus}, + models::{AdbCommand, AdbRequestStatus, AdbVersion}, AdbCommandProvider, Device, DeviceState, Result, RustADBError, }; @@ -49,13 +50,22 @@ impl AdbTcpConnexion { let mut request_status = [0; 4]; tcp_stream.read_exact(&mut request_status)?; - match AdbRequestStatus::from_str(String::from_utf8(request_status.to_vec())?.as_str())? { + match AdbRequestStatus::from_str(str::from_utf8(&request_status.to_vec())?)? { AdbRequestStatus::Okay => { - // Read 4 bytes length - // Read exact length + let mut length = [0; 4]; + tcp_stream.read_exact(&mut length)?; - let mut body: Vec = Vec::new(); - tcp_stream.read_to_end(&mut body)?; + let u32_length = u32::from_str_radix(str::from_utf8(&length)?, 16)?; + + let mut body = vec![ + 0; + u32_length + .try_into() + .map_err(|_| RustADBError::ConvertionError)? + ]; + if u32_length > 0 { + tcp_stream.read_exact(&mut body)?; + } Ok(body) } @@ -78,12 +88,13 @@ impl Default for AdbTcpConnexion { } impl AdbCommandProvider for AdbTcpConnexion { - fn version(&self) -> Result<()> { + fn version(&self) -> Result { let version = self.proxy_connexion(AdbCommand::Version)?; - println!("Version: {:?}", version.to_ascii_uppercase()); - - Ok(()) + Ok(AdbVersion::new( + u32::from_str_radix(str::from_utf8(&version[0..2])?, 16)?, + u32::from_str_radix(str::from_utf8(&version[2..4])?, 16)?, + )) } fn devices(&self) -> Result> { @@ -96,14 +107,12 @@ impl AdbCommandProvider for AdbTcpConnexion { } let mut iter = device.split(|x| x.eq(&b'\t')); - let identifier = iter.next().unwrap(); - let state = iter.next().unwrap(); + let identifier = iter.next().ok_or(RustADBError::IteratorError)?; + let state = iter.next().ok_or(RustADBError::IteratorError)?; vec_devices.push(Device { - identifier: String::from_utf8(identifier.to_ascii_lowercase()).unwrap(), - state: DeviceState::from_str( - &String::from_utf8(state.to_ascii_lowercase()).unwrap(), - )?, + identifier: String::from_utf8(identifier.to_vec())?, + state: DeviceState::from_str(str::from_utf8(state)?)?, }); } @@ -118,11 +127,66 @@ impl AdbCommandProvider for AdbTcpConnexion { // Identifier = [0] // Device state = [1] - println!( - "Devices long: {:?}", - std::str::from_utf8(&devices_long.to_ascii_lowercase()) - ); + println!("Devices long: {:?}", std::str::from_utf8(&devices_long)); Ok(vec![]) } + + fn kill(&self) -> Result<()> { + self.proxy_connexion(AdbCommand::Kill).map(|_| ()) + } + + // TODO: Change with Generator when feature stabilizes + fn track_devices(&self, callback: fn(Device) -> Result<()>) -> Result<()> { + let mut tcp_stream = TcpStream::connect(self.socket_addr)?; + + let adb_command_string = AdbCommand::TrackDevices.to_string(); + + let adb_request = format!( + "{}{}", + format!("{:04x}", adb_command_string.len()), + adb_command_string + ); + + tcp_stream.write_all(adb_request.as_bytes())?; + + let mut request_status = [0; 4]; + tcp_stream.read_exact(&mut request_status)?; + + match AdbRequestStatus::from_str(str::from_utf8(&request_status.to_vec())?)? { + AdbRequestStatus::Okay => { + loop { + // Reads first 4 bytes indicating payload length + let mut length = [0; 4]; + tcp_stream.read_exact(&mut length)?; + + let u32_length = u32::from_str_radix(str::from_utf8(&length)?, 16)?; + + if u32_length > 0 { + let mut body = vec![ + 0; + u32_length + .try_into() + .map_err(|_| RustADBError::ConvertionError)? + ]; + tcp_stream.read_exact(&mut body)?; + + let mut iter = body.split(|x| x.eq(&b'\t')); + let identifier = iter.next().ok_or(RustADBError::IteratorError)?; + let state = iter.next().ok_or(RustADBError::IteratorError)?; + + let device = Device { + identifier: String::from_utf8(identifier.to_vec())?, + state: DeviceState::from_str( + str::from_utf8(state)?.trim_matches('\n'), + )?, + }; + + callback(device)?; + } + } + } + AdbRequestStatus::Fail => Err(RustADBError::ADBRequestFailed), + } + } } diff --git a/src/error.rs b/src/error.rs index 33fe195..2dabc11 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,7 +13,15 @@ pub enum RustADBError { #[error("Unknown device state {0}")] UnknownDeviceState(String), #[error(transparent)] - Utf8Error(#[from] std::string::FromUtf8Error), + Utf8StrError(#[from] std::str::Utf8Error), + #[error(transparent)] + Utf8StringError(#[from] std::string::FromUtf8Error), #[error(transparent)] AddrParseError(#[from] std::net::AddrParseError), + #[error(transparent)] + ParseIntError(#[from] std::num::ParseIntError), + #[error("Nothing to iterate over")] + IteratorError, + #[error("Convertion error")] + ConvertionError, } diff --git a/src/models/adb_command.rs b/src/models/adb_command.rs index 01d9fa1..d344a3a 100644 --- a/src/models/adb_command.rs +++ b/src/models/adb_command.rs @@ -1,15 +1,19 @@ pub enum AdbCommand { Version, + Kill, Devices, DevicesLong, + TrackDevices, } impl ToString for AdbCommand { fn to_string(&self) -> String { match self { AdbCommand::Version => "host:version".into(), + AdbCommand::Kill => "host:kill".into(), AdbCommand::Devices => "host:devices".into(), AdbCommand::DevicesLong => "host:devices-l".into(), + AdbCommand::TrackDevices => "host:track-devices".into(), } } } diff --git a/src/models/adb_version.rs b/src/models/adb_version.rs new file mode 100644 index 0000000..f269956 --- /dev/null +++ b/src/models/adb_version.rs @@ -0,0 +1,23 @@ +use std::fmt::Display; + +pub struct AdbVersion { + major: u32, + minor: u32, + revision: u32, +} + +impl AdbVersion { + pub fn new(minor: u32, revision: u32) -> Self { + Self { + major: 1, + minor, + revision, + } + } +} + +impl Display for AdbVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.revision) + } +} diff --git a/src/models/device.rs b/src/models/device.rs index 56b1120..6594f5e 100644 --- a/src/models/device.rs +++ b/src/models/device.rs @@ -1,6 +1,14 @@ +use std::fmt::Display; + use crate::DeviceState; pub struct Device { pub identifier: String, pub state: DeviceState, } + +impl Display for Device { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}\t{}", self.identifier, self.state) + } +} diff --git a/src/models/mod.rs b/src/models/mod.rs index f2e16ac..ee66953 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,9 +1,11 @@ mod adb_command; mod adb_request_status; +mod adb_version; mod device; mod device_state; pub use adb_command::AdbCommand; pub use adb_request_status::AdbRequestStatus; +pub use adb_version::AdbVersion; pub use device::Device; pub use device_state::DeviceState; diff --git a/src/traits/adb_command_provider.rs b/src/traits/adb_command_provider.rs index e096351..4e99288 100644 --- a/src/traits/adb_command_provider.rs +++ b/src/traits/adb_command_provider.rs @@ -1,11 +1,15 @@ -use crate::{Device, Result}; +use crate::{models::AdbVersion, Device, Result}; /// Represents the property to serve ADB commands. pub trait AdbCommandProvider { /// Gets server's internal version number. - fn version(&self) -> Result<()>; + fn version(&self) -> Result; /// Gets a list of connected devices. fn devices(&self) -> Result>; /// Gets an extended list of connected devices including the device paths in the state. fn devices_long(&self) -> Result>; + /// Asks the ADB server to quit immediately. + fn kill(&self) -> Result<()>; + /// Tracks new devices showing up. + fn track_devices(&self, callback: fn(Device) -> Result<()>) -> Result<()>; }