feat: adb list
ADB List (ls) for both direct device communication and for communication though the adb server.
This commit is contained in:
committed by
Corentin LIAUD
parent
739b3e1cee
commit
757b0f9523
@@ -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)?;
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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<()>;
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
174
adb_client/src/device/commands/list.rs
Normal file
174
adb_client/src/device/commands/list.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod framebuffer;
|
||||
mod install;
|
||||
mod list;
|
||||
mod pull;
|
||||
mod push;
|
||||
mod reboot;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
25
adb_client/src/models/list_info.rs
Normal file
25
adb_client/src/models/list_info.rs
Normal 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,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user