Sync functionality (#11)

* Sync functionality

Added the following sync commands:
SEND - Working as intended
RECV - Working as intended
STAT - Only checks if file exist
LIST - Untested due to device issues
---------

Co-authored-by: Fredrik Jagenheim <fjagenheim@jabra.com>
Co-authored-by: LIAUD Corentin <corentin.liaud@orange.fr>
This commit is contained in:
jagenheim
2023-08-15 21:07:41 +02:00
committed by GitHub
parent fc98bdc5a8
commit fff8bd0c20
17 changed files with 452 additions and 57 deletions

View File

@@ -1,10 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.5.0] - 2023-04-07
- [Breaking] Commands previously using `serial` argument now takes `&Option<S: ToString>` instead of `Option<String>`. (#8)
- Adds `serial` argument for `host-feature` command. (#8)
Thanks @jagenheim for contributing !

View File

@@ -17,9 +17,11 @@ name = "adb_cli"
path = "examples/adb_cli.rs"
[dependencies]
byteorder = { version = "1.4.3" }
chrono = { version = "0.4.26" }
regex = { version = "1.9.3", features = ["perf", "std", "unicode"] }
termios = { version = "0.3.3" }
thiserror = { version = "1.0.44" }
thiserror = { version = "1.0.46" }
## Binary-only dependencies
## Marked as optional so that lib users do not depend on them

View File

@@ -48,7 +48,6 @@ cargo install adb_client --example adb_cli
## Missing features
- Pull / Push files
- USB protocol
All pull requests are welcome !

View File

@@ -1,4 +1,6 @@
use std::fs::File;
use std::net::Ipv4Addr;
use std::path::Path;
use adb_client::{AdbTcpConnexion, Device, RebootType, RustADBError};
use clap::Parser;
@@ -34,6 +36,14 @@ pub enum Command {
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
@@ -99,6 +109,23 @@ fn main() -> Result<(), RustADBError> {
println!("Live list of devices attached");
connexion.track_devices(callback)?;
}
Command::Pull { path, filename } => {
let mut output = File::create(Path::new(&filename)).unwrap(); // TODO: Better error handling
connexion.recv(opt.serial, &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
connexion.send(opt.serial, &mut input, &path)?;
println!("Uploaded {filename} to {path}");
}
Command::List { path } => {
connexion.list(opt.serial, path)?;
}
Command::Stat { path } => {
let stat_response = connexion.stat(opt.serial, path)?;
println!("{}", stat_response);
}
Command::Shell { command } => {
if command.is_empty() {
connexion.shell(&opt.serial)?;

View File

@@ -6,7 +6,7 @@ use std::{
};
use crate::{
models::{AdbCommand, AdbRequestStatus},
models::{AdbCommand, AdbRequestStatus, SyncCommand},
Result, RustADBError,
};
@@ -41,10 +41,10 @@ impl AdbTcpConnexion {
adb_command: AdbCommand,
with_response: bool,
) -> Result<Vec<u8>> {
Self::send_adb_request(&mut self.tcp_stream, adb_command)?;
self.send_adb_request(adb_command)?;
if with_response {
let length = Self::get_body_length(&mut self.tcp_stream)?;
let length = self.get_body_length()?;
let mut body = vec![
0;
length
@@ -63,20 +63,20 @@ impl AdbTcpConnexion {
/// Sends the given [AdbCommand] to ADB server, and checks that the request has been taken in consideration.
/// If an error occured, a [RustADBError] is returned with the response error string.
pub(crate) fn send_adb_request(tcp_stream: &mut TcpStream, command: AdbCommand) -> Result<()> {
pub(crate) fn send_adb_request(&mut self, command: AdbCommand) -> Result<()> {
let adb_command_string = command.to_string();
let adb_request = format!("{:04x}{}", adb_command_string.len(), adb_command_string);
tcp_stream.write_all(adb_request.as_bytes())?;
self.tcp_stream.write_all(adb_request.as_bytes())?;
// Reads returned status code from ADB server
let mut request_status = [0; 4];
tcp_stream.read_exact(&mut request_status)?;
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(tcp_stream)?;
let length = self.get_body_length()?;
let mut body = vec![
0;
@@ -85,7 +85,7 @@ impl AdbTcpConnexion {
.map_err(|_| RustADBError::ConvertionError)?
];
if length > 0 {
tcp_stream.read_exact(&mut body)?;
self.tcp_stream.read_exact(&mut body)?;
}
Err(RustADBError::ADBRequestFailed(String::from_utf8(body)?))
@@ -94,9 +94,16 @@ impl AdbTcpConnexion {
}
}
pub(crate) fn get_body_length(tcp_stream: &mut TcpStream) -> Result<u32> {
/// 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())?)
}
pub(crate) fn get_body_length(&mut self) -> Result<u32> {
let mut length = [0; 4];
tcp_stream.read_exact(&mut length)?;
self.tcp_stream.read_exact(&mut length)?;
Ok(u32::from_str_radix(str::from_utf8(&length)?, 16)?)
}

View File

@@ -38,10 +38,10 @@ impl AdbTcpConnexion {
/// 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(&mut self.tcp_stream, AdbCommand::TrackDevices)?;
self.send_adb_request(AdbCommand::TrackDevices)?;
loop {
let length = Self::get_body_length(&mut self.tcp_stream)?;
let length = self.get_body_length()?;
if length > 0 {
let mut body = vec![

View File

@@ -7,11 +7,10 @@ impl AdbTcpConnexion {
/// 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(&mut self.tcp_stream, AdbCommand::TransportAny)?,
Some(serial) => Self::send_adb_request(
&mut self.tcp_stream,
AdbCommand::TransportSerial(serial.to_string()),
)?,
None => self.send_adb_request(AdbCommand::TransportAny)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()))?
}
}
let features = self.proxy_connexion(AdbCommand::HostFeatures, true)?;

72
src/commands/list.rs Normal file
View File

@@ -0,0 +1,72 @@
use crate::{
models::{AdbCommand, SyncCommand},
AdbTcpConnexion, Result,
};
use byteorder::{ByteOrder, LittleEndian};
use std::{
io::{Read, Write},
str,
};
impl AdbTcpConnexion {
/// Lists files in [path] on the device.
pub fn list<S: ToString, A: AsRef<str>>(&mut self, serial: Option<S>, path: A) -> Result<()> {
self.new_connection()?;
match serial {
None => self.send_adb_request(AdbCommand::TransportAny)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()))?
}
}
// Set device in SYNC mode
self.send_adb_request(AdbCommand::Sync)?;
// Send a list command
self.send_sync_request(SyncCommand::List(path.as_ref()))?;
self.handle_list_command(path)
}
// This command does not seem to work correctly. The devices I test it on just resturn
// 'DONE' directly without listing anything.
fn handle_list_command<S: AsRef<str>>(&mut self, path: S) -> Result<()> {
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)?;
// List sends the string of the directory to list, and then the server sends a list of files
self.tcp_stream
.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)?;
match str::from_utf8(response.as_ref())? {
"DENT" => {
// TODO: Move this to a struct that extract this data, but as the device
// I test this on does not return anything, I can't test it.
let mut file_mod = [0_u8; 4];
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 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)?;
}
"DONE" => {
return Ok(());
}
x => println!("Unknown response {}", x),
}
}
}
}

View File

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

View File

@@ -11,11 +11,10 @@ impl AdbTcpConnexion {
reboot_type: RebootType,
) -> Result<()> {
match serial {
None => Self::send_adb_request(&mut self.tcp_stream, AdbCommand::TransportAny)?,
Some(serial) => Self::send_adb_request(
&mut self.tcp_stream,
AdbCommand::TransportSerial(serial.to_string()),
)?,
None => self.send_adb_request(AdbCommand::TransportAny)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()))?
}
}
self.proxy_connexion(AdbCommand::Reboot(reboot_type), false)

77
src/commands/recv.rs Normal file
View File

@@ -0,0 +1,77 @@
use crate::{
models::{AdbCommand, SyncCommand},
AdbTcpConnexion, Result, RustADBError,
};
use byteorder::{ByteOrder, LittleEndian};
use std::io::{Read, Write};
impl AdbTcpConnexion {
/// 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<()> {
self.new_connection()?;
match serial {
None => self.send_adb_request(AdbCommand::TransportAny)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()))?
}
}
// Set device in SYNC mode
self.send_adb_request(AdbCommand::Sync)?;
// Send a recv command
self.send_sync_request(SyncCommand::Recv(path.as_ref(), stream))?;
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
let mut len_header = [0_u8; 4]; // <len>
loop {
self.tcp_stream.read_exact(&mut data_header)?;
// Check if data_header is DATA or DONE
if data_header.eq(b"DATA") {
self.tcp_stream.read_exact(&mut len_header)?;
let length: usize = LittleEndian::read_u32(&len_header).try_into().unwrap();
self.tcp_stream.read_exact(&mut buffer[..length])?;
output.write_all(&buffer)?;
} else if data_header.eq(b"DONE") {
// We're done here
break;
} else if data_header.eq(b"FAIL") {
// Handle fail
self.tcp_stream.read_exact(&mut len_header)?;
let length: usize = LittleEndian::read_u32(&len_header).try_into().unwrap();
self.tcp_stream.read_exact(&mut buffer[..length])?;
Err(RustADBError::ADBRequestFailed(String::from_utf8(
buffer[..length].to_vec(),
)?))?;
} else {
panic!("Unknown response from device {:#?}", data_header);
}
}
// Connection should've left SYNC by now
Ok(())
}
}

100
src/commands/send.rs Normal file
View File

@@ -0,0 +1,100 @@
use crate::{
models::{AdbCommand, AdbRequestStatus, SyncCommand},
AdbTcpConnexion, Result, RustADBError,
};
use byteorder::{ByteOrder, LittleEndian};
use std::{
convert::TryInto,
io::{Read, Write},
str::{self, FromStr},
time::SystemTime,
};
impl AdbTcpConnexion {
/// 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<()> {
self.new_connection()?;
match serial {
None => self.send_adb_request(AdbCommand::TransportAny)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()))?
}
}
// Set device in SYNC mode
self.send_adb_request(AdbCommand::Sync)?;
// Send a send command
self.send_sync_request(SyncCommand::Send(stream, path.as_ref()))?;
self.handle_send_command(stream, path)
}
fn handle_send_command<S: AsRef<str>>(&mut self, input: &mut dyn Read, to: S) -> Result<()> {
// 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
let mut len_buf = [0_u8; 4];
LittleEndian::write_u32(&mut len_buf, to.len() as u32);
self.tcp_stream.write_all(&len_buf)?;
// Send appends the filemode to the string sent
self.tcp_stream.write_all(to.as_bytes())?;
// Then we send the byte data in chunks of up to 64k
// Chunk looks like 'DATA' <length> <data>
let mut buffer = [0_u8; 64 * 1024];
loop {
let bytes_read = input.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
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])?;
}
// When we are done sending, we send 'DONE' <last modified time>
// Re-use len_buf to send the last modified time
let last_modified = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
Ok(n) => n,
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)?;
// We expect 'OKAY' response from this
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::ConvertionError)?
];
if length > 0 {
self.tcp_stream.read_exact(&mut body)?;
}
Err(RustADBError::ADBRequestFailed(String::from_utf8(body)?))
}
AdbRequestStatus::Okay => Ok(()),
}
}
}

View File

@@ -23,22 +23,18 @@ impl AdbTcpConnexion {
self.new_connection()?;
match serial {
None => Self::send_adb_request(&mut self.tcp_stream, AdbCommand::TransportAny)?,
Some(serial) => Self::send_adb_request(
&mut self.tcp_stream,
AdbCommand::TransportSerial(serial.to_string()),
)?,
None => self.send_adb_request(AdbCommand::TransportAny)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()))?
}
}
Self::send_adb_request(
&mut self.tcp_stream,
AdbCommand::ShellCommand(
command
.into_iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" "),
),
)?;
self.send_adb_request(AdbCommand::ShellCommand(
command
.into_iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" "),
))?;
let buffer_size = 512;
loop {
@@ -66,7 +62,7 @@ impl AdbTcpConnexion {
self.tcp_stream.set_nodelay(true)?;
// FORWARD CRTL+C !!
// FORWARD CTRL+C !!
let supported_features = self.host_features(serial)?;
if !supported_features.contains(&HostFeatures::ShellV2)
@@ -78,13 +74,12 @@ impl AdbTcpConnexion {
self.new_connection()?;
match serial {
None => Self::send_adb_request(&mut self.tcp_stream, AdbCommand::TransportAny)?,
Some(serial) => Self::send_adb_request(
&mut self.tcp_stream,
AdbCommand::TransportSerial(serial.to_string()),
)?,
None => self.send_adb_request(AdbCommand::TransportAny)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()))?
}
}
Self::send_adb_request(&mut self.tcp_stream, AdbCommand::Shell)?;
self.send_adb_request(AdbCommand::Shell)?;
// let read_stream = Arc::new(self.tcp_stream);
let mut read_stream = self.tcp_stream.try_clone()?;

99
src/commands/stat.rs Normal file
View File

@@ -0,0 +1,99 @@
use std::{
fmt::Display,
io::{Read, Write},
time::{Duration, UNIX_EPOCH},
};
use byteorder::{ByteOrder, LittleEndian};
use chrono::{DateTime, Utc};
use crate::{
models::{AdbCommand, SyncCommand},
AdbTcpConnexion, Result, RustADBError,
};
#[derive(Debug)]
pub struct AdbStatResponse {
pub file_perm: u32,
pub file_size: u32,
pub mod_time: u32,
}
impl From<[u8; 12]> for AdbStatResponse {
fn from(value: [u8; 12]) -> Self {
Self {
file_perm: LittleEndian::read_u32(&value[0..4]),
file_size: LittleEndian::read_u32(&value[4..8]),
mod_time: LittleEndian::read_u32(&value[8..]),
}
}
}
impl Display for AdbStatResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let d = UNIX_EPOCH + Duration::from_secs(self.mod_time.into());
// Create DateTime from SystemTime
let datetime = DateTime::<Utc>::from(d);
writeln!(f, "File permissions: {}", self.file_perm)?;
writeln!(f, "File size: {} bytes", self.file_size)?;
write!(
f,
"Modification time: {}",
datetime.format("%Y-%m-%d %H:%M:%S.%f %Z")
)?;
Ok(())
}
}
impl AdbTcpConnexion {
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
.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)?;
match std::str::from_utf8(response.as_ref())? {
"STAT" => {
let mut data = [0_u8; 12];
self.tcp_stream.read_exact(&mut data)?;
Ok(data.into())
}
x => Err(RustADBError::UnknownResponseType(format!(
"Unknown response {}",
x
))),
}
}
/// 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> {
self.new_connection()?;
match serial {
None => self.send_adb_request(AdbCommand::TransportAny)?,
Some(serial) => {
self.send_adb_request(AdbCommand::TransportSerial(serial.to_string()))?
}
}
// Set device in SYNC mode
self.send_adb_request(AdbCommand::Sync)?;
// Send a "Stat" command
self.send_sync_request(SyncCommand::Stat(path.as_ref()))?;
self.handle_stat_command(path)
}
}

View File

@@ -40,7 +40,7 @@ pub enum AdbCommand {
// FrameBuffer,
// JDWP(u32),
// TrackJDWP,
// Sync,
Sync,
// Reverse(String),
Reboot(RebootType),
}
@@ -52,6 +52,7 @@ impl ToString for AdbCommand {
AdbCommand::Kill => "host:kill".into(),
AdbCommand::Devices => "host:devices".into(),
AdbCommand::DevicesLong => "host:devices-l".into(),
AdbCommand::Sync => "sync:".into(),
AdbCommand::TrackDevices => "host:track-devices".into(),
AdbCommand::TransportAny => "host:transport-any".into(),
AdbCommand::TransportSerial(serial) => format!("host:transport:{serial}"),

View File

@@ -6,6 +6,7 @@ mod device_long;
mod device_state;
mod host_features;
mod reboot_type;
mod sync_command;
pub use adb_command::AdbCommand;
pub use adb_request_status::AdbRequestStatus;
@@ -15,3 +16,4 @@ pub use device_long::DeviceLong;
pub use device_state::DeviceState;
pub use host_features::HostFeatures;
pub use reboot_type::RebootType;
pub use sync_command::SyncCommand;

View File

@@ -0,0 +1,22 @@
pub enum SyncCommand<'a> {
/// List files in a folder
List(&'a str),
/// Receive a file from the device
Recv(&'a str, &'a mut dyn std::io::Write),
/// Send a file to the device
Send(&'a mut dyn std::io::Read, &'a str),
// Stat a file
Stat(&'a str),
}
impl ToString for SyncCommand<'_> {
fn to_string(&self) -> String {
match self {
SyncCommand::List(_) => "LIST",
SyncCommand::Recv(_, _) => "RECV",
SyncCommand::Send(_, _) => "SEND",
SyncCommand::Stat(_) => "STAT",
}
.to_string()
}
}