radb 0.1.8

A Rust Implement Python AdbUtils
Documentation
use crate::beans::{AppInfo, ForwardItem};
use crate::errors::{AdbError, AdbResult};
use chrono::{DateTime, NaiveDateTime, Utc};
use once_cell::sync::Lazy;
use regex::Regex;
use std::process::{ExitStatus, Output};

static IP_REGEXES: Lazy<Vec<Regex>> = Lazy::new(|| {
    vec![
        Regex::new(r"inet\s+addr:([\d.]+)").unwrap(),
        Regex::new(r"inet\s+([\d.]+)/\d+").unwrap(),
        Regex::new(r"inet\s+([\d.]+)\s+netmask").unwrap(),
    ]
});

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceListEntry {
    pub serial: String,
    pub state: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceTextCommand {
    State,
    SerialNo,
    DevPath,
    Features,
    SdkVersion,
    AndroidVersion,
    DeviceModel,
    DeviceBrand,
    DeviceManufacturer,
    DeviceProduct,
    DeviceAbi,
}

impl DeviceTextCommand {
    pub fn transport_command(&self) -> Option<&'static str> {
        match self {
            DeviceTextCommand::State => Some("get-state"),
            DeviceTextCommand::SerialNo => Some("get-serialno"),
            DeviceTextCommand::DevPath => Some("get-devpath"),
            DeviceTextCommand::Features => Some("get-features"),
            _ => None,
        }
    }

    pub fn shell_args(&self) -> Option<&'static [&'static str]> {
        match self {
            DeviceTextCommand::SdkVersion => Some(&["getprop", "ro.build.version.sdk"]),
            DeviceTextCommand::AndroidVersion => Some(&["getprop", "ro.build.version.release"]),
            DeviceTextCommand::DeviceModel => Some(&["getprop", "ro.product.model"]),
            DeviceTextCommand::DeviceBrand => Some(&["getprop", "ro.product.brand"]),
            DeviceTextCommand::DeviceManufacturer => Some(&["getprop", "ro.product.manufacturer"]),
            DeviceTextCommand::DeviceProduct => Some(&["getprop", "ro.product.product"]),
            DeviceTextCommand::DeviceAbi => Some(&["getprop", "ro.product.cpu.abi"]),
            _ => None,
        }
    }

    pub fn trim_output(&self) -> bool {
        self.shell_args().is_some()
    }
}

pub fn build_forward_command(local: &str, remote: &str, norebind: bool) -> String {
    if norebind {
        format!("forward:norebind:{};{}", local, remote)
    } else {
        format!("forward:{};{}", local, remote)
    }
}

pub fn build_reverse_command(remote: &str, local: &str, norebind: bool) -> String {
    if norebind {
        format!("reverse:forward:norebind:{};{}", remote, local)
    } else {
        format!("reverse:forward:{};{}", remote, local)
    }
}

pub fn build_uninstall_command(package_name: &str) -> [&str; 3] {
    ["pm", "uninstall", package_name]
}

pub fn build_transport_prefix(
    serial: Option<&str>,
    transport_id: Option<u8>,
    command: Option<&str>,
) -> AdbResult<String> {
    match (serial, transport_id, command) {
        (_, Some(id), Some(command)) => Ok(format!("host-transport-id:{}:{}", id, command)),
        (Some(serial), None, Some(command)) => Ok(format!("host-serial:{}:{}", serial, command)),
        (_, Some(id), None) => Ok(format!("host-transport-id:{}", id)),
        (Some(serial), None, None) => Ok(format!("host:transport:{}", serial)),
        (None, None, _) => Err(AdbError::protocol_error(
            "TransportID and Serial Can Not Been None At Same Time",
        )),
    }
}

pub fn parse_device_list(lines: &str) -> Vec<DeviceListEntry> {
    lines
        .lines()
        .filter_map(|line| {
            let mut parts = line.split_whitespace();
            let serial = parts.next()?.to_string();
            let state = parts.next().map(str::to_string);
            Some(DeviceListEntry { serial, state })
        })
        .collect()
}

pub fn adb_output_to_result(status: ExitStatus, stdout: &str, stderr: &str) -> AdbResult<String> {
    if status.success() {
        Ok(stdout.to_string())
    } else {
        let reason = if stderr.trim().is_empty() {
            stdout.trim()
        } else {
            stderr.trim()
        };
        Err(AdbError::command_failed("adb", reason))
    }
}

pub fn command_output_to_result(command: &[&str], output: Output) -> AdbResult<String> {
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    if output.status.success() {
        Ok(stdout.to_string())
    } else {
        let reason = if stderr.trim().is_empty() {
            stdout.trim().to_string()
        } else {
            stderr.trim().to_string()
        };
        Err(AdbError::command_failed(command.join(" "), reason))
    }
}

pub fn extract_forward_item_from_output(output: &str) -> Vec<ForwardItem> {
    output
        .lines()
        .filter(|line| !line.trim().is_empty())
        .filter_map(|line| {
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 3 {
                Some(ForwardItem::new(parts[0], parts[1], parts[2]))
            } else {
                log::warn!("Invalid forward list line: {}", line);
                None
            }
        })
        .collect()
}

pub fn is_valid_ipv4(ip: &str) -> bool {
    let mut parts = ip.split('.');
    let valid_parts = parts
        .by_ref()
        .take(4)
        .all(|part| !part.is_empty() && part.parse::<u8>().is_ok());
    valid_parts && parts.next().is_none() && ip.split('.').count() == 4
}

pub fn extract_ip_from_output(output: &str) -> Option<String> {
    for regex in IP_REGEXES.iter() {
        if let Some(captures) = regex.captures(output) {
            if let Some(ip_match) = captures.get(1) {
                let ip = ip_match.as_str();
                if is_valid_ipv4(ip) {
                    return Some(ip.to_string());
                }
            }
        }
    }
    None
}

pub fn extract_port_from_tcp_spec(tcp_spec: &str) -> Option<u16> {
    tcp_spec.strip_prefix("tcp:")?.parse().ok()
}

pub fn escape_shell_arg(arg: &str) -> String {
    if arg.is_empty() {
        return "\"\"".to_string();
    }

    if !arg.chars().any(|c| " \"'\\$`(){}[]|&;<>?*~".contains(c)) {
        return arg.to_string();
    }

    let mut escaped = String::with_capacity(arg.len() + 10);
    escaped.push('"');

    for c in arg.chars() {
        match c {
            '"' => escaped.push_str("\\\""),
            '\\' => escaped.push_str("\\\\"),
            '$' => escaped.push_str("\\$"),
            '`' => escaped.push_str("\\`"),
            _ => escaped.push(c),
        }
    }

    escaped.push('"');
    escaped
}

pub fn extract_app_version_info(output: &str, app_info: &mut AppInfo) {
    if let Ok(version_name_regex) = Regex::new(r"versionName=(\S+)") {
        if let Some(cap) = version_name_regex.captures(output) {
            if let Some(version_name) = cap.get(1) {
                app_info.version_name = Some(version_name.as_str().to_string());
            }
        }
    }

    if let Ok(version_code_regex) = Regex::new(r"versionCode=(\d+)") {
        if let Some(cap) = version_code_regex.captures(output) {
            if let Some(version_code) = cap.get(1) {
                if let Ok(code) = version_code.as_str().parse::<u32>() {
                    app_info.version_code = Some(code);
                }
            }
        }
    }
}

pub fn extract_app_signature(output: &str, app_info: &mut AppInfo) {
    if let Ok(signature_regex) = Regex::new(r"PackageSignatures\{[^}]*\[([^]]+)]") {
        if let Some(cap) = signature_regex.captures(output) {
            if let Some(signature) = cap.get(1) {
                app_info.signature = Some(signature.as_str().to_string());
            }
        }
    }
}

pub fn extract_app_flags(output: &str, app_info: &mut AppInfo) {
    if let Ok(flags_regex) = Regex::new(r"pkgFlags=\[\s*([^]]+)\s*]") {
        if let Some(cap) = flags_regex.captures(output) {
            if let Some(flags_str) = cap.get(1) {
                app_info.flags = flags_str
                    .as_str()
                    .split_whitespace()
                    .map(str::to_string)
                    .collect();
            }
        }
    }
}

pub fn extract_app_timestamps(output: &str, app_info: &mut AppInfo) {
    if let Ok(first_install_regex) = Regex::new(r"firstInstallTime=([\d-]+\s+[:\d]+)") {
        if let Some(cap) = first_install_regex.captures(output) {
            if let Some(time_str) = cap.get(1) {
                app_info.first_install_time = parse_android_datetime(time_str.as_str());
            }
        }
    }

    if let Ok(last_update_regex) = Regex::new(r"lastUpdateTime=([\d-]+\s+[:\d]+)") {
        if let Some(cap) = last_update_regex.captures(output) {
            if let Some(time_str) = cap.get(1) {
                app_info.last_update_time = parse_android_datetime(time_str.as_str());
            }
        }
    }
}

pub fn populate_app_info(package_name: &str, output: &str) -> AppInfo {
    let mut app_info = AppInfo::new(package_name);
    extract_app_version_info(output, &mut app_info);
    extract_app_signature(output, &mut app_info);
    extract_app_flags(output, &mut app_info);
    extract_app_timestamps(output, &mut app_info);
    app_info
}

fn parse_android_datetime(input: &str) -> Option<DateTime<Utc>> {
    NaiveDateTime::parse_from_str(input, "%Y-%m-%d %H:%M:%S")
        .ok()
        .map(|naive| DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc))
}