1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
use std::env;
use std::process::Stdio;

use tokio::fs::{create_dir_all, File};
use tokio::process::Command;

use super::*;

#[derive(Default)]
pub struct Apt {
    pub(super) config_file: String,
    pub(super) ignore_phased_updates: bool,
}

impl Apt {
    pub async fn new(ignore_phased_updates: bool) -> Result<Self> {
        let mut apt = Apt {
            config_file: String::new(),
            ignore_phased_updates,
        };

        apt.setup().await?;

        Ok(apt)
    }

    async fn is_phased_update(&self, package_line: &str) -> Result<bool> {
        let package_name_regex = regex!(r#"(.*)/.*"#);
        let package_name = &package_name_regex
            .captures(package_line)
            .error("Couldn't find package name")?[1];

        let output = String::from_utf8(
            Command::new("apt-cache")
                .args(["-c", &self.config_file, "policy", package_name])
                .output()
                .await
                .error("Problem running apt-cache command")?
                .stdout,
        )
        .error("Problem capturing apt-cache command output")?;

        let phased_regex = regex!(r".*\(phased (\d+)%\).*");
        Ok(match phased_regex.captures(&output) {
            Some(matches) => &matches[1] != "100",
            None => false,
        })
    }

    async fn setup(&mut self) -> Result<()> {
        let mut cache_dir = env::temp_dir();
        cache_dir.push("i3rs-apt");
        if !cache_dir.exists() {
            create_dir_all(&cache_dir)
                .await
                .error("Failed to create temp dir")?;
        }

        let apt_config = format!(
            "Dir::State \"{}\";\n
         Dir::State::lists \"lists\";\n
         Dir::Cache \"{}\";\n
         Dir::Cache::srcpkgcache \"srcpkgcache.bin\";\n
         Dir::Cache::pkgcache \"pkgcache.bin\";",
            cache_dir.display(),
            cache_dir.display(),
        );

        let mut config_file = cache_dir;
        config_file.push("apt.conf");
        let config_file = config_file.to_str().unwrap();

        self.config_file = config_file.to_string();

        let mut file = File::create(&config_file)
            .await
            .error("Failed to create config file")?;
        file.write_all(apt_config.as_bytes())
            .await
            .error("Failed to write to config file")?;

        Ok(())
    }
}

#[async_trait]
impl Backend for Apt {
    fn name(&self) -> Cow<'static, str> {
        "apt".into()
    }

    async fn get_updates_list(&self) -> Result<Vec<String>> {
        Command::new("apt")
            .env("APT_CONFIG", &self.config_file)
            .args(["update"])
            .stdout(Stdio::null())
            .stdin(Stdio::null())
            .spawn()
            .error("Failed to run `apt update`")?
            .wait()
            .await
            .error("Failed to run `apt update`")?;
        let stdout = Command::new("apt")
            .env("LANG", "C")
            .env("APT_CONFIG", &self.config_file)
            .args(["list", "--upgradable"])
            .output()
            .await
            .error("Problem running apt command")?
            .stdout;

        let updates = String::from_utf8(stdout).error("apt produced non-UTF8 output")?;
        let mut updates_list: Vec<String> = Vec::new();

        for update in updates.lines().filter(|line| line.contains("[upgradable")) {
            if !self.ignore_phased_updates || !self.is_phased_update(update).await? {
                updates_list.push(update.to_string());
            }
        }

        Ok(updates_list)
    }
}