1mod error;
2pub mod repository;
3mod repository_cache;
4
5use std::{
6 fs::File,
7 path::{Path, PathBuf},
8};
9
10use debpkg::DebPkg;
11use url::Url;
12
13pub use self::{
14 error::Error, repository::UbuntuRepositoryEntry, repository_cache::UbuntuPackageCache,
15};
16use crate::{LinuxBanner, LinuxVersionSignature, UbuntuVersionSignature};
17
18pub const DEFAULT_DDEBS_URL: &str = "http://ddebs.ubuntu.com";
19pub const DEFAULT_ARCHIVE_URL: &str = "http://cz.archive.ubuntu.com/ubuntu";
20pub const DEFAULT_ARCH: &str = "amd64";
21pub const DEFAULT_DISTS: &[&str] = &[
22 "trusty", "xenial", "bionic", "focal", "focal-updates", "jammy", "jammy-updates", "noble", "noble-updates", ];
32
33enum Filename {
34 Original,
35 Custom(PathBuf),
36}
37
38pub struct UbuntuDownloader {
39 arch: String,
40 dists: Vec<String>,
41
42 release: String,
43 version: String,
44
45 archive_url: Url,
46 ddebs_url: Url,
47
48 output_directory: Option<PathBuf>,
49 subdirectory: String,
50 skip_existing: bool,
51
52 linux_image_deb: Option<Filename>,
53 linux_image_dbgsym_deb: Option<Filename>,
54 linux_modules_deb: Option<Filename>,
55 extract_linux_image: Option<Filename>,
56 extract_linux_image_dbgsym: Option<Filename>,
57 extract_systemmap: Option<Filename>,
58}
59
60#[derive(Debug, Default)]
61pub struct UbuntuPaths {
62 pub output_directory: PathBuf,
63 pub linux_image_deb: Option<PathBuf>,
64 pub linux_image_dbgsym_deb: Option<PathBuf>,
65 pub linux_modules_deb: Option<PathBuf>,
66 pub linux_image: Option<PathBuf>,
67 pub linux_image_dbgsym: Option<PathBuf>,
68 pub systemmap: Option<PathBuf>,
69}
70
71impl UbuntuDownloader {
72 pub fn new(release: &str, revision: &str, variant: &str) -> Self {
73 let revision_short = match revision.split_once('.') {
90 Some((revision_short, _)) => revision_short,
91 None => revision,
92 };
93
94 let kernel_release = format!("{release}-{revision_short}-{variant}");
95 let kernel_version = format!("{release}-{revision}");
96 let subdirectory = format!("{kernel_version}-{variant}");
97
98 Self {
99 arch: DEFAULT_ARCH.into(),
100 dists: DEFAULT_DISTS.iter().map(ToString::to_string).collect(),
101 release: kernel_release,
102 version: kernel_version,
103 archive_url: DEFAULT_ARCHIVE_URL.try_into().unwrap(),
104 ddebs_url: DEFAULT_DDEBS_URL.try_into().unwrap(),
105 output_directory: None,
106 subdirectory,
107 skip_existing: false,
108 linux_image_deb: None,
109 linux_image_dbgsym_deb: None,
110 linux_modules_deb: None,
111 extract_linux_image: None,
112 extract_linux_image_dbgsym: None,
113 extract_systemmap: None,
114 }
115 }
116
117 pub fn from_banner(banner: &LinuxBanner) -> Result<Self, Error> {
118 match &banner.version_signature {
119 Some(LinuxVersionSignature::Ubuntu(UbuntuVersionSignature {
120 release,
121 revision,
122 kernel_flavour,
123 ..
124 })) => Ok(Self::new(release, revision, kernel_flavour)),
125 _ => Err(Error::InvalidBanner),
126 }
127 }
128
129 pub fn destination_path(&self) -> PathBuf {
130 match &self.output_directory {
131 Some(output_directory) => PathBuf::from(output_directory).join(&self.subdirectory),
132 None => PathBuf::from(&self.subdirectory),
133 }
134 }
135
136 pub fn with_arch(self, arch: impl Into<String>) -> Self {
137 Self {
138 arch: arch.into(),
139 ..self
140 }
141 }
142
143 pub fn with_dists(self, dists: impl IntoIterator<Item = impl Into<String>>) -> Self {
144 Self {
145 dists: dists.into_iter().map(Into::into).collect(),
146 ..self
147 }
148 }
149
150 pub fn with_archive_url(self, archive_url: Url) -> Self {
151 Self {
152 archive_url,
153 ..self
154 }
155 }
156
157 pub fn with_ddebs_url(self, ddebs_url: Url) -> Self {
158 Self { ddebs_url, ..self }
159 }
160
161 pub fn with_output_directory(self, directory: impl Into<PathBuf>) -> Self {
162 Self {
163 output_directory: Some(directory.into()),
164 ..self
165 }
166 }
167
168 pub fn skip_existing(self) -> Self {
169 Self {
170 skip_existing: true,
171 ..self
172 }
173 }
174
175 pub fn download_linux_image(self) -> Self {
176 Self {
177 linux_image_deb: Some(Filename::Original),
178 ..self
179 }
180 }
181
182 pub fn download_linux_image_as(self, filename: impl Into<PathBuf>) -> Self {
183 Self {
184 linux_image_deb: Some(Filename::Custom(filename.into())),
185 ..self
186 }
187 }
188
189 pub fn download_linux_image_dbgsym(self) -> Self {
190 Self {
191 linux_image_dbgsym_deb: Some(Filename::Original),
192 ..self
193 }
194 }
195
196 pub fn download_linux_image_dbgsym_as(self, filename: impl Into<PathBuf>) -> Self {
197 Self {
198 linux_image_dbgsym_deb: Some(Filename::Custom(filename.into())),
199 ..self
200 }
201 }
202
203 pub fn download_linux_modules(self) -> Self {
204 Self {
205 linux_modules_deb: Some(Filename::Original),
206 ..self
207 }
208 }
209
210 pub fn download_linux_modules_as(self, filename: impl Into<PathBuf>) -> Self {
211 Self {
212 linux_modules_deb: Some(Filename::Custom(filename.into())),
213 ..self
214 }
215 }
216
217 pub fn extract_linux_image(self) -> Self {
218 Self {
219 extract_linux_image: Some(Filename::Original),
220 ..self
221 }
222 }
223
224 pub fn extract_linux_image_as(self, filename: impl Into<PathBuf>) -> Self {
225 Self {
226 extract_linux_image: Some(Filename::Custom(filename.into())),
227 ..self
228 }
229 }
230
231 pub fn extract_linux_image_dbgsym(self) -> Self {
232 Self {
233 extract_linux_image_dbgsym: Some(Filename::Original),
234 ..self
235 }
236 }
237
238 pub fn extract_linux_image_dbgsym_as(self, filename: impl Into<PathBuf>) -> Self {
239 Self {
240 extract_linux_image_dbgsym: Some(Filename::Custom(filename.into())),
241 ..self
242 }
243 }
244
245 pub fn extract_systemmap(self) -> Self {
246 Self {
247 extract_systemmap: Some(Filename::Original),
248 ..self
249 }
250 }
251
252 pub fn extract_systemmap_as(self, filename: impl Into<PathBuf>) -> Self {
253 Self {
254 extract_systemmap: Some(Filename::Custom(filename.into())),
255 ..self
256 }
257 }
258
259 pub fn download(self) -> Result<UbuntuPaths, Error> {
260 if self.extract_linux_image.is_some() && self.linux_image_deb.is_none() {
265 tracing::error!("extract_linux_image requires download_linux_image");
266 return Err(Error::InvalidOptions);
267 }
268
269 if self.extract_linux_image_dbgsym.is_some() && self.linux_image_dbgsym_deb.is_none() {
270 tracing::error!("extract_linux_image_dbgsym requires download_linux_image_dbgsym");
271 return Err(Error::InvalidOptions);
272 }
273
274 if self.extract_systemmap.is_some() && self.linux_modules_deb.is_none() {
275 tracing::error!("extract_systemmap requires download_linux_modules");
276 return Err(Error::InvalidOptions);
277 }
278
279 if self.linux_image_deb.is_none()
280 && self.linux_image_dbgsym_deb.is_none()
281 && self.linux_modules_deb.is_none()
282 {
283 tracing::warn!("no download options specified");
284 return Err(Error::InvalidOptions);
285 }
286
287 let destination_path = self.destination_path();
288 std::fs::create_dir_all(&destination_path)?;
289
290 let mut result = UbuntuPaths {
291 output_directory: destination_path.clone(),
292 ..Default::default()
293 };
294
295 if self.linux_image_deb.is_some() || self.linux_modules_deb.is_some() {
296 let packages = UbuntuPackageCache::fetch(self.archive_url, &self.arch, &self.dists)?;
297
298 (result.linux_image_deb, result.linux_image) = find_and_download_and_extract(
299 &packages,
300 &self.release,
301 &self.version,
302 &destination_path,
303 self.skip_existing,
304 find_linux_image_url,
305 &format!("./boot/vmlinuz-{}", self.release),
306 self.linux_image_deb,
307 self.extract_linux_image,
308 )?;
309
310 (result.linux_modules_deb, result.systemmap) = find_and_download_and_extract(
311 &packages,
312 &self.release,
313 &self.version,
314 &destination_path,
315 self.skip_existing,
316 find_linux_modules_url,
317 &format!("./boot/System.map-{}", self.release),
318 self.linux_modules_deb,
319 self.extract_systemmap,
320 )?;
321 }
322
323 if self.linux_image_dbgsym_deb.is_some() {
324 let packages = UbuntuPackageCache::fetch(self.ddebs_url, &self.arch, &self.dists)?;
325
326 (result.linux_image_dbgsym_deb, result.linux_image_dbgsym) =
327 find_and_download_and_extract(
328 &packages,
329 &self.release,
330 &self.version,
331 &destination_path,
332 self.skip_existing,
333 find_linux_image_dbgsym_url,
334 &format!("./usr/lib/debug/boot/vmlinux-{}", self.release),
335 self.linux_image_dbgsym_deb,
336 self.extract_linux_image_dbgsym,
337 )?;
338 }
339
340 Ok(result)
341 }
342}
343
344#[expect(clippy::too_many_arguments)]
345fn find_and_download_and_extract(
346 packages: &UbuntuPackageCache,
347 release: &str,
348 version: &str,
349 output_directory: &Path,
350 skip_existing: bool,
351 find_package_fn: impl Fn(&UbuntuPackageCache, &str, &str) -> Result<Url, Error>,
352 deb_entry: &str,
353 deb_filename: Option<Filename>,
354 extract_filename: Option<Filename>,
355) -> Result<(Option<PathBuf>, Option<PathBuf>), Error> {
356 let deb_filename = match deb_filename {
357 Some(deb_filename) => deb_filename,
358 None => return Ok((None, None)),
359 };
360
361 let url = find_package_fn(packages, release, version)?;
362 let deb_path = path_from_url(&url, output_directory, deb_filename)?;
363
364 if !deb_path.exists() || !skip_existing {
365 download(url, &deb_path)?;
366 }
367 else {
368 tracing::info!(path = %deb_path.display(), "skipping download");
369 }
370
371 let extract_filename = match extract_filename {
372 Some(extract_filename) => extract_filename,
373 None => return Ok((Some(deb_path), None)),
374 };
375
376 let path = path_from_deb_entry(deb_entry, output_directory, extract_filename)?;
377
378 if !path.exists() || !skip_existing {
379 unpack_deb_entry(&deb_path, deb_entry, &path)?;
380 }
381 else {
382 tracing::info!(path = %path.display(), "skipping extraction");
383 }
384
385 Ok((Some(deb_path), Some(path)))
386}
387
388fn find_linux_image_url(
389 packages: &UbuntuPackageCache,
390 release: &str,
391 version: &str,
392) -> Result<Url, Error> {
393 let package = format!("linux-image-{release}");
394 if let Some(candidate) = packages.find_package(&package, version)? {
395 return packages.package_url(candidate);
396 }
397
398 let package = format!("linux-image-unsigned-{release}");
399 if let Some(candidate) = packages.find_package(&package, version)? {
400 return packages.package_url(candidate);
401 }
402
403 Err(Error::PackageNotFound)
404}
405
406fn find_linux_image_dbgsym_url(
407 packages: &UbuntuPackageCache,
408 release: &str,
409 version: &str,
410) -> Result<Url, Error> {
411 let package = format!("linux-image-{release}-dbgsym");
412 if let Some(candidate) = packages.find_dbgsym_package(&package, version)? {
413 return packages.package_url(candidate);
414 }
415
416 let package = format!("linux-image-unsigned-{release}-dbgsym");
417 if let Some(candidate) = packages.find_dbgsym_package(&package, version)? {
418 return packages.package_url(candidate);
419 }
420
421 Err(Error::PackageNotFound)
422}
423
424fn find_linux_modules_url(
425 packages: &UbuntuPackageCache,
426 release: &str,
427 version: &str,
428) -> Result<Url, Error> {
429 let package = format!("linux-modules-{release}");
430 if let Some(candidate) = packages.find_package(&package, version)? {
431 return packages.package_url(candidate);
432 }
433
434 Err(Error::PackageNotFound)
435}
436
437fn path_from_url(
438 url: &Url,
439 destination_directory: &Path,
440 filename: Filename,
441) -> Result<PathBuf, Error> {
442 fn extract_file_name_from_url(url: &Url) -> Option<String> {
443 url.path_segments()?.next_back().map(ToString::to_string)
444 }
445
446 match filename {
447 Filename::Original => match extract_file_name_from_url(url) {
448 Some(filename) => Ok(destination_directory.join(filename)),
449 None => {
450 tracing::error!("failed to extract filename from URL");
451 Err(Error::UrlDoesNotContainFilename)
452 }
453 },
454 Filename::Custom(path) => Ok(destination_directory.join(path)),
455 }
456}
457
458fn download(url: Url, destination_path: impl AsRef<Path>) -> Result<(), Error> {
459 let destination_path = destination_path.as_ref();
460
461 tracing::info!(%url, "downloading");
462 let mut response = reqwest::blocking::get(url)?.error_for_status()?;
463 let mut file = File::create(destination_path)?;
464 response.copy_to(&mut file)?;
465
466 Ok(())
467}
468
469fn path_from_deb_entry(
470 deb_entry_path: impl AsRef<Path>,
471 destination_directory: &Path,
472 filename: Filename,
473) -> Result<PathBuf, Error> {
474 match filename {
475 Filename::Original => match deb_entry_path.as_ref().file_name() {
476 Some(filename) => Ok(destination_directory.join(filename)),
477 None => {
478 tracing::error!("failed to extract filename from deb entry path");
479 Err(Error::UrlDoesNotContainFilename)
480 }
481 },
482 Filename::Custom(path) => Ok(destination_directory.join(path)),
483 }
484}
485
486fn unpack_deb_entry(
487 deb_path: impl AsRef<Path>,
488 deb_entry_path: impl AsRef<Path>,
489 destination_path: impl AsRef<Path>,
490) -> Result<(), Error> {
491 let deb_path = deb_path.as_ref();
492 let deb_entry_path = deb_entry_path.as_ref();
493 let destination_path = destination_path.as_ref();
494
495 let file = File::open(deb_path)?;
496 let mut pkg = DebPkg::parse(file)?;
497
498 let mut data = pkg.data()?;
499 for entry in data.entries()? {
500 let mut entry = entry?;
501
502 if entry.header().path()? == deb_entry_path {
503 tracing::info!(path = %deb_entry_path.display(), "unpacking");
504 entry.unpack(destination_path)?;
505 return Ok(());
506 }
507 }
508
509 Err(Error::DebEntryNotFound)
510}