Skip to main content

isr_dl_linux/ubuntu/
artifacts.rs

1//! Resolution of Ubuntu kernel package names + URLs from a version signature.
2//!
3//! This module is the only place that knows Ubuntu's kernel-naming conventions:
4//! - `linux-image-{release}` (with `linux-image-unsigned-` fallback)
5//! - `linux-image-{release}-dbgsym` (with `linux-image-unsigned-` fallback)
6//! - `linux-modules-{release}`
7//!
8//! And the deb-internal extraction paths:
9//! - `./boot/vmlinuz-{release}`
10//! - `./usr/lib/debug/boot/vmlinux-{release}`
11//! - `./boot/System.map-{release}`
12
13use std::path::PathBuf;
14
15use url::Url;
16
17use super::{
18    error::UbuntuError,
19    index::{PackageIndex, PackageQuery},
20};
21use crate::UbuntuVersionSignature;
22
23/// A reference to a single downloadable artifact (one .deb).
24#[derive(Debug, Clone)]
25pub struct ArtifactRef {
26    /// Full URL of the .deb file on its repository host.
27    pub deb_url: Url,
28
29    /// Canonical filename of the .deb (the basename portion of the URL).
30    pub deb_filename: String,
31
32    /// Path inside the .deb to the file we care about extracting.
33    pub extract_path: PathBuf,
34}
35
36/// Resolved kernel artifacts for one version signature.
37#[derive(Debug, Default, Clone)]
38pub struct KernelArtifacts {
39    /// The `linux-image-*` kernel package.
40    pub linux_image: Option<ArtifactRef>,
41
42    /// The `linux-image-*-dbgsym` debug symbols package.
43    pub linux_image_dbgsym: Option<ArtifactRef>,
44
45    /// The `linux-modules-*` kernel modules package.
46    pub linux_modules: Option<ArtifactRef>,
47}
48
49impl KernelArtifacts {
50    /// Resolves the three kernel artifacts for a version signature against the
51    /// provided indices. An artifact is `None` if its package is not found in
52    /// any index. Returns an error only on internal lookup failures (e.g.
53    /// multiple candidates within one index).
54    pub fn resolve(
55        version: &UbuntuVersionSignature,
56        indices: &[PackageIndex],
57    ) -> Result<Self, UbuntuError> {
58        let release = version.kernel_release();
59        let kernel_version = version.kernel_version();
60
61        Ok(Self {
62            linux_image: lookup(
63                indices,
64                &PackageQuery {
65                    package: format!("linux-image-{release}"),
66                    version: kernel_version.clone(),
67                    dbgsym: false,
68                    unsigned_fallback: true,
69                },
70                PathBuf::from(format!("./boot/vmlinuz-{release}")),
71            )?,
72            linux_image_dbgsym: lookup(
73                indices,
74                &PackageQuery {
75                    package: format!("linux-image-{release}-dbgsym"),
76                    version: kernel_version.clone(),
77                    dbgsym: true,
78                    unsigned_fallback: true,
79                },
80                PathBuf::from(format!("./usr/lib/debug/boot/vmlinux-{release}")),
81            )?,
82            linux_modules: lookup(
83                indices,
84                &PackageQuery {
85                    package: format!("linux-modules-{release}"),
86                    version: kernel_version.clone(),
87                    dbgsym: false,
88                    unsigned_fallback: false,
89                },
90                PathBuf::from(format!("./boot/System.map-{release}")),
91            )?,
92        })
93    }
94}
95
96fn lookup(
97    indices: &[PackageIndex],
98    query: &PackageQuery,
99    extract_path: PathBuf,
100) -> Result<Option<ArtifactRef>, UbuntuError> {
101    for index in indices {
102        if let Some(entry) = index.find(query)? {
103            let deb_url = index.resolve_url(entry)?;
104            let deb_filename =
105                filename_from_url(&deb_url).ok_or(UbuntuError::UrlMissingFilename)?;
106            return Ok(Some(ArtifactRef {
107                deb_url,
108                deb_filename,
109                extract_path,
110            }));
111        }
112    }
113    Ok(None)
114}
115
116fn filename_from_url(url: &Url) -> Option<String> {
117    url.path_segments()?.next_back().map(ToString::to_string)
118}
119
120#[cfg(test)]
121mod tests {
122    use indexmap::IndexMap;
123
124    use super::*;
125    use crate::ubuntu::parse::UbuntuRepositoryEntry;
126
127    fn entry(package: &str, version: &str, filename: &str) -> UbuntuRepositoryEntry {
128        UbuntuRepositoryEntry {
129            package: Some(package.into()),
130            version: Some(version.into()),
131            filename: Some(filename.into()),
132            ..Default::default()
133        }
134    }
135
136    fn index_with(host: &str, dist: &str, entries: Vec<UbuntuRepositoryEntry>) -> PackageIndex {
137        let mut by_dist = IndexMap::new();
138        let mut map = IndexMap::new();
139        for entry in entries {
140            map.insert(entry.package.clone().unwrap(), entry);
141        }
142        by_dist.insert(dist.into(), map);
143        PackageIndex::new(host.try_into().unwrap(), by_dist)
144    }
145
146    fn signature() -> UbuntuVersionSignature {
147        UbuntuVersionSignature {
148            release: "6.8.0".into(),
149            revision: "40.40~22.04.3".into(),
150            kernel_flavour: "generic".into(),
151            mainline_kernel_version: "6.8.12".into(),
152        }
153    }
154
155    #[test]
156    fn resolves_signed_image() {
157        // Package name uses the {release}-{revision_short}-{flavour} form
158        // = "6.8.0-40-generic". Version uses {release}-{revision}
159        // = "6.8.0-40.40~22.04.3".
160        let archive = index_with(
161            "http://archive.ubuntu.com/ubuntu/",
162            "noble",
163            vec![entry(
164                "linux-image-6.8.0-40-generic",
165                "6.8.0-40.40~22.04.3",
166                "pool/main/l/linux/linux-image-6.8.0-40-generic_6.8.0-40.40~22.04.3_amd64.deb",
167            )],
168        );
169        let artifacts = KernelArtifacts::resolve(&signature(), &[archive]).unwrap();
170        let img = artifacts.linux_image.expect("linux_image");
171        assert_eq!(
172            img.deb_filename,
173            "linux-image-6.8.0-40-generic_6.8.0-40.40~22.04.3_amd64.deb"
174        );
175        assert_eq!(
176            img.extract_path.to_str().unwrap(),
177            "./boot/vmlinuz-6.8.0-40-generic"
178        );
179    }
180
181    #[test]
182    fn resolves_unsigned_when_signed_missing() {
183        let archive = index_with(
184            "http://archive.ubuntu.com/ubuntu/",
185            "noble",
186            vec![entry(
187                "linux-image-unsigned-6.8.0-40-generic",
188                "6.8.0-40.40~22.04.3",
189                "pool/main/l/linux/linux-image-unsigned-6.8.0-40-generic_6.8.0-40.40~22.04.3_amd64.deb",
190            )],
191        );
192        let artifacts = KernelArtifacts::resolve(&signature(), &[archive]).unwrap();
193        let img = artifacts.linux_image.expect("linux_image");
194        assert_eq!(
195            img.deb_filename,
196            "linux-image-unsigned-6.8.0-40-generic_6.8.0-40.40~22.04.3_amd64.deb"
197        );
198        assert_eq!(
199            img.extract_path.to_str().unwrap(),
200            "./boot/vmlinuz-6.8.0-40-generic"
201        );
202    }
203
204    #[test]
205    fn dbgsym_resolves_against_ddebs_when_archive_missing() {
206        let archive = index_with("http://archive.ubuntu.com/ubuntu/", "noble", vec![]);
207        let ddebs = index_with(
208            "http://ddebs.ubuntu.com/",
209            "noble",
210            vec![entry(
211                "linux-image-unsigned-6.8.0-40-generic-dbgsym",
212                "6.8.0-40.40~22.04.3",
213                "pool/main/l/linux/linux-image-unsigned-6.8.0-40-generic-dbgsym_6.8.0-40.40~22.04.3_amd64.ddeb",
214            )],
215        );
216        let artifacts = KernelArtifacts::resolve(&signature(), &[archive, ddebs]).unwrap();
217        let dbgsym = artifacts.linux_image_dbgsym.expect("linux_image_dbgsym");
218        assert!(
219            dbgsym
220                .deb_url
221                .as_str()
222                .starts_with("http://ddebs.ubuntu.com/")
223        );
224        assert!(dbgsym.deb_filename.ends_with(".ddeb"));
225        assert_eq!(
226            dbgsym.extract_path.to_str().unwrap(),
227            "./usr/lib/debug/boot/vmlinux-6.8.0-40-generic"
228        );
229    }
230
231    #[test]
232    fn missing_modules_yields_none_not_error() {
233        let archive = index_with("http://archive.ubuntu.com/ubuntu/", "noble", vec![]);
234        let artifacts = KernelArtifacts::resolve(&signature(), &[archive]).unwrap();
235        assert!(artifacts.linux_modules.is_none());
236    }
237}