1use std::path::PathBuf;
14
15use url::Url;
16
17use super::{
18 error::UbuntuError,
19 index::{PackageIndex, PackageQuery},
20};
21use crate::UbuntuVersionSignature;
22
23#[derive(Debug, Clone)]
25pub struct ArtifactRef {
26 pub deb_url: Url,
28
29 pub deb_filename: String,
31
32 pub extract_path: PathBuf,
34}
35
36#[derive(Debug, Default, Clone)]
38pub struct KernelArtifacts {
39 pub linux_image: Option<ArtifactRef>,
41
42 pub linux_image_dbgsym: Option<ArtifactRef>,
44
45 pub linux_modules: Option<ArtifactRef>,
47}
48
49impl KernelArtifacts {
50 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 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}