1mod error;
73
74#[cfg(any(feature = "linux", feature = "windows"))]
75use std::cell::OnceCell;
76use std::{
77 fs::File,
78 io::{BufWriter, Write},
79 path::{Path, PathBuf},
80 sync::Arc,
81};
82
83pub use isr_core::Profile;
84pub use isr_dl::{ProgressContext, ProgressEvent, ProgressFn, ProgressWriter};
85#[cfg(feature = "linux")]
86pub use isr_dl_linux::{
87 ArtifactPolicy, FilenamePolicy, LinuxBanner, LinuxVersionSignature, UbuntuSymbolDownloader,
88 UbuntuSymbolPaths, UbuntuSymbolRequest, UbuntuVersionSignature,
89};
90#[cfg(feature = "windows")]
91pub use isr_dl_windows::{CodeView, ImageSignature, SymbolDownloader, SymbolRequest};
92use memmap2::Mmap;
93use rkyv::ser::{Serializer, allocator::Arena, writer::IoWriter};
94
95pub use self::error::Error;
96
97pub const PROFILE_FILE_EXTENSION: &str = "isr";
99
100pub struct Entry {
102 profile_path: PathBuf,
104
105 data: Mmap,
107}
108
109impl Entry {
110 pub fn new(profile_path: PathBuf) -> Result<Self, Error> {
112 let data = unsafe { Mmap::map(&File::open(&profile_path)?)? };
113 Ok(Self { profile_path, data })
114 }
115
116 pub fn profile_path(&self) -> &Path {
118 &self.profile_path
119 }
120
121 pub fn profile(&self) -> Result<Profile<'_>, Error> {
123 let archived = rkyv::access::<_, rkyv::rancor::Error>(&self.data)?;
124 Ok(Profile::from_archived(archived))
125 }
126
127 pub unsafe fn profile_unchecked(&self) -> Result<Profile<'_>, Error> {
134 let archived = unsafe { rkyv::access_unchecked(&self.data) };
135 Ok(Profile::from_archived(archived))
136 }
137
138 #[cfg(feature = "json")]
140 pub fn to_json(&self) -> Result<serde_json::Value, Error> {
141 let archived =
142 rkyv::access::<isr_core::schema::ArchivedProfile, rkyv::rancor::Error>(&self.data)?;
143 let deserialized =
144 rkyv::deserialize::<isr_core::schema::Profile, rkyv::rancor::Error>(archived)?;
145
146 Ok(serde_json::to_value(&deserialized)?)
147 }
148
149 #[allow(unused)]
151 fn encode(writer: impl Write, profile: &isr_core::schema::Profile) -> Result<(), Error> {
152 let writer = BufWriter::new(writer);
153 let mut writer = IoWriter::new(writer);
154 let mut arena = Arena::new();
155
156 let mut serializer = Serializer::new(&mut writer, arena.acquire(), ());
157 rkyv::api::serialize_using::<_, rkyv::rancor::Error>(profile, &mut serializer)?;
158
159 Ok(())
160 }
161}
162
163pub struct IsrCache {
167 #[cfg(feature = "linux")]
168 ubuntu_downloader: OnceCell<UbuntuSymbolDownloader>,
169
170 #[cfg(feature = "windows")]
171 symbol_downloader: OnceCell<SymbolDownloader>,
172
173 #[allow(unused)]
175 output_directory: PathBuf,
176
177 #[allow(unused)]
179 progress: Option<ProgressFn>,
180
181 #[allow(unused)]
184 offline: bool,
185}
186
187impl IsrCache {
188 pub fn new(output_directory: impl Into<PathBuf>) -> Result<Self, Error> {
191 let output_directory = output_directory.into();
192 std::fs::create_dir_all(&output_directory)?;
193
194 Ok(Self {
195 #[cfg(feature = "linux")]
196 ubuntu_downloader: OnceCell::new(),
197 #[cfg(feature = "windows")]
198 symbol_downloader: OnceCell::new(),
199
200 output_directory,
201 progress: None,
202 offline: false,
203 })
204 }
205
206 pub fn with_progress(self, f: impl Fn(ProgressEvent<'_>) + Send + Sync + 'static) -> Self {
208 Self {
209 progress: Some(Arc::new(f)),
210 ..self
211 }
212 }
213
214 pub fn with_offline(self, offline: bool) -> Self {
219 Self { offline, ..self }
220 }
221
222 #[cfg(feature = "linux")]
224 pub fn with_ubuntu_downloader(self, ubuntu_downloader: UbuntuSymbolDownloader) -> Self {
225 Self {
226 ubuntu_downloader: OnceCell::from(ubuntu_downloader),
227 ..self
228 }
229 }
230
231 #[cfg(feature = "linux")]
233 pub fn ubuntu_downloader(&self) -> &UbuntuSymbolDownloader {
234 self.ubuntu_downloader.get_or_init(|| {
235 UbuntuSymbolDownloader::builder()
236 .output_directory(self.output_directory.join("ubuntu"))
237 .maybe_progress(self.progress.clone())
238 .build()
239 })
240 }
241
242 #[cfg(feature = "windows")]
244 pub fn with_symbol_downloader(self, symbol_downloader: SymbolDownloader) -> Self {
245 Self {
246 symbol_downloader: OnceCell::from(symbol_downloader),
247 ..self
248 }
249 }
250
251 #[cfg(feature = "windows")]
253 pub fn symbol_downloader(&self) -> &SymbolDownloader {
254 self.symbol_downloader.get_or_init(|| {
255 SymbolDownloader::builder()
256 .output_directory(self.output_directory.join("windows"))
257 .maybe_progress(self.progress.clone())
258 .build()
259 })
260 }
261
262 #[cfg(feature = "linux")]
268 pub fn entry_from_linux_banner(&self, linux_banner: &str) -> Result<Entry, Error> {
269 let banner = linux_banner
270 .parse::<LinuxBanner>()
271 .map_err(isr_dl::Error::from)?;
272
273 let output_paths = match banner.version_signature {
274 Some(LinuxVersionSignature::Ubuntu(version_signature)) => {
275 self.download_from_ubuntu_version_signature(version_signature)?
276 }
277 _ => {
278 return Err(Error::Downloader(isr_dl::Error::Other(Box::new(
280 isr_dl_linux::DownloaderError::InvalidBanner,
281 ))));
282 }
283 };
284
285 let output_directory = output_paths.output_directory;
286 let profile_path = output_directory
287 .join("profile")
288 .with_extension(PROFILE_FILE_EXTENSION);
289
290 with_part_file(profile_path, |profile_file| {
291 let kernel_file = File::open(output_directory.join("vmlinux-dbgsym"))?;
292 let systemmap_file = File::open(output_directory.join("System.map"))?;
293 isr_dwarf::create_profile(kernel_file, systemmap_file, |profile| {
294 Entry::encode(profile_file, profile)
295 })?;
296
297 Ok(())
298 })
299 }
300
301 #[cfg(feature = "linux")]
307 pub fn download_from_ubuntu_version_signature(
308 &self,
309 version_signature: UbuntuVersionSignature,
310 ) -> Result<UbuntuSymbolPaths, Error> {
311 let request = UbuntuSymbolRequest::builder()
312 .version_signature(version_signature)
313 .linux_image(
314 ArtifactPolicy::builder()
315 .deb(FilenamePolicy::custom("linux-image.deb"))
317 .extract(FilenamePolicy::custom("vmlinuz"))
319 .build(),
320 )
321 .linux_image_dbgsym(
322 ArtifactPolicy::builder()
323 .deb(FilenamePolicy::custom("linux-image-dbgsym.deb"))
325 .extract(FilenamePolicy::custom("vmlinux-dbgsym"))
327 .build(),
328 )
329 .linux_modules(
330 ArtifactPolicy::builder()
331 .deb(FilenamePolicy::custom("linux-modules.deb"))
333 .extract(FilenamePolicy::custom("System.map"))
335 .build(),
336 )
337 .build();
338
339 if let Some(paths) = self.ubuntu_downloader().lookup(&request) {
340 return Ok(paths);
341 }
342
343 if self.offline {
344 return Err(Error::Downloader(isr_dl::Error::ArtifactNotFound));
345 }
346
347 let paths = self.ubuntu_downloader().download(request)?;
348
349 Ok(paths)
350 }
351
352 #[cfg(feature = "windows")]
360 pub fn entry_from_codeview(&self, codeview: CodeView) -> Result<Entry, Error> {
361 let output_directory = self
363 .output_directory
364 .join("windows")
365 .join(codeview.subdirectory());
366
367 let pdb_path = self.download_from_codeview(codeview)?;
369
370 let profile_path = output_directory
372 .join("profile")
373 .with_extension(PROFILE_FILE_EXTENSION);
374
375 with_part_file(profile_path, |profile_file| {
376 let pdb_file = File::open(&pdb_path)?;
377 isr_pdb::create_profile(pdb_file, |profile| Entry::encode(profile_file, profile))?;
378
379 Ok(())
380 })
381 }
382
383 #[cfg(feature = "windows")]
390 pub fn entry_from_pe(&self, path: impl AsRef<Path>) -> Result<Entry, Error> {
391 self.entry_from_codeview(CodeView::from_path(path).map_err(isr_dl::Error::from)?)
392 }
393
394 #[cfg(feature = "windows")]
396 pub fn download_from_codeview(&self, codeview: CodeView) -> Result<PathBuf, Error> {
397 let request = codeview.into();
398
399 if let Some(pdb_path) = self.symbol_downloader().lookup(&request) {
400 tracing::debug!(path = %pdb_path.display(), "found cached PE image");
401 return Ok(pdb_path);
402 }
403
404 if self.offline {
405 return Err(Error::Downloader(isr_dl::Error::ArtifactNotFound));
406 }
407
408 let pdb_path = self.symbol_downloader().download(request)?;
410
411 Ok(pdb_path)
412 }
413
414 #[cfg(feature = "windows")]
421 pub fn download_from_image_signature(
422 &self,
423 image_signature: ImageSignature,
424 ) -> Result<PathBuf, Error> {
425 let request = image_signature.into();
426
427 if let Some(image_path) = self.symbol_downloader().lookup(&request) {
428 tracing::debug!(path = %image_path.display(), "found cached PE image");
429 return Ok(image_path);
430 }
431
432 if self.offline {
433 return Err(Error::Downloader(isr_dl::Error::ArtifactNotFound));
434 }
435
436 let image_path = self.symbol_downloader().download(request)?;
438
439 Ok(image_path)
440 }
441}
442
443#[allow(unused)]
445fn with_part_file<F>(profile_path: PathBuf, f: F) -> Result<Entry, Error>
446where
447 F: FnOnce(File) -> Result<(), Error>,
448{
449 if profile_path.exists() {
450 tracing::debug!(
451 profile_path = %profile_path.display(),
452 "profile already exists"
453 );
454
455 return Entry::new(profile_path);
456 }
457
458 let tmp = profile_path.with_added_extension("part");
459 let file = File::create(&tmp)?;
460 f(file)?;
461 std::fs::rename(&tmp, &profile_path)?;
462
463 Entry::new(profile_path)
464}