1mod artifacts;
4mod error;
5mod fetcher;
6mod index;
7mod parse;
8mod repository;
9mod request;
10
11use std::{
12 cell::OnceCell,
13 path::{Path, PathBuf},
14 time::Duration,
15};
16
17use bon::Builder;
18use isr_dl::{Error, ProgressFn};
19use reqwest::blocking::Client;
20use url::Url;
21
22pub use self::{
23 artifacts::{ArtifactRef, KernelArtifacts},
24 error::UbuntuError,
25 index::{PackageIndex, PackageQuery},
26 parse::UbuntuRepositoryEntry,
27 request::{
28 ArtifactPaths, ArtifactPolicy, FilenamePolicy, UbuntuSymbolPaths, UbuntuSymbolRequest,
29 },
30};
31use self::{fetcher::Fetcher, repository::Repository};
32use crate::{DownloaderError, UbuntuVersionSignature};
33
34pub const DEFAULT_ARCHIVE_URL: &str = "https://archive.ubuntu.com/ubuntu/";
36
37pub const DEFAULT_DDEBS_URL: &str = "https://ddebs.ubuntu.com/";
39
40pub const DEFAULT_ARCH: &str = "amd64";
42
43pub const DEFAULT_DISTS: &[&str] = &[
45 "trusty", "xenial", "bionic", "focal", "focal-updates", "jammy", "jammy-updates", "noble", "noble-updates", "resolute", ];
56
57#[derive(Builder)]
59pub struct UbuntuSymbolDownloader {
60 #[builder(field)]
61 indices: OnceCell<Vec<PackageIndex>>,
62
63 #[builder(default)]
64 client: Client,
65
66 #[builder(into, default = DEFAULT_ARCH)]
67 arch: String,
68
69 #[builder(
70 default = DEFAULT_DISTS.iter().map(ToString::to_string).collect(),
71 with = |iter: impl IntoIterator<Item = impl Into<String>>| {
72 iter.into_iter().map(Into::into).collect()
73 }
74 )]
75 dists: Vec<String>,
76
77 #[builder(
78 default = vec![
79 DEFAULT_ARCHIVE_URL.try_into().unwrap(),
80 DEFAULT_DDEBS_URL.try_into().unwrap(),
81 ],
82 with = |iter: impl IntoIterator<Item = impl Into<Url>>| {
83 iter.into_iter().map(Into::into).collect()
84 }
85 )]
86 repository_hosts: Vec<Url>,
87
88 #[builder(into)]
89 output_directory: PathBuf,
90
91 progress: Option<ProgressFn>,
92
93 #[builder(default = Duration::from_secs(24 * 3600))]
96 index_max_age: Duration,
97}
98
99impl UbuntuSymbolDownloader {
100 pub fn lookup(&self, request: &UbuntuSymbolRequest) -> Option<UbuntuSymbolPaths> {
103 let indices = self.load_cached_indices();
104 let artifacts = KernelArtifacts::resolve(&request.version_signature, &indices).ok()?;
105 let version_dir = self.version_dir(&request.version_signature);
106
107 Some(UbuntuSymbolPaths {
108 output_directory: version_dir.clone(),
109 linux_image: lookup_artifact(
110 artifacts.linux_image.as_ref(),
111 request.linux_image.as_ref(),
112 &version_dir,
113 )?,
114 linux_image_dbgsym: lookup_artifact(
115 artifacts.linux_image_dbgsym.as_ref(),
116 request.linux_image_dbgsym.as_ref(),
117 &version_dir,
118 )?,
119 linux_modules: lookup_artifact(
120 artifacts.linux_modules.as_ref(),
121 request.linux_modules.as_ref(),
122 &version_dir,
123 )?,
124 })
125 }
126
127 pub fn download(&self, request: UbuntuSymbolRequest) -> Result<UbuntuSymbolPaths, Error> {
130 self.download_inner(request)
131 .map_err(|err| Error::Other(Box::new(DownloaderError::Ubuntu(err))))
132 }
133
134 fn download_inner(
135 &self,
136 request: UbuntuSymbolRequest,
137 ) -> Result<UbuntuSymbolPaths, UbuntuError> {
138 let indices = self.fetch_indices()?;
139 let artifacts = KernelArtifacts::resolve(&request.version_signature, indices)?;
140
141 let version_dir = self.version_dir(&request.version_signature);
142 std::fs::create_dir_all(&version_dir)?;
143
144 let fetcher = Fetcher::new(&self.client, self.progress.as_ref());
145
146 let linux_image = fetch_artifact(
147 &fetcher,
148 artifacts.linux_image.as_ref(),
149 request.linux_image.as_ref(),
150 &version_dir,
151 )?;
152 let linux_image_dbgsym = fetch_artifact(
153 &fetcher,
154 artifacts.linux_image_dbgsym.as_ref(),
155 request.linux_image_dbgsym.as_ref(),
156 &version_dir,
157 )?;
158 let linux_modules = fetch_artifact(
159 &fetcher,
160 artifacts.linux_modules.as_ref(),
161 request.linux_modules.as_ref(),
162 &version_dir,
163 )?;
164
165 Ok(UbuntuSymbolPaths {
166 output_directory: version_dir,
167 linux_image,
168 linux_image_dbgsym,
169 linux_modules,
170 })
171 }
172
173 fn fetch_indices(&self) -> Result<&[PackageIndex], UbuntuError> {
174 let indices = match self.indices.get() {
176 Some(indices) => indices,
177 None => {
178 let mut indices = Vec::with_capacity(self.repository_hosts.len());
179 let mut last_error = None;
180
181 for host in &self.repository_hosts {
182 let repo = Repository::new(
183 self.client.clone(),
184 host.clone(),
185 self.arch.clone(),
186 self.dists.clone(),
187 );
188
189 let index = match repo.fetch_index(
190 &self.index_dir(),
191 self.index_max_age,
192 self.progress.clone(),
193 ) {
194 Ok(index) => index,
195 Err(err) => {
196 tracing::warn!(%err, %host, "failed to fetch index, skipping");
197 last_error = Some(err);
198 continue;
199 }
200 };
201
202 indices.push(index);
203 }
204
205 if indices.is_empty() {
206 return Err(last_error.unwrap_or(UbuntuError::PackageNotFound));
207 }
208
209 self.indices.get_or_init(|| indices)
210 }
211 };
212
213 Ok(indices.as_slice())
214 }
215
216 fn load_cached_indices(&self) -> Vec<PackageIndex> {
217 let mut indices = Vec::with_capacity(self.repository_hosts.len());
218 for host in &self.repository_hosts {
219 let repo = Repository::new(
220 self.client.clone(),
221 host.clone(),
222 self.arch.clone(),
223 self.dists.clone(),
224 );
225
226 let index = match repo.load_cached_index(&self.index_dir(), self.progress.clone()) {
227 Ok(index) => index,
228 Err(err) => {
229 tracing::debug!(%host, %err, "cached index unavailable, skipping");
230 continue;
231 }
232 };
233
234 indices.push(index);
235 }
236
237 indices
238 }
239
240 fn version_dir(&self, signature: &UbuntuVersionSignature) -> PathBuf {
241 self.output_directory.join(signature.subdirectory())
242 }
243
244 fn index_dir(&self) -> PathBuf {
245 self.output_directory.join("_index")
246 }
247}
248
249fn lookup_artifact(
252 artifact: Option<&ArtifactRef>,
253 policy: Option<&ArtifactPolicy>,
254 version_dir: &Path,
255) -> Option<Option<ArtifactPaths>> {
256 let policy = match policy {
257 Some(policy) => policy,
258 None => return Some(None),
259 };
260 let artifact = artifact?;
261
262 let deb_path = resolve_filename(&policy.deb, &artifact.deb_filename, version_dir);
263 if !deb_path.exists() {
264 return None;
265 }
266
267 let extracted = match &policy.extract {
268 Some(extracted) => {
269 let basename = artifact
270 .extract_path
271 .file_name()
272 .and_then(|filename| filename.to_str())?;
273 let path = resolve_filename(extracted, basename, version_dir);
274
275 if !path.exists() {
276 return None;
277 }
278
279 Some(path)
280 }
281 None => None,
282 };
283
284 Some(Some(ArtifactPaths {
285 deb: deb_path,
286 extracted,
287 }))
288}
289
290fn fetch_artifact(
292 fetcher: &Fetcher<'_>,
293 artifact: Option<&ArtifactRef>,
294 policy: Option<&ArtifactPolicy>,
295 version_dir: &Path,
296) -> Result<Option<ArtifactPaths>, UbuntuError> {
297 let policy = match policy {
298 Some(policy) => policy,
299 None => return Ok(None),
300 };
301 let artifact = artifact.ok_or(UbuntuError::PackageNotFound)?;
302
303 let deb_path = resolve_filename(&policy.deb, &artifact.deb_filename, version_dir);
304 fetcher.fetch_deb(artifact, &deb_path)?;
305
306 let extracted = match &policy.extract {
307 Some(extracted) => {
308 let basename = artifact
309 .extract_path
310 .file_name()
311 .and_then(|filename| filename.to_str())
312 .ok_or(UbuntuError::UrlMissingFilename)?;
313 let dest = resolve_filename(extracted, basename, version_dir);
314 fetcher.extract_deb_entry(&deb_path, &artifact.extract_path, &dest)?;
315 Some(dest)
316 }
317 None => None,
318 };
319
320 Ok(Some(ArtifactPaths {
321 deb: deb_path,
322 extracted,
323 }))
324}
325
326fn resolve_filename(policy: &FilenamePolicy, original: &str, version_dir: &Path) -> PathBuf {
329 match policy {
330 FilenamePolicy::Original => version_dir.join(original),
331 FilenamePolicy::Custom(custom) => version_dir.join(custom),
332 }
333}