refactor: more pre-parsing

This commit is contained in:
Rudxain 2024-12-14 20:41:31 -04:00
commit 77a52be830
No known key found for this signature in database
GPG key ID: 0DAC837DDEF8E96C
2 changed files with 63 additions and 36 deletions

View file

@ -19,8 +19,9 @@
//!
//! Despite being "low-level", we can still "have cake and eat it too";
//! After all, what's the point of an abstraction if it doesn't come with goodies?:
//! We can take some freedoms, such as:
//! - pre-parsing or validanting output, to provide types with invariants.
//! We can reserve some artistic license, such as:
//! - shorter names, complemented by context
//! - pre-parsing or validanting output, to provide types with invariants
//! - strongly-typed rather than "stringly-typed" APIs
//! - nicer IDE support
//! - compile-time prevention of malformed cmds
@ -77,14 +78,31 @@ impl Cmd {
self.0.arg("shell");
ShCmd(self)
}
/// List all detected devices:
/// List attached devices (as serials) and their status:
/// - USB
/// - TCP/IP: WIFI, Ethernet, etc...
/// - Local emulators
/// Some may not be authorized by the user (yet)
pub fn devices(mut self) -> Result<String, String> {
/// Status can be (but not limited to):
/// - "unauthorized"
/// - "device"
pub fn devices(mut self) -> Result<Vec<(String, String)>, String> {
self.0.arg("devices");
self.run()
Ok(self
.run()?
.lines()
.skip(1) // header
.map(|dev_stat| {
let tab_idx = dev_stat
.find('\t')
.expect("There must be 1 tab after serial");
(
// serial
dev_stat[..tab_idx].to_string(),
// status
dev_stat[(tab_idx + 1)..].to_string(),
)
})
.collect())
}
/// Reboots default device
pub fn reboot(mut self) -> Result<String, String> {
@ -182,9 +200,9 @@ impl ToString for PmLsPackFlag {
}
}
pub const PACK_URI_SCHEME: &str = "package:";
pub const PACK_PREFIX: &str = "package:";
#[expect(clippy::cast_possible_truncation, reason = "")]
pub const PACK_URI_LEN: u8 = PACK_URI_SCHEME.len() as _;
pub const PACK_URI_LEN: u8 = PACK_PREFIX.len() as _;
pub const PM_LIST_PACKS: &str = "pm list packages";
pub const PM_CLEAR_PACK: &str = "pm clear";
@ -195,7 +213,8 @@ pub const PM_CLEAR_PACK: &str = "pm clear";
#[derive(Debug)]
pub struct PmCmd(ShCmd);
impl PmCmd {
/// `list packages` sub-command
/// `list packages` sub-command,
/// stripped of "package:" prefix
pub fn ls_packs(
mut self,
f: Option<PmLsPackFlag>,
@ -214,13 +233,14 @@ impl PmCmd {
pack_ls
.lines()
.map(|p_ln| {
debug_assert!(p_ln.starts_with(PACK_URI_SCHEME));
debug_assert!(p_ln.starts_with(PACK_PREFIX));
String::from(&p_ln[PACK_URI_LEN as usize..])
})
.collect()
})
}
/// `list packages` sub-command, but pre-validated
/// `list packages` sub-command, pre-validated.
/// This is strongly-typed, at the cost of regex overhead.
pub fn ls_packs_valid(
self,
f: Option<PmLsPackFlag>,
@ -230,9 +250,12 @@ impl PmCmd {
.into_iter()
.map(|p| PackId::new(p).expect("One of these is wrong: `PackId` regex, ADB implementation. Or the spec now allows a wider char-set")).collect())
}
/// `list users` sub-command
pub fn ls_users(mut self) -> Result<String, String> {
#[allow(clippy::doc_markdown, reason = "Multi URL")]
/// `list users` sub-command.
/// - https://source.android.com/docs/devices/admin/multi-user-testing
/// - https://stackoverflow.com/questions/37495126/android-get-list-of-users-and-profile-name
pub fn ls_users(mut self) -> Result<Vec<String>, String> {
self.0 .0 .0.args(["list", "users"]);
self.0 .0.run()
Ok(self.0 .0.run()?.lines().skip(1).map(String::from).collect())
}
}

View file

@ -7,8 +7,7 @@ use regex::Regex;
use retry::{delay::Fixed, retry, OperationResult};
use serde::{Deserialize, Serialize};
use static_init::dynamic;
use std::collections::HashSet;
use std::process::Command;
use std::{collections::HashSet, process::Command};
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
@ -327,45 +326,50 @@ pub fn is_protected_user(user_id: &str, device_serial: &str) -> bool {
}
/// `pm list users` parsed into a vec with extra info.
pub fn list_users_parsed(device_serial: &str) -> Vec<User> {
pub fn ls_users_parsed(device_serial: &str) -> Vec<User> {
#[dynamic]
static RE: Regex = Regex::new(r"\{([0-9]+)").unwrap_or_else(|_| unreachable!());
AdbCmd::new()
.sh(device_serial)
.pm()
.ls_users()
.map(|users| {
RE.find_iter(&users)
.enumerate()
.map(|(i, u)| User {
id: u.as_str()[1..].parse().unwrap(),
index: i,
protected: is_protected_user(&u.as_str()[1..], device_serial),
})
.collect()
})
// if default, then empty iter, which becomes empty vec (again)
.unwrap_or_default()
.into_iter()
.enumerate()
.map(|(i, user)| {
// It seems each line is a user,
// optionally associated with a work-profile.
// This will ignore the work-profiles!
let u = &RE.captures(&user).expect("Each user should have an ID")[1];
User {
id: u
.parse()
.unwrap_or_else(|_| unreachable!("User ID must be valid `u16`")),
index: i,
protected: is_protected_user(u, device_serial),
}
})
.collect()
}
// getprop ro.serialno
/// This matches serials (`getprop ro.serialno`)
/// that are authorized by the user.
pub async fn get_devices_list() -> Vec<Device> {
/// This matches serials that are authorized by the user.
#[dynamic]
static RE: Regex = Regex::new(r"\n(\S+)\s+device").unwrap_or_else(|_| unreachable!());
retry(Fixed::from_millis(500).take(120), || {
match AdbCmd::new().devices() {
Ok(devices) => {
let mut device_list: Vec<Device> = vec![];
if !RE.is_match(&devices) {
if devices.iter().all(|(_, stat)| stat != "device") {
return OperationResult::Retry(vec![]);
}
for device in RE.captures_iter(&devices) {
let serial = &device[1];
for device in devices {
let serial = &device.0;
device_list.push(Device {
model: get_device_brand(serial),
android_sdk: get_android_sdk(serial),
user_list: list_users_parsed(serial),
user_list: ls_users_parsed(serial),
adb_id: serial.to_string(),
});
}