apt_cmd/
apt.rs

1// Copyright 2021-2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use anyhow::Context;
5use futures::stream::{Stream, StreamExt};
6use std::cmp::Ordering;
7use std::collections::HashMap;
8use std::pin::Pin;
9use std::process::Stdio;
10use tokio::io::{AsyncBufReadExt, BufReader};
11use tokio::process::{Child, Command};
12use tokio_stream::wrappers::LinesStream;
13
14pub type Packages = Pin<Box<dyn Stream<Item = String> + Send>>;
15
16/// It is orphaned if the only source is `/var/lib/dpkg/status`.
17fn is_orphaned_version(sources: &[String]) -> bool {
18    sources.len() == 1 && sources[0].contains("/var/lib/dpkg/status")
19}
20
21/// The version of the package installed which has no repository.
22fn orphaned_version(version_table: &HashMap<String, Vec<String>>) -> Option<&str> {
23    for (status, sources) in version_table {
24        if is_orphaned_version(sources) {
25            return Some(status.as_str());
26        }
27    }
28
29    None
30}
31
32/// A list of package versions associated with a repository.
33fn repository_versions(version_table: &HashMap<String, Vec<String>>) -> impl Iterator<Item = &str> {
34    version_table.iter().filter_map(|(version, sources)| {
35        if is_orphaned_version(sources) {
36            None
37        } else {
38            Some(version.as_str())
39        }
40    })
41}
42
43fn greatest_repository_version(version_table: &HashMap<String, Vec<String>>) -> Option<&str> {
44    let mut iterator = repository_versions(version_table);
45    if let Some(mut greatest_nonlocal) = iterator.next() {
46        for nonlocal in iterator {
47            if let Ordering::Less = deb_version::compare_versions(greatest_nonlocal, nonlocal) {
48                greatest_nonlocal = nonlocal;
49            }
50        }
51
52        return Some(greatest_nonlocal);
53    }
54
55    None
56}
57
58// Locates packages which can be downgraded.
59pub async fn downgradable_packages() -> anyhow::Result<Vec<(String, String)>> {
60    let installed = crate::AptMark::installed().await?;
61    let (mut child, mut stream) = crate::AptCache::new().policy(&installed).await?;
62
63    let mut packages = Vec::new();
64
65    'outer: while let Some(policy) = stream.next().await {
66        if let Some(local) = orphaned_version(&policy.version_table) {
67            if let Some(nonlocal) = greatest_repository_version(&policy.version_table) {
68                if let Ordering::Greater = deb_version::compare_versions(local, nonlocal) {
69                    packages.push((policy.package, nonlocal.to_owned()));
70                    continue 'outer;
71                }
72            }
73        }
74    }
75
76    let _ = child
77        .wait()
78        .await
79        .context("`apt-cache policy` exited in error")?;
80
81    Ok(packages)
82}
83
84/// Locates all packages which do not belong to a repository
85pub async fn remoteless_packages() -> anyhow::Result<Vec<String>> {
86    let installed = crate::AptMark::installed().await?;
87    let (mut child, mut stream) = crate::AptCache::new().policy(&installed).await?;
88
89    let mut packages = Vec::new();
90
91    'outer: while let Some(policy) = stream.next().await {
92        for sources in policy.version_table.values() {
93            if !is_orphaned_version(sources) {
94                continue 'outer;
95            }
96        }
97
98        packages.push(policy.package);
99    }
100
101    let _ = child
102        .wait()
103        .await
104        .context("`apt-cache policy` exited in error")?;
105
106    Ok(packages)
107}
108
109/// Fetch all upgradeable debian packages from system apt repositories.
110pub async fn upgradable_packages() -> anyhow::Result<(Child, Packages)> {
111    let mut child = Command::new("apt")
112        .args(["list", "--upgradable"])
113        .stdout(Stdio::piped())
114        .stderr(Stdio::null())
115        .spawn()
116        .context("failed to launch `apt`")?;
117
118    let stdout = child.stdout.take().unwrap();
119
120    let stream = Box::pin(async_stream::stream! {
121        let mut lines = LinesStream::new(BufReader::new(stdout).lines()).skip(1);
122
123        while let Some(Ok(line)) = lines.next().await {
124            if let Some(package) = line.split('/').next() {
125                yield package.into();
126            }
127        }
128    });
129
130    Ok((child, stream))
131}
132
133/// Fetch debian packages which are necessary security updates, only.
134pub async fn security_updates() -> anyhow::Result<(Child, Packages)> {
135    let mut child = Command::new("apt")
136        .args(["-s", "dist-upgrade"])
137        .stdout(Stdio::piped())
138        .stderr(Stdio::null())
139        .spawn()
140        .context("could not launch `apt` process")?;
141
142    let stdout = child
143        .stdout
144        .take()
145        .context("`apt` didn't have stdout pipe")?;
146
147    let stream = Box::pin(async_stream::stream! {
148        let mut lines = LinesStream::new(BufReader::new(stdout).lines()).skip(1);
149
150        while let Some(Ok(line)) = lines.next().await {
151            if let Some(package) = parse_security_update(&line) {
152                yield package.into()
153            }
154        }
155    });
156
157    Ok((child, stream))
158}
159
160fn parse_security_update(simulated_line: &str) -> Option<&str> {
161    if simulated_line.starts_with("Inst") && simulated_line.contains("-security") {
162        simulated_line.split_ascii_whitespace().nth(1)
163    } else {
164        None
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    #[test]
171    fn parse_security_update() {
172        assert_eq!(
173            Some("libcaca0:i386"),
174            super::parse_security_update("Inst libcaca0:i386 [0.99.beta19-2.2ubuntu2] (0.99.beta19-2.2ubuntu2.1 Ubuntu:21.10/impish-security, Ubuntu:21.10/impish-updates [amd64])")
175        );
176
177        assert_eq!(
178            None,
179            super::parse_security_update("Conf libcaca0:i386 [0.99.beta19-2.2ubuntu2] (0.99.beta19-2.2ubuntu2.1 Ubuntu:21.10/impish-security, Ubuntu:21.10/impish-updates [amd64])")
180        );
181    }
182}