1use 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
16fn is_orphaned_version(sources: &[String]) -> bool {
18 sources.len() == 1 && sources[0].contains("/var/lib/dpkg/status")
19}
20
21fn 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
32fn 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
58pub 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
84pub 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
109pub 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
133pub 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}