debian_packaging/repository/
http.rs1use {
12 crate::{
13 error::{DebianError, Result},
14 io::DataResolver,
15 repository::{release::ReleaseFile, Compression, ReleaseReader, RepositoryRootReader},
16 },
17 async_trait::async_trait,
18 futures::{stream::TryStreamExt, AsyncRead},
19 reqwest::{Client, ClientBuilder, IntoUrl, StatusCode, Url},
20 std::pin::Pin,
21};
22
23pub const USER_AGENT: &str =
25 "debian-packaging Rust crate (https://crates.io/crates/debian-packaging)";
26
27async fn fetch_url(
28 client: &Client,
29 root_url: &Url,
30 path: &str,
31) -> Result<Pin<Box<dyn AsyncRead + Send>>> {
32 let request_url = root_url.join(path)?;
33
34 let res = client.get(request_url.clone()).send().await.map_err(|e| {
35 DebianError::RepositoryIoPath(
36 path.to_string(),
37 std::io::Error::new(
38 std::io::ErrorKind::Other,
39 format!("error sending HTTP request: {:?}", e),
40 ),
41 )
42 })?;
43
44 let res = res.error_for_status().map_err(|e| {
45 if e.status() == Some(StatusCode::NOT_FOUND) {
46 DebianError::RepositoryIoPath(
47 path.to_string(),
48 std::io::Error::new(
49 std::io::ErrorKind::NotFound,
50 format!("HTTP 404 for {}", request_url),
51 ),
52 )
53 } else {
54 DebianError::RepositoryIoPath(
55 path.to_string(),
56 std::io::Error::new(
57 std::io::ErrorKind::Other,
58 format!("bad HTTP status code: {:?}", e),
59 ),
60 )
61 }
62 })?;
63
64 Ok(Box::pin(
65 res.bytes_stream()
66 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{:?}", e)))
67 .into_async_read(),
68 ))
69}
70
71#[derive(Debug)]
78pub struct HttpRepositoryClient {
79 client: Client,
81
82 root_url: Url,
86}
87
88impl HttpRepositoryClient {
89 pub fn new(url: impl IntoUrl) -> Result<Self> {
91 let builder = ClientBuilder::new().user_agent(USER_AGENT);
92
93 Self::new_client(builder.build()?, url)
94 }
95
96 pub fn new_client(client: Client, url: impl IntoUrl) -> Result<Self> {
104 let mut root_url = url.into_url()?;
105
106 if !root_url.path().ends_with('/') {
109 root_url.set_path(&format!("{}/", root_url.path()));
110 }
111
112 Ok(Self { client, root_url })
113 }
114}
115
116#[async_trait]
117impl DataResolver for HttpRepositoryClient {
118 async fn get_path(&self, path: &str) -> Result<Pin<Box<dyn AsyncRead + Send>>> {
119 fetch_url(&self.client, &self.root_url, path).await
120 }
121}
122
123#[async_trait]
124impl RepositoryRootReader for HttpRepositoryClient {
125 fn url(&self) -> Result<Url> {
126 Ok(self.root_url.clone())
127 }
128
129 async fn release_reader_with_distribution_path(
130 &self,
131 path: &str,
132 ) -> Result<Box<dyn ReleaseReader>> {
133 let distribution_path = path.trim_matches('/').to_string();
134 let inrelease_path = join_path(&distribution_path, "InRelease");
135 let release_path = join_path(&distribution_path, "Release");
136 let mut root_url = self.root_url.join(&distribution_path)?;
137
138 if !root_url.path().ends_with('/') {
141 root_url.set_path(&format!("{}/", root_url.path()));
142 }
143
144 let release = self
145 .fetch_inrelease_or_release(&inrelease_path, &release_path)
146 .await?;
147
148 let fetch_compression = Compression::default_preferred_order()
149 .next()
150 .expect("iterator should not be empty");
151
152 Ok(Box::new(HttpReleaseClient {
153 client: self.client.clone(),
154 root_url,
155 relative_path: distribution_path,
156 release,
157 fetch_compression,
158 }))
159 }
160}
161
162fn join_path(a: &str, b: &str) -> String {
163 format!("{}/{}", a.trim_matches('/'), b.trim_start_matches('/'))
164}
165
166pub struct HttpReleaseClient {
168 client: Client,
169 root_url: Url,
170 relative_path: String,
171 release: ReleaseFile<'static>,
172 fetch_compression: Compression,
173}
174
175#[async_trait]
176impl DataResolver for HttpReleaseClient {
177 async fn get_path(&self, path: &str) -> Result<Pin<Box<dyn AsyncRead + Send>>> {
178 fetch_url(&self.client, &self.root_url, path).await
179 }
180}
181
182#[async_trait]
183impl ReleaseReader for HttpReleaseClient {
184 fn url(&self) -> Result<Url> {
185 Ok(self.root_url.clone())
186 }
187
188 fn root_relative_path(&self) -> &str {
189 &self.relative_path
190 }
191
192 fn release_file(&self) -> &ReleaseFile<'static> {
193 &self.release
194 }
195
196 fn preferred_compression(&self) -> Compression {
197 self.fetch_compression
198 }
199
200 fn set_preferred_compression(&mut self, compression: Compression) {
201 self.fetch_compression = compression;
202 }
203}
204
205#[cfg(test)]
206mod test {
207 use {
208 super::*,
209 crate::{
210 dependency::BinaryDependency, dependency_resolution::DependencyResolver, error::Result,
211 io::ContentDigest, repository::release::ChecksumType,
212 },
213 };
214
215 const BULLSEYE_URL: &str = "http://snapshot.debian.org/archive/debian/20211120T085721Z";
216
217 #[tokio::test]
218 async fn bullseye_release() -> Result<()> {
219 let root = HttpRepositoryClient::new(BULLSEYE_URL)?;
220
221 let release = root.release_reader("bullseye").await?;
222
223 let packages = release.resolve_packages("main", "amd64", false).await?;
224 assert_eq!(packages.len(), 58606);
225
226 let sources = release.sources_indices_entries()?;
227 assert_eq!(sources.len(), 9);
228
229 let p = packages.iter().next().unwrap();
230 assert_eq!(p.package()?, "0ad");
231 assert_eq!(
232 p.field_str("SHA256"),
233 Some("610e9f9c41be18af516dd64a6dc1316dbfe1bb8989c52bafa556de9e381d3e29")
234 );
235
236 let p = packages.iter().last().unwrap();
237 assert_eq!(p.package()?, "python3-zzzeeksphinx");
238 assert_eq!(
239 p.field_str("SHA256"),
240 Some("6e35f5805e808c19becd3b9ce25c4cf40c41aa0cf5d81fab317198ded917fec1")
241 );
242
243 let mut resolver = DependencyResolver::default();
245 resolver.load_binary_packages(packages.iter()).unwrap();
246
247 for p in packages.iter() {
248 resolver
249 .find_direct_binary_package_dependencies(p, BinaryDependency::Depends)
250 .unwrap();
251 }
252
253 let deps = resolver
254 .find_transitive_binary_package_dependencies(
255 p,
256 [
257 BinaryDependency::Depends,
258 BinaryDependency::PreDepends,
259 BinaryDependency::Recommends,
260 ]
261 .into_iter(),
262 )
263 .unwrap();
264
265 let sources = deps.packages_with_sources().collect::<Vec<_>>();
266 assert_eq!(sources.len(), 128);
267
268 Ok(())
269 }
270
271 #[tokio::test]
272 async fn bullseye_sources() -> Result<()> {
273 let root = HttpRepositoryClient::new(BULLSEYE_URL)?;
274
275 let release = root.release_reader("bullseye").await?;
276
277 let sources_entries = release.sources_indices_entries()?;
278 assert_eq!(sources_entries.len(), 9);
279
280 let entry = release.sources_entry("main")?;
281 assert_eq!(entry.path, "main/source/Sources.xz");
282 assert_eq!(
283 entry.digest,
284 ContentDigest::sha256_hex(
285 "1801d18c1135168d5dd86a8cb85fb5cd5bd81e16174acc25d900dee11389e9cd"
286 )
287 .unwrap()
288 );
289 assert_eq!(entry.size, 8616784);
290 assert_eq!(entry.component, "main");
291 assert_eq!(entry.compression, Compression::Xz);
292
293 let sources = release.resolve_sources("main").await?;
294 assert_eq!(sources.len(), 30952);
295
296 let source = sources.iter().next().unwrap();
297 assert_eq!(source.binary().unwrap().collect::<Vec<_>>(), vec!["0ad"]);
298 assert_eq!(source.version_str()?, "0.0.23.1-5");
299
300 for source in sources.iter() {
302 source.required_field_str("Package")?;
303 source.required_field_str("Directory")?;
304 source.format()?;
305 source.version()?;
306 source.maintainer()?;
307 source.package_dependency_fields()?;
308 if let Some(packages) = source.package_list() {
309 for p in packages {
310 p?;
311 }
312 }
313 if let Some(entries) = source.checksums_sha1() {
314 for entry in entries {
315 entry?;
316 }
317 }
318 if let Some(entries) = source.checksums_sha256() {
319 for entry in entries {
320 entry?;
321 }
322 }
323 for entry in source.files()? {
324 entry?;
325 }
326 for fetch in source.file_fetches(ChecksumType::Sha256)? {
327 fetch?;
328 }
329 }
330
331 let controls = sources
332 .iter_with_package_name("libzstd".to_string())
333 .collect::<Vec<_>>();
334 assert_eq!(controls.len(), 1);
335
336 let controls = sources
337 .iter_with_binary_package("zstd".to_string())
338 .collect::<Vec<_>>();
339 assert_eq!(controls.len(), 1);
340
341 let controls = sources
342 .iter_with_architecture("amd64".to_string())
343 .collect::<Vec<_>>();
344 assert_eq!(controls.len(), 297);
345
346 Ok(())
347 }
348
349 #[tokio::test]
350 async fn bullseye_contents() -> Result<()> {
351 let root = HttpRepositoryClient::new(BULLSEYE_URL)?;
352
353 let release = root.release_reader("bullseye").await?;
354
355 let contents_entries = release.contents_indices_entries()?;
356 assert_eq!(contents_entries.len(), 126);
357
358 let contents = release
359 .resolve_contents(Some("contrib"), "all", false)
360 .await?;
361
362 let packages = contents
363 .packages_with_path("etc/cron.d/zfs-auto-snapshot")
364 .collect::<Vec<_>>();
365 assert_eq!(packages, vec!["contrib/utils/zfs-auto-snapshot"]);
366
367 let paths = contents
368 .package_paths("contrib/utils/zfs-auto-snapshot")
369 .collect::<Vec<_>>();
370 assert_eq!(
371 paths,
372 vec![
373 "etc/cron.d/zfs-auto-snapshot",
374 "etc/cron.daily/zfs-auto-snapshot",
375 "etc/cron.hourly/zfs-auto-snapshot",
376 "etc/cron.monthly/zfs-auto-snapshot",
377 "etc/cron.weekly/zfs-auto-snapshot",
378 "usr/sbin/zfs-auto-snapshot",
379 "usr/share/doc/zfs-auto-snapshot/changelog.Debian.gz",
380 "usr/share/doc/zfs-auto-snapshot/copyright",
381 "usr/share/man/man8/zfs-auto-snapshot.8.gz"
382 ]
383 );
384
385 Ok(())
386 }
387}