Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(ADB): improve internal API #757

Merged
merged 49 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8ddbe68
refactor(sync): `const` `pm {list packages | clear}`
Rudxain Sep 24, 2024
8a80cdc
refactor(set_var-serial): define `set_adb_serial`
Rudxain Sep 24, 2024
6032b8e
build(lint): improve warns about `unsafe`
Rudxain Sep 24, 2024
8bead68
style: rm unused imports
Rudxain Sep 24, 2024
63707f3
style(sync): readable `const`s
Rudxain Sep 25, 2024
c0fa888
chore: `derive` `Copy` for `Error`
Rudxain Dec 7, 2024
cf054c8
docs: `Phone` -> `Device`, and explain fields
Rudxain Dec 7, 2024
fed1580
refactor: avoid `set_var` by specifying serial
Rudxain Dec 7, 2024
db15305
style: format code with Rustfmt
deepsource-autofix[bot] Dec 7, 2024
5300bef
refactor: `str`s are `Option`s already!
Rudxain Dec 7, 2024
5761e8d
Merge branch 'main' into refactor/sync-and-set_var
Rudxain Dec 7, 2024
65349ec
Merge branch 'main' into refactor/sync-and-set_var
Rudxain Dec 8, 2024
1da5b69
refactor: `cargo clippy --fix`
Rudxain Dec 8, 2024
7052646
style: format code with Rustfmt
deepsource-autofix[bot] Dec 8, 2024
f0f6c51
docs: vec cap can't overflow
Rudxain Dec 8, 2024
dc1ce19
style: vec macro
Rudxain Dec 8, 2024
ac056dd
perf: Optimize `*system_packages` and improve `list_all_system_packag…
Rudxain Dec 8, 2024
89680fc
perf: use more `Vec::with_capacity`
Rudxain Dec 8, 2024
04b3ef5
docs(sync): mention `pm list` format assumption
Rudxain Dec 8, 2024
b74947e
feat: add type-state cmd builders
Rudxain Dec 8, 2024
87fa332
docs(sync): Mention type-sys patterns on `AdbCmd`
Rudxain Dec 8, 2024
30359cb
refactor: use `AdbCmd`, finish `list_packs` and add `list_users`
Rudxain Dec 8, 2024
c172bae
fix: `User` not `&User`
Rudxain Dec 14, 2024
a566733
refactor: add `PackId`, replace `list_all_system_packages` by `list_p…
Rudxain Dec 14, 2024
240dde1
refactor: create `adb` module, and add **LOTS** of docs
Rudxain Dec 14, 2024
77a52be
refactor: more pre-parsing
Rudxain Dec 15, 2024
4c61dc3
docs(ADB): more docs for `ls_packs*`, and validated returns `Set`
Rudxain Dec 15, 2024
4ec1acd
refactor: replace `hashset_system_packages` by new API
Rudxain Dec 16, 2024
9aeba11
refactor: `user_id: u16` not `u: User`
Rudxain Dec 16, 2024
2e2114d
docs(ADB): more specific; mention assumptions
Rudxain Dec 16, 2024
e84e081
refactor(ADB): add `getprop` API, inline `adb_cmd` into `adb_sh_cmd`
Rudxain Dec 16, 2024
5e27a52
refactor(utils): less dupes in `open_url`
Rudxain Dec 16, 2024
fc72efc
docs: `iced::Command as ICmd`, and potential RCE warn
Rudxain Dec 16, 2024
ef16036
Merge branch 'main' into refactor/adb-api
Rudxain Jan 20, 2025
12ca4f6
chore: forgot to rm `UNINSTALLED_PACKAGES_FILE_NAME`
Rudxain Jan 20, 2025
74a5c5e
style: `cargo fmt`
Rudxain Jan 20, 2025
0f48489
docs(ADB): readable names, and better comments
Rudxain Jan 23, 2025
a1762c2
style(settings-view): fully-qualified `iced::Command`
Rudxain Jan 23, 2025
09f3ce1
style: revert rename of `Phone` to `Device` (now "Phone")
Rudxain Jan 24, 2025
4f484ae
refactor: undo questionable `with_capacity` usage
Rudxain Jan 24, 2025
a16525a
feat(log): `info` logs for ADB API `run`
Rudxain Jan 24, 2025
bfb97d5
refactor: make use of `reboot` ADB API
Rudxain Jan 24, 2025
8e8395f
docs(adb): add `_sys` to `list_packages*` names
Rudxain Jan 24, 2025
83f053c
style: revert another "phone" rename. `ls_users_parsed`->`list_users_…
Rudxain Jan 24, 2025
0238982
docs(sync): explain why multi-user is unreliable
Rudxain Jan 24, 2025
080fef9
docs(list_users): fmt
Rudxain Jan 24, 2025
0636134
docs(ADB): more correct module comments
Rudxain Jan 24, 2025
81a6f84
test: add `PackageId` debug-assertion to `list_packages_sys`
Rudxain Jan 26, 2025
0045a6c
test(ADB): add tests for `PackageId::new`
Rudxain Jan 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ panic = "abort"
embed-resource = "2.4.2"

[lints.rust]
# "forbid" may cause conflicts with libs (static and dynamic)
unsafe_code = "deny"
# "forbid" may cause conflicts with libs (static and dynamic).
# "deny" is annoying.
unsafe_code = "warn"
deprecated_safe = "warn"

[lints.clippy]
undocumented_unsafe_blocks = "forbid"
Expand Down
291 changes: 291 additions & 0 deletions src/core/adb.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
//! This module is intended to group everything that's "intrinsic" of ADB.
//!
//! Following the design philosophy of `Vec` and `thread`,
//! `*Command` are intended to be thin wrappers ("low-level" abstractions)
//! around the ADB CLI or `adb_client`
//! ([in the future](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/issues/700) ),
//! which implies:
//! - no "magic"
//! - no custom commands
//! - no chaining ("piping") of existing commands
//!
//! This guarantees a 1-to-1 mapping between methods and cmds,
//! thereby reducing surprises such as:
//! - Non-atomic operations: consider what happens if a pack changes state
//! in the middle of listing enabled and disabled packs!
//! - Non-standard semantics: what would happen if a new ADB version
//! supports a feature we already defined,
//! but has _slightly_ different behavior?
//!
//! 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 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
//! - implicit enforcement of a narrow set of operations
//!
//! About that last point, if there's ever a need for an ADB feature
//! which these APIs don't expose,
//! please, **PLEASE** refrain from falling-back to any `Command`-like API.
//! Rather, please extend these APIs in a consistent way.
//!
//! Thank you! ❤️
//!
//! For comprehensive info about ADB,
//! [see this](https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/master/docs/)

use regex::Regex;
use serde::{Deserialize, Serialize};
use static_init::dynamic;
use std::{collections::HashSet, process::Command};

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

pub fn to_trimmed_utf8(v: Vec<u8>) -> String {
String::from_utf8(v)
.expect("ADB should always output valid ASCII (or UTF-8, at least)")
.trim_end()
.to_string()
}

/// Builder object for an Android Debug Bridge CLI command,
/// using the type-state and new-type patterns.
///
/// This is not intended to model the entire ADB API.
/// It only models the subset that concerns UADNG.
///
/// [More info here](https://developer.android.com/tools/adb)
#[derive(Debug)]
pub struct ACommand(Command);
impl ACommand {
/// `adb` command builder
pub fn new() -> Self {
Self(Command::new("adb"))
}
/// `shell` sub-command builder.
///
/// If `device_serial` is empty, it lets ADB choose the default device.
pub fn shell<S: AsRef<str>>(mut self, device_serial: S) -> ShellCommand {
let serial = device_serial.as_ref();
if !serial.is_empty() {
self.0.args(["-s", serial]);
}
self.0.arg("shell");
ShellCommand(self)
}
/// Header-less list of attached devices (as serials) and their statuses:
/// - USB
/// - TCP/IP: WIFI, Ethernet, etc...
/// - Local emulators
/// Status can be (but not limited to):
/// - "unauthorized"
/// - "device"
pub fn devices(mut self) -> Result<Vec<(String, String)>, String> {
self.0.arg("devices");
Ok(self
.run()?
.lines()
.skip(1) // header
.map(|dev_stat| {
let tab_idx = dev_stat
// OS-specific?
.find('\t')
// True on Linux,
// no matter if ADB is piped or connected to terminal
.expect("There must be 1 tab after serial");
(
// serial
dev_stat[..tab_idx].to_string(),
// status
dev_stat[(tab_idx + 1)..].to_string(),
Comment on lines +102 to +103
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've found info on adb help:

scripting:
 wait-for[-TRANSPORT]-STATE...
     wait for device to be in a given state
     STATE: device, recovery, rescue, sideload, bootloader, or disconnect
     TRANSPORT: usb, local, or any [default=any]

So now we know the full list of states, which we can turn into a (non_exhaustive) enum!

As a bonus, wait-for-device allows UAD-NG to stop polling adb devices (no need for retry!)

)
})
.collect())
}
/// Reboots default device
pub fn reboot(mut self) -> Result<String, String> {
self.0.arg("reboot");
self.run()
}
/// General executor
fn run(self) -> Result<String, String> {
let mut cmd = self.0;
#[cfg(target_os = "windows")]
let cmd = cmd.creation_flags(0x0800_0000); // do not open a cmd window

info!(
"Ran command: adb '{}'",
cmd.get_args()
.map(|s| s.to_str().unwrap_or_else(|| unreachable!()))
.collect::<Vec<_>>()
.join("' '")
);
match cmd.output() {
Err(e) => {
error!("ADB: {}", e);
Err("Cannot run ADB, likely not found".to_string())
}
Ok(o) => {
let stdout = to_trimmed_utf8(o.stdout);
if o.status.success() {
Ok(stdout)
} else {
let stderr = to_trimmed_utf8(o.stderr);
// ADB does really weird things:
// Some errors are not redirected to `stderr`
let err = if stdout.is_empty() { stderr } else { stdout };
Err(err)
}
}
}
}
}

/// Builder object for a command that runs on the device's default `sh` implementation.
/// Typically MKSH, but could be Ash.
///
/// [More info](https://chromium.googlesource.com/aosp/platform/system/core/+/refs/heads/upstream/shell_and_utilities).
#[derive(Debug)]
pub struct ShellCommand(ACommand);
impl ShellCommand {
/// `pm` command builder
pub fn pm(mut self) -> PmCommand {
self.0 .0.arg("pm");
PmCommand(self)
}
/// Query a device property value, by its key.
/// These can be of any type:
/// - `boolean`
/// - `int`
/// - chars
/// - etc...
/// So to avoid lossy conversions, we return strs.
pub fn getprop(mut self, key: &str) -> Result<String, String> {
self.0 .0.args(["getprop", key]);
self.0.run()
}
/// Reboots device
pub fn reboot(mut self) -> Result<String, String> {
self.0 .0.arg("reboot");
self.0.run()
}
}

/// `String` with the invariant of being a valid package-name.
/// See its `new` constructor for more info.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
pub struct PackageId(String);
impl PackageId {
/// Creates a package-ID if it's valid according to
/// [this](https://developer.android.com/build/configure-app-module#set-application-id)
pub fn new<S: AsRef<str>>(pid: S) -> Option<Self> {
#[dynamic]
static RE: Regex = Regex::new(r"^[a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*)+$")
.unwrap_or_else(|_| unreachable!());

let pid = pid.as_ref();

if RE.is_match(pid) {
Some(Self(pid.to_string()))
} else {
None
}
}
}

/// `pm list packages` flag/state/type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PmListPacksFlag {
/// `-u`, not to be confused with `-a`
IncludeUninstalled,
/// `-e`
OnlyEnabled,
/// `-d`
OnlyDisabled,
}
impl PmListPacksFlag {
// is there a trait for this?
fn to_str(self) -> &'static str {
match self {
Self::IncludeUninstalled => "-u",
Self::OnlyEnabled => "-e",
Self::OnlyDisabled => "-d",
}
}
}
#[expect(clippy::to_string_trait_impl, reason = "This is not user-facing")]
impl ToString for PmListPacksFlag {
fn to_string(&self) -> String {
self.to_str().to_string()
}
}

const PACK_PREFIX: &str = "package:";

pub const PM_CLEAR_PACK: &str = "pm clear";

/// Builder object for an Android Package Manager command.
///
/// [More info](https://developer.android.com/tools/adb#pm)
#[derive(Debug)]
pub struct PmCommand(ShellCommand);
impl PmCommand {
/// `list packages -s` sub-command, [`PACK_PREFIX`] stripped.
/// This is "the rawest" version (minimal overhead).
///
/// `Ok` variant:
/// - isn't sorted
/// - duplicates never _seem_ to happen, but don't assume uniqueness
///
/// See also [`list_packages_sys_valid`].
pub fn list_packages_sys(
Rudxain marked this conversation as resolved.
Show resolved Hide resolved
mut self,
f: Option<PmListPacksFlag>,
user_id: Option<u16>,
) -> Result<Vec<String>, String> {
let cmd = &mut self.0 .0 .0;
cmd.args(["list", "packages", "-s"]);
if let Some(s) = f {
cmd.arg(s.to_str());
};
if let Some(u) = user_id {
cmd.arg("--user");
cmd.arg(u.to_string());
};
self.0 .0.run().map(|pack_ls| {
pack_ls
.lines()
.map(|p_ln| {
debug_assert!(p_ln.starts_with(PACK_PREFIX));
String::from(&p_ln[PACK_PREFIX.len()..])
})
.collect()
})
}
/// `list packages -s` sub-command, pre-validated.
/// This is strongly-typed, at the cost of regex & hash overhead.
///
/// See also [`list_packages_sys`].
pub fn list_packages_sys_valid(
self,
f: Option<PmListPacksFlag>,
user_id: Option<u16>,
) -> Result<HashSet<PackageId>, String> {
Ok(self.list_packages_sys(f, user_id)?
.into_iter()
.map(|p| PackageId::new(p).expect("One of these is wrong: `PackId` regex, ADB implementation. Or the spec now allows a wider char-set")).collect())
}
#[allow(clippy::doc_markdown, reason = "Multi URL")]
/// `list users` sub-command (header-less).
/// - 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 list_users(mut self) -> Result<Vec<String>, String> {
self.0 .0 .0.args(["list", "users"]);
// is it actually multi-line?
Ok(self.0 .0.run()?.lines().skip(1).map(String::from).collect())
}
}
19 changes: 3 additions & 16 deletions src/core/config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
use crate::core::utils::DisplayablePath;
use crate::core::{
sync::{get_android_sdk, User},
theme::Theme,
};
use crate::core::{sync::User, theme::Theme};
use crate::gui::views::settings::Settings;
use crate::CACHE_DIR;
use crate::CONFIG_DIR;
Expand Down Expand Up @@ -34,8 +31,9 @@ pub struct BackupSettings {
pub backup_state: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct DeviceSettings {
/// Unique serial identifier
pub device_id: String,
pub disable_mode: bool,
pub multi_user_mode: bool,
Expand All @@ -53,17 +51,6 @@ impl Default for GeneralSettings {
}
}

impl Default for DeviceSettings {
fn default() -> Self {
Self {
device_id: String::default(),
multi_user_mode: get_android_sdk() > 21,
disable_mode: false,
backup: BackupSettings::default(),
}
}
}

#[dynamic]
static CONFIG_FILE: PathBuf = CONFIG_DIR.join("config.toml");

Expand Down
1 change: 1 addition & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod adb;
pub mod config;
pub mod helpers;
pub mod save;
Expand Down
2 changes: 1 addition & 1 deletion src/core/save.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ pub fn restore_backup(
let p_commands = apply_pkg_state_commands(
&package,
backup_package.state,
&settings
settings
.backup
.selected_user
.ok_or("field should be Some type")?,
Expand Down
Loading
Loading