diff --git a/adb_cli/src/handlers/local_commands.rs b/adb_cli/src/handlers/local_commands.rs index 92411ad3..3f4f0aed 100644 --- a/adb_cli/src/handlers/local_commands.rs +++ b/adb_cli/src/handlers/local_commands.rs @@ -1,6 +1,6 @@ use std::{fs::File, io::Write}; -use adb_client::ADBServerDevice; +use adb_client::{ADBListItemType, ADBServerDevice}; use anyhow::{Result, anyhow}; use crate::models::LocalDeviceCommand; @@ -21,7 +21,6 @@ pub fn handle_local_commands( Ok(()) } - LocalDeviceCommand::List { path } => Ok(device.list(path)?), LocalDeviceCommand::Logcat { path } => { let writer: Box = if let Some(path) = path { let f = File::create(path)?; diff --git a/adb_cli/src/main.rs b/adb_cli/src/main.rs index 14605b21..ae9e8b54 100644 --- a/adb_cli/src/main.rs +++ b/adb_cli/src/main.rs @@ -8,7 +8,8 @@ mod models; mod utils; use adb_client::{ - ADBDeviceExt, ADBServer, ADBServerDevice, ADBTcpDevice, ADBUSBDevice, MDNSDiscoveryService, + ADBDeviceExt, ADBListItemType, ADBServer, ADBServerDevice, ADBTcpDevice, ADBUSBDevice, + MDNSDiscoveryService, }; #[cfg(any(target_os = "linux", target_os = "macos"))] @@ -159,6 +160,24 @@ fn main() -> Result<()> { device.framebuffer(&path)?; log::info!("Successfully dumped framebuffer at path {path}"); } + DeviceCommands::List { path } => { + let dirs = device.list(&path)?; + for dir in dirs { + let list_item_type = match dir.item_type { + ADBListItemType::File => "File".to_string(), + ADBListItemType::Directory => "Directory".to_string(), + ADBListItemType::Symlink => "Symlink".to_string(), + }; + log::info!( + "type: {}, name: {}, time: {}, size: {}, permissions: {:#o}", + list_item_type, + dir.name, + dir.time, + dir.size, + dir.permissions + ); + } + } } Ok(()) diff --git a/adb_cli/src/models/device.rs b/adb_cli/src/models/device.rs index bcaa9ce1..71b9d235 100644 --- a/adb_cli/src/models/device.rs +++ b/adb_cli/src/models/device.rs @@ -7,13 +7,23 @@ use super::RebootTypeCommand; #[derive(Parser, Debug)] pub enum DeviceCommands { /// Spawn an interactive shell or run a list of commands on the device - Shell { commands: Vec }, + Shell { + commands: Vec, + }, /// Pull a file from device - Pull { source: String, destination: String }, + Pull { + source: String, + destination: String, + }, /// Push a file on device - Push { filename: String, path: String }, + Push { + filename: String, + path: String, + }, /// Stat a file on device - Stat { path: String }, + Stat { + path: String, + }, /// Run an activity on device specified by the intent Run { /// The package whose activity is to be invoked @@ -43,4 +53,7 @@ pub enum DeviceCommands { /// Framebuffer image destination path path: String, }, + List { + path: String, + }, } diff --git a/adb_cli/src/models/local.rs b/adb_cli/src/models/local.rs index 188b610d..c04014cf 100644 --- a/adb_cli/src/models/local.rs +++ b/adb_cli/src/models/local.rs @@ -14,8 +14,6 @@ pub enum LocalCommand { pub enum LocalDeviceCommand { /// List available server features. HostFeatures, - /// List a directory on device - List { path: String }, /// Get logs of device Logcat { /// Path to output file (created if not exists) diff --git a/adb_client/src/adb_device_ext.rs b/adb_client/src/adb_device_ext.rs index 9fc99a4a..fea0bb16 100644 --- a/adb_client/src/adb_device_ext.rs +++ b/adb_client/src/adb_device_ext.rs @@ -4,7 +4,7 @@ use std::path::Path; use image::{ImageBuffer, ImageFormat, Rgba}; use crate::models::AdbStatResponse; -use crate::{RebootType, Result}; +use crate::{ADBListItem, RebootType, Result}; /// Trait representing all features available on both [`crate::ADBServerDevice`] and [`crate::ADBUSBDevice`] pub trait ADBDeviceExt { @@ -24,6 +24,9 @@ pub trait ADBDeviceExt { /// Push `stream` to `path` on the device. fn push(&mut self, stream: &mut dyn Read, path: &dyn AsRef) -> Result<()>; + /// List the items in a directory on the device + fn list(&mut self, path: &dyn AsRef) -> Result>; + /// Reboot the device using given reboot type fn reboot(&mut self, reboot_type: RebootType) -> Result<()>; diff --git a/adb_client/src/device/adb_message_device_commands.rs b/adb_client/src/device/adb_message_device_commands.rs index 78b9e020..748f8a29 100644 --- a/adb_client/src/device/adb_message_device_commands.rs +++ b/adb_client/src/device/adb_message_device_commands.rs @@ -42,4 +42,8 @@ impl ADBDeviceExt for ADBMessageDevice { fn framebuffer_inner(&mut self) -> Result, Vec>> { self.framebuffer_inner() } + + fn list(&mut self, path: &dyn AsRef) -> Result> { + self.list(path) + } } diff --git a/adb_client/src/device/adb_tcp_device.rs b/adb_client/src/device/adb_tcp_device.rs index 5f2e8d3a..4d7b6643 100644 --- a/adb_client/src/device/adb_tcp_device.rs +++ b/adb_client/src/device/adb_tcp_device.rs @@ -113,6 +113,10 @@ impl ADBDeviceExt for ADBTcpDevice { fn framebuffer_inner(&mut self) -> Result, Vec>> { self.inner.framebuffer_inner() } + + fn list(&mut self, path: &dyn AsRef) -> Result> { + self.inner.list(path) + } } impl Drop for ADBTcpDevice { diff --git a/adb_client/src/device/adb_usb_device.rs b/adb_client/src/device/adb_usb_device.rs index 0c9d0a2c..7aaa48c2 100644 --- a/adb_client/src/device/adb_usb_device.rs +++ b/adb_client/src/device/adb_usb_device.rs @@ -282,6 +282,10 @@ impl ADBDeviceExt for ADBUSBDevice { fn framebuffer_inner(&mut self) -> Result, Vec>> { self.inner.framebuffer_inner() } + + fn list(&mut self, path: &dyn AsRef) -> Result> { + self.inner.list(path) + } } impl Drop for ADBUSBDevice { diff --git a/adb_client/src/device/commands/list.rs b/adb_client/src/device/commands/list.rs new file mode 100644 index 00000000..4cb3e07a --- /dev/null +++ b/adb_client/src/device/commands/list.rs @@ -0,0 +1,174 @@ +use crate::{ + ADBListItem, ADBListItemType, ADBMessageTransport, Result, RustADBError, + device::{ + ADBTransportMessage, MessageCommand, MessageSubcommand, + adb_message_device::ADBMessageDevice, + }, +}; +use byteorder::ByteOrder; +use byteorder::LittleEndian; +use std::str; + +impl ADBMessageDevice { + /// List the entries in the given directory on the device. + /// note: path uses internal file paths, so Documents is at /storage/emulated/0/Documents + pub(crate) fn list>(&mut self, path: A) -> Result> { + self.begin_synchronization()?; + + let output = self.handle_list(path); + + self.end_transaction()?; + output + } + + /// Request amount of bytes from transport, potentially across payloads + /// + /// This automatically request a new payload by sending back "Okay" and waiting for the next payload + /// It reads the request bytes across the existing payload, and if there is not enough bytes left, + /// reads the rest from the next payload + /// + /// Current index + /// ┼───────────────┼ Requested + /// ┌─────────────┐ + /// ┌───────────────┼───────┐ │ + /// └───────────────────────┘ + /// Current └─────┘ + /// payload Wanted in + /// Next payload + fn read_bytes_from_transport( + requested_bytes: &usize, + current_index: &mut usize, + transport: &mut T, + payload: &mut Vec, + local_id: &u32, + remote_id: &u32, + ) -> Result> { + if *current_index + requested_bytes <= payload.len() { + // if there is enough bytes in this payload + // Copy from existing payload + let slice = &payload[*current_index..*current_index + requested_bytes]; + *current_index += requested_bytes; + Ok(slice.to_vec()) + } else { + // Read the rest of the existing payload, then continue with the next message + let mut slice = Vec::new(); + let bytes_read_from_existing_payload = payload.len() - *current_index; + slice.extend_from_slice( + &payload[*current_index..*current_index + bytes_read_from_existing_payload], + ); + + // Request the next message + let send_message = + ADBTransportMessage::new(MessageCommand::Okay, *local_id, *remote_id, &[]); + transport.write_message(send_message)?; + // Read the new message + *payload = transport.read_message()?.into_payload(); + let bytes_read_from_new_payload = requested_bytes - bytes_read_from_existing_payload; + slice.extend_from_slice(&payload[..bytes_read_from_new_payload]); + *current_index = bytes_read_from_new_payload; + Ok(slice) + } + } + + fn handle_list>(&mut self, path: A) -> Result> { + // TODO: use LIS2 to support files over 2.14 GB in size. + // SEE: https://github.com/cstyan/adbDocumentation?tab=readme-ov-file#adb-list + let local_id = self.get_local_id()?; + let remote_id = self.get_remote_id()?; + { + let mut len_buf = Vec::from([0_u8; 4]); + LittleEndian::write_u32(&mut len_buf, path.as_ref().len() as u32); + + let subcommand_data = MessageSubcommand::List; //.with_arg(path.len() as u32); + + let mut serialized_message = + bincode::serialize(&subcommand_data).map_err(|_e| RustADBError::ConversionError)?; + + serialized_message.append(&mut len_buf); + let mut path_bytes: Vec = Vec::from(path.as_ref().as_bytes()); + serialized_message.append(&mut path_bytes); + + let message = ADBTransportMessage::new( + MessageCommand::Write, + local_id, + remote_id, + &serialized_message, + ); + self.send_and_expect_okay(message)?; + } + + let mut list_items = Vec::new(); + + let transport = self.get_transport_mut(); + let mut payload = transport.read_message()?.into_payload(); + let mut current_index = 0; + loop { + // Loop though the response for all the entries + const STATUS_CODE_LENGTH_IN_BYTES: usize = 4; + let status_code = Self::read_bytes_from_transport( + &STATUS_CODE_LENGTH_IN_BYTES, + &mut current_index, + transport, + &mut payload, + &local_id, + &remote_id, + )?; + match str::from_utf8(&status_code)? { + "DENT" => { + // Read the file mode, size, mod time and name length in one go, since all their sizes are predictable + const U32_SIZE_IN_BYTES: usize = 4; + const SIZE_OF_METADATA: usize = U32_SIZE_IN_BYTES * 4; + let metadata = Self::read_bytes_from_transport( + &SIZE_OF_METADATA, + &mut current_index, + transport, + &mut payload, + &local_id, + &remote_id, + )?; + let mode = metadata[..U32_SIZE_IN_BYTES].to_vec(); + let size = metadata[U32_SIZE_IN_BYTES..2 * U32_SIZE_IN_BYTES].to_vec(); + let time = metadata[2 * U32_SIZE_IN_BYTES..3 * U32_SIZE_IN_BYTES].to_vec(); + let name_len = metadata[3 * U32_SIZE_IN_BYTES..4 * U32_SIZE_IN_BYTES].to_vec(); + + let mode = LittleEndian::read_u32(&mode); + let size = LittleEndian::read_u32(&size); + let time = LittleEndian::read_u32(&time); + let name_len = LittleEndian::read_u32(&name_len) as usize; + // Read the file name, since it requires the length from the name_len + let name_buf = Self::read_bytes_from_transport( + &name_len, + &mut current_index, + transport, + &mut payload, + &local_id, + &remote_id, + )?; + let name = String::from_utf8(name_buf)?; + + // First 9 bits are the file permissions + let permissions = mode & 0b111111111; + // Bits 14 to 16 are the file type + let item_type = match (mode >> 13) & 0b111 { + 0b010 => ADBListItemType::Directory, + 0b100 => ADBListItemType::File, + 0b101 => ADBListItemType::Symlink, + type_code => return Err(RustADBError::UnknownFileMode(type_code)), + }; + let entry = ADBListItem { + item_type, + name, + time, + size, + permissions, + }; + list_items.push(entry); + } + "DONE" => { + return Ok(list_items); + } + x => log::error!("Got an unknown response {}", x), + } + } + } +} diff --git a/adb_client/src/device/commands/mod.rs b/adb_client/src/device/commands/mod.rs index d1d3de3c..2bae4615 100644 --- a/adb_client/src/device/commands/mod.rs +++ b/adb_client/src/device/commands/mod.rs @@ -1,5 +1,6 @@ mod framebuffer; mod install; +mod list; mod pull; mod push; mod reboot; diff --git a/adb_client/src/error.rs b/adb_client/src/error.rs index 01642116..bcf40f43 100644 --- a/adb_client/src/error.rs +++ b/adb_client/src/error.rs @@ -120,6 +120,9 @@ pub enum RustADBError { /// An unknown transport has been provided #[error("unknown transport: {0}")] UnknownTransport(String), + /// An unknown file mode was encountered in list + #[error("Unknown file mode {0}")] + UnknownFileMode(u32), } impl From> for RustADBError { diff --git a/adb_client/src/lib.rs b/adb_client/src/lib.rs index fa509b85..a43b5100 100644 --- a/adb_client/src/lib.rs +++ b/adb_client/src/lib.rs @@ -21,7 +21,7 @@ pub use device::{ADBTcpDevice, ADBUSBDevice}; pub use emulator_device::ADBEmulatorDevice; pub use error::{Result, RustADBError}; pub use mdns::*; -pub use models::{AdbStatResponse, RebootType}; +pub use models::{ADBListItem, ADBListItemType, AdbStatResponse, RebootType}; pub use server::*; pub use server_device::ADBServerDevice; pub use transports::*; diff --git a/adb_client/src/models/list_info.rs b/adb_client/src/models/list_info.rs new file mode 100644 index 00000000..d99e6946 --- /dev/null +++ b/adb_client/src/models/list_info.rs @@ -0,0 +1,25 @@ +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] +/// A list entry on the remote device +pub struct ADBListItem { + /// The name of the file, not the path + pub name: String, + /// The unix time stamp of when it was last modified + pub time: u32, + /// The unix mode of the file, used for permissions and special bits + pub permissions: u32, + /// The size of the file + pub size: u32, + /// The type of item this is, file, directory or symlink + pub item_type: ADBListItemType, +} + +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] +/// The different types of item that the list item can be +pub enum ADBListItemType { + /// The entry is a file + File, + /// The entry is a directory + Directory, + /// The entry is a symlink + Symlink, +} diff --git a/adb_client/src/models/mod.rs b/adb_client/src/models/mod.rs index 66f3cefc..921c67d5 100644 --- a/adb_client/src/models/mod.rs +++ b/adb_client/src/models/mod.rs @@ -3,6 +3,7 @@ mod adb_server_command; mod adb_stat_response; mod framebuffer_info; mod host_features; +mod list_info; mod reboot_type; mod sync_command; @@ -11,5 +12,6 @@ pub(crate) use adb_server_command::AdbServerCommand; pub use adb_stat_response::AdbStatResponse; pub(crate) use framebuffer_info::{FrameBufferInfoV1, FrameBufferInfoV2}; pub use host_features::HostFeatures; +pub use list_info::{ADBListItem, ADBListItemType}; pub use reboot_type::RebootType; pub use sync_command::SyncCommand; diff --git a/adb_client/src/server_device/adb_server_device_commands.rs b/adb_client/src/server_device/adb_server_device_commands.rs index af276ed0..05d6c6c9 100644 --- a/adb_client/src/server_device/adb_server_device_commands.rs +++ b/adb_client/src/server_device/adb_server_device_commands.rs @@ -119,4 +119,8 @@ impl ADBDeviceExt for ADBServerDevice { fn framebuffer_inner(&mut self) -> Result, Vec>> { self.framebuffer_inner() } + + fn list(&mut self, path: &dyn AsRef) -> Result> { + self.list(path) + } } diff --git a/adb_client/src/server_device/commands/list.rs b/adb_client/src/server_device/commands/list.rs index 25e29148..2f0cc2b3 100644 --- a/adb_client/src/server_device/commands/list.rs +++ b/adb_client/src/server_device/commands/list.rs @@ -1,8 +1,8 @@ use crate::{ - ADBServerDevice, Result, - models::{AdbServerCommand, SyncCommand}, + ADBServerDevice, Result, RustADBError, + models::{ADBListItem, ADBListItemType, AdbServerCommand, SyncCommand}, }; -use byteorder::{ByteOrder, LittleEndian}; +use byteorder::{ByteOrder, LittleEndian, ReadBytesExt}; use std::{ io::{Read, Write}, str, @@ -10,7 +10,8 @@ use std::{ impl ADBServerDevice { /// Lists files in path on the device. - pub fn list>(&mut self, path: A) -> Result<()> { + /// note: path uses internal file paths, so Documents is at /storage/emulated/0/Documents + pub fn list>(&mut self, path: A) -> Result> { self.set_serial_transport()?; // Set device in SYNC mode @@ -22,9 +23,9 @@ impl ADBServerDevice { 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>(&mut self, path: S) -> Result<()> { + fn handle_list_command>(&mut self, path: A) -> Result> { + // TODO: use LIS2 to support files over 2.14 GB in size. + // SEE: https://github.com/cstyan/adbDocumentation?tab=readme-ov-file#adb-list let mut len_buf = [0_u8; 4]; LittleEndian::write_u32(&mut len_buf, path.as_ref().len() as u32); @@ -36,6 +37,8 @@ impl ADBServerDevice { .get_raw_connection()? .write_all(path.as_ref().to_string().as_bytes())?; + let mut list_items = Vec::new(); + // Reads returned status code from ADB server let mut response = [0_u8; 4]; loop { @@ -44,25 +47,36 @@ impl ADBServerDevice { .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]; - let mut connection = self.transport.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 mode = connection.read_u32::()?; + let size = connection.read_u32::()?; + let time = connection.read_u32::()?; + let name_len = connection.read_u32::()?; let mut name_buf = vec![0_u8; name_len as usize]; connection.read_exact(&mut name_buf)?; + let name = String::from_utf8(name_buf)?; + + // First 9 bits are the file permissions + let permissions = mode & 0b111111111; + // Bits 14 to 16 are the file type + let item_type = match (mode >> 13) & 0b111 { + 0b010 => ADBListItemType::Directory, + 0b100 => ADBListItemType::File, + 0b101 => ADBListItemType::Symlink, + type_code => return Err(RustADBError::UnknownFileMode(type_code)), + }; + let entry = ADBListItem { + item_type, + name, + time, + size, + permissions, + }; + list_items.push(entry); } "DONE" => { - return Ok(()); + return Ok(list_items); } x => log::error!("Got an unknown response {}", x), }