rustpm 0.2.6

A fast, friendly APT frontend with kernel, desktop, and sources management
#[derive(Debug, Clone, PartialEq)]
pub enum ChangeKind {
    Install,
    Remove,
    Upgrade,
    Configure,
}

#[derive(Debug, Clone)]
pub struct PackageChange {
    pub kind: ChangeKind,
    pub name: String,
    pub old_version: Option<String>,
    pub new_version: Option<String>,
    pub arch: Option<String>,
    pub size_bytes: Option<u64>,
}

/// Parse a single `Inst` line from `apt-get --simulate` output.
/// Format: `Inst name [(old_ver ...)] [(new_ver repo ...)]`
fn parse_inst(line: &str) -> Option<PackageChange> {
    let rest = line.strip_prefix("Inst ")?;
    let (name, rest) = rest.split_once(' ').unwrap_or((rest, ""));
    let rest = rest.trim();

    let (old_ver, rest) = if rest.starts_with('[') {
        let end = rest.find(']').unwrap_or(rest.len());
        (Some(rest[1..end].to_string()), rest[end + 1..].trim())
    } else {
        (None, rest)
    };

    let new_ver = if rest.starts_with('(') {
        rest[1..].split_ascii_whitespace().next().map(|s| s.to_string())
    } else {
        None
    };

    let kind = if old_ver.is_some() { ChangeKind::Upgrade } else { ChangeKind::Install };

    Some(PackageChange {
        kind,
        name: name.to_string(),
        old_version: old_ver,
        new_version: new_ver,
        arch: None,
        size_bytes: None,
    })
}

fn parse_remv(line: &str) -> Option<PackageChange> {
    let rest = line.strip_prefix("Remv ")?;
    let (name, rest) = rest.split_once(' ').unwrap_or((rest, ""));
    let old_ver = rest.trim().strip_prefix('[')
        .and_then(|s| s.find(']').map(|i| s[..i].to_string()));

    Some(PackageChange {
        kind: ChangeKind::Remove,
        name: name.to_string(),
        old_version: old_ver,
        new_version: None,
        arch: None,
        size_bytes: None,
    })
}

fn parse_conf(line: &str) -> Option<PackageChange> {
    let rest = line.strip_prefix("Conf ")?;
    let name = rest.split_ascii_whitespace().next()?.to_string();
    Some(PackageChange {
        kind: ChangeKind::Configure,
        name,
        old_version: None,
        new_version: None,
        arch: None,
        size_bytes: None,
    })
}

pub fn parse_dry_run_output(output: &str) -> Vec<PackageChange> {
    output.lines().filter_map(|line| {
        if line.starts_with("Inst ") {
            parse_inst(line)
        } else if line.starts_with("Remv ") {
            parse_remv(line)
        } else if line.starts_with("Conf ") {
            parse_conf(line)
        } else {
            None
        }
    }).collect()
}