feat: adb list

ADB List (ls) for both direct device communication and for communication though the adb server.
This commit is contained in:
J05HM0N5TER
2025-04-05 17:30:10 +11:00
committed by Corentin LIAUD
parent 739b3e1cee
commit 757b0f9523
15 changed files with 322 additions and 24 deletions

View File

@@ -1,6 +1,7 @@
use std::{fs::File, io::Write};
use adb_client::ADBServerDevice;
use adb_client::{ADBListItemType, ADBServerDevice};
use anyhow::{Result, anyhow};
use crate::models::{ADBCliResult, LocalDeviceCommand};
@@ -20,7 +21,25 @@ pub fn handle_local_commands(
Ok(())
}
LocalDeviceCommand::List { path } => Ok(device.list(path)?),
LocalDeviceCommand::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 => "Dir".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(())
}
LocalDeviceCommand::Logcat { path } => {
let writer: Box<dyn Write> = if let Some(path) = path {
let f = File::create(path)?;

View File

@@ -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"))]
@@ -84,6 +85,24 @@ fn run_command(mut device: Box<dyn ADBDeviceExt>, command: DeviceCommands) -> AD
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(())

View File

@@ -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<String> },
Shell {
commands: Vec<String>,
},
/// 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,
},
}

View File

@@ -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<str>) -> Result<()>;
/// List the items in a directory on the device
fn list(&mut self, path: &str) -> Result<Vec<ADBListItem>>;
/// Reboot the device using given reboot type
fn reboot(&mut self, reboot_type: RebootType) -> Result<()>;

View File

@@ -42,4 +42,8 @@ impl<T: ADBMessageTransport> ADBDeviceExt for ADBMessageDevice<T> {
fn framebuffer_inner(&mut self) -> Result<image::ImageBuffer<image::Rgba<u8>, Vec<u8>>> {
self.framebuffer_inner()
}
fn list(&mut self, path: &str) -> Result<Vec<crate::ADBListItem>> {
self.list(path)
}
}

View File

@@ -136,6 +136,10 @@ impl ADBDeviceExt for ADBTcpDevice {
fn framebuffer_inner(&mut self) -> Result<image::ImageBuffer<image::Rgba<u8>, Vec<u8>>> {
self.inner.framebuffer_inner()
}
fn list(&mut self, path: &str) -> Result<Vec<crate::ADBListItem>> {
self.inner.list(path)
}
}
impl Drop for ADBTcpDevice {

View File

@@ -247,6 +247,10 @@ impl ADBDeviceExt for ADBUSBDevice {
fn framebuffer_inner(&mut self) -> Result<image::ImageBuffer<image::Rgba<u8>, Vec<u8>>> {
self.inner.framebuffer_inner()
}
fn list(&mut self, path: &str) -> Result<Vec<crate::ADBListItem>> {
self.inner.list(path)
}
}
impl Drop for ADBUSBDevice {

View File

@@ -0,0 +1,174 @@
use crate::{
device::{
adb_message_device::ADBMessageDevice, ADBTransportMessage, MessageCommand,
MessageSubcommand,
},
ADBListItem, ADBListItemType, ADBMessageTransport, Result, RustADBError,
};
use byteorder::ByteOrder;
use byteorder::LittleEndian;
use std::str;
impl<T: ADBMessageTransport> ADBMessageDevice<T> {
/// 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: &str) -> Result<Vec<ADBListItem>> {
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<u8>,
local_id: &u32,
remote_id: &u32,
) -> Result<Vec<u8>> {
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: &str) -> Result<Vec<ADBListItem>> {
// 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.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<u8> = Vec::from(path.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),
}
}
}
}

View File

@@ -1,5 +1,6 @@
mod framebuffer;
mod install;
mod list;
mod pull;
mod push;
mod reboot;

View File

@@ -123,6 +123,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 type {0}")]
UnknownFileMode(u32),
}
impl<T> From<std::sync::PoisonError<T>> for RustADBError {

View File

@@ -21,7 +21,7 @@ pub use device::{ADBTcpDevice, ADBUSBDevice, is_adb_device, search_adb_devices};
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::*;

View File

@@ -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,
}

View File

@@ -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;

View File

@@ -119,4 +119,8 @@ impl ADBDeviceExt for ADBServerDevice {
fn framebuffer_inner(&mut self) -> Result<image::ImageBuffer<image::Rgba<u8>, Vec<u8>>> {
self.framebuffer_inner()
}
fn list(&mut self, path: &str) -> Result<Vec<crate::ADBListItem>> {
self.list(path)
}
}

View File

@@ -1,6 +1,6 @@
use crate::{
ADBServerDevice, Result,
models::{AdbServerCommand, SyncCommand},
ADBServerDevice, Result, RustADBError,
models::{ADBListItem, ADBListItemType, AdbServerCommand, SyncCommand},
};
use byteorder::{ByteOrder, LittleEndian};
use std::{
@@ -10,7 +10,8 @@ use std::{
impl ADBServerDevice {
/// Lists files in path on the device.
pub fn list<A: AsRef<str>>(&mut self, path: A) -> Result<()> {
/// note: path uses internal file paths, so Documents is at /storage/emulated/0/Documents
pub fn list<A: AsRef<str>>(&mut self, path: A) -> Result<Vec<ADBListItem>> {
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<S: AsRef<str>>(&mut self, path: S) -> Result<()> {
fn handle_list_command<S: AsRef<str>>(&mut self, path: S) -> Result<Vec<ADBListItem>> {
// 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, u32::try_from(path.as_ref().len())?);
@@ -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,45 @@ 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 mode = [0_u8; 4];
let mut size = [0_u8; 4];
let mut 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 mode)?;
connection.read_exact(&mut size)?;
connection.read_exact(&mut time)?;
connection.read_exact(&mut name_len)?;
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);
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}"),
}