1use anyhow::Context;
5use as_result::{IntoResult, MapResult};
6use futures::stream::{Stream, StreamExt};
7use std::collections::HashMap;
8use std::io;
9use std::pin::Pin;
10use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
11use tokio::process::{Child, ChildStdout, Command};
12use tokio_stream::wrappers::LinesStream;
13
14pub type PackageStream = Pin<Box<dyn Stream<Item = String>>>;
15
16#[derive(Debug, Clone)]
17pub struct Policy {
18 pub package: String,
19 pub installed: String,
20 pub candidate: String,
21 pub version_table: HashMap<String, Vec<String>>,
22}
23
24pub type Policies = Pin<Box<dyn Stream<Item = Policy>>>;
25
26pub fn policies(lines: impl Stream<Item = io::Result<String>>) -> impl Stream<Item = Policy> {
27 async_stream::stream! {
28 futures::pin_mut!(lines);
29
30 let mut policy = Policy {
31 package: String::new(),
32 installed: String::new(),
33 candidate: String::new(),
34 version_table: HashMap::new()
35 };
36
37 while let Some(Ok(line)) = lines.next().await {
38 if line.is_empty() {
39 continue
40 }
41
42 if !line.starts_with(' ') {
43 policy.package = line;
44 if policy.package.ends_with(':') {
45 policy.package.truncate(policy.package.len() - 1);
46 }
47 continue
48 }
49
50 let line = line.trim();
51
52 if line.starts_with('I') {
53 if let Some(v) = line.split_ascii_whitespace().nth(1) {
54 policy.installed = v.to_owned();
55 }
56 } else if line.starts_with('C') {
57 if let Some(v) = line.split_ascii_whitespace().nth(1) {
58 policy.candidate = v.to_owned();
59 }
60 } else if line.starts_with('V') {
61 let mut current_version = String::from("unknown");
63 while let Some(Ok(line)) = lines.next().await {
64
65
66 if let Some(source) = line.strip_prefix(" ") {
67 policy.version_table.entry(current_version.clone())
68 .or_insert_with(Vec::new)
69 .push(source.trim().to_owned());
70 } else if let Some(version_and_pin) = line.strip_prefix(" *** ") {
71 let mut current_version_and_pin = version_and_pin.trim().split_whitespace();
72 if let Some(version) = current_version_and_pin.next() {
73 current_version = version.to_owned();
74 }
75 } else if let Some(version_and_pin) = line.strip_prefix(" ") {
77 let mut current_version_and_pin = version_and_pin.trim().split_whitespace();
78 if let Some(version) = current_version_and_pin.next() {
79 current_version = version.to_owned();
80 }
81 } else {
83 yield policy.clone();
84 policy.version_table.clear();
85 policy.package = line;
86 if policy.package.ends_with(':') {
87 policy.package.truncate(policy.package.len() - 1);
88 }
89 break
90 }
91 }
92 }
93 }
94
95 yield policy;
96 }
97}
98
99#[derive(AsMut, Deref, DerefMut)]
100#[as_mut(forward)]
101pub struct AptCache(Command);
102
103impl AptCache {
104 #[allow(clippy::new_without_default)]
105 pub fn new() -> Self {
106 let mut cmd = Command::new("apt-cache");
107 cmd.env("LANG", "C");
108 Self(cmd)
109 }
110
111 pub async fn depends<I, S>(mut self, packages: I) -> io::Result<(Child, ChildStdout)>
112 where
113 I: IntoIterator<Item = S>,
114 S: AsRef<std::ffi::OsStr>,
115 {
116 self.arg("depends");
117 self.args(packages);
118 self.spawn_with_stdout().await
119 }
120
121 pub async fn rdepends<I, S>(mut self, packages: I) -> io::Result<(Child, PackageStream)>
122 where
123 I: IntoIterator<Item = S>,
124 S: AsRef<std::ffi::OsStr>,
125 {
126 self.arg("rdepends");
127 self.args(packages);
128 self.stream_packages().await
129 }
130
131 pub async fn policy<S: AsRef<std::ffi::OsStr>>(
132 mut self,
133 packages: &[S],
134 ) -> anyhow::Result<(Child, Policies)> {
135 self.arg("policy");
136 self.args(packages);
137 self.env("LANG", "C");
138
139 let (child, stdout) = self.spawn_with_stdout().await?;
140
141 let lines = LinesStream::new(BufReader::new(stdout).lines());
142
143 let stream = Box::pin(policies(lines));
144
145 Ok((child, stream))
146 }
147
148 pub async fn predepends_of<'a>(
149 out: &'a mut String,
150 package: &'a str,
151 ) -> anyhow::Result<Vec<&'a str>> {
152 let (mut child, mut packages) = AptCache::new()
153 .rdepends(&[&package])
154 .await
155 .with_context(|| format!("failed to launch `apt-cache rdepends {}`", package))?;
156
157 let mut depends = Vec::new();
158 while let Some(package) = packages.next().await {
159 depends.push(package);
160 }
161
162 child
163 .wait()
164 .await
165 .map_result()
166 .with_context(|| format!("bad status from `apt-cache rdepends {}`", package))?;
167
168 let (mut child, mut stdout) = AptCache::new()
169 .depends(&depends)
170 .await
171 .with_context(|| format!("failed to launch `apt-cache depends {}`", package))?;
172
173 stdout
174 .read_to_string(out)
175 .await
176 .with_context(|| format!("failed to get output of `apt-cache depends {}`", package))?;
177
178 child.wait().await.map_result()?;
179
180 Ok(PreDependsIter::new(out.as_str(), package)?.collect::<Vec<_>>())
181 }
182
183 async fn stream_packages(self) -> io::Result<(Child, PackageStream)> {
184 let (child, stdout) = self.spawn_with_stdout().await?;
185
186 let mut lines = LinesStream::new(BufReader::new(stdout).lines()).skip(2);
187
188 let stream = async_stream::stream! {
189 while let Some(Ok(package)) = lines.next().await {
190 yield package.trim_start().to_owned();
191 }
192 };
193
194 Ok((child, Box::pin(stream)))
195 }
196
197 pub async fn status(mut self) -> io::Result<()> {
198 self.0.status().await?.into_result()
199 }
200
201 pub async fn spawn_with_stdout(self) -> io::Result<(Child, ChildStdout)> {
202 crate::utils::spawn_with_stdout(self.0).await
203 }
204}
205pub struct PreDependsIter<'a> {
206 lines: std::str::Lines<'a>,
207 predepend: &'a str,
208 active: &'a str,
209}
210
211impl<'a> PreDependsIter<'a> {
212 pub fn new(output: &'a str, predepend: &'a str) -> io::Result<Self> {
213 let mut lines = output.lines();
214
215 let active = lines.next().ok_or_else(|| {
216 io::Error::new(
217 io::ErrorKind::NotFound,
218 "expected the first line of the output of apt-cache depends to be a package name",
219 )
220 })?;
221
222 Ok(Self {
223 lines,
224 predepend,
225 active: active.trim(),
226 })
227 }
228}
229
230impl<'a> Iterator for PreDependsIter<'a> {
231 type Item = &'a str;
232
233 fn next(&mut self) -> Option<Self::Item> {
234 let mut found = false;
235 for line in &mut self.lines {
236 if !line.starts_with(' ') {
237 let prev = self.active;
238 self.active = line.trim();
239 if found {
240 return Some(prev);
241 }
242 } else if !found && line.starts_with(" PreDepends: ") && &line[14..] == self.predepend
243 {
244 found = true;
245 }
246 }
247
248 None
249 }
250}