18 Commits

Author SHA1 Message Date
LIAUD Corentin
83d716d685 release: 1.0.6 2024-09-13 13:56:42 +02:00
Samuel Cavalcanti
101fafe4ec Fix pairing (#30)
* fix: pairing code that starts with 0

* feat: add`no_run` end `std::io::stdout` to pass in examples
---------

Co-authored-by: LIAUD Corentin <corentin.liaud@orange.fr>
2024-09-13 13:49:46 +02:00
LIAUD Corentin
251276c766 feat: add logcat + log crate 2024-09-05 16:54:39 +02:00
LIAUD Corentin
d00290c450 feat: release adb_cli version 1.0.4 2024-08-09 17:25:53 +02:00
LIAUD Corentin
265971bd6d bump: version 1.0.4 2024-08-09 17:20:57 +02:00
LIAUD Corentin
7abfa451d2 feat: add emu commands interface 2024-08-09 15:32:51 +02:00
LIAUD Corentin
fbb65373a8 fix: windows server start 2024-08-02 10:51:44 +02:00
LIAUD Corentin
aa22472952 feat: license; start server if not running 2024-08-02 10:49:22 +02:00
LIAUD Corentin
2919003a9b feat: add framebuffer command 2024-07-19 14:09:10 +02:00
LIAUD Corentin
1a62105565 feat: remove serial param from functions 2024-07-17 21:48:24 +02:00
LIAUD Corentin
1805c60e32 feat: add disconnect + rework with new archi 2024-07-17 20:43:11 +02:00
Valentin MILLET
24a6e49ab8 Remove useless error when connecting device 2024-07-17 20:43:11 +02:00
Valentin MILLET
13b69120f5 Fix regex for displaying devices in long format with IP:PORT 2024-07-17 20:43:11 +02:00
Valentin MILLET
a7b4cf7d80 Add pair and connect command 2024-07-17 20:43:11 +02:00
LIAUD Corentin
1342b4c34a feat: rework interface to add new transports 2024-07-17 20:07:52 +02:00
LIAUD Corentin
8a682f7472 feat: improve TCP connection management 2024-07-15 16:31:57 +02:00
LIAUD Corentin
3d3546106e feat: add AdbTcpConnection::new_with_default method 2024-07-15 15:25:21 +02:00
LIAUD Corentin
c6ffa2ff6b fix: Error message handling when FAIL 2024-07-15 10:33:51 +02:00
68 changed files with 1875 additions and 800 deletions

View File

@@ -1,32 +1,13 @@
[package]
description = "Rust ADB (Android Debug Bridge) client library"
[workspace]
members = ["adb_cli", "adb_client"]
resolver = "2"
[workspace.package]
edition = "2021"
keywords = ["adb", "android"]
license = "MIT"
name = "adb_client"
readme = "README.md"
repository = "https://github.com/cocool97/adb_client"
version = "1.0.3"
version = "1.0.6"
[lib]
name = "adb_client"
path = "src/lib.rs"
[[example]]
name = "adb_cli"
path = "examples/adb_cli.rs"
[dependencies]
byteorder = { version = "1.5.0" }
chrono = { version = "0.4.38" }
lazy_static = { version = "1.5.0" }
mio = { version = "1.0.0", features = ["os-ext", "os-poll"] }
regex = { version = "1.10.5", features = ["perf", "std", "unicode"] }
termios = { version = "0.3.3" }
thiserror = { version = "1.0.61" }
## Binary-only dependencies
## Marked as optional so that lib users do not depend on them
[dev-dependencies]
clap = { version = "4.5.4", features = ["derive"] }
rand = { version = "0.8.5" }
# To build locally when working on a new version
[patch.crates-io]
adb_client = { path = "./adb_client" }

View File

@@ -1,72 +1,31 @@
# adb_client
# Rust adb_client
Android Debug Bridge (ADB) client implementation in pure Rust !
[![Latest version](https://img.shields.io/crates/v/adb_client.svg)](https://crates.io/crates/adb_client)
[![dependency status](https://deps.rs/repo/github/cocool97/adb_client/status.svg)](https://deps.rs/repo/github/cocool97/adb_client)
**A**ndroid **D**ebug **B**ridge (ADB) client implementation in pure Rust !
Main features :
- Full Rust, no need to use shell commands
- Full Rust, no need to use `adb *` shell commands
- Currently only support server TCP/IP protocol
- Highly configurable
- Easy to use !
## Examples
## adb_client
First declare `adb_client` as a dependency by simply adding this to your `Cargo.toml`:
Rust library implementing ADB protocol and providing high-level abstraction over commands.
```toml
[dependencies]
adb_client = "*"
```
Improved documentation [here](./adb_client/README.md).
### Launch a command on host device
## adb_cli
```rust
use adb_client::AdbTcpConnection;
use std::net::Ipv4Addr;
Rust binary providing an improved version of `adb` CLI, using `adb_client` library. Can be used as an usage example of the library.
let mut connection = AdbTcpConnection::new(Ipv4Addr::from([127,0,0,1]), 5037).unwrap();
connection.shell_command(None, ["df", "-h"]);
```
### Get available ADB devices
```rust
use adb_client::AdbTcpConnection;
use std::net::Ipv4Addr;
let mut connection = AdbTcpConnection::new(Ipv4Addr::from([127,0,0,1]), 5037).unwrap();
connection.devices();
```
### Push a file to the device
```rust
use adb_client::AdbTcpConnection;
use std::net::Ipv4Addr;
use std::fs::File;
use std::path::Path;
let mut connection = AdbTcpConnection::new(Ipv4Addr::from([127,0,0,1]), 5037).unwrap();
let mut input = File::open(Path::new("/tmp")).unwrap();
connection.send::<&str,&str>(None, &mut input, "/data/local/tmp");
```
## Rust binary
This crate also provides a lightweight binary based on the `adb_client` crate. You can install it by running the following command :
```shell
cargo install adb_client --example adb_cli
```
Improved documentation [here](./adb_cli/README.md).
## Missing features
- USB protocol
- USB protocol (Work in progress)
All pull requests are welcome !
## Documentation
- <https://developer.android.com/studio/command-line/adb>
- <https://github.com/cstyan/adbDocumentation>

16
adb_cli/Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
description = "Rust ADB (Android Debug Bridge) CLI"
edition.workspace = true
keywords = ["adb", "android"]
license.workspace = true
name = "adb_cli"
readme = "README.md"
repository.workspace = true
version.workspace = true
[dependencies]
adb_client = { version = "1.0.6" }
anyhow = { version = "1.0.86" }
clap = { version = "4.5.17", features = ["derive"] }
env_logger = { version = "0.11.5" }
log = "0.4.22"

51
adb_cli/README.md Normal file
View File

@@ -0,0 +1,51 @@
# adb_cli
[![MIT licensed](https://img.shields.io/crates/l/adb_cli.svg)](./LICENSE-MIT)
![Crates.io Total Downloads](https://img.shields.io/crates/d/adb_cli)
Rust binary providing an improved version of `adb` CLI.
## Rust binary
This crate provides a lightweight binary based on the `adb_client` crate. You can install it by running the following command :
```shell
cargo install adb_cli
```
Usage is quite simple, and tends to look like `adb`:
```bash
user@laptop ~/adb_client (main)> adb_cli --help
Rust ADB (Android Debug Bridge) CLI
Usage: adb_cli [OPTIONS] <COMMAND>
Commands:
host-features List available server features
push Push a file on device
pull Pull a file from device
list List a directory on device
stat Stat a file specified on device
shell Spawn an interactive shell or run a list of commands on the device
reboot Reboot the device
framebuffer Dump framebuffer of device
logcat Get logs of device
version Print current ADB version
kill Ask ADB server to quit immediately
devices List connected devices
track-devices Track new devices showing up
pair Pair device with a given code
connect Connect device over WI-FI
disconnect Disconnect device over WI-FI
sms Send a SMS with given phone number and given content
rotate Rotate device screen from 90°
help Print this message or the help of the given subcommand(s)
Options:
-d, --debug
-a, --address <ADDRESS> [default: 127.0.0.1:5037]
-s, --serial <SERIAL> Serial id of a specific device. Every request will be sent to this device
-h, --help Print help
-V, --version Print version
```

View File

@@ -0,0 +1,12 @@
use clap::Parser;
#[derive(Parser, Debug)]
pub enum EmuCommand {
/// Send a SMS with given phone number and given content
Sms {
phone_number: String,
content: String,
},
/// Rotate device screen from 90°
Rotate,
}

View File

@@ -0,0 +1,24 @@
use std::net::SocketAddrV4;
use clap::Parser;
#[derive(Parser, Debug)]
pub enum HostCommand {
/// Print current ADB version.
Version,
/// Ask ADB server to quit immediately.
Kill,
/// List connected devices.
Devices {
#[clap(short = 'l', long = "long")]
long: bool,
},
/// Track new devices showing up.
TrackDevices,
/// Pair device with a given code
Pair { address: SocketAddrV4, code: String },
/// Connect device over WI-FI
Connect { address: SocketAddrV4 },
/// Disconnect device over WI-FI
Disconnect { address: SocketAddrV4 },
}

View File

@@ -0,0 +1,31 @@
use clap::Parser;
use crate::models::RebootTypeCommand;
#[derive(Parser, Debug)]
pub enum LocalCommand {
/// List available server features.
HostFeatures,
/// Push a file on device
Push { filename: String, path: String },
/// Pull a file from device
Pull { path: String, filename: String },
/// List a directory on device
List { path: String },
/// Stat a file specified on device
Stat { path: String },
/// Spawn an interactive shell or run a list of commands on the device
Shell { command: Vec<String> },
/// Reboot the device
Reboot {
#[clap(subcommand)]
sub_command: RebootTypeCommand,
},
/// Dump framebuffer of device
Framebuffer { path: String },
/// Get logs of device
Logcat {
/// Path to output file (created if not exists)
path: Option<String>,
},
}

View File

@@ -0,0 +1,7 @@
mod emu;
mod host;
mod local;
pub use emu::EmuCommand;
pub use host::HostCommand;
pub use local::LocalCommand;

156
adb_cli/src/main.rs Normal file
View File

@@ -0,0 +1,156 @@
mod commands;
mod models;
use adb_client::{ADBEmulatorDevice, ADBServer, DeviceShort};
use anyhow::{anyhow, Result};
use clap::Parser;
use commands::{EmuCommand, HostCommand, LocalCommand};
use env_logger::Builder;
use log::LevelFilter;
use models::{Command, Opts};
use std::fs::File;
use std::io::Write;
use std::path::Path;
fn main() -> Result<()> {
let opt = Opts::parse();
let max_level = if opt.verbose {
LevelFilter::Trace
} else {
LevelFilter::Info
};
let mut builder = Builder::default();
builder.filter_level(max_level);
builder.init();
match opt.command {
Command::Local(local) => {
let mut adb_server = ADBServer::new(opt.address);
let mut device = match opt.serial {
Some(serial) => adb_server.get_device_by_name(&serial)?,
None => adb_server.get_device()?,
};
match local {
LocalCommand::Pull { path, filename } => {
let mut output = File::create(Path::new(&filename))?;
device.recv(&path, &mut output)?;
log::info!("Downloaded {path} as {filename}");
}
LocalCommand::Push { filename, path } => {
let mut input = File::open(Path::new(&filename))?;
device.send(&mut input, &path)?;
log::info!("Uploaded {filename} to {path}");
}
LocalCommand::List { path } => {
device.list(path)?;
}
LocalCommand::Stat { path } => {
let stat_response = device.stat(path)?;
log::info!("{}", stat_response);
}
LocalCommand::Shell { command } => {
if command.is_empty() {
device.shell()?;
} else {
device.shell_command(command, std::io::stdout())?;
}
}
LocalCommand::HostFeatures => {
let features = device
.host_features()?
.iter()
.map(|v| v.to_string())
.reduce(|a, b| format!("{a},{b}"))
.ok_or(anyhow!("cannot list features"))?;
log::info!("Available host features: {features}");
}
LocalCommand::Reboot { sub_command } => {
log::info!("Reboots device");
device.reboot(sub_command.into())?
}
LocalCommand::Framebuffer { path } => {
device.framebuffer(&path)?;
log::info!("Framebuffer dropped: {path}");
}
LocalCommand::Logcat { path } => {
let writer: Box<dyn Write> = if let Some(path) = path {
let f = File::create(path)?;
Box::new(f)
} else {
Box::new(std::io::stdout())
};
device.get_logs(writer)?;
}
}
}
Command::Host(host) => {
let mut adb_server = ADBServer::new(opt.address);
match host {
HostCommand::Version => {
let version = adb_server.version()?;
log::info!("Android Debug Bridge version {}", version);
log::info!("Package version {}-rust", std::env!("CARGO_PKG_VERSION"));
}
HostCommand::Kill => {
adb_server.kill()?;
}
HostCommand::Devices { long } => {
if long {
log::info!("List of devices attached (extended)");
for device in adb_server.devices_long()? {
log::info!("{}", device);
}
} else {
log::info!("List of devices attached");
for device in adb_server.devices()? {
log::info!("{}", device);
}
}
}
HostCommand::TrackDevices => {
let callback = |device: DeviceShort| {
log::info!("{}", device);
Ok(())
};
log::info!("Live list of devices attached");
adb_server.track_devices(callback)?;
}
HostCommand::Pair { address, code } => {
adb_server.pair(address, code)?;
log::info!("Paired device {address}");
}
HostCommand::Connect { address } => {
adb_server.connect_device(address)?;
log::info!("Connected to {address}");
}
HostCommand::Disconnect { address } => {
adb_server.disconnect_device(address)?;
log::info!("Disconnected {address}");
}
}
}
Command::Emu(emu) => {
let mut emulator = match opt.serial {
Some(serial) => ADBEmulatorDevice::new(serial, None)?,
None => return Err(anyhow!("Serial must be set to use emulators !")),
};
match emu {
EmuCommand::Sms {
phone_number,
content,
} => {
emulator.send_sms(&phone_number, &content)?;
log::info!("SMS sent to {phone_number}");
}
EmuCommand::Rotate => emulator.rotate()?,
}
}
}
Ok(())
}

View File

@@ -0,0 +1,5 @@
mod opts;
mod reboot_type;
pub use opts::{Command, Opts};
pub use reboot_type::RebootTypeCommand;

View File

@@ -0,0 +1,29 @@
use std::net::SocketAddrV4;
use clap::Parser;
use crate::commands::{EmuCommand, HostCommand, LocalCommand};
#[derive(Parser, Debug)]
#[clap(about, version, author)]
pub struct Opts {
#[clap(short = 'v', long = "verbose")]
pub verbose: bool,
#[clap(short = 'a', long = "address", default_value = "127.0.0.1:5037")]
pub address: SocketAddrV4,
/// Serial id of a specific device. Every request will be sent to this device.
#[clap(short = 's', long = "serial")]
pub serial: Option<String>,
#[clap(subcommand)]
pub command: Command,
}
#[derive(Parser, Debug)]
pub enum Command {
#[clap(flatten)]
Local(LocalCommand),
#[clap(flatten)]
Host(HostCommand),
#[clap(flatten)]
Emu(EmuCommand),
}

View File

@@ -0,0 +1,23 @@
use adb_client::RebootType;
use clap::Parser;
#[derive(Parser, Debug)]
pub enum RebootTypeCommand {
System,
Bootloader,
Recovery,
Sideload,
SideloadAutoReboot,
}
impl From<RebootTypeCommand> for RebootType {
fn from(value: RebootTypeCommand) -> Self {
match value {
RebootTypeCommand::System => RebootType::System,
RebootTypeCommand::Bootloader => RebootType::Bootloader,
RebootTypeCommand::Recovery => RebootType::Recovery,
RebootTypeCommand::Sideload => RebootType::Sideload,
RebootTypeCommand::SideloadAutoReboot => RebootType::SideloadAutoReboot,
}
}
}

21
adb_client/Cargo.toml Normal file
View File

@@ -0,0 +1,21 @@
[package]
description = "Rust ADB (Android Debug Bridge) client library"
edition.workspace = true
keywords = ["adb", "android"]
license.workspace = true
name = "adb_client"
readme = "README.md"
repository.workspace = true
version.workspace = true
[dependencies]
byteorder = { version = "1.5.0" }
chrono = { version = "0.4.38" }
homedir = "0.3.3"
image = { version = "0.25.2" }
lazy_static = { version = "1.5.0" }
log = "0.4.22"
mio = { version = "1.0.2", features = ["os-ext", "os-poll"] }
regex = { version = "1.10.6", features = ["perf", "std", "unicode"] }
termios = { version = "0.3.3" }
thiserror = { version = "1.0.63" }

56
adb_client/README.md Normal file
View File

@@ -0,0 +1,56 @@
# adb_client
[![MIT licensed](https://img.shields.io/crates/l/adb_client.svg)](./LICENSE-MIT)
[![Documentation](https://docs.rs/adb_client/badge.svg)](https://docs.rs/adb_client)
![Crates.io Total Downloads](https://img.shields.io/crates/d/adb_client)
Rust library implementing ADB protocol.
## Installation
Add `adb_client` crate as a dependency by simply adding it to your `Cargo.toml`:
```toml
[dependencies]
adb_client = "*"
```
## Examples
### Launch a command on device via ADB server
```rust no_run
use adb_client::ADBServer;
let mut server = ADBServer::default();
let mut device = server.get_device().expect("cannot get device");
device.shell_command(["df", "-h"],std::io::stdout());
```
### Get available ADB devices
```rust no_run
use adb_client::ADBServer;
use std::net::{SocketAddrV4, Ipv4Addr};
// A custom server address can be provided
let server_ip = Ipv4Addr::new(127, 0, 0, 1);
let server_port = 5037;
let mut server = ADBServer::new(SocketAddrV4::new(server_ip, server_port));
server.devices();
```
### Push a file to the device
```rust no_run
use adb_client::ADBServer;
use std::net::Ipv4Addr;
use std::fs::File;
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")).unwrap();
device.send(&mut input, "/data/local/tmp");
```

View File

@@ -0,0 +1,2 @@
mod rotate;
mod sms;

View File

@@ -0,0 +1,10 @@
use crate::{models::ADBEmulatorCommand, ADBEmulatorDevice, Result};
impl ADBEmulatorDevice {
/// Send a SMS to this emulator with given content with given phone number
pub fn rotate(&mut self) -> Result<()> {
let transport = self.connect()?;
transport.send_command(ADBEmulatorCommand::Rotate)?;
Ok(())
}
}

View File

@@ -0,0 +1,13 @@
use crate::{models::ADBEmulatorCommand, ADBEmulatorDevice, Result};
impl ADBEmulatorDevice {
/// Send a SMS to this emulator with given content with given phone number
pub fn send_sms(&mut self, phone_number: &str, content: &str) -> Result<()> {
let transport = self.connect()?;
transport.send_command(ADBEmulatorCommand::Sms(
phone_number.to_string(),
content.to_string(),
))?;
Ok(())
}
}

View File

@@ -42,4 +42,19 @@ pub enum RustADBError {
/// Remote ADB server does not support shell feature.
#[error("Remote ADB server does not support shell feature")]
ADBShellNotSupported,
/// Desired device has not been found
#[error("Device not found: {0}")]
DeviceNotFound(String),
/// Indicates that the device must be paired before attempting a connection over WI-FI
#[error("Device not paired before attempting to connect")]
ADBDeviceNotPaired,
/// An error occurred when getting device's framebuffer image
#[error(transparent)]
FramebufferImageError(#[from] image::error::ImageError),
/// An error occurred when converting framebuffer content
#[error("Cannot convert framebuffer into image")]
FramebufferConversionError,
/// An error occurred while getting user's home directory
#[error(transparent)]
HomeError(#[from] homedir::GetHomeError),
}

View File

@@ -4,11 +4,15 @@
#![forbid(missing_docs)]
#![doc = include_str!("../README.md")]
mod adb_tcp_connection;
mod adb_termios;
mod commands;
mod emulator;
mod error;
mod models;
pub use adb_tcp_connection::AdbTcpConnection;
mod server;
mod transports;
mod utils;
pub use error::{Result, RustADBError};
pub use models::{AdbVersion, Device, DeviceLong, DeviceState, RebootType};
pub use models::{AdbVersion, DeviceLong, DeviceShort, DeviceState, RebootType};
pub use server::*;
pub use transports::*;

View File

@@ -0,0 +1,31 @@
use std::fmt::Display;
pub(crate) enum ADBEmulatorCommand {
Authenticate(String),
Sms(String, String),
Rotate,
}
impl Display for ADBEmulatorCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Need to call `writeln!` because emulator commands are '\n' terminated
match self {
ADBEmulatorCommand::Authenticate(token) => writeln!(f, "auth {token}"),
ADBEmulatorCommand::Sms(phone_number, content) => {
writeln!(f, "sms send {phone_number} {content}")
}
ADBEmulatorCommand::Rotate => writeln!(f, "rotate"),
}
}
}
impl ADBEmulatorCommand {
/// Return the number of lines to skip per command when checking its result
pub(crate) fn skip_response_lines(&self) -> u8 {
match self {
ADBEmulatorCommand::Authenticate(_) => 1,
ADBEmulatorCommand::Sms(_, _) => 0,
ADBEmulatorCommand::Rotate => 0,
}
}
}

View File

@@ -0,0 +1,69 @@
use std::fmt::Display;
use super::RebootType;
use std::net::SocketAddrV4;
pub(crate) enum AdbServerCommand {
// Host commands
Version,
Kill,
Devices,
DevicesLong,
TrackDevices,
HostFeatures,
Connect(SocketAddrV4),
Disconnect(SocketAddrV4),
Pair(SocketAddrV4, String),
TransportAny,
TransportSerial(String),
// Local commands
ShellCommand(String),
Shell,
FrameBuffer,
Sync,
Reboot(RebootType),
}
impl Display for AdbServerCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AdbServerCommand::Version => write!(f, "host:version"),
AdbServerCommand::Kill => write!(f, "host:kill"),
AdbServerCommand::Devices => write!(f, "host:devices"),
AdbServerCommand::DevicesLong => write!(f, "host:devices-l"),
AdbServerCommand::Sync => write!(f, "sync:"),
AdbServerCommand::TrackDevices => write!(f, "host:track-devices"),
AdbServerCommand::TransportAny => write!(f, "host:transport-any"),
AdbServerCommand::TransportSerial(serial) => write!(f, "host:transport:{serial}"),
AdbServerCommand::ShellCommand(command) => match std::env::var("TERM") {
Ok(term) => write!(f, "shell,TERM={term},raw:{command}"),
Err(_) => write!(f, "shell,raw:{command}"),
},
AdbServerCommand::Shell => match std::env::var("TERM") {
Ok(term) => write!(f, "shell,TERM={term},raw:"),
Err(_) => write!(f, "shell,raw:"),
},
AdbServerCommand::HostFeatures => write!(f, "host:features"),
AdbServerCommand::Reboot(reboot_type) => {
write!(f, "reboot:{reboot_type}")
}
AdbServerCommand::Connect(addr) => write!(f, "host:connect:{}", addr),
AdbServerCommand::Disconnect(addr) => write!(f, "host:disconnect:{}", addr),
AdbServerCommand::Pair(addr, code) => {
write!(f, "host:pair:{code}:{addr}")
}
AdbServerCommand::FrameBuffer => write!(f, "framebuffer:"),
}
}
}
#[test]
fn test_pair_command() {
let host = "192.168.0.197:34783";
let code = "091102";
let code_u32 = code.parse::<u32>().expect("cannot parse u32");
let pair = AdbServerCommand::Pair(host.parse().expect("cannot parse host"), code.into());
assert_eq!(pair.to_string(), format!("host:pair:{code}:{host}"));
assert_ne!(pair.to_string(), format!("host:pair:{code_u32}:{host}"))
}

View File

@@ -1,9 +1,14 @@
use lazy_static::lazy_static;
use std::str::FromStr;
use std::{fmt::Display, str};
use crate::{DeviceState, RustADBError};
use regex::bytes::Regex;
use crate::{DeviceState, RustADBError};
lazy_static! {
static ref DEVICES_LONG_REGEX: Regex = Regex::new("^(?P<identifier>\\S+)\\s+(?P<state>\\w+) ((usb:(?P<usb1>.*)|(?P<usb2>\\d-\\d)) )?(product:(?P<product>\\w+) model:(?P<model>\\w+) device:(?P<device>\\w+) )?transport_id:(?P<transport_id>\\d+)$").expect("cannot build devices long regex");
}
/// Represents a new device with more informations helded.
#[derive(Debug)]
@@ -43,11 +48,8 @@ impl Display for DeviceLong {
impl TryFrom<Vec<u8>> for DeviceLong {
type Error = RustADBError;
// TODO: Prevent regex compilation every call to try_from()
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
let parse_regex = Regex::new("^(?P<identifier>\\w+)\\s+(?P<state>\\w+) usb:(?P<usb>.*) (product:(?P<product>\\w+) model:(?P<model>\\w+) device:(?P<device>\\w+) )?transport_id:(?P<transport_id>\\d+)$")?;
let groups = parse_regex
let groups = DEVICES_LONG_REGEX
.captures(&value)
.ok_or(RustADBError::RegexParsingError)?;
@@ -68,7 +70,8 @@ impl TryFrom<Vec<u8>> for DeviceLong {
)?)?,
usb: String::from_utf8(
groups
.name("usb")
.name("usb1")
.or_else(|| groups.name("usb2"))
.ok_or(RustADBError::RegexParsingError)?
.as_bytes()
.to_vec(),

View File

@@ -9,27 +9,27 @@ lazy_static! {
}
/// Represents a device connected to the ADB server.
#[derive(Debug)]
pub struct Device {
#[derive(Debug, Clone)]
pub struct DeviceShort {
/// Unique device identifier.
pub identifier: String,
/// Connection state of the device.
pub state: DeviceState,
}
impl Display for Device {
impl Display for DeviceShort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}\t{}", self.identifier, self.state)
}
}
impl TryFrom<Vec<u8>> for Device {
impl TryFrom<Vec<u8>> for DeviceShort {
type Error = RustADBError;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
// Optional final '\n' is used to match TrackDevices inputs
let groups = DEVICES_REGEX.captures(&value).unwrap();
Ok(Device {
Ok(DeviceShort {
identifier: String::from_utf8(
groups
.get(1)

View File

@@ -3,7 +3,7 @@ use std::{fmt::Display, str::FromStr};
use crate::RustADBError;
/// Represents the connection state of the device.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum DeviceState {
/// The device is not connected to adb or is not responding.
Offline,

View File

@@ -1,18 +1,20 @@
mod adb_command;
mod adb_emulator_command;
mod adb_request_status;
mod adb_server_command;
mod adb_version;
mod device;
mod device_long;
mod device_short;
mod device_state;
mod host_features;
mod reboot_type;
mod sync_command;
pub use adb_command::AdbCommand;
pub(crate) use adb_emulator_command::ADBEmulatorCommand;
pub use adb_request_status::AdbRequestStatus;
pub(crate) use adb_server_command::AdbServerCommand;
pub use adb_version::AdbVersion;
pub use device::Device;
pub use device_long::DeviceLong;
pub use device_short::DeviceShort;
pub use device_state::DeviceState;
pub use host_features::HostFeatures;
pub use reboot_type::RebootType;

View File

@@ -0,0 +1,81 @@
use std::net::{Ipv4Addr, SocketAddrV4};
use crate::{ADBTransport, Result, RustADBError, TCPEmulatorTransport};
use lazy_static::lazy_static;
use regex::Regex;
use super::ADBServerDevice;
lazy_static! {
pub static ref EMULATOR_REGEX: Regex =
Regex::new("^emulator-(?P<port>\\d+)$").expect("wrong syntax for emulator regex");
}
/// Represents an emulator connected to the ADB server.
#[derive(Debug)]
pub struct ADBEmulatorDevice {
/// Unique device identifier.
pub identifier: String,
/// Internal [TCPEmulatorTransport]
transport: TCPEmulatorTransport,
}
impl ADBEmulatorDevice {
/// Instantiates a new [ADBEmulatorDevice]
pub fn new(identifier: String, ip_address: Option<Ipv4Addr>) -> Result<Self> {
let ip_address = match ip_address {
Some(ip_address) => ip_address,
None => Ipv4Addr::new(127, 0, 0, 1),
};
let groups = EMULATOR_REGEX
.captures(&identifier)
.ok_or(RustADBError::DeviceNotFound(format!(
"Device {} is likely not an emulator",
identifier
)))?;
let port = groups
.name("port")
.ok_or(RustADBError::RegexParsingError)?
.as_str()
.parse::<u16>()?;
let socket_addr = SocketAddrV4::new(ip_address, port);
let transport = TCPEmulatorTransport::new(socket_addr);
Ok(Self {
identifier,
transport,
})
}
pub(crate) fn get_transport_mut(&mut self) -> &mut TCPEmulatorTransport {
&mut self.transport
}
/// Connect to underlying transport
pub(crate) fn connect(&mut self) -> Result<&mut TCPEmulatorTransport> {
self.transport.connect()?;
Ok(self.get_transport_mut())
}
}
impl TryFrom<ADBServerDevice> for ADBEmulatorDevice {
type Error = RustADBError;
fn try_from(value: ADBServerDevice) -> std::result::Result<Self, Self::Error> {
ADBEmulatorDevice::new(
value.identifier.clone(),
Some(*value.get_transport().get_socketaddr().ip()),
)
}
}
impl Drop for ADBEmulatorDevice {
fn drop(&mut self) {
// Best effort here
let _ = self.transport.disconnect();
}
}

View File

@@ -0,0 +1,76 @@
use crate::ADBTransport;
use crate::Result;
use crate::RustADBError;
use crate::TCPServerTransport;
use std::net::SocketAddrV4;
use std::process::Command;
/// Represents an ADB Server
#[derive(Debug, Default)]
pub struct ADBServer {
/// Internal [TcpStream], lazily initialized
pub(crate) transport: Option<TCPServerTransport>,
/// Address to connect to
pub(crate) socket_addr: Option<SocketAddrV4>,
}
impl ADBServer {
/// Instantiates a new [ADBServer]
pub fn new(address: SocketAddrV4) -> Self {
Self {
transport: None,
socket_addr: Some(address),
}
}
/// Returns the current selected transport
pub(crate) fn get_transport(&mut self) -> Result<&mut TCPServerTransport> {
self.transport
.as_mut()
.ok_or(RustADBError::IOError(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"server connection not initialized",
)))
}
/// Connect to underlying transport
pub(crate) fn connect(&mut self) -> Result<&mut TCPServerTransport> {
let mut is_local_ip = false;
let mut transport = if let Some(addr) = &self.socket_addr {
let ip = addr.ip();
if ip.is_loopback() || ip.is_unspecified() {
is_local_ip = true;
}
TCPServerTransport::new(*addr)
} else {
is_local_ip = true;
TCPServerTransport::default()
};
if is_local_ip {
// ADB Server is local, we start it if not already running
let child = Command::new("adb").arg("start-server").spawn();
match child {
Ok(mut child) => {
if let Err(e) = child.wait() {
log::error!("error while starting adb server: {e}")
}
}
Err(e) => log::error!("error while starting adb server: {e}"),
}
}
transport.connect()?;
self.transport = Some(transport);
self.get_transport()
}
}
impl Drop for ADBServer {
fn drop(&mut self) {
if let Some(ref mut transport) = &mut self.transport {
let _ = transport.disconnect();
}
}
}

View File

@@ -0,0 +1,49 @@
use crate::{ADBTransport, Result, TCPServerTransport};
use std::net::SocketAddrV4;
/// Represents a device connected to the ADB server.
#[derive(Debug)]
pub struct ADBServerDevice {
/// Unique device identifier.
pub identifier: String,
/// Internal [TCPServerTransport]
transport: TCPServerTransport,
}
impl ADBServerDevice {
/// Instantiates a new [ADBServerDevice]
pub fn new(identifier: String, socket_addr: Option<SocketAddrV4>) -> Self {
let transport = if let Some(addr) = socket_addr {
TCPServerTransport::new(addr)
} else {
TCPServerTransport::default()
};
Self {
identifier,
transport,
}
}
pub(crate) fn get_transport(&self) -> &TCPServerTransport {
&self.transport
}
pub(crate) fn get_transport_mut(&mut self) -> &mut TCPServerTransport {
&mut self.transport
}
/// Connect to underlying transport
pub(crate) fn connect(&mut self) -> Result<&mut TCPServerTransport> {
self.transport.connect()?;
Ok(self.get_transport_mut())
}
}
impl Drop for ADBServerDevice {
fn drop(&mut self) {
// Best effort here
let _ = self.transport.disconnect();
}
}

View File

@@ -0,0 +1,176 @@
use std::{io::Read, iter::Map, path::Path, slice::ChunksExact};
use byteorder::{LittleEndian, ReadBytesExt};
use image::{ImageBuffer, Rgba};
use crate::{models::AdbServerCommand, utils, ADBServerDevice, Result, RustADBError};
type U32ChunkIter<'a> = Map<ChunksExact<'a, u8>, fn(&[u8]) -> Result<u32>>;
fn read_next(chunks: &mut U32ChunkIter) -> Result<u32> {
chunks
.next()
.ok_or(RustADBError::FramebufferConversionError)?
}
#[derive(Debug)]
struct FrameBufferInfoV1 {
pub _bpp: u32,
pub size: u32,
pub width: u32,
pub height: u32,
pub _red_offset: u32,
pub _red_length: u32,
pub _blue_offset: u32,
pub _blue_length: u32,
pub _green_offset: u32,
pub _green_length: u32,
pub _alpha_offset: u32,
pub _alpha_length: u32,
}
impl TryFrom<[u8; std::mem::size_of::<Self>()]> for FrameBufferInfoV1 {
type Error = RustADBError;
fn try_from(
value: [u8; std::mem::size_of::<Self>()],
) -> std::result::Result<Self, Self::Error> {
let mut chunks: U32ChunkIter = value.chunks_exact(4).map(utils::u32_from_le);
Ok(Self {
_bpp: read_next(&mut chunks)?,
size: read_next(&mut chunks)?,
width: read_next(&mut chunks)?,
height: read_next(&mut chunks)?,
_red_offset: read_next(&mut chunks)?,
_red_length: read_next(&mut chunks)?,
_blue_offset: read_next(&mut chunks)?,
_blue_length: read_next(&mut chunks)?,
_green_offset: read_next(&mut chunks)?,
_green_length: read_next(&mut chunks)?,
_alpha_offset: read_next(&mut chunks)?,
_alpha_length: read_next(&mut chunks)?,
})
}
}
#[derive(Debug)]
struct FrameBufferInfoV2 {
pub _bpp: u32,
pub _color_space: u32,
pub size: u32,
pub width: u32,
pub height: u32,
pub _red_offset: u32,
pub _red_length: u32,
pub _blue_offset: u32,
pub _blue_length: u32,
pub _green_offset: u32,
pub _green_length: u32,
pub _alpha_offset: u32,
pub _alpha_length: u32,
}
impl TryFrom<[u8; std::mem::size_of::<Self>()]> for FrameBufferInfoV2 {
type Error = RustADBError;
fn try_from(
value: [u8; std::mem::size_of::<Self>()],
) -> std::result::Result<Self, Self::Error> {
let mut chunks: U32ChunkIter = value.chunks_exact(4).map(utils::u32_from_le);
Ok(Self {
_bpp: read_next(&mut chunks)?,
_color_space: read_next(&mut chunks)?,
size: read_next(&mut chunks)?,
width: read_next(&mut chunks)?,
height: read_next(&mut chunks)?,
_red_offset: read_next(&mut chunks)?,
_red_length: read_next(&mut chunks)?,
_blue_offset: read_next(&mut chunks)?,
_blue_length: read_next(&mut chunks)?,
_green_offset: read_next(&mut chunks)?,
_green_length: read_next(&mut chunks)?,
_alpha_offset: read_next(&mut chunks)?,
_alpha_length: read_next(&mut chunks)?,
})
}
}
impl ADBServerDevice {
/// Dump framebuffer of this device
/// Big help from source code (https://android.googlesource.com/platform/system/adb/+/refs/heads/main/framebuffer_service.cpp)
pub fn framebuffer<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let img = self.framebuffer_inner()?;
Ok(img.save(path.as_ref())?)
}
/// Inner method requesting framebuffer from Android device
fn framebuffer_inner(&mut self) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>> {
let serial: String = self.identifier.clone();
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;
self.get_transport_mut()
.send_adb_request(AdbServerCommand::FrameBuffer)?;
let version = self
.get_transport_mut()
.get_raw_connection()?
.read_u32::<LittleEndian>()?;
match version {
// RGBA_8888
1 => {
let mut buf = [0u8; std::mem::size_of::<FrameBufferInfoV1>()];
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut buf)?;
let h: FrameBufferInfoV1 = buf.try_into()?;
let mut data = vec![
0_u8;
h.size
.try_into()
.map_err(|_| RustADBError::ConversionError)?
];
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut data)?;
Ok(
ImageBuffer::<Rgba<u8>, Vec<u8>>::from_vec(h.width, h.height, data)
.ok_or_else(|| RustADBError::FramebufferConversionError)?,
)
}
// RGBX_8888
2 => {
let mut buf = [0u8; std::mem::size_of::<FrameBufferInfoV2>()];
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut buf)?;
let h: FrameBufferInfoV2 = buf.try_into()?;
let mut data = vec![
0_u8;
h.size
.try_into()
.map_err(|_| RustADBError::ConversionError)?
];
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut data)?;
Ok(
ImageBuffer::<Rgba<u8>, Vec<u8>>::from_vec(h.width, h.height, data)
.ok_or_else(|| RustADBError::FramebufferConversionError)?,
)
}
v => unimplemented!("Version {} not implemented", v),
}
}
}

View File

@@ -0,0 +1,22 @@
use crate::{
models::{AdbServerCommand, HostFeatures},
ADBServerDevice, Result,
};
impl ADBServerDevice {
/// Lists available ADB server features.
pub fn host_features(&mut self) -> Result<Vec<HostFeatures>> {
let serial = self.identifier.clone();
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;
let features = self
.get_transport_mut()
.proxy_connection(AdbServerCommand::HostFeatures, true)?;
Ok(features
.split(|x| x.eq(&b','))
.filter_map(|v| HostFeatures::try_from(v).ok())
.collect())
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
models::{AdbCommand, SyncCommand},
AdbTcpConnection, Result,
models::{AdbServerCommand, SyncCommand},
ADBServerDevice, Result,
};
use byteorder::{ByteOrder, LittleEndian};
use std::{
@@ -8,21 +8,20 @@ use std::{
str,
};
impl AdbTcpConnection {
impl ADBServerDevice {
/// Lists files in [path] on the device.
pub fn list<S: ToString, A: AsRef<str>>(&mut self, serial: Option<&S>, path: A) -> Result<()> {
match serial {
None => self.send_adb_request(AdbCommand::TransportAny, false)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()), false)?
}
}
pub fn list<A: AsRef<str>>(&mut self, path: A) -> Result<()> {
let serial = self.identifier.clone();
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;
// Set device in SYNC mode
self.send_adb_request(AdbCommand::Sync, false)?;
self.get_transport_mut()
.send_adb_request(AdbServerCommand::Sync)?;
// Send a list command
self.send_sync_request(SyncCommand::List)?;
self.get_transport_mut()
.send_sync_request(SyncCommand::List)?;
self.handle_list_command(path)
}
@@ -34,16 +33,21 @@ impl AdbTcpConnection {
LittleEndian::write_u32(&mut len_buf, path.as_ref().len() as u32);
// 4 bytes of command name is already sent by send_sync_request
self.tcp_stream.write_all(&len_buf)?;
self.get_transport_mut()
.get_raw_connection()?
.write_all(&len_buf)?;
// List sends the string of the directory to list, and then the server sends a list of files
self.tcp_stream
// List send the string of the directory to list, and then the server send a list of files
self.get_transport_mut()
.get_raw_connection()?
.write_all(path.as_ref().to_string().as_bytes())?;
// Reads returned status code from ADB server
let mut response = [0_u8; 4];
loop {
self.tcp_stream.read_exact(&mut response)?;
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut response)?;
match str::from_utf8(response.as_ref())? {
"DENT" => {
// TODO: Move this to a struct that extract this data, but as the device
@@ -52,18 +56,21 @@ impl AdbTcpConnection {
let mut file_size = [0_u8; 4];
let mut mod_time = [0_u8; 4];
let mut name_len = [0_u8; 4];
self.tcp_stream.read_exact(&mut file_mod)?;
self.tcp_stream.read_exact(&mut file_size)?;
self.tcp_stream.read_exact(&mut mod_time)?;
self.tcp_stream.read_exact(&mut name_len)?;
let mut connection = self.get_transport_mut().get_raw_connection()?;
connection.read_exact(&mut file_mod)?;
connection.read_exact(&mut file_size)?;
connection.read_exact(&mut mod_time)?;
connection.read_exact(&mut name_len)?;
let name_len = LittleEndian::read_u32(&name_len);
let mut name_buf = vec![0_u8; name_len as usize];
self.tcp_stream.read_exact(&mut name_buf)?;
connection.read_exact(&mut name_buf)?;
}
"DONE" => {
return Ok(());
}
x => println!("Unknown response {}", x),
x => log::error!("Got an unknown response {}", x),
}
}
}

View File

@@ -0,0 +1,56 @@
use std::io::{self, Write};
use crate::{ADBServerDevice, Result};
struct LogFilter<W: Write> {
writer: W,
buffer: Vec<u8>,
}
impl<W: Write> LogFilter<W> {
pub fn new(writer: W) -> Self {
LogFilter {
writer,
buffer: Vec::new(),
}
}
fn should_write(&self, _line: &[u8]) -> bool {
// Can implement checks here to ensure if logs have to be written
true
}
}
impl<W: Write> Write for LogFilter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.buffer.extend_from_slice(buf);
let buf_clone = self.buffer.clone();
let mut lines = buf_clone.split_inclusive(|&byte| byte == b'\n').peekable();
while let Some(line) = lines.next() {
if lines.peek().is_some() {
if self.should_write(line) {
self.writer.write_all(line)?;
}
} else {
// This is the last (unfinished) element, we keep it for next round
self.buffer = line.to_vec();
break;
}
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
impl ADBServerDevice {
/// Get logs from device
pub fn get_logs<W: Write>(&mut self, output: W) -> Result<()> {
self.shell_command(["exec logcat"], LogFilter::new(output))
}
}

View File

@@ -1,11 +1,10 @@
mod devices;
mod framebuffer;
mod host_features;
mod kill;
mod list;
mod logcat;
mod reboot;
mod recv;
mod send;
mod shell;
mod stat;
mod transport;
mod version;

View File

@@ -0,0 +1,17 @@
use crate::{
models::{AdbServerCommand, RebootType},
ADBServerDevice, Result,
};
impl ADBServerDevice {
/// Reboots the device
pub fn reboot(&mut self, reboot_type: RebootType) -> Result<()> {
let serial = self.identifier.clone();
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;
self.get_transport_mut()
.proxy_connection(AdbServerCommand::Reboot(reboot_type), false)
.map(|_| ())
}
}

View File

@@ -0,0 +1,85 @@
use crate::{
models::{AdbServerCommand, SyncCommand},
ADBServerDevice, Result, RustADBError,
};
use byteorder::{ByteOrder, LittleEndian};
use std::io::{Read, Write};
impl ADBServerDevice {
/// Receives [path] to [stream] from the device.
pub fn recv<A: AsRef<str>>(&mut self, path: A, stream: &mut dyn Write) -> Result<()> {
let serial = self.identifier.clone();
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;
// Set device in SYNC mode
self.get_transport_mut()
.send_adb_request(AdbServerCommand::Sync)?;
// Send a recv command
self.get_transport_mut()
.send_sync_request(SyncCommand::Recv)?;
self.handle_recv_command(path, stream)
}
fn handle_recv_command<S: AsRef<str>>(
&mut self,
from: S,
output: &mut dyn Write,
) -> Result<()> {
// First send 8 byte common header
let mut len_buf = [0_u8; 4];
LittleEndian::write_u32(&mut len_buf, from.as_ref().len() as u32);
self.get_transport_mut()
.get_raw_connection()?
.write_all(&len_buf)?;
self.get_transport_mut()
.get_raw_connection()?
.write_all(from.as_ref().as_bytes())?;
// Then we receive the byte data in chunks of up to 64k
// Chunk looks like 'DATA' <length> <data>
let mut buffer = [0_u8; 64 * 1024]; // Should this be Boxed?
let mut data_header = [0_u8; 4]; // DATA
loop {
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut data_header)?;
// Check if data_header is DATA or DONE or FAIL
match &data_header {
b"DATA" => {
// Handle received data
let length: usize = self
.get_transport_mut()
.get_body_length()?
.try_into()
.unwrap();
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut buffer[..length])?;
output.write_all(&buffer[..length])?;
}
b"DONE" => break, // We're done here
b"FAIL" => {
// Handle fail
let length: usize = self
.get_transport_mut()
.get_body_length()?
.try_into()
.unwrap();
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut buffer[..length])?;
Err(RustADBError::ADBRequestFailed(String::from_utf8(
buffer[..length].to_vec(),
)?))?;
}
_ => panic!("Unknown response from device {:#?}", data_header),
}
}
// Connection should've left SYNC by now
Ok(())
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
models::{AdbCommand, AdbRequestStatus, SyncCommand},
AdbTcpConnection, Result, RustADBError,
models::{AdbRequestStatus, AdbServerCommand, SyncCommand},
ADBServerDevice, Result, RustADBError,
};
use byteorder::{ByteOrder, LittleEndian};
use std::{
@@ -10,26 +10,20 @@ use std::{
time::SystemTime,
};
impl AdbTcpConnection {
/// Sends [stream] to [path] on the device.
pub fn send<S: ToString, A: AsRef<str>>(
&mut self,
serial: Option<&S>,
stream: &mut dyn Read,
path: A,
) -> Result<()> {
match serial {
None => self.send_adb_request(AdbCommand::TransportAny, true)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()), true)?
}
}
impl ADBServerDevice {
/// Send [stream] to [path] on the device.
pub fn send<A: AsRef<str>>(&mut self, stream: &mut dyn Read, path: A) -> Result<()> {
let serial = self.identifier.clone();
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;
// Set device in SYNC mode
self.send_adb_request(AdbCommand::Sync, false)?;
self.get_transport_mut()
.send_adb_request(AdbServerCommand::Sync)?;
// Send a send command
self.send_sync_request(SyncCommand::Send)?;
self.get_transport_mut()
.send_sync_request(SyncCommand::Send)?;
self.handle_send_command(stream, path)
}
@@ -38,13 +32,17 @@ impl AdbTcpConnection {
// Append the permission flags to the filename
let to = to.as_ref().to_string() + ",0777";
// The name of command is already sent by send_sync_request
// The name of command is already sent by get_transport()?.send_sync_request
let mut len_buf = [0_u8; 4];
LittleEndian::write_u32(&mut len_buf, to.len() as u32);
self.tcp_stream.write_all(&len_buf)?;
self.get_transport_mut()
.get_raw_connection()?
.write_all(&len_buf)?;
// Send appends the filemode to the string sent
self.tcp_stream.write_all(to.as_bytes())?;
self.get_transport_mut()
.get_raw_connection()?
.write_all(to.as_bytes())?;
// Then we send the byte data in chunks of up to 64k
// Chunk looks like 'DATA' <length> <data>
@@ -56,9 +54,15 @@ impl AdbTcpConnection {
}
let mut chunk_len_buf = [0_u8; 4];
LittleEndian::write_u32(&mut chunk_len_buf, bytes_read as u32);
self.tcp_stream.write_all(b"DATA")?;
self.tcp_stream.write_all(&chunk_len_buf)?;
self.tcp_stream.write_all(&buffer[..bytes_read])?;
self.get_transport_mut()
.get_raw_connection()?
.write_all(b"DATA")?;
self.get_transport_mut()
.get_raw_connection()?
.write_all(&chunk_len_buf)?;
self.get_transport_mut()
.get_raw_connection()?
.write_all(&buffer[..bytes_read])?;
}
// When we are done sending, we send 'DONE' <last modified time>
@@ -68,17 +72,23 @@ impl AdbTcpConnection {
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
};
LittleEndian::write_u32(&mut len_buf, last_modified.as_secs() as u32);
self.tcp_stream.write_all(b"DONE")?;
self.tcp_stream.write_all(&len_buf)?;
self.get_transport_mut()
.get_raw_connection()?
.write_all(b"DONE")?;
self.get_transport_mut()
.get_raw_connection()?
.write_all(&len_buf)?;
// We expect 'OKAY' response from this
let mut request_status = [0; 4];
self.tcp_stream.read_exact(&mut request_status)?;
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut request_status)?;
match AdbRequestStatus::from_str(str::from_utf8(request_status.as_ref())?)? {
AdbRequestStatus::Fail => {
// We can keep reading to get further details
let length = self.get_body_length()?;
let length = self.get_transport_mut().get_body_length()?;
let mut body = vec![
0;
@@ -87,7 +97,9 @@ impl AdbTcpConnection {
.map_err(|_| RustADBError::ConversionError)?
];
if length > 0 {
self.tcp_stream.read_exact(&mut body)?;
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut body)?;
}
Err(RustADBError::ADBRequestFailed(String::from_utf8(body)?))

View File

@@ -8,8 +8,8 @@ use mio::{unix::SourceFd, Events, Interest, Poll, Token};
use crate::{
adb_termios::ADBTermios,
models::{AdbCommand, HostFeatures},
AdbTcpConnection, Result, RustADBError,
models::{AdbServerCommand, HostFeatures},
ADBServerDevice, Result, RustADBError,
};
const STDIN: Token = Token(0);
@@ -25,84 +25,78 @@ fn setup_poll_stdin() -> std::result::Result<Poll, io::Error> {
Ok(poll)
}
impl AdbTcpConnection {
/// Runs 'command' in a shell on the device, and return its output and error streams.
pub fn shell_command<S: ToString>(
impl ADBServerDevice {
/// Runs 'command' in a shell on the device, and write its output and error streams into [`output`].
pub fn shell_command<S: ToString, W: Write>(
&mut self,
serial: Option<&S>,
command: impl IntoIterator<Item = S>,
) -> Result<Vec<u8>> {
let supported_features = self.host_features(serial)?;
mut output: W,
) -> Result<()> {
let supported_features = self.host_features()?;
if !supported_features.contains(&HostFeatures::ShellV2)
&& !supported_features.contains(&HostFeatures::Cmd)
{
return Err(RustADBError::ADBShellNotSupported);
}
match serial {
None => self.send_adb_request(AdbCommand::TransportAny, true)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()), true)?
}
}
self.send_adb_request(
AdbCommand::ShellCommand(
let serial = self.identifier.clone();
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;
self.get_transport_mut()
.send_adb_request(AdbServerCommand::ShellCommand(
command
.into_iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" "),
),
false,
)?;
))?;
const BUFFER_SIZE: usize = 512;
(|| {
let mut result = Vec::new();
loop {
let mut buffer = [0; BUFFER_SIZE];
match self.tcp_stream.read(&mut buffer) {
Ok(size) => {
if size == 0 {
return Ok(result);
} else {
result.extend_from_slice(&buffer[..size]);
}
}
Err(e) => {
return Err(RustADBError::IOError(e));
const BUFFER_SIZE: usize = 4096;
loop {
let mut buffer = [0; BUFFER_SIZE];
match self
.get_transport_mut()
.get_raw_connection()?
.read(&mut buffer)
{
Ok(size) => {
if size == 0 {
return Ok(());
} else {
output.write_all(&buffer[..size])?;
}
}
Err(e) => {
return Err(RustADBError::IOError(e));
}
}
})()
}
}
/// Starts an interactive shell session on the device. Redirects stdin/stdout/stderr as appropriate.
pub fn shell<S: ToString>(&mut self, serial: Option<&S>) -> Result<()> {
pub fn shell(&mut self) -> Result<()> {
let mut adb_termios = ADBTermios::new(std::io::stdin())?;
adb_termios.set_adb_termios()?;
self.tcp_stream.set_nodelay(true)?;
self.connect()?.get_raw_connection()?.set_nodelay(true)?;
// TODO: FORWARD CTRL+C !!
let supported_features = self.host_features(serial)?;
let supported_features = self.host_features()?;
if !supported_features.contains(&HostFeatures::ShellV2)
&& !supported_features.contains(&HostFeatures::Cmd)
{
return Err(RustADBError::ADBShellNotSupported);
}
match serial {
None => self.send_adb_request(AdbCommand::TransportAny, true)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()), true)?
}
}
self.send_adb_request(AdbCommand::Shell, false)?;
let serial = self.identifier.clone();
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;
self.get_transport_mut()
.send_adb_request(AdbServerCommand::Shell)?;
// let read_stream = Arc::new(self.tcp_stream);
let mut read_stream = self.tcp_stream.try_clone()?;
let mut read_stream = self.get_transport_mut().get_raw_connection()?.try_clone()?;
let (tx, rx) = mpsc::channel::<bool>();

View File

@@ -8,8 +8,8 @@ use byteorder::{ByteOrder, LittleEndian};
use chrono::{DateTime, Utc};
use crate::{
models::{AdbCommand, SyncCommand},
AdbTcpConnection, Result, RustADBError,
models::{AdbServerCommand, SyncCommand},
ADBServerDevice, Result, RustADBError,
};
#[derive(Debug)]
@@ -46,23 +46,30 @@ impl Display for AdbStatResponse {
}
}
impl AdbTcpConnection {
impl ADBServerDevice {
fn handle_stat_command<S: AsRef<str>>(&mut self, path: S) -> Result<AdbStatResponse> {
let mut len_buf = [0_u8; 4];
LittleEndian::write_u32(&mut len_buf, path.as_ref().len() as u32);
// 4 bytes of command name is already sent by send_sync_request
self.tcp_stream.write_all(&len_buf)?;
self.tcp_stream
self.get_transport_mut()
.get_raw_connection()?
.write_all(&len_buf)?;
self.get_transport_mut()
.get_raw_connection()?
.write_all(path.as_ref().to_string().as_bytes())?;
// Reads returned status code from ADB server
let mut response = [0_u8; 4];
self.tcp_stream.read_exact(&mut response)?;
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut response)?;
match std::str::from_utf8(response.as_ref())? {
"STAT" => {
let mut data = [0_u8; 12];
self.tcp_stream.read_exact(&mut data)?;
self.get_transport_mut()
.get_raw_connection()?
.read_exact(&mut data)?;
Ok(data.into())
}
@@ -74,23 +81,18 @@ impl AdbTcpConnection {
}
/// Stat file given as [path] on the device.
pub fn stat<S: ToString, A: AsRef<str>>(
&mut self,
serial: Option<S>,
path: A,
) -> Result<AdbStatResponse> {
match serial {
None => self.send_adb_request(AdbCommand::TransportAny, false)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()), false)?
}
}
pub fn stat<A: AsRef<str>>(&mut self, path: A) -> Result<AdbStatResponse> {
let serial = self.identifier.clone();
self.connect()?
.send_adb_request(AdbServerCommand::TransportSerial(serial))?;
// Set device in SYNC mode
self.send_adb_request(AdbCommand::Sync, false)?;
self.get_transport_mut()
.send_adb_request(AdbServerCommand::Sync)?;
// Send a "Stat" command
self.send_sync_request(SyncCommand::Stat)?;
self.get_transport_mut()
.send_sync_request(SyncCommand::Stat)?;
self.handle_stat_command(path)
}

View File

@@ -1,9 +1,10 @@
use crate::{models::AdbCommand, AdbTcpConnection, Result};
use crate::{models::AdbServerCommand, ADBServerDevice, Result};
impl AdbTcpConnection {
impl ADBServerDevice {
/// Asks ADB server to switch the connection to either the device or emulator connect to/running on the host. Will fail if there is more than one such device/emulator available.
pub fn transport_any(&mut self) -> Result<()> {
self.proxy_connection(AdbCommand::TransportAny, false, true)
self.connect()?
.proxy_connection(AdbServerCommand::TransportAny, false)
.map(|_| ())
}
}

View File

@@ -0,0 +1,9 @@
mod adb_emulator_device;
mod adb_server;
mod adb_server_device;
mod device_commands;
mod server_commands;
pub use adb_emulator_device::ADBEmulatorDevice;
pub use adb_server::ADBServer;
pub use adb_server_device::ADBServerDevice;

View File

@@ -0,0 +1,16 @@
use crate::{models::AdbServerCommand, ADBServer, Result, RustADBError};
use std::net::SocketAddrV4;
impl ADBServer {
/// Connect device over tcp with address and port
pub fn connect_device(&mut self, address: SocketAddrV4) -> Result<()> {
let response = self
.connect()?
.proxy_connection(AdbServerCommand::Connect(address), true)?;
match String::from_utf8(response).unwrap() {
s if s.starts_with("connected to") => Ok(()),
s => Err(RustADBError::ADBRequestFailed(s)),
}
}
}

View File

@@ -0,0 +1,119 @@
use std::io::Read;
use crate::{
models::{AdbServerCommand, DeviceShort},
ADBEmulatorDevice, ADBServer, ADBServerDevice, DeviceLong, Result, RustADBError,
};
impl ADBServer {
/// Gets a list of connected devices.
pub fn devices(&mut self) -> Result<Vec<DeviceShort>> {
let devices = self
.connect()?
.proxy_connection(AdbServerCommand::Devices, true)?;
let mut vec_devices: Vec<DeviceShort> = vec![];
for device in devices.split(|x| x.eq(&b'\n')) {
if device.is_empty() {
break;
}
vec_devices.push(DeviceShort::try_from(device.to_vec())?);
}
Ok(vec_devices)
}
/// Gets an extended list of connected devices including the device paths in the state.
pub fn devices_long(&mut self) -> Result<Vec<DeviceLong>> {
let devices_long = self
.connect()?
.proxy_connection(AdbServerCommand::DevicesLong, true)?;
let mut vec_devices: Vec<DeviceLong> = vec![];
for device in devices_long.split(|x| x.eq(&b'\n')) {
if device.is_empty() {
break;
}
vec_devices.push(DeviceLong::try_from(device.to_vec())?);
}
Ok(vec_devices)
}
/// Get a device, assuming that only this device is connected.
pub fn get_device(&mut self) -> Result<ADBServerDevice> {
let mut devices = self.devices()?.into_iter();
match devices.next() {
Some(device) => match devices.next() {
Some(_) => Err(RustADBError::DeviceNotFound(
"too many devices connected".to_string(),
)),
None => Ok(ADBServerDevice::new(device.identifier, self.socket_addr)),
},
None => Err(RustADBError::DeviceNotFound(
"no device connected".to_string(),
)),
}
}
/// Get a device matching the given name, if existing.
/// - There is no device connected => Error
/// - There is a single device connected => Ok
/// - There are more than 1 device connected => Error
pub fn get_device_by_name(&mut self, name: &str) -> Result<ADBServerDevice> {
let nb_devices = self
.devices()?
.into_iter()
.filter(|d| d.identifier.as_str() == name)
.collect::<Vec<DeviceShort>>()
.len();
if nb_devices != 1 {
Err(RustADBError::DeviceNotFound(format!(
"could not find device {name}"
)))
} else {
Ok(ADBServerDevice::new(name.to_string(), self.socket_addr))
}
}
/// Tracks new devices showing up.
// TODO: Change with Generator when feature stabilizes
pub fn track_devices(&mut self, callback: impl Fn(DeviceShort) -> Result<()>) -> Result<()> {
self.connect()?
.send_adb_request(AdbServerCommand::TrackDevices)?;
loop {
let length = self.get_transport()?.get_hex_body_length()?;
if length > 0 {
let mut body = vec![
0;
length
.try_into()
.map_err(|_| RustADBError::ConversionError)?
];
self.get_transport()?
.get_raw_connection()?
.read_exact(&mut body)?;
callback(DeviceShort::try_from(body)?)?;
}
}
}
/// Get an emulator, assuming that only this device is connected.
pub fn get_emulator_device(&mut self) -> Result<ADBEmulatorDevice> {
let device = self.get_device()?;
ADBEmulatorDevice::try_from(device)
}
/// Get an emulator by its name
pub fn get_emulator_device_by_name(&mut self, name: &str) -> Result<ADBEmulatorDevice> {
let device = self.get_device_by_name(name)?;
ADBEmulatorDevice::try_from(device)
}
}

View File

@@ -0,0 +1,16 @@
use crate::{models::AdbServerCommand, ADBServer, Result, RustADBError};
use std::net::SocketAddrV4;
impl ADBServer {
/// Connect device over tcp with address and port
pub fn disconnect_device(&mut self, address: SocketAddrV4) -> Result<()> {
let response = self
.connect()?
.proxy_connection(AdbServerCommand::Disconnect(address), true)?;
match String::from_utf8(response).unwrap() {
s if s.starts_with("disconnected") => Ok(()),
s => Err(RustADBError::ADBRequestFailed(s)),
}
}
}

View File

@@ -0,0 +1,10 @@
use crate::{models::AdbServerCommand, ADBServer, Result};
impl ADBServer {
/// Asks the ADB server to quit immediately.
pub fn kill(&mut self) -> Result<()> {
self.connect()?
.proxy_connection(AdbServerCommand::Kill, false)
.map(|_| ())
}
}

View File

@@ -0,0 +1,6 @@
mod connect;
mod devices;
mod disconnect;
mod kill;
mod pair;
mod version;

View File

@@ -0,0 +1,17 @@
use crate::models::AdbServerCommand;
use crate::{ADBServer, Result, RustADBError};
use std::net::SocketAddrV4;
impl ADBServer {
/// Pair device on a specific port with a generated 'code'
pub fn pair(&mut self, address: SocketAddrV4, code: String) -> Result<()> {
let response = self
.connect()?
.proxy_connection(AdbServerCommand::Pair(address, code), true)?;
match String::from_utf8(response).unwrap() {
s if s.starts_with("Successfully paired to") => Ok(()),
s => Err(RustADBError::ADBRequestFailed(s)),
}
}
}

View File

@@ -0,0 +1,12 @@
use crate::{models::AdbServerCommand, ADBServer, AdbVersion, Result};
impl ADBServer {
/// Gets server's internal version number.
pub fn version(&mut self) -> Result<AdbVersion> {
let version = self
.connect()?
.proxy_connection(AdbServerCommand::Version, true)?;
AdbVersion::try_from(version)
}
}

View File

@@ -0,0 +1,6 @@
mod tcp_emulator_transport;
mod tcp_server_transport;
mod transport_trait;
pub use tcp_emulator_transport::TCPEmulatorTransport;
pub use tcp_server_transport::TCPServerTransport;
pub use transport_trait::ADBTransport;

View File

@@ -0,0 +1,127 @@
use std::{
fs::File,
io::{BufRead, BufReader, Read, Write},
net::{SocketAddrV4, TcpStream},
};
use homedir::my_home;
use super::ADBTransport;
use crate::{models::ADBEmulatorCommand, Result, RustADBError};
/// Emulator transport running on top on TCP.
#[derive(Debug)]
pub struct TCPEmulatorTransport {
socket_addr: SocketAddrV4,
tcp_stream: Option<TcpStream>,
}
impl TCPEmulatorTransport {
/// Instantiates a new instance of [TCPEmulatorTransport]
pub fn new(socket_addr: SocketAddrV4) -> Self {
Self {
socket_addr,
tcp_stream: None,
}
}
pub(crate) fn get_raw_connection(&self) -> Result<&TcpStream> {
self.tcp_stream
.as_ref()
.ok_or(RustADBError::IOError(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"not connected",
)))
}
/// Return authentication token stored in $HOME/.emulator_console_auth_token
pub fn get_authentication_token(&mut self) -> Result<String> {
let home = match my_home()? {
Some(home) => home,
None => todo!(),
};
let mut f = File::open(home.join(".emulator_console_auth_token"))?;
let mut token = String::new();
f.read_to_string(&mut token)?;
Ok(token)
}
/// Send an authenticate request to this emulator
pub fn authenticate(&mut self) -> Result<()> {
let token = self.get_authentication_token()?;
self.send_command(ADBEmulatorCommand::Authenticate(token))
}
/// Send an [ADBEmulatorCommand] to this emulator
pub(crate) fn send_command(&mut self, command: ADBEmulatorCommand) -> Result<()> {
let mut connection = self.get_raw_connection()?;
// Send command
connection.write_all(command.to_string().as_bytes())?;
// Check is an error occurred skipping lines depending on command
self.check_error(command.skip_response_lines())?;
Ok(())
}
fn check_error(&mut self, skipping: u8) -> Result<()> {
let mut reader = BufReader::new(self.get_raw_connection()?);
for _ in 0..skipping {
let mut line = String::new();
reader.read_line(&mut line)?;
if line.starts_with("KO:") {
return Err(RustADBError::ADBRequestFailed(line));
}
}
let mut line = String::new();
reader.read_line(&mut line)?;
match line.starts_with("OK") {
true => Ok(()),
false => Err(RustADBError::ADBRequestFailed(line)),
}
}
}
impl ADBTransport for TCPEmulatorTransport {
fn disconnect(&mut self) -> Result<()> {
if let Some(conn) = &mut self.tcp_stream {
conn.shutdown(std::net::Shutdown::Both)?;
log::trace!("Disconnected from {}", conn.peer_addr()?);
}
Ok(())
}
/// Connect to current emulator and authenticate
fn connect(&mut self) -> Result<()> {
if self.tcp_stream.is_none() {
let stream = TcpStream::connect(self.socket_addr)?;
log::trace!("Successfully connected to {}", self.socket_addr);
self.tcp_stream = Some(stream.try_clone()?);
let mut reader = BufReader::new(stream);
// Android Console: Authentication required
// Android Console: type 'auth <auth_token>' to authenticate
// Android Console: you can find your <auth_token> in
// '/home/xxx/.emulator_console_auth_token'
for _ in 0..=4 {
let mut line = String::new();
reader.read_line(&mut line)?;
}
self.authenticate()?;
log::trace!("Authentication successful");
}
Ok(())
}
}

View File

@@ -0,0 +1,162 @@
use std::io::{Read, Write};
use std::net::{Ipv4Addr, SocketAddrV4, TcpStream};
use std::str::FromStr;
use byteorder::{ByteOrder, LittleEndian};
use crate::models::{AdbRequestStatus, SyncCommand};
use crate::{models::AdbServerCommand, ADBTransport};
use crate::{Result, RustADBError};
const DEFAULT_SERVER_IP: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
const DEFAULT_SERVER_PORT: u16 = 5037;
/// Server transport running on top on TCP
#[derive(Debug)]
pub struct TCPServerTransport {
socket_addr: SocketAddrV4,
tcp_stream: Option<TcpStream>,
}
impl Default for TCPServerTransport {
fn default() -> Self {
Self::new(SocketAddrV4::new(DEFAULT_SERVER_IP, DEFAULT_SERVER_PORT))
}
}
impl TCPServerTransport {
/// Instantiates a new instance of [TCPServerTransport]
pub fn new(socket_addr: SocketAddrV4) -> Self {
Self {
socket_addr,
tcp_stream: None,
}
}
/// Get underlying [SocketAddrV4]
pub fn get_socketaddr(&self) -> SocketAddrV4 {
self.socket_addr
}
pub(crate) fn proxy_connection(
&mut self,
adb_command: AdbServerCommand,
with_response: bool,
) -> Result<Vec<u8>> {
self.send_adb_request(adb_command)?;
if with_response {
let length = self.get_hex_body_length()?;
let mut body = vec![
0;
length
.try_into()
.map_err(|_| RustADBError::ConversionError)?
];
if length > 0 {
self.get_raw_connection()?.read_exact(&mut body)?;
}
Ok(body)
} else {
Ok(vec![])
}
}
pub(crate) fn get_raw_connection(&self) -> Result<&TcpStream> {
self.tcp_stream
.as_ref()
.ok_or(RustADBError::IOError(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"not connected",
)))
}
/// Gets the body length from hexadecimal value
pub(crate) fn get_hex_body_length(&mut self) -> Result<u32> {
let length_buffer = self.read_body_length()?;
Ok(u32::from_str_radix(
std::str::from_utf8(&length_buffer)?,
16,
)?)
}
/// Send the given [SyncCommand] to ADB server, and checks that the request has been taken in consideration.
pub(crate) fn send_sync_request(&mut self, command: SyncCommand) -> Result<()> {
// First 4 bytes are the name of the command we want to send
// (e.g. "SEND", "RECV", "STAT", "LIST")
Ok(self
.get_raw_connection()?
.write_all(command.to_string().as_bytes())?)
}
/// Gets the body length from a LittleEndian value
pub(crate) fn get_body_length(&mut self) -> Result<u32> {
let length_buffer = self.read_body_length()?;
Ok(LittleEndian::read_u32(&length_buffer))
}
/// Read 4 bytes representing body length
fn read_body_length(&mut self) -> Result<[u8; 4]> {
let mut length_buffer = [0; 4];
self.get_raw_connection()?.read_exact(&mut length_buffer)?;
Ok(length_buffer)
}
/// Send the given [AdbCommand] to ADB server, and checks that the request has been taken in consideration.
/// If an error occurred, a [RustADBError] is returned with the response error string.
pub(crate) fn send_adb_request(&mut self, command: AdbServerCommand) -> Result<()> {
let adb_command_string = command.to_string();
let adb_request = format!("{:04x}{}", adb_command_string.len(), adb_command_string);
self.get_raw_connection()?
.write_all(adb_request.as_bytes())?;
// Reads returned status code from ADB server
let mut request_status = [0; 4];
self.get_raw_connection()?.read_exact(&mut request_status)?;
match AdbRequestStatus::from_str(std::str::from_utf8(request_status.as_ref())?)? {
AdbRequestStatus::Fail => {
// We can keep reading to get further details
let length = self.get_hex_body_length()?;
let mut body = vec![
0;
length
.try_into()
.map_err(|_| RustADBError::ConversionError)?
];
if length > 0 {
self.get_raw_connection()?.read_exact(&mut body)?;
}
Err(RustADBError::ADBRequestFailed(String::from_utf8(body)?))
}
AdbRequestStatus::Okay => Ok(()),
}
}
}
impl ADBTransport for TCPServerTransport {
fn disconnect(&mut self) -> Result<()> {
if let Some(conn) = &mut self.tcp_stream {
conn.shutdown(std::net::Shutdown::Both)?;
log::trace!("Disconnected from {}", conn.peer_addr()?);
}
Ok(())
}
fn connect(&mut self) -> Result<()> {
if let Some(previous) = &self.tcp_stream {
// Ignoring underlying error, we will recreate a new connection
let _ = previous.shutdown(std::net::Shutdown::Both);
}
self.tcp_stream = Some(TcpStream::connect(self.socket_addr)?);
log::trace!("Successfully connected to {}", self.socket_addr);
Ok(())
}
}

View File

@@ -0,0 +1,10 @@
use crate::Result;
/// Trait representing a transport
pub trait ADBTransport {
/// Initializes the connection
fn connect(&mut self) -> Result<()>;
/// Shuts down the connection
fn disconnect(&mut self) -> Result<()>;
}

9
adb_client/src/utils.rs Normal file
View File

@@ -0,0 +1,9 @@
use crate::{Result, RustADBError};
pub fn u32_from_le(value: &[u8]) -> Result<u32> {
Ok(u32::from_le_bytes(
value
.try_into()
.map_err(|_| RustADBError::ConversionError)?,
))
}

View File

@@ -1,149 +0,0 @@
use std::fs::File;
use std::net::Ipv4Addr;
use std::path::Path;
use adb_client::{AdbTcpConnection, Device, RebootType, RustADBError};
use clap::Parser;
#[derive(Parser, Debug)]
#[clap(about, version, author)]
pub struct Args {
/// Sets the listening address of ADB server
#[clap(short = 'a', long = "address", default_value = "127.0.0.1")]
pub address: Ipv4Addr,
/// Sets the listening port of ADB server
#[clap(short = 'p', long = "port", default_value = "5037")]
pub port: u16,
/// Serial id of a specific device. Every request will be sent to this device.
#[clap(short = 's', long = "serial")]
pub serial: Option<String>,
#[clap(subcommand)]
pub command: Command,
}
#[derive(Parser, Debug)]
pub 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,
/// Lists available server features.
HostFeatures,
/// Pushes 'filename' to the 'path' on device
Push { filename: String, path: String },
/// Pushes 'path' on the device to 'filename'
Pull { path: String, filename: String },
/// List files for 'path' on device
List { path: String },
/// Stat file specified as 'path' on device
Stat { path: String },
/// Run 'command' in a shell on the device, and return its output and error streams.
Shell { command: Vec<String> },
/// Reboots the device
Reboot {
#[clap(subcommand)]
sub_command: RebootTypeCommand,
},
}
#[derive(Parser, Debug)]
pub enum RebootTypeCommand {
System,
Bootloader,
Recovery,
Sideload,
SideloadAutoReboot,
}
impl From<RebootTypeCommand> for RebootType {
fn from(value: RebootTypeCommand) -> Self {
match value {
RebootTypeCommand::System => RebootType::System,
RebootTypeCommand::Bootloader => RebootType::Bootloader,
RebootTypeCommand::Recovery => RebootType::Recovery,
RebootTypeCommand::Sideload => RebootType::Sideload,
RebootTypeCommand::SideloadAutoReboot => RebootType::SideloadAutoReboot,
}
}
}
fn main() -> Result<(), RustADBError> {
let opt = Args::parse();
let mut connection = AdbTcpConnection::new(opt.address, opt.port)?;
match opt.command {
Command::Version => {
let version = connection.version()?;
println!("Android Debug Bridge version {}", version);
println!("Package version {}-rust", std::env!("CARGO_PKG_VERSION"));
}
Command::Kill => {
connection.kill()?;
}
Command::Devices { long } => {
if long {
println!("List of devices attached (extended)");
for device in connection.devices_long()? {
println!("{}", device);
}
} else {
println!("List of devices attached");
for device in connection.devices()? {
println!("{}", device);
}
}
}
Command::TrackDevices => {
let callback = |device: Device| {
println!("{}", device);
Ok(())
};
println!("Live list of devices attached");
connection.track_devices(callback)?;
}
Command::Pull { path, filename } => {
let mut output = File::create(Path::new(&filename)).unwrap(); // TODO: Better error handling
connection.recv(opt.serial.as_ref(), &path, &mut output)?;
println!("Downloaded {path} as {filename}");
}
Command::Push { filename, path } => {
let mut input = File::open(Path::new(&filename)).unwrap(); // TODO: Better error handling
connection.send(opt.serial.as_ref(), &mut input, &path)?;
println!("Uploaded {filename} to {path}");
}
Command::List { path } => {
connection.list(opt.serial.as_ref(), path)?;
}
Command::Stat { path } => {
let stat_response = connection.stat(opt.serial, path)?;
println!("{}", stat_response);
}
Command::Shell { command } => {
if command.is_empty() {
connection.shell(opt.serial.as_ref())?;
} else {
connection.shell_command(opt.serial.as_ref(), command)?;
}
}
Command::HostFeatures => {
println!("Available host features");
for feature in connection.host_features(opt.serial.as_ref())? {
println!("- {}", feature);
}
}
Command::Reboot { sub_command } => {
println!("Reboots device");
connection.reboot(opt.serial.as_ref(), sub_command.into())?
}
}
Ok(())
}

View File

@@ -1,135 +0,0 @@
use std::{
io::{Read, Write},
net::{Ipv4Addr, SocketAddrV4, TcpStream},
str,
str::FromStr,
};
use byteorder::{ByteOrder, LittleEndian};
use crate::{
models::{AdbCommand, AdbRequestStatus, SyncCommand},
Result, RustADBError,
};
/// Represents an ADB-over-TCP connection.
#[derive(Debug)]
pub struct AdbTcpConnection {
pub(crate) socket_addr: SocketAddrV4,
pub(crate) tcp_stream: TcpStream,
}
impl AdbTcpConnection {
/// Instantiates a new instance of [AdbTcpConnection]
pub fn new(address: Ipv4Addr, port: u16) -> Result<Self> {
let addr = SocketAddrV4::new(address, port);
Ok(Self {
socket_addr: addr,
tcp_stream: TcpStream::connect(addr)?,
})
}
/// Creates a new connection to ADB server.
///
/// Can be used after requests that closes connection.
pub(crate) fn new_connection(&mut self) -> Result<()> {
self.tcp_stream = TcpStream::connect(self.socket_addr)?;
Ok(())
}
pub(crate) fn proxy_connection(
&mut self,
adb_command: AdbCommand,
with_response: bool,
fresh_connection: bool
) -> Result<Vec<u8>> {
self.send_adb_request(adb_command, fresh_connection)?;
if with_response {
let length = self.get_hex_body_length()?;
let mut body = vec![
0;
length
.try_into()
.map_err(|_| RustADBError::ConversionError)?
];
if length > 0 {
self.tcp_stream.read_exact(&mut body)?;
}
Ok(body)
} else {
Ok(vec![])
}
}
/// Sends the given [AdbCommand] to ADB server, and checks that the request has been taken in consideration.
/// If an error occurred, a [RustADBError] is returned with the response error string.
pub(crate) fn send_adb_request(
&mut self,
command: AdbCommand,
fresh_connection: bool,
) -> Result<()> {
if fresh_connection {
// Recreate a new connection (likely because command does not need "state" server side)
self.new_connection()?;
}
let adb_command_string = command.to_string();
let adb_request = format!("{:04x}{}", adb_command_string.len(), adb_command_string);
self.tcp_stream.write_all(adb_request.as_bytes())?;
// Reads returned status code from ADB server
let mut request_status = [0; 4];
self.tcp_stream.read_exact(&mut request_status)?;
match AdbRequestStatus::from_str(str::from_utf8(request_status.as_ref())?)? {
AdbRequestStatus::Fail => {
// We can keep reading to get further details
let length = self.get_body_length()?;
let mut body = vec![
0;
length
.try_into()
.map_err(|_| RustADBError::ConversionError)?
];
if length > 0 {
self.tcp_stream.read_exact(&mut body)?;
}
Err(RustADBError::ADBRequestFailed(String::from_utf8(body)?))
}
AdbRequestStatus::Okay => Ok(()),
}
}
/// Sends the given [SyncCommand] to ADB server, and checks that the request has been taken in consideration.
pub(crate) fn send_sync_request(&mut self, command: SyncCommand) -> Result<()> {
// First 4 bytes are the name of the command we want to send
// (e.g. "SEND", "RECV", "STAT", "LIST")
Ok(self.tcp_stream.write_all(command.to_string().as_bytes())?)
}
/// Gets the body length from hexadecimal value
pub(crate) fn get_hex_body_length(&mut self) -> Result<u32> {
let length_buffer = self.read_body_length()?;
Ok(u32::from_str_radix(str::from_utf8(&length_buffer)?, 16)?)
}
/// Gets the body length from a LittleEndian value
pub(crate) fn get_body_length(&mut self) -> Result<u32> {
let length_buffer = self.read_body_length()?;
Ok(LittleEndian::read_u32(&length_buffer))
}
/// Read 4 bytes representing body length
fn read_body_length(&mut self) -> Result<[u8; 4]> {
let mut length_buffer = [0; 4];
self.tcp_stream.read_exact(&mut length_buffer)?;
Ok(length_buffer)
}
}

View File

@@ -1,59 +0,0 @@
use std::io::Read;
use crate::{models::AdbCommand, AdbTcpConnection, Device, DeviceLong, Result, RustADBError};
impl AdbTcpConnection {
/// Gets a list of connected devices.
pub fn devices(&mut self) -> Result<Vec<Device>> {
let devices = self.proxy_connection(AdbCommand::Devices, true, true)?;
let mut vec_devices: Vec<Device> = vec![];
for device in devices.split(|x| x.eq(&b'\n')) {
if device.is_empty() {
break;
}
vec_devices.push(Device::try_from(device.to_vec())?);
}
Ok(vec_devices)
}
/// Gets an extended list of connected devices including the device paths in the state.
pub fn devices_long(&mut self) -> Result<Vec<DeviceLong>> {
let devices_long = self.proxy_connection(AdbCommand::DevicesLong, true, true)?;
let mut vec_devices: Vec<DeviceLong> = vec![];
for device in devices_long.split(|x| x.eq(&b'\n')) {
if device.is_empty() {
break;
}
vec_devices.push(DeviceLong::try_from(device.to_vec())?);
}
Ok(vec_devices)
}
/// Tracks new devices showing up.
// TODO: Change with Generator when feature stabilizes
pub fn track_devices(&mut self, callback: impl Fn(Device) -> Result<()>) -> Result<()> {
self.send_adb_request(AdbCommand::TrackDevices, true)?;
loop {
let length = self.get_hex_body_length()?;
if length > 0 {
let mut body = vec![
0;
length
.try_into()
.map_err(|_| RustADBError::ConversionError)?
];
self.tcp_stream.read_exact(&mut body)?;
callback(Device::try_from(body)?)?;
}
}
}
}

View File

@@ -1,23 +0,0 @@
use crate::{
models::{AdbCommand, HostFeatures},
AdbTcpConnection, Result,
};
impl AdbTcpConnection {
/// Lists available ADB server features.
pub fn host_features<S: ToString>(&mut self, serial: Option<&S>) -> Result<Vec<HostFeatures>> {
match serial {
None => self.send_adb_request(AdbCommand::TransportAny, true)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()), true)?
}
}
let features = self.proxy_connection(AdbCommand::HostFeatures, true, false)?;
Ok(features
.split(|x| x.eq(&b','))
.filter_map(|v| HostFeatures::try_from(v).ok())
.collect())
}
}

View File

@@ -1,8 +0,0 @@
use crate::{models::AdbCommand, AdbTcpConnection, Result};
impl AdbTcpConnection {
/// Asks the ADB server to quit immediately.
pub fn kill(&mut self) -> Result<()> {
self.proxy_connection(AdbCommand::Kill, false, true).map(|_| ())
}
}

View File

@@ -1,23 +0,0 @@
use crate::{
models::{AdbCommand, RebootType},
AdbTcpConnection, Result,
};
impl AdbTcpConnection {
/// Reboots the device
pub fn reboot<S: ToString>(
&mut self,
serial: Option<&S>,
reboot_type: RebootType,
) -> Result<()> {
match serial {
None => self.send_adb_request(AdbCommand::TransportAny, true)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()), true)?
}
}
self.proxy_connection(AdbCommand::Reboot(reboot_type), false, false)
.map(|_| ())
}
}

View File

@@ -1,73 +0,0 @@
use crate::{
models::{AdbCommand, SyncCommand},
AdbTcpConnection, Result, RustADBError,
};
use byteorder::{ByteOrder, LittleEndian};
use std::io::{Read, Write};
impl AdbTcpConnection {
/// Receives [path] to [stream] from the device.
pub fn recv<S: ToString, A: AsRef<str>>(
&mut self,
serial: Option<&S>,
path: A,
stream: &mut dyn Write,
) -> Result<()> {
match serial {
None => self.send_adb_request(AdbCommand::TransportAny, true)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()), true)?
}
}
// Set device in SYNC mode
self.send_adb_request(AdbCommand::Sync, false)?;
// Send a recv command
self.send_sync_request(SyncCommand::Recv)?;
self.handle_recv_command(path, stream)
}
fn handle_recv_command<S: AsRef<str>>(
&mut self,
from: S,
output: &mut dyn Write,
) -> Result<()> {
// First send 8 byte common header
let mut len_buf = [0_u8; 4];
LittleEndian::write_u32(&mut len_buf, from.as_ref().len() as u32);
self.tcp_stream.write_all(&len_buf)?;
self.tcp_stream.write_all(from.as_ref().as_bytes())?;
// Then we receive the byte data in chunks of up to 64k
// Chunk looks like 'DATA' <length> <data>
let mut buffer = [0_u8; 64 * 1024]; // Should this be Boxed?
let mut data_header = [0_u8; 4]; // DATA
loop {
self.tcp_stream.read_exact(&mut data_header)?;
// Check if data_header is DATA or DONE or FAIL
match &data_header {
b"DATA" => {
// Handle received data
let length: usize = self.get_body_length()?.try_into().unwrap();
self.tcp_stream.read_exact(&mut buffer[..length])?;
output.write_all(&buffer[..length])?;
}
b"DONE" => break, // We're done here
b"FAIL" => {
// Handle fail
let length: usize = self.get_body_length()?.try_into().unwrap();
self.tcp_stream.read_exact(&mut buffer[..length])?;
Err(RustADBError::ADBRequestFailed(String::from_utf8(
buffer[..length].to_vec(),
)?))?;
}
_ => panic!("Unknown response from device {:#?}", data_header),
}
}
// Connection should've left SYNC by now
Ok(())
}
}

View File

@@ -1,10 +0,0 @@
use crate::{models::AdbCommand, AdbTcpConnection, AdbVersion, Result};
impl AdbTcpConnection {
/// Gets server's internal version number.
pub fn version(&mut self) -> Result<AdbVersion> {
let version = self.proxy_connection(AdbCommand::Version, true, true)?;
AdbVersion::try_from(version)
}
}

View File

@@ -1,75 +0,0 @@
use std::fmt::Display;
use super::RebootType;
pub enum AdbCommand {
Version,
Kill,
Devices,
DevicesLong,
TrackDevices,
HostFeatures,
// TODO: NOT IMPLEMENTED YET
// Emulator(u16),
// Transport(String),
// TransportUSB,
// TransportLocal,
TransportAny,
TransportSerial(String),
// Serial((String, String)),
// USB(String),
// Local(String),
// Request(String),
// GetProduct(String),
// GetSerialNo(String),
// GetDevPath(String),
// GetState(String),
// Forward((String, String, String)),
// ForwardNoRebind((String, String, String)),
// KillForward((String, String)),
// KillForwardAll(String),
// ListForward(String),
ShellCommand(String),
Shell,
// Remount,
// DevPath(String),
// Tcp(u16),
// Tcp((u16, String)),
// Local(String),
// LocalReserved(String),
// LocalAbstract(String),
// LocalFileSystem(String),
// FrameBuffer,
// JDWP(u32),
// TrackJDWP,
Sync,
// Reverse(String),
Reboot(RebootType),
}
impl Display for AdbCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AdbCommand::Version => write!(f, "host:version"),
AdbCommand::Kill => write!(f, "host:kill"),
AdbCommand::Devices => write!(f, "host:devices"),
AdbCommand::DevicesLong => write!(f, "host:devices-l"),
AdbCommand::Sync => write!(f, "sync:"),
AdbCommand::TrackDevices => write!(f, "host:track-devices"),
AdbCommand::TransportAny => write!(f, "host:transport-any"),
AdbCommand::TransportSerial(serial) => write!(f, "host:transport:{serial}"),
AdbCommand::ShellCommand(command) => match std::env::var("TERM") {
Ok(term) => write!(f, "shell,TERM={term},raw:{command}"),
Err(_) => write!(f, "shell,raw:{command}"),
},
AdbCommand::Shell => match std::env::var("TERM") {
Ok(term) => write!(f, "shell,TERM={term},raw:"),
Err(_) => write!(f, "shell,raw:"),
},
AdbCommand::HostFeatures => write!(f, "host:features"),
AdbCommand::Reboot(reboot_type) => {
write!(f, "reboot:{reboot_type}")
}
}
}
}

View File

@@ -1,15 +1,17 @@
#[cfg(test)]
mod tests {
use std::io::Cursor;
use std::net::Ipv4Addr;
use std::str::FromStr;
use adb_client::AdbTcpConnection;
use adb_client::{ADBServer, ADBServerDevice, DeviceLong};
use rand::Rng;
fn new_client() -> AdbTcpConnection {
let address = Ipv4Addr::from_str("127.0.0.1").unwrap();
AdbTcpConnection::new(address, 5037).expect("Could not build ADB client...")
fn new_client() -> ADBServer {
ADBServer::default()
}
fn new_device() -> ADBServerDevice {
let mut client = new_client();
return client.get_device().expect("cannot get device");
}
#[test]
@@ -20,9 +22,10 @@ mod tests {
#[test]
fn test_shell() {
let mut adb = new_client();
adb.shell_command(None, vec!["ls"]).unwrap();
adb.shell_command(None, vec!["pwd"]).unwrap();
let mut device = new_device();
device.shell_command(vec!["ls"]).unwrap();
device.shell_command(vec!["pwd"]).unwrap();
}
#[test]
@@ -38,10 +41,12 @@ mod tests {
}
#[test]
#[should_panic]
fn test_wrong_addr() {
let address = Ipv4Addr::from_str("127.0.0.300").unwrap();
let _ = AdbTcpConnection::new(address, 5037).expect("Could not create ADB connection...");
fn test_static_devices_long() {
let inputs = ["7a5158f05122195aa device 1-5 product:gts210vewifixx model:SM_T813 device:gts210vewifi transport_id:4"];
for input in inputs {
DeviceLong::try_from(input.as_bytes().to_vec())
.expect(&format!("cannot parse input: '{input}'"));
}
}
#[test]
@@ -51,25 +56,25 @@ mod tests {
rand::thread_rng().fill(&mut key[..]);
let mut c: Cursor<Vec<u8>> = Cursor::new(key.to_vec());
let mut connection = new_client();
let mut device = new_device();
const TEST_FILENAME: &'static str = "/data/local/tmp/test_file";
// Send it
connection
.send::<&str, &str>(None, &mut c, TEST_FILENAME)
device
.send(&mut c, TEST_FILENAME)
.expect("cannot send file");
// Pull it to memory
let mut res = vec![];
connection
.recv::<&str, &str>(None, TEST_FILENAME, &mut res)
device
.recv(TEST_FILENAME, &mut res)
.expect("cannot recv file");
// diff
assert_eq!(c.get_ref(), &res);
connection
.shell_command::<&str>(None, [format!("rm {TEST_FILENAME}").as_str()])
device
.shell_command::<&str>([format!("rm {TEST_FILENAME}").as_str()])
.expect("cannot remove test file");
}
@@ -81,4 +86,13 @@ mod tests {
let _ = connection.devices().expect("cannot get version");
}
}
#[test]
fn command_emulator() {
let mut connection = new_client();
let mut emulator = connection
.get_emulator_device()
.expect("no emulator running");
emulator.hello().expect("cannot hello");
}
}