Skip to content

Commit

Permalink
feat: implement adb pull for usb (#42)
Browse files Browse the repository at this point in the history
* feat: implement adb pull for usb

* feat: add subcommands for pull and shell

* feat: add recv method to `ADBDeviceExt` trait, use existing impl for server

* feat: merge stat commands for server and usb `impl`s

* feat: improve shell; fix: minor rewording

* fix: reply with quit usb subcommand at the end of a transaction

* fix: adb stat works on a file, not always an apk

* fix: ignore extra stat field for now

---------

Co-authored-by: LIAUD Corentin <[email protected]>
  • Loading branch information
lavafroth and cocool97 authored Oct 25, 2024
1 parent 576e82d commit 73021b1
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 72 deletions.
4 changes: 4 additions & 0 deletions adb_cli/src/commands/usb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ pub struct UsbCommand {
pub enum UsbCommands {
/// Spawn an interactive shell or run a list of commands on the device
Shell { commands: Vec<String> },
/// Pull a file from device
Pull { source: String, destination: String },
/// Stat a file on device
Stat { path: String },
/// Reboot the device
Reboot {
#[clap(subcommand)]
Expand Down
16 changes: 14 additions & 2 deletions adb_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn main() -> Result<()> {
match local {
LocalCommand::Pull { path, filename } => {
let mut output = File::create(Path::new(&filename))?;
device.recv(&path, &mut output)?;
device.pull(&path, &mut output)?;
log::info!("Downloaded {path} as {filename}");
}
LocalCommand::Push { filename, path } => {
Expand All @@ -44,7 +44,7 @@ fn main() -> Result<()> {
}
LocalCommand::Stat { path } => {
let stat_response = device.stat(path)?;
log::info!("{}", stat_response);
println!("{}", stat_response);
}
LocalCommand::Shell { commands } => {
if commands.is_empty() {
Expand Down Expand Up @@ -181,6 +181,18 @@ fn main() -> Result<()> {
device.shell_command(commands, std::io::stdout())?;
}
}
UsbCommands::Pull {
source,
destination,
} => {
let mut output = File::create(Path::new(&destination))?;
device.pull(&source, &mut output)?;
log::info!("Downloaded {source} as {destination}");
}
UsbCommands::Stat { path } => {
let stat_response = device.stat(&path)?;
println!("{}", stat_response);
}
UsbCommands::Reboot { reboot_type } => {
log::info!("Reboots device in mode {:?}", reboot_type);
device.reboot(reboot_type.into())?
Expand Down
7 changes: 7 additions & 0 deletions adb_client/src/adb_device_ext.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::io::{Read, Write};

use crate::models::AdbStatResponse;
use crate::{RebootType, Result};

/// Trait representing all features available on both [`ADBServerDevice`] and [`ADBUSBDevice`]
Expand All @@ -16,6 +17,12 @@ pub trait ADBDeviceExt {
/// [W] has a 'static bound as it is internally used in a thread.
fn shell<R: Read, W: Write + Send + 'static>(&mut self, reader: R, writer: W) -> Result<()>;

/// Display the stat information for a remote file
fn stat(&mut self, remote_path: &str) -> Result<AdbStatResponse>;

/// Pull the remote file pointed to by [source] and write its contents into [`output`]
fn pull<A: AsRef<str>, W: Write>(&mut self, source: A, output: W) -> Result<()>;

/// Reboots the device using given reboot type
fn reboot(&mut self, reboot_type: RebootType) -> Result<()>;
}
43 changes: 43 additions & 0 deletions adb_client/src/models/adb_stat_response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use byteorder::ByteOrder;
use chrono::{DateTime, Utc};
use std::{
fmt::Display,
time::{Duration, UNIX_EPOCH},
};

use byteorder::LittleEndian;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
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(())
}
}
2 changes: 2 additions & 0 deletions adb_client/src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod adb_emulator_command;
mod adb_request_status;
mod adb_server_command;
mod adb_stat_response;
mod adb_version;
mod device_long;
mod device_short;
Expand All @@ -12,6 +13,7 @@ mod sync_command;
pub(crate) use adb_emulator_command::ADBEmulatorCommand;
pub use adb_request_status::AdbRequestStatus;
pub(crate) use adb_server_command::AdbServerCommand;
pub use adb_stat_response::AdbStatResponse;
pub use adb_version::AdbVersion;
pub use device_long::DeviceLong;
pub use device_short::DeviceShort;
Expand Down
10 changes: 9 additions & 1 deletion adb_client/src/server/adb_server_device_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::io::{Read, Write};

use crate::{
constants::BUFFER_SIZE,
models::{AdbServerCommand, HostFeatures},
models::{AdbServerCommand, AdbStatResponse, HostFeatures},
ADBDeviceExt, ADBServerDevice, Result, RustADBError,
};

Expand Down Expand Up @@ -53,6 +53,10 @@ impl ADBDeviceExt for ADBServerDevice {
}
}

fn stat(&mut self, remote_path: &str) -> Result<AdbStatResponse> {
self.stat(remote_path)
}

fn shell<R: Read, W: Write + Send + 'static>(
&mut self,
mut reader: R,
Expand Down Expand Up @@ -106,6 +110,10 @@ impl ADBDeviceExt for ADBServerDevice {
Ok(())
}

fn pull<A: AsRef<str>, W: Write>(&mut self, source: A, mut output: W) -> Result<()> {
self.pull(source, &mut output)
}

fn reboot(&mut self, reboot_type: crate::RebootType) -> Result<()> {
self.reboot(reboot_type)
}
Expand Down
2 changes: 1 addition & 1 deletion adb_client/src/server/device_commands/recv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl<R: Read> Read for ADBRecvCommandReader<R> {

impl ADBServerDevice {
/// Receives [path] to [stream] from the device.
pub fn recv<A: AsRef<str>>(&mut self, path: A, stream: &mut dyn Write) -> Result<()> {
pub fn pull<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))?;
Expand Down
43 changes: 2 additions & 41 deletions adb_client/src/server/device_commands/stat.rs
Original file line number Diff line number Diff line change
@@ -1,51 +1,12 @@
use std::{
fmt::Display,
io::{Read, Write},
time::{Duration, UNIX_EPOCH},
};
use std::io::{Read, Write};

use byteorder::{ByteOrder, LittleEndian};
use chrono::{DateTime, Utc};

use crate::{
models::{AdbServerCommand, SyncCommand},
models::{AdbServerCommand, AdbStatResponse, SyncCommand},
ADBServerDevice, 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 ADBServerDevice {
fn handle_stat_command<S: AsRef<str>>(&mut self, path: S) -> Result<AdbStatResponse> {
let mut len_buf = [0_u8; 4];
Expand Down
148 changes: 147 additions & 1 deletion adb_client/src/usb/adb_usb_device.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
use byteorder::ReadBytesExt;
use rand::Rng;
use std::fs::read_to_string;
use std::io::Cursor;
use std::io::Read;
use std::io::Seek;
use std::path::PathBuf;
use std::time::Duration;

use byteorder::LittleEndian;

use super::{ADBRsaKey, ADBUsbMessage};
use crate::models::AdbStatResponse;
use crate::usb::adb_usb_message::{AUTH_RSAPUBLICKEY, AUTH_SIGNATURE, AUTH_TOKEN};
use crate::{usb::usb_commands::USBCommand, ADBTransport, Result, RustADBError, USBTransport};
use crate::{
usb::usb_commands::{USBCommand, USBSubcommand},
ADBTransport, Result, RustADBError, USBTransport,
};

/// Represent a device reached directly over USB
#[derive(Debug)]
Expand Down Expand Up @@ -128,6 +139,141 @@ impl ADBUSBDevice {

Ok(())
}
/// Receive a message and acknowledge it by replying with an `OKAY` command
pub(crate) fn recv_and_reply_okay(
&mut self,
local_id: u32,
remote_id: u32,
) -> Result<ADBUsbMessage> {
let message = self.transport.read_message()?;
self.transport.write_message(ADBUsbMessage::new(
USBCommand::Okay,
local_id,
remote_id,
"".into(),
))?;
Ok(message)
}

/// Expect a message with an `OKAY` command after sending a message.
/// Return the value if it conforms to the constraint or error out.
pub(crate) fn send_and_expect_okay(&mut self, message: ADBUsbMessage) -> Result<ADBUsbMessage> {
self.transport.write_message(message)?;
let message = self.transport.read_message()?;
if message.header().command() != USBCommand::Okay {
return Err(RustADBError::ADBRequestFailed(format!(
"expected command OKAY after message, got {}",
message.header().command()
)));
}
Ok(message)
}

pub(crate) fn recv_file<W: std::io::Write>(
&mut self,
local_id: u32,
remote_id: u32,
mut output: W,
) -> std::result::Result<(), RustADBError> {
let mut len: Option<u64> = None;
loop {
let payload = self
.recv_and_reply_okay(local_id, remote_id)?
.into_payload();
let mut rdr = Cursor::new(&payload);
while rdr.position() != payload.len() as u64 {
match len.take() {
Some(0) | None => {
rdr.seek_relative(4)?;
len.replace(rdr.read_u32::<LittleEndian>()? as u64);
}
Some(length) => {
log::debug!("len = {length}");
let remaining_bytes = payload.len() as u64 - rdr.position();
log::debug!(
"payload length {} - reader_position {} = {remaining_bytes}",
payload.len(),
rdr.position()
);
if length < remaining_bytes {
let read = std::io::copy(&mut rdr.by_ref().take(length), &mut output)?;
log::debug!(
"expected to read {length} bytes, actually read {read} bytes"
);
} else {
let read = std::io::copy(&mut rdr.take(remaining_bytes), &mut output)?;
len.replace(length - remaining_bytes);
log::debug!("expected to read {remaining_bytes} bytes, actually read {read} bytes");
// this payload is exhausted
break;
}
}
}
}
if Cursor::new(&payload[(payload.len() - 8)..(payload.len() - 4)])
.read_u32::<LittleEndian>()?
== USBSubcommand::Done as u32
{
break;
}
}
Ok(())
}

pub(crate) fn begin_transaction(&mut self) -> Result<(u32, u32)> {
let sync_directive = "sync:.\0";

let mut rng = rand::thread_rng();
let message = ADBUsbMessage::new(
USBCommand::Open,
rng.gen(), /* Our 'local-id' */
0,
sync_directive.into(),
);
let message = self.send_and_expect_okay(message)?;
let local_id = message.header().arg1();
let remote_id = message.header().arg0();
Ok((local_id, remote_id))
}

pub(crate) fn stat_with_explicit_ids(
&mut self,
remote_path: &str,
local_id: u32,
remote_id: u32,
) -> Result<AdbStatResponse> {
let stat_buffer = USBSubcommand::Stat.with_arg(remote_path.len() as u32);
let message = ADBUsbMessage::new(
USBCommand::Write,
local_id,
remote_id,
bincode::serialize(&stat_buffer).map_err(|_e| RustADBError::ConversionError)?,
);
self.send_and_expect_okay(message)?;
self.send_and_expect_okay(ADBUsbMessage::new(
USBCommand::Write,
local_id,
remote_id,
remote_path.into(),
))?;
let response = self.transport.read_message()?;
// Skip first 4 bytes as this is the literal "STAT".
// Interesting part starts right after
bincode::deserialize(&response.into_payload()[4..])
.map_err(|_e| RustADBError::ConversionError)
}

pub(crate) fn end_transaction(&mut self, local_id: u32, remote_id: u32) -> Result<()> {
let quit_buffer = USBSubcommand::Quit.with_arg(0u32);
self.send_and_expect_okay(ADBUsbMessage::new(
USBCommand::Write,
local_id,
remote_id,
bincode::serialize(&quit_buffer).map_err(|_e| RustADBError::ConversionError)?,
))?;
let _discard_close = self.transport.read_message()?;
Ok(())
}
}

impl Drop for ADBUSBDevice {
Expand Down
Loading

0 comments on commit 73021b1

Please sign in to comment.