diff options
Diffstat (limited to 'testing/mozbase/rust/mozdevice')
-rw-r--r-- | testing/mozbase/rust/mozdevice/Cargo.toml | 26 | ||||
-rw-r--r-- | testing/mozbase/rust/mozdevice/src/adb.rs | 38 | ||||
-rw-r--r-- | testing/mozbase/rust/mozdevice/src/lib.rs | 1068 | ||||
-rw-r--r-- | testing/mozbase/rust/mozdevice/src/shell.rs | 66 | ||||
-rw-r--r-- | testing/mozbase/rust/mozdevice/src/test.rs | 760 |
5 files changed, 1958 insertions, 0 deletions
diff --git a/testing/mozbase/rust/mozdevice/Cargo.toml b/testing/mozbase/rust/mozdevice/Cargo.toml new file mode 100644 index 0000000000..0954b93643 --- /dev/null +++ b/testing/mozbase/rust/mozdevice/Cargo.toml @@ -0,0 +1,26 @@ +[package] +edition = "2018" +name = "mozdevice" +version = "0.5.1" +authors = ["Mozilla"] +description = "Client library for the Android Debug Bridge (adb)" +keywords = [ + "adb", + "android", + "firefox", + "geckoview", + "mozilla", +] +license = "MPL-2.0" +repository = "https://hg.mozilla.org/mozilla-central/file/tip/testing/mozbase/rust/mozdevice" + +[dependencies] +log = { version = "0.4", features = ["std"] } +once_cell = "1.4.0" +regex = { version = "1", default-features = false, features = ["perf", "std"] } +tempfile = "3" +thiserror = "1.0.25" +walkdir = "2" +unix_path = "1.0" +uuid = { version = "1.0", features = ["serde", "v4"] } + diff --git a/testing/mozbase/rust/mozdevice/src/adb.rs b/testing/mozbase/rust/mozdevice/src/adb.rs new file mode 100644 index 0000000000..9d9c91fd07 --- /dev/null +++ b/testing/mozbase/rust/mozdevice/src/adb.rs @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#[derive(Debug, PartialEq)] +pub enum SyncCommand { + Data, + Dent, + Done, + Fail, + List, + Okay, + Quit, + Recv, + Send, + Stat, +} + +impl SyncCommand { + // Returns the byte serialisation of the protocol status. + pub fn code(&self) -> &'static [u8; 4] { + use self::SyncCommand::*; + match *self { + Data => b"DATA", + Dent => b"DENT", + Done => b"DONE", + Fail => b"FAIL", + List => b"LIST", + Okay => b"OKAY", + Quit => b"QUIT", + Recv => b"RECV", + Send => b"SEND", + Stat => b"STAT", + } + } +} + +pub type DeviceSerial = String; diff --git a/testing/mozbase/rust/mozdevice/src/lib.rs b/testing/mozbase/rust/mozdevice/src/lib.rs new file mode 100644 index 0000000000..631c4acf2a --- /dev/null +++ b/testing/mozbase/rust/mozdevice/src/lib.rs @@ -0,0 +1,1068 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod adb; +pub mod shell; + +#[cfg(test)] +pub mod test; + +use log::{debug, info, trace, warn}; +use once_cell::sync::Lazy; +use regex::Regex; +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::fs::File; +use std::io::{self, Read, Write}; +use std::iter::FromIterator; +use std::net::TcpStream; +use std::num::{ParseIntError, TryFromIntError}; +use std::path::{Component, Path}; +use std::str::{FromStr, Utf8Error}; +use std::time::{Duration, SystemTime}; +use thiserror::Error; +pub use unix_path::{Path as UnixPath, PathBuf as UnixPathBuf}; +use uuid::Uuid; +use walkdir::WalkDir; + +use crate::adb::{DeviceSerial, SyncCommand}; + +pub type Result<T> = std::result::Result<T, DeviceError>; + +static SYNC_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[^A-Za-z0-9_@%+=:,./-]").unwrap()); + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum AndroidStorageInput { + Auto, + App, + Internal, + Sdcard, +} + +impl FromStr for AndroidStorageInput { + type Err = DeviceError; + + fn from_str(s: &str) -> Result<Self> { + match s { + "auto" => Ok(AndroidStorageInput::Auto), + "app" => Ok(AndroidStorageInput::App), + "internal" => Ok(AndroidStorageInput::Internal), + "sdcard" => Ok(AndroidStorageInput::Sdcard), + _ => Err(DeviceError::InvalidStorage), + } + } +} + +impl Default for AndroidStorageInput { + fn default() -> Self { + AndroidStorageInput::Auto + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum AndroidStorage { + App, + Internal, + Sdcard, +} + +#[derive(Debug, Error)] +pub enum DeviceError { + #[error("{0}")] + Adb(String), + #[error(transparent)] + FromInt(#[from] TryFromIntError), + #[error("Invalid storage")] + InvalidStorage, + #[error(transparent)] + Io(#[from] io::Error), + #[error("Missing package")] + MissingPackage, + #[error("Multiple Android devices online")] + MultipleDevices, + #[error(transparent)] + ParseInt(#[from] ParseIntError), + #[error("Unknown Android device with serial '{0}'")] + UnknownDevice(String), + #[error(transparent)] + Utf8(#[from] Utf8Error), + #[error(transparent)] + WalkDir(#[from] walkdir::Error), +} + +fn encode_message(payload: &str) -> Result<String> { + let hex_length = u16::try_from(payload.len()).map(|len| format!("{:0>4X}", len))?; + + Ok(format!("{}{}", hex_length, payload)) +} + +fn parse_device_info(line: &str) -> Option<DeviceInfo> { + // Turn "serial\tdevice key1:value1 key2:value2 ..." into a `DeviceInfo`. + let mut pairs = line.split_whitespace(); + let serial = pairs.next(); + let state = pairs.next(); + if let (Some(serial), Some("device")) = (serial, state) { + let info: BTreeMap<String, String> = pairs + .filter_map(|pair| { + let mut kv = pair.split(':'); + if let (Some(k), Some(v), None) = (kv.next(), kv.next(), kv.next()) { + Some((k.to_owned(), v.to_owned())) + } else { + None + } + }) + .collect(); + + Some(DeviceInfo { + serial: serial.to_owned(), + info, + }) + } else { + None + } +} + +/// Reads the payload length of a host message from the stream. +fn read_length<R: Read>(stream: &mut R) -> Result<usize> { + let mut bytes: [u8; 4] = [0; 4]; + stream.read_exact(&mut bytes)?; + + let response = std::str::from_utf8(&bytes)?; + + Ok(usize::from_str_radix(response, 16)?) +} + +/// Reads the payload length of a device message from the stream. +fn read_length_little_endian(reader: &mut dyn Read) -> Result<usize> { + let mut bytes: [u8; 4] = [0; 4]; + reader.read_exact(&mut bytes)?; + + let n: usize = (bytes[0] as usize) + + ((bytes[1] as usize) << 8) + + ((bytes[2] as usize) << 16) + + ((bytes[3] as usize) << 24); + + Ok(n) +} + +/// Writes the payload length of a device message to the stream. +fn write_length_little_endian(writer: &mut dyn Write, n: usize) -> Result<usize> { + let mut bytes = [0; 4]; + bytes[0] = (n & 0xFF) as u8; + bytes[1] = ((n >> 8) & 0xFF) as u8; + bytes[2] = ((n >> 16) & 0xFF) as u8; + bytes[3] = ((n >> 24) & 0xFF) as u8; + + writer.write(&bytes[..]).map_err(DeviceError::Io) +} + +fn read_response(stream: &mut TcpStream, has_output: bool, has_length: bool) -> Result<Vec<u8>> { + let mut bytes: [u8; 1024] = [0; 1024]; + + stream.read_exact(&mut bytes[0..4])?; + + if !bytes.starts_with(SyncCommand::Okay.code()) { + let n = bytes.len().min(read_length(stream)?); + stream.read_exact(&mut bytes[0..n])?; + + let message = std::str::from_utf8(&bytes[0..n]).map(|s| format!("adb error: {}", s))?; + + return Err(DeviceError::Adb(message)); + } + + let mut response = Vec::new(); + + if has_output { + stream.read_to_end(&mut response)?; + + if response.starts_with(SyncCommand::Okay.code()) { + // Sometimes the server produces OKAYOKAY. Sometimes there is a transport OKAY and + // then the underlying command OKAY. This is straight from `chromedriver`. + response = response.split_off(4); + } + + if response.starts_with(SyncCommand::Fail.code()) { + // The server may even produce OKAYFAIL, which means the underlying + // command failed. First split-off the `FAIL` and length of the message. + response = response.split_off(8); + + let message = std::str::from_utf8(&response).map(|s| format!("adb error: {}", s))?; + + return Err(DeviceError::Adb(message)); + } + + if has_length { + if response.len() >= 4 { + let message = response.split_off(4); + let slice: &mut &[u8] = &mut &*response; + + let n = read_length(slice)?; + if n != message.len() { + warn!("adb server response contained hexstring len {} but remaining message length is {}", n, message.len()); + } + + trace!( + "adb server response was {:?}", + std::str::from_utf8(&message)? + ); + + return Ok(message); + } else { + return Err(DeviceError::Adb(format!( + "adb server response did not contain expected hexstring length: {:?}", + std::str::from_utf8(&response)? + ))); + } + } + } + + Ok(response) +} + +/// Detailed information about an ADB device. +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct DeviceInfo { + pub serial: DeviceSerial, + pub info: BTreeMap<String, String>, +} + +/// Represents a connection to an ADB host, which multiplexes the connections to +/// individual devices. +#[derive(Debug)] +pub struct Host { + /// The TCP host to connect to. Defaults to `"localhost"`. + pub host: Option<String>, + /// The TCP port to connect to. Defaults to `5037`. + pub port: Option<u16>, + /// Optional TCP read timeout duration. Defaults to 2s. + pub read_timeout: Option<Duration>, + /// Optional TCP write timeout duration. Defaults to 2s. + pub write_timeout: Option<Duration>, +} + +impl Default for Host { + fn default() -> Host { + Host { + host: Some("localhost".to_string()), + port: Some(5037), + read_timeout: Some(Duration::from_secs(2)), + write_timeout: Some(Duration::from_secs(2)), + } + } +} + +impl Host { + /// Searches for available devices, and selects the one as specified by `device_serial`. + /// + /// If multiple devices are online, and no device has been specified, + /// the `ANDROID_SERIAL` environment variable can be used to select one. + pub fn device_or_default<T: AsRef<str>>( + self, + device_serial: Option<&T>, + storage: AndroidStorageInput, + ) -> Result<Device> { + let serials: Vec<String> = self + .devices::<Vec<_>>()? + .into_iter() + .map(|d| d.serial) + .collect(); + + if let Some(ref serial) = device_serial + .map(|v| v.as_ref().to_owned()) + .or_else(|| std::env::var("ANDROID_SERIAL").ok()) + { + if !serials.contains(serial) { + return Err(DeviceError::UnknownDevice(serial.clone())); + } + + return Device::new(self, serial.to_owned(), storage); + } + + if serials.len() > 1 { + return Err(DeviceError::MultipleDevices); + } + + if let Some(ref serial) = serials.first() { + return Device::new(self, serial.to_owned().to_string(), storage); + } + + Err(DeviceError::Adb("No Android devices are online".to_owned())) + } + + pub fn connect(&self) -> Result<TcpStream> { + let stream = TcpStream::connect(format!( + "{}:{}", + self.host.clone().unwrap_or_else(|| "localhost".to_owned()), + self.port.unwrap_or(5037) + ))?; + stream.set_read_timeout(self.read_timeout)?; + stream.set_write_timeout(self.write_timeout)?; + Ok(stream) + } + + pub fn execute_command( + &self, + command: &str, + has_output: bool, + has_length: bool, + ) -> Result<String> { + let mut stream = self.connect()?; + + stream.write_all(encode_message(command)?.as_bytes())?; + let bytes = read_response(&mut stream, has_output, has_length)?; + // TODO: should we assert no bytes were read? + + let response = std::str::from_utf8(&bytes)?; + + Ok(response.to_owned()) + } + + pub fn execute_host_command( + &self, + host_command: &str, + has_length: bool, + has_output: bool, + ) -> Result<String> { + self.execute_command(&format!("host:{}", host_command), has_output, has_length) + } + + pub fn features<B: FromIterator<String>>(&self) -> Result<B> { + let features = self.execute_host_command("features", true, true)?; + Ok(features.split(',').map(|x| x.to_owned()).collect()) + } + + pub fn devices<B: FromIterator<DeviceInfo>>(&self) -> Result<B> { + let response = self.execute_host_command("devices-l", true, true)?; + + let infos: B = response.lines().filter_map(parse_device_info).collect(); + + Ok(infos) + } +} + +/// Represents an ADB device. +#[derive(Debug)] +pub struct Device { + /// ADB host that controls this device. + pub host: Host, + + /// Serial number uniquely identifying this ADB device. + pub serial: DeviceSerial, + + /// adb running as root + pub adbd_root: bool, + + /// Flag for rooted device + pub is_rooted: bool, + + /// "su 0" command available + pub su_0_root: bool, + + /// "su -c" command available + pub su_c_root: bool, + + pub run_as_package: Option<String>, + + pub storage: AndroidStorage, + + /// Cache intermediate tempfile name used in pushing via run_as. + pub tempfile: UnixPathBuf, +} + +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct RemoteDirEntry { + depth: usize, + metadata: RemoteMetadata, + name: String, +} + +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum RemoteMetadata { + RemoteFile(RemoteFileMetadata), + RemoteDir, + RemoteSymlink, +} +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct RemoteFileMetadata { + mode: usize, + size: usize, +} + +impl Device { + pub fn new(host: Host, serial: DeviceSerial, storage: AndroidStorageInput) -> Result<Device> { + let mut device = Device { + host, + serial, + adbd_root: false, + is_rooted: false, + run_as_package: None, + storage: AndroidStorage::App, + su_c_root: false, + su_0_root: false, + tempfile: UnixPathBuf::from("/data/local/tmp"), + }; + device + .tempfile + .push(Uuid::new_v4().as_hyphenated().to_string()); + + // check for rooted devices + let uid_check = |id: String| id.contains("uid=0"); + device.adbd_root = device + .execute_host_shell_command("id") + .map_or(false, uid_check); + device.su_0_root = device + .execute_host_shell_command("su 0 id") + .map_or(false, uid_check); + device.su_c_root = device + .execute_host_shell_command("su -c id") + .map_or(false, uid_check); + device.is_rooted = device.adbd_root || device.su_0_root || device.su_c_root; + + device.storage = match storage { + AndroidStorageInput::App => AndroidStorage::App, + AndroidStorageInput::Internal => AndroidStorage::Internal, + AndroidStorageInput::Sdcard => AndroidStorage::Sdcard, + AndroidStorageInput::Auto => AndroidStorage::Sdcard, + }; + + if device.is_rooted { + info!("Device is rooted"); + + // Set Permissive=1 if we have root. + device.execute_host_shell_command("setenforce permissive")?; + } else { + info!("Device is unrooted"); + } + + Ok(device) + } + + pub fn clear_app_data(&self, package: &str) -> Result<bool> { + self.execute_host_shell_command(&format!("pm clear {}", package)) + .map(|v| v.contains("Success")) + } + + pub fn create_dir(&self, path: &UnixPath) -> Result<()> { + debug!("Creating {}", path.display()); + + let enable_run_as = self.enable_run_as_for_path(path); + self.execute_host_shell_command_as(&format!("mkdir -p {}", path.display()), enable_run_as)?; + + Ok(()) + } + + pub fn chmod(&self, path: &UnixPath, mask: &str, recursive: bool) -> Result<()> { + let enable_run_as = self.enable_run_as_for_path(path); + + let recursive = match recursive { + true => " -R", + false => "", + }; + + self.execute_host_shell_command_as( + &format!("chmod {} {} {}", recursive, mask, path.display()), + enable_run_as, + )?; + + Ok(()) + } + + pub fn execute_host_command( + &self, + command: &str, + has_output: bool, + has_length: bool, + ) -> Result<String> { + let mut stream = self.host.connect()?; + + let switch_command = format!("host:transport:{}", self.serial); + trace!("execute_host_command: >> {:?}", &switch_command); + stream.write_all(encode_message(&switch_command)?.as_bytes())?; + let _bytes = read_response(&mut stream, false, false)?; + trace!("execute_host_command: << {:?}", _bytes); + // TODO: should we assert no bytes were read? + + trace!("execute_host_command: >> {:?}", &command); + stream.write_all(encode_message(command)?.as_bytes())?; + let bytes = read_response(&mut stream, has_output, has_length)?; + let response = std::str::from_utf8(&bytes)?; + trace!("execute_host_command: << {:?}", response); + + // Unify new lines by removing possible carriage returns + Ok(response.replace("\r\n", "\n")) + } + + pub fn enable_run_as_for_path(&self, path: &UnixPath) -> bool { + match &self.run_as_package { + Some(package) => { + let mut p = UnixPathBuf::from("/data/data/"); + p.push(package); + path.starts_with(p) + } + None => false, + } + } + + pub fn execute_host_shell_command(&self, shell_command: &str) -> Result<String> { + self.execute_host_shell_command_as(shell_command, false) + } + + pub fn execute_host_shell_command_as( + &self, + shell_command: &str, + enable_run_as: bool, + ) -> Result<String> { + // We don't want to duplicate su invocations. + if shell_command.starts_with("su") { + return self.execute_host_command(&format!("shell:{}", shell_command), true, false); + } + + let has_outer_quotes = shell_command.starts_with('"') && shell_command.ends_with('"') + || shell_command.starts_with('\'') && shell_command.ends_with('\''); + + if self.adbd_root { + return self.execute_host_command(&format!("shell:{}", shell_command), true, false); + } + + if self.su_0_root { + return self.execute_host_command( + &format!("shell:su 0 {}", shell_command), + true, + false, + ); + } + + if self.su_c_root { + if has_outer_quotes { + return self.execute_host_command( + &format!("shell:su -c {}", shell_command), + true, + false, + ); + } + + if SYNC_REGEX.is_match(shell_command) { + let arg: &str = &shell_command.replace('\'', "'\"'\"'")[..]; + return self.execute_host_command(&format!("shell:su -c '{}'", arg), true, false); + } + + return self.execute_host_command( + &format!("shell:su -c \"{}\"", shell_command), + true, + false, + ); + } + + // Execute command as package + if enable_run_as { + let run_as_package = self + .run_as_package + .as_ref() + .ok_or(DeviceError::MissingPackage)?; + + if has_outer_quotes { + return self.execute_host_command( + &format!("shell:run-as {} {}", run_as_package, shell_command), + true, + false, + ); + } + + if SYNC_REGEX.is_match(shell_command) { + let arg: &str = &shell_command.replace('\'', "'\"'\"'")[..]; + return self.execute_host_command( + &format!("shell:run-as {} {}", run_as_package, arg), + true, + false, + ); + } + + return self.execute_host_command( + &format!("shell:run-as {} \"{}\"", run_as_package, shell_command), + true, + false, + ); + } + + self.execute_host_command(&format!("shell:{}", shell_command), true, false) + } + + pub fn is_app_installed(&self, package: &str) -> Result<bool> { + self.execute_host_shell_command(&format!("pm path {}", package)) + .map(|v| v.contains("package:")) + } + + pub fn launch<T: AsRef<str>>( + &self, + package: &str, + activity: &str, + am_start_args: &[T], + ) -> Result<bool> { + let mut am_start = format!("am start -W -n {}/{}", package, activity); + + for arg in am_start_args { + am_start.push(' '); + if SYNC_REGEX.is_match(arg.as_ref()) { + am_start.push_str(&format!("\"{}\"", &shell::escape(arg.as_ref()))); + } else { + am_start.push_str(&shell::escape(arg.as_ref())); + }; + } + + self.execute_host_shell_command(&am_start) + .map(|v| v.contains("Complete")) + } + + pub fn force_stop(&self, package: &str) -> Result<()> { + debug!("Force stopping Android package: {}", package); + self.execute_host_shell_command(&format!("am force-stop {}", package)) + .and(Ok(())) + } + + pub fn forward_port(&self, local: u16, remote: u16) -> Result<u16> { + let command = format!( + "host-serial:{}:forward:tcp:{};tcp:{}", + self.serial, local, remote + ); + let response = self.host.execute_command(&command, true, false)?; + + if local == 0 { + Ok(response.parse::<u16>()?) + } else { + Ok(local) + } + } + + pub fn kill_forward_port(&self, local: u16) -> Result<()> { + let command = format!("host-serial:{}:killforward:tcp:{}", self.serial, local); + self.execute_host_command(&command, true, false).and(Ok(())) + } + + pub fn kill_forward_all_ports(&self) -> Result<()> { + let command = format!("host-serial:{}:killforward-all", self.serial); + self.execute_host_command(&command, false, false) + .and(Ok(())) + } + + pub fn reverse_port(&self, remote: u16, local: u16) -> Result<u16> { + let command = format!("reverse:forward:tcp:{};tcp:{}", remote, local); + let response = self.execute_host_command(&command, true, false)?; + + if remote == 0 { + Ok(response.parse::<u16>()?) + } else { + Ok(remote) + } + } + + pub fn kill_reverse_port(&self, remote: u16) -> Result<()> { + let command = format!("reverse:killforward:tcp:{}", remote); + self.execute_host_command(&command, true, true).and(Ok(())) + } + + pub fn kill_reverse_all_ports(&self) -> Result<()> { + let command = "reverse:killforward-all".to_owned(); + self.execute_host_command(&command, false, false) + .and(Ok(())) + } + + pub fn list_dir(&self, src: &UnixPath) -> Result<Vec<RemoteDirEntry>> { + let src = src.to_path_buf(); + let mut queue = vec![(src.clone(), 0, "".to_string())]; + + let mut listings = Vec::new(); + + while let Some((next, depth, prefix)) = queue.pop() { + for listing in self.list_dir_flat(&next, depth, prefix)? { + if listing.metadata == RemoteMetadata::RemoteDir { + let mut child = src.clone(); + child.push(listing.name.clone()); + queue.push((child, depth + 1, listing.name.clone())); + } + + listings.push(listing); + } + } + + Ok(listings) + } + + fn list_dir_flat( + &self, + src: &UnixPath, + depth: usize, + prefix: String, + ) -> Result<Vec<RemoteDirEntry>> { + // Implement the ADB protocol to list a directory from the device. + let mut stream = self.host.connect()?; + + // Send "host:transport" command with device serial + let message = encode_message(&format!("host:transport:{}", self.serial))?; + stream.write_all(message.as_bytes())?; + let _bytes = read_response(&mut stream, false, true)?; + + // Send "sync:" command to initialize file transfer + let message = encode_message("sync:")?; + stream.write_all(message.as_bytes())?; + let _bytes = read_response(&mut stream, false, true)?; + + // Send "LIST" command with name of the directory + stream.write_all(SyncCommand::List.code())?; + let args_ = format!("{}", src.display()); + let args = args_.as_bytes(); + write_length_little_endian(&mut stream, args.len())?; + stream.write_all(args)?; + + // Use the maximum 64KB buffer to transfer the file contents. + let mut buf = [0; 64 * 1024]; + + let mut listings = Vec::new(); + + // Read "DENT" command one or more times for the directory entries + loop { + stream.read_exact(&mut buf[0..4])?; + + if &buf[0..4] == SyncCommand::Dent.code() { + // From https://github.com/cstyan/adbDocumentation/blob/6d025b3e4af41be6f93d37f516a8ac7913688623/README.md: + // + // A four-byte integer representing file mode - first 9 bits of this mode represent + // the file permissions, as with chmod mode. Bits 14 to 16 seem to represent the + // file type, one of 0b100 (file), 0b010 (directory), 0b101 (symlink) + // A four-byte integer representing file size. + // A four-byte integer representing last modified time in seconds since Unix Epoch. + // A four-byte integer representing file name length. + // A utf-8 string representing the file name. + let mode = read_length_little_endian(&mut stream)?; + let size = read_length_little_endian(&mut stream)?; + let _time = read_length_little_endian(&mut stream)?; + let name_length = read_length_little_endian(&mut stream)?; + stream.read_exact(&mut buf[0..name_length])?; + + let mut name = std::str::from_utf8(&buf[0..name_length])?.to_owned(); + + if name == "." || name == ".." { + continue; + } + + if !prefix.is_empty() { + name = format!("{}/{}", prefix, &name); + } + + let file_type = (mode >> 13) & 0b111; + let metadata = match file_type { + 0b010 => RemoteMetadata::RemoteDir, + 0b100 => RemoteMetadata::RemoteFile(RemoteFileMetadata { + mode: mode & 0b111111111, + size, + }), + 0b101 => RemoteMetadata::RemoteSymlink, + _ => return Err(DeviceError::Adb(format!("Invalid file mode {}", file_type))), + }; + + listings.push(RemoteDirEntry { + name, + depth, + metadata, + }); + } else if &buf[0..4] == SyncCommand::Done.code() { + // "DONE" command indicates end of file transfer + break; + } else if &buf[0..4] == SyncCommand::Fail.code() { + let n = buf.len().min(read_length_little_endian(&mut stream)?); + + stream.read_exact(&mut buf[0..n])?; + + let message = std::str::from_utf8(&buf[0..n]) + .map(|s| format!("adb error: {}", s)) + .unwrap_or_else(|_| "adb error was not utf-8".into()); + + return Err(DeviceError::Adb(message)); + } else { + return Err(DeviceError::Adb("FAIL (unknown)".to_owned())); + } + } + + Ok(listings) + } + + pub fn path_exists(&self, path: &UnixPath, enable_run_as: bool) -> Result<bool> { + self.execute_host_shell_command_as(format!("ls {}", path.display()).as_str(), enable_run_as) + .map(|path| !path.contains("No such file or directory")) + } + + pub fn pull(&self, src: &UnixPath, buffer: &mut dyn Write) -> Result<()> { + // Implement the ADB protocol to receive a file from the device. + let mut stream = self.host.connect()?; + + // Send "host:transport" command with device serial + let message = encode_message(&format!("host:transport:{}", self.serial))?; + stream.write_all(message.as_bytes())?; + let _bytes = read_response(&mut stream, false, true)?; + + // Send "sync:" command to initialize file transfer + let message = encode_message("sync:")?; + stream.write_all(message.as_bytes())?; + let _bytes = read_response(&mut stream, false, true)?; + + // Send "RECV" command with name of the file + stream.write_all(SyncCommand::Recv.code())?; + let args_string = format!("{}", src.display()); + let args = args_string.as_bytes(); + write_length_little_endian(&mut stream, args.len())?; + stream.write_all(args)?; + + // Use the maximum 64KB buffer to transfer the file contents. + let mut buf = [0; 64 * 1024]; + + // Read "DATA" command one or more times for the file content + loop { + stream.read_exact(&mut buf[0..4])?; + + if &buf[0..4] == SyncCommand::Data.code() { + let len = read_length_little_endian(&mut stream)?; + stream.read_exact(&mut buf[0..len])?; + buffer.write_all(&buf[0..len])?; + } else if &buf[0..4] == SyncCommand::Done.code() { + // "DONE" command indicates end of file transfer + break; + } else if &buf[0..4] == SyncCommand::Fail.code() { + let n = buf.len().min(read_length_little_endian(&mut stream)?); + + stream.read_exact(&mut buf[0..n])?; + + let message = std::str::from_utf8(&buf[0..n]) + .map(|s| format!("adb error: {}", s)) + .unwrap_or_else(|_| "adb error was not utf-8".into()); + + return Err(DeviceError::Adb(message)); + } else { + return Err(DeviceError::Adb("FAIL (unknown)".to_owned())); + } + } + + Ok(()) + } + + pub fn pull_dir(&self, src: &UnixPath, dest_dir: &Path) -> Result<()> { + let src = src.to_path_buf(); + let dest_dir = dest_dir.to_path_buf(); + + for entry in self.list_dir(&src)? { + match entry.metadata { + RemoteMetadata::RemoteSymlink => {} // Ignored. + RemoteMetadata::RemoteDir => { + let mut d = dest_dir.clone(); + d.push(&entry.name); + + std::fs::create_dir_all(&d)?; + } + RemoteMetadata::RemoteFile(_) => { + let mut s = src.clone(); + s.push(&entry.name); + let mut d = dest_dir.clone(); + d.push(&entry.name); + + self.pull(&s, &mut File::create(d)?)?; + } + } + } + + Ok(()) + } + + pub fn push(&self, buffer: &mut dyn Read, dest: &UnixPath, mode: u32) -> Result<()> { + // Implement the ADB protocol to send a file to the device. + // The protocol consists of the following steps: + // * Send "host:transport" command with device serial + // * Send "sync:" command to initialize file transfer + // * Send "SEND" command with name and mode of the file + // * Send "DATA" command one or more times for the file content + // * Send "DONE" command to indicate end of file transfer + + let enable_run_as = self.enable_run_as_for_path(&dest.to_path_buf()); + let dest1 = match enable_run_as { + true => self.tempfile.as_path(), + false => UnixPath::new(dest), + }; + + // If the destination directory does not exist, adb will + // create it and any necessary ancestors however it will not + // set the directory permissions to 0o777. In addition, + // Android 9 (P) has a bug in its push implementation which + // will cause a push which creates directories to fail with + // the error `secure_mkdirs failed: Operation not + // permitted`. We can work around this by creating the + // destination directories prior to the push. Collect the + // ancestors of the destination directory which do not yet + // exist so we can create them and adjust their permissions + // prior to performing the push. + let mut current = dest.parent(); + let mut leaf: Option<&UnixPath> = None; + let mut root: Option<&UnixPath> = None; + + while let Some(path) = current { + if self.path_exists(path, enable_run_as)? { + break; + } + if leaf.is_none() { + leaf = Some(path); + } + root = Some(path); + current = path.parent(); + } + + if let Some(path) = leaf { + self.create_dir(path)?; + } + + if let Some(path) = root { + self.chmod(path, "777", true)?; + } + + let mut stream = self.host.connect()?; + + let message = encode_message(&format!("host:transport:{}", self.serial))?; + stream.write_all(message.as_bytes())?; + let _bytes = read_response(&mut stream, false, true)?; + + let message = encode_message("sync:")?; + stream.write_all(message.as_bytes())?; + let _bytes = read_response(&mut stream, false, true)?; + + stream.write_all(SyncCommand::Send.code())?; + let args_ = format!("{},{}", dest1.display(), mode); + let args = args_.as_bytes(); + write_length_little_endian(&mut stream, args.len())?; + stream.write_all(args)?; + + // Use a 32KB buffer to transfer the file contents + // TODO: Maybe adjust to maxdata (256KB) + let mut buf = [0; 32 * 1024]; + + loop { + let len = buffer.read(&mut buf)?; + + if len == 0 { + break; + } + + stream.write_all(SyncCommand::Data.code())?; + write_length_little_endian(&mut stream, len)?; + stream.write_all(&buf[0..len])?; + } + + // https://android.googlesource.com/platform/system/core/+/master/adb/SYNC.TXT#66 + // + // When the file is transferred a sync request "DONE" is sent, where length is set + // to the last modified time for the file. The server responds to this last + // request (but not to chunk requests) with an "OKAY" sync response (length can + // be ignored). + let time: u32 = ((SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)) + .unwrap() + .as_secs() + & 0xFFFF_FFFF) as u32; + + stream.write_all(SyncCommand::Done.code())?; + write_length_little_endian(&mut stream, time as usize)?; + + // Status. + stream.read_exact(&mut buf[0..4])?; + + if buf.starts_with(SyncCommand::Okay.code()) { + if enable_run_as { + // Use cp -a to preserve the permissions set by push. + let result = self.execute_host_shell_command_as( + format!("cp -aR {} {}", dest1.display(), dest.display()).as_str(), + enable_run_as, + ); + if self.remove(dest1).is_err() { + warn!("Failed to remove {}", dest1.display()); + } + result?; + } + Ok(()) + } else if buf.starts_with(SyncCommand::Fail.code()) { + if enable_run_as && self.remove(dest1).is_err() { + warn!("Failed to remove {}", dest1.display()); + } + let n = buf.len().min(read_length_little_endian(&mut stream)?); + + stream.read_exact(&mut buf[0..n])?; + + let message = std::str::from_utf8(&buf[0..n]) + .map(|s| format!("adb error: {}", s)) + .unwrap_or_else(|_| "adb error was not utf-8".into()); + + Err(DeviceError::Adb(message)) + } else { + if self.remove(dest1).is_err() { + warn!("Failed to remove {}", dest1.display()); + } + Err(DeviceError::Adb("FAIL (unknown)".to_owned())) + } + } + + pub fn push_dir(&self, source: &Path, dest_dir: &UnixPath, mode: u32) -> Result<()> { + debug!("Pushing {} to {}", source.display(), dest_dir.display()); + + let walker = WalkDir::new(source).follow_links(false).into_iter(); + + for entry in walker { + let entry = entry?; + let path = entry.path(); + + if !entry.metadata()?.is_file() { + continue; + } + + let mut file = File::open(path)?; + + let tail = path + .strip_prefix(source) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + + let dest = append_components(dest_dir, tail)?; + self.push(&mut file, &dest, mode)?; + } + + Ok(()) + } + + pub fn remove(&self, path: &UnixPath) -> Result<()> { + debug!("Deleting {}", path.display()); + + self.execute_host_shell_command_as( + &format!("rm -rf {}", path.display()), + self.enable_run_as_for_path(path), + )?; + + Ok(()) + } +} + +pub(crate) fn append_components( + base: &UnixPath, + tail: &Path, +) -> std::result::Result<UnixPathBuf, io::Error> { + let mut buf = base.to_path_buf(); + + for component in tail.components() { + if let Component::Normal(segment) = component { + let utf8 = segment.to_str().ok_or_else(|| { + io::Error::new( + io::ErrorKind::Other, + "Could not represent path segment as UTF-8", + ) + })?; + buf.push(utf8); + } else { + return Err(io::Error::new( + io::ErrorKind::Other, + "Unexpected path component".to_owned(), + )); + } + } + + Ok(buf) +} diff --git a/testing/mozbase/rust/mozdevice/src/shell.rs b/testing/mozbase/rust/mozdevice/src/shell.rs new file mode 100644 index 0000000000..55a71c41d5 --- /dev/null +++ b/testing/mozbase/rust/mozdevice/src/shell.rs @@ -0,0 +1,66 @@ +// Copyright (c) 2017 Jimmy Cuadra +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +use regex::Regex; + +/// Escapes a string so it will be interpreted as a single word by the UNIX Bourne shell. +/// +/// If the input string is empty, this function returns an empty quoted string. +pub fn escape(input: &str) -> String { + // Stolen from + // https://docs.rs/shellwords/1.0.0/src/shellwords/lib.rs.html#24-37. + // Added space to the pattern to exclude spaces from being escaped + // which can cause problems when combining strings to form a full + // command. + let escape_pattern: Regex = Regex::new(r"([^A-Za-z0-9_\-.,:/@ \n])").unwrap(); + + if input.is_empty() { + return "''".to_owned(); + } + + let output = &escape_pattern.replace_all(input, "\\$1"); + + output.replace("'\n'", r"\n") +} + +#[cfg(test)] +mod tests { + use super::escape; + + #[test] + fn empty_escape() { + assert_eq!(escape(""), "''"); + } + + #[test] + fn full_escape() { + assert_eq!(escape("foo '\"' bar"), "foo \\'\\\"\\' bar"); + } + + #[test] + fn escape_multibyte() { + assert_eq!(escape("あい"), "\\あ\\い"); + } + + #[test] + fn escape_newline() { + assert_eq!(escape(r"'\n'"), "\\\'\\\\n\\\'"); + } +} diff --git a/testing/mozbase/rust/mozdevice/src/test.rs b/testing/mozbase/rust/mozdevice/src/test.rs new file mode 100644 index 0000000000..b4173e3649 --- /dev/null +++ b/testing/mozbase/rust/mozdevice/src/test.rs @@ -0,0 +1,760 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Currently the mozdevice API is not safe for multiple requests at the same +// time. It is recommended to run each of the unit tests on its own. Also adb +// specific tests cannot be run in CI yet. To check those locally, also run +// the ignored tests. +// +// Use the following command to accomplish that: +// +// $ cargo test -- --ignored --test-threads=1 + +use crate::*; + +use std::collections::BTreeSet; +use std::panic; +use std::path::PathBuf; +use tempfile::{tempdir, TempDir}; + +#[test] +fn read_length_from_valid_string() { + fn test(message: &str) -> Result<usize> { + read_length(&mut io::BufReader::new(message.as_bytes())) + } + + assert_eq!(test("0000").unwrap(), 0); + assert_eq!(test("0001").unwrap(), 1); + assert_eq!(test("000F").unwrap(), 15); + assert_eq!(test("00FF").unwrap(), 255); + assert_eq!(test("0FFF").unwrap(), 4095); + assert_eq!(test("FFFF").unwrap(), 65535); + + assert_eq!(test("FFFF0").unwrap(), 65535); +} + +#[test] +fn read_length_from_invalid_string() { + fn test(message: &str) -> Result<usize> { + read_length(&mut io::BufReader::new(message.as_bytes())) + } + + test("").expect_err("empty string"); + test("G").expect_err("invalid hex character"); + test("-1").expect_err("negative number"); + test("000").expect_err("shorter than 4 bytes"); +} + +#[test] +fn encode_message_with_valid_string() { + assert_eq!(encode_message("").unwrap(), "0000".to_string()); + assert_eq!(encode_message("a").unwrap(), "0001a".to_string()); + assert_eq!( + encode_message(&"a".repeat(15)).unwrap(), + format!("000F{}", "a".repeat(15)) + ); + assert_eq!( + encode_message(&"a".repeat(255)).unwrap(), + format!("00FF{}", "a".repeat(255)) + ); + assert_eq!( + encode_message(&"a".repeat(4095)).unwrap(), + format!("0FFF{}", "a".repeat(4095)) + ); + assert_eq!( + encode_message(&"a".repeat(65535)).unwrap(), + format!("FFFF{}", "a".repeat(65535)) + ); +} + +#[test] +fn encode_message_with_invalid_string() { + encode_message(&"a".repeat(65536)).expect_err("string lengths exceeds 4 bytes"); +} + +fn run_device_test<F>(test: F) +where + F: FnOnce(&Device, &TempDir, &UnixPath) + panic::UnwindSafe, +{ + let host = Host { + ..Default::default() + }; + let device = host + .device_or_default::<String>(None, AndroidStorageInput::Auto) + .expect("device_or_default"); + + let tmp_dir = tempdir().expect("create temp dir"); + let response = device + .execute_host_shell_command("echo $EXTERNAL_STORAGE") + .unwrap(); + let mut test_root = UnixPathBuf::from(response.trim_end_matches('\n')); + test_root.push("mozdevice"); + + let _ = device.remove(&test_root); + + let result = panic::catch_unwind(|| test(&device, &tmp_dir, &test_root)); + + let _ = device.kill_forward_all_ports(); + // let _ = device.kill_reverse_all_ports(); + + assert!(result.is_ok()) +} + +#[test] +#[ignore] +fn host_features() { + let host = Host { + ..Default::default() + }; + + let set = host.features::<BTreeSet<_>>().expect("to query features"); + assert!(set.contains("cmd")); + assert!(set.contains("shell_v2")); +} + +#[test] +#[ignore] +fn host_devices() { + let host = Host { + ..Default::default() + }; + + let set: BTreeSet<_> = host.devices().expect("to query devices"); + assert_eq!(1, set.len()); +} + +#[test] +#[ignore] +fn host_device_or_default() { + let host = Host { + ..Default::default() + }; + + let devices: Vec<_> = host.devices().expect("to query devices"); + let expected_device = devices.first().expect("found a device"); + + let device = host + .device_or_default::<String>(Some(&expected_device.serial), AndroidStorageInput::App) + .expect("connected device with serial"); + assert_eq!(device.run_as_package, None); + assert_eq!(device.serial, expected_device.serial); + assert!(device.tempfile.starts_with("/data/local/tmp")); +} + +#[test] +#[ignore] +fn host_device_or_default_invalid_serial() { + let host = Host { + ..Default::default() + }; + + host.device_or_default::<String>(Some(&"foobar".to_owned()), AndroidStorageInput::Auto) + .expect_err("invalid serial"); +} + +#[test] +#[ignore] +fn host_device_or_default_no_serial() { + let host = Host { + ..Default::default() + }; + + let devices: Vec<_> = host.devices().expect("to query devices"); + let expected_device = devices.first().expect("found a device"); + + let device = host + .device_or_default::<String>(None, AndroidStorageInput::Auto) + .expect("connected device with serial"); + assert_eq!(device.serial, expected_device.serial); +} + +#[test] +#[ignore] +fn host_device_or_default_storage_as_app() { + let host = Host { + ..Default::default() + }; + + let device = host + .device_or_default::<String>(None, AndroidStorageInput::App) + .expect("connected device"); + assert_eq!(device.storage, AndroidStorage::App); +} + +#[test] +#[ignore] +fn host_device_or_default_storage_as_auto() { + let host = Host { + ..Default::default() + }; + + let device = host + .device_or_default::<String>(None, AndroidStorageInput::Auto) + .expect("connected device"); + assert_eq!(device.storage, AndroidStorage::Sdcard); +} + +#[test] +#[ignore] +fn host_device_or_default_storage_as_internal() { + let host = Host { + ..Default::default() + }; + + let device = host + .device_or_default::<String>(None, AndroidStorageInput::Internal) + .expect("connected device"); + assert_eq!(device.storage, AndroidStorage::Internal); +} + +#[test] +#[ignore] +fn host_device_or_default_storage_as_sdcard() { + let host = Host { + ..Default::default() + }; + + let device = host + .device_or_default::<String>(None, AndroidStorageInput::Sdcard) + .expect("connected device"); + assert_eq!(device.storage, AndroidStorage::Sdcard); +} + +#[test] +#[ignore] +fn device_shell_command() { + run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { + assert_eq!( + "Linux\n", + device + .execute_host_shell_command("uname") + .expect("to have shell output") + ); + }); +} + +#[test] +#[ignore] +fn device_forward_port_hardcoded() { + run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { + assert_eq!( + 3035, + device + .forward_port(3035, 3036) + .expect("forwarded local port") + ); + // TODO: check with forward --list + }); +} + +// #[test] +// #[ignore] +// TODO: "adb server response to `forward tcp:0 ...` was not a u16: \"000559464\"") +// fn device_forward_port_system_allocated() { +// run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { +// let local_port = device.forward_port(0, 3037).expect("local_port"); +// assert_ne!(local_port, 0); +// // TODO: check with forward --list +// }); +// } + +#[test] +#[ignore] +fn device_kill_forward_port_no_forwarded_port() { + run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { + device + .kill_forward_port(3038) + .expect_err("adb error: listener 'tcp:3038' "); + }); +} + +#[test] +#[ignore] +fn device_kill_forward_port_twice() { + run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { + let local_port = device + .forward_port(3039, 3040) + .expect("forwarded local port"); + assert_eq!(local_port, 3039); + // TODO: check with forward --list + device + .kill_forward_port(local_port) + .expect("to remove forwarded port"); + device + .kill_forward_port(local_port) + .expect_err("adb error: listener 'tcp:3039' "); + }); +} + +#[test] +#[ignore] +fn device_kill_forward_all_ports_no_forwarded_port() { + run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { + device + .kill_forward_all_ports() + .expect("to not fail for no forwarded ports"); + }); +} + +#[test] +#[ignore] +fn device_kill_forward_all_ports_twice() { + run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { + let local_port1 = device + .forward_port(3039, 3040) + .expect("forwarded local port"); + assert_eq!(local_port1, 3039); + let local_port2 = device + .forward_port(3041, 3042) + .expect("forwarded local port"); + assert_eq!(local_port2, 3041); + // TODO: check with forward --list + device + .kill_forward_all_ports() + .expect("to remove all forwarded ports"); + device + .kill_forward_all_ports() + .expect("to not fail for no forwarded ports"); + }); +} + +#[test] +#[ignore] +fn device_reverse_port_hardcoded() { + run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { + assert_eq!(4035, device.reverse_port(4035, 4036).expect("remote_port")); + // TODO: check with reverse --list + }); +} + +// #[test] +// #[ignore] +// TODO: No adb response: ParseInt(ParseIntError { kind: Empty }) +// fn device_reverse_port_system_allocated() { +// run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { +// let reverse_port = device.reverse_port(0, 4037).expect("remote port"); +// assert_ne!(reverse_port, 0); +// // TODO: check with reverse --list +// }); +// } + +#[test] +#[ignore] +fn device_kill_reverse_port_no_reverse_port() { + run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { + device + .kill_reverse_port(4038) + .expect_err("listener 'tcp:4038' not found"); + }); +} + +// #[test] +// #[ignore] +// TODO: "adb error: adb server response did not contain expected hexstring length: \"\"" +// fn device_kill_reverse_port_twice() { +// run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { +// let remote_port = device +// .reverse_port(4039, 4040) +// .expect("reversed local port"); +// assert_eq!(remote_port, 4039); +// // TODO: check with reverse --list +// device +// .kill_reverse_port(remote_port) +// .expect("to remove reverse port"); +// device +// .kill_reverse_port(remote_port) +// .expect_err("listener 'tcp:4039' not found"); +// }); +// } + +#[test] +#[ignore] +fn device_kill_reverse_all_ports_no_reversed_port() { + run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { + device + .kill_reverse_all_ports() + .expect("to not fail for no reversed ports"); + }); +} + +#[test] +#[ignore] +fn device_kill_reverse_all_ports_twice() { + run_device_test(|device: &Device, _: &TempDir, _: &UnixPath| { + let local_port1 = device + .forward_port(4039, 4040) + .expect("forwarded local port"); + assert_eq!(local_port1, 4039); + let local_port2 = device + .forward_port(4041, 4042) + .expect("forwarded local port"); + assert_eq!(local_port2, 4041); + // TODO: check with reverse --list + device + .kill_reverse_all_ports() + .expect("to remove all reversed ports"); + device + .kill_reverse_all_ports() + .expect("to not fail for no reversed ports"); + }); +} + +#[test] +#[ignore] +fn device_push_pull_text_file() { + run_device_test( + |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { + let content = "test"; + let remote_path = remote_root_path.join("foo.txt"); + + device + .push( + &mut io::BufReader::new(content.as_bytes()), + &remote_path, + 0o777, + ) + .expect("file has been pushed"); + + let file_content = device + .execute_host_shell_command(&format!("cat {}", remote_path.display())) + .expect("host shell command for 'cat' to succeed"); + + assert_eq!(file_content, content); + + // And as second step pull it off the device. + let mut buffer = Vec::new(); + device + .pull(&remote_path, &mut buffer) + .expect("file has been pulled"); + assert_eq!(buffer, content.as_bytes()); + }, + ); +} + +#[test] +#[ignore] +fn device_push_pull_large_binary_file() { + run_device_test( + |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { + let remote_path = remote_root_path.join("foo.binary"); + + let mut content = Vec::new(); + + // Needs to be larger than 64kB to test multiple chunks. + for i in 0..100000u32 { + content.push('0' as u8 + (i % 10) as u8); + } + + device + .push( + &mut std::io::Cursor::new(content.clone()), + &remote_path, + 0o777, + ) + .expect("large file has been pushed"); + + let output = device + .execute_host_shell_command(&format!("ls -l {}", remote_path.display())) + .expect("host shell command for 'ls' to succeed"); + + assert!(output.contains(remote_path.to_str().unwrap())); + + let mut buffer = Vec::new(); + + device + .pull(&remote_path, &mut buffer) + .expect("large binary file has been pulled"); + assert_eq!(buffer, content); + }, + ); +} + +#[test] +#[ignore] +fn device_push_permission() { + run_device_test( + |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { + fn adjust_mode(mode: u32) -> u32 { + // Adjust the mode by copying the user permissions to + // group and other as indicated in + // [send_impl](https://android.googlesource.com/platform/system/core/+/master/adb/daemon/file_sync_service.cpp#516). + // This ensures that group and other can both access a + // file if the user can access it. + let mut m = mode & 0o777; + m |= (m >> 3) & 0o070; + m |= (m >> 3) & 0o007; + m + } + + fn get_permissions(mode: u32) -> String { + // Convert the mode integer into the string representation + // of the mode returned by `ls`. This assumes the object is + // a file and not a directory. + let mut perms = vec!["-", "r", "w", "x", "r", "w", "x", "r", "w", "x"]; + let mut bit_pos = 0; + while bit_pos < 9 { + if (1 << bit_pos) & mode == 0 { + perms[9 - bit_pos] = "-" + } + bit_pos += 1; + } + perms.concat() + } + let content = "test"; + let remote_path = remote_root_path.join("foo.bar"); + + // First push the file to the device + let modes = vec![0o421, 0o644, 0o666, 0o777]; + for mode in modes { + let adjusted_mode = adjust_mode(mode); + let adjusted_perms = get_permissions(adjusted_mode); + device + .push( + &mut io::BufReader::new(content.as_bytes()), + &remote_path, + mode, + ) + .expect("file has been pushed"); + + let output = device + .execute_host_shell_command(&format!("ls -l {}", remote_path.display())) + .expect("host shell command for 'ls' to succeed"); + + assert!(output.contains(remote_path.to_str().unwrap())); + assert!(output.starts_with(&adjusted_perms)); + } + + let output = device + .execute_host_shell_command(&format!("ls -ld {}", remote_root_path.display())) + .expect("host shell command for 'ls parent' to succeed"); + + assert!(output.contains(remote_root_path.to_str().unwrap())); + assert!(output.starts_with("drwxrwxrwx")); + }, + ); +} + +#[test] +#[ignore] +fn device_pull_fails_for_missing_file() { + run_device_test( + |device: &Device, _: &TempDir, remote_root_path: &UnixPath| { + let mut buffer = Vec::new(); + + device + .pull(&remote_root_path.join("missing"), &mut buffer) + .expect_err("missing file should not be pulled"); + }, + ); +} + +#[test] +#[ignore] +fn device_push_and_list_dir() { + run_device_test( + |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| { + let files = ["foo1.bar", "foo2.bar", "bar/foo3.bar", "bar/more/foo3.bar"]; + + for file in files.iter() { + let path = tmp_dir.path().join(Path::new(file)); + let _ = std::fs::create_dir_all(path.parent().unwrap()); + + let f = File::create(path).expect("to create file"); + let mut f = io::BufWriter::new(f); + f.write_all(file.as_bytes()).expect("to write data"); + } + + device + .push_dir(tmp_dir.path(), &remote_root_path, 0o777) + .expect("to push_dir"); + + for file in files.iter() { + let path = append_components(remote_root_path, Path::new(file)).unwrap(); + let output = device + .execute_host_shell_command(&format!("ls {}", path.display())) + .expect("host shell command for 'ls' to succeed"); + + assert!(output.contains(path.to_str().unwrap())); + } + + let mut listings = device.list_dir(&remote_root_path).expect("to list_dir"); + listings.sort(); + assert_eq!( + listings, + vec![ + RemoteDirEntry { + depth: 0, + name: "foo1.bar".to_string(), + metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { + mode: 0b110110000, + size: 8 + }) + }, + RemoteDirEntry { + depth: 0, + name: "foo2.bar".to_string(), + metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { + mode: 0b110110000, + size: 8 + }) + }, + RemoteDirEntry { + depth: 0, + name: "bar".to_string(), + metadata: RemoteMetadata::RemoteDir + }, + RemoteDirEntry { + depth: 1, + name: "bar/foo3.bar".to_string(), + metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { + mode: 0b110110000, + size: 12 + }) + }, + RemoteDirEntry { + depth: 1, + name: "bar/more".to_string(), + metadata: RemoteMetadata::RemoteDir + }, + RemoteDirEntry { + depth: 2, + name: "bar/more/foo3.bar".to_string(), + metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { + mode: 0b110110000, + size: 17 + }) + } + ] + ); + }, + ); +} + +#[test] +#[ignore] +fn device_push_and_pull_dir() { + run_device_test( + |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| { + let files = ["foo1.bar", "foo2.bar", "bar/foo3.bar", "bar/more/foo3.bar"]; + + let src_dir = tmp_dir.path().join(Path::new("src")); + let dest_dir = tmp_dir.path().join(Path::new("src")); + + for file in files.iter() { + let path = src_dir.join(Path::new(file)); + let _ = std::fs::create_dir_all(path.parent().unwrap()); + + let f = File::create(path).expect("to create file"); + let mut f = io::BufWriter::new(f); + f.write_all(file.as_bytes()).expect("to write data"); + } + + device + .push_dir(&src_dir, &remote_root_path, 0o777) + .expect("to push_dir"); + + device + .pull_dir(remote_root_path, &dest_dir) + .expect("to pull_dir"); + + for file in files.iter() { + let path = dest_dir.join(Path::new(file)); + let mut f = File::open(path).expect("to open file"); + let mut buf = String::new(); + f.read_to_string(&mut buf).expect("to read content"); + assert_eq!(buf, *file); + } + }, + ) +} + +#[test] +#[ignore] +fn device_push_and_list_dir_flat() { + run_device_test( + |device: &Device, tmp_dir: &TempDir, remote_root_path: &UnixPath| { + let content = "test"; + + let files = [ + PathBuf::from("foo1.bar"), + PathBuf::from("foo2.bar"), + PathBuf::from("bar").join("foo3.bar"), + ]; + + for file in files.iter() { + let path = tmp_dir.path().join(&file); + let _ = std::fs::create_dir_all(path.parent().unwrap()); + + let f = File::create(path).expect("to create file"); + let mut f = io::BufWriter::new(f); + f.write_all(content.as_bytes()).expect("to write data"); + } + + device + .push_dir(tmp_dir.path(), &remote_root_path, 0o777) + .expect("to push_dir"); + + for file in files.iter() { + let path = append_components(remote_root_path, file).unwrap(); + let output = device + .execute_host_shell_command(&format!("ls {}", path.display())) + .expect("host shell command for 'ls' to succeed"); + + assert!(output.contains(path.to_str().unwrap())); + } + + let mut listings = device + .list_dir_flat(&remote_root_path, 7, "prefix".to_string()) + .expect("to list_dir_flat"); + listings.sort(); + assert_eq!( + listings, + vec![ + RemoteDirEntry { + depth: 7, + metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { + mode: 0b110110000, + size: 4 + }), + name: "prefix/foo1.bar".to_string(), + }, + RemoteDirEntry { + depth: 7, + metadata: RemoteMetadata::RemoteFile(RemoteFileMetadata { + mode: 0b110110000, + size: 4 + }), + name: "prefix/foo2.bar".to_string(), + }, + RemoteDirEntry { + depth: 7, + metadata: RemoteMetadata::RemoteDir, + name: "prefix/bar".to_string(), + }, + ] + ); + }, + ); +} + +#[test] +fn format_own_device_error_types() { + assert_eq!( + format!("{}", DeviceError::InvalidStorage), + "Invalid storage".to_string() + ); + assert_eq!( + format!("{}", DeviceError::MissingPackage), + "Missing package".to_string() + ); + assert_eq!( + format!("{}", DeviceError::MultipleDevices), + "Multiple Android devices online".to_string() + ); + + assert_eq!( + format!("{}", DeviceError::Adb("foo".to_string())), + "foo".to_string() + ); +} |