1#![deny(unsafe_code)]
2#![warn(missing_docs)]
3use std::path::Path;
6
7#[derive(Debug, thiserror::Error)]
9pub enum Error {
10 #[error("I/O error: {0}")]
12 Io(#[from] std::io::Error),
13
14 #[error("unknown dump format")]
16 UnknownFormat,
17
18 #[error("ambiguous format: multiple plugins scored >= 50")]
20 AmbiguousFormat,
21
22 #[error("corrupt dump: {0}")]
24 Corrupt(String),
25
26 #[error("decompression error: {0}")]
28 Decompression(String),
29}
30
31pub type Result<T> = std::result::Result<T, Error>;
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct PhysicalRange {
37 pub start: u64,
39 pub end: u64,
41}
42
43impl PhysicalRange {
44 #[must_use]
46 pub fn len(&self) -> u64 {
47 self.end.saturating_sub(self.start)
48 }
49
50 #[must_use]
52 pub fn is_empty(&self) -> bool {
53 self.len() == 0
54 }
55
56 #[must_use]
58 pub fn contains_addr(&self, addr: u64) -> bool {
59 addr >= self.start && addr < self.end
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum MachineType {
66 Amd64,
68 I386,
70 Aarch64,
72}
73
74#[derive(Debug, Clone, Default)]
81pub struct DumpMetadata {
82 pub cr3: Option<u64>,
84 pub machine_type: Option<MachineType>,
86 pub os_version: Option<(u32, u32)>,
88 pub num_processors: Option<u32>,
90 pub ps_active_process_head: Option<u64>,
92 pub ps_loaded_module_list: Option<u64>,
94 pub kd_debugger_data_block: Option<u64>,
96 pub system_time: Option<u64>,
98 pub dump_type: Option<String>,
100}
101
102pub trait PhysicalMemoryProvider: Send + Sync {
104 fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize>;
107
108 fn ranges(&self) -> &[PhysicalRange];
110
111 fn total_size(&self) -> u64 {
113 self.ranges().iter().map(PhysicalRange::len).sum()
114 }
115
116 fn format_name(&self) -> &str;
118
119 fn metadata(&self) -> Option<DumpMetadata> {
122 None
123 }
124}
125
126impl PhysicalMemoryProvider for Box<dyn PhysicalMemoryProvider> {
127 fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
128 (**self).read_phys(addr, buf)
129 }
130
131 fn ranges(&self) -> &[PhysicalRange] {
132 (**self).ranges()
133 }
134
135 fn total_size(&self) -> u64 {
136 (**self).total_size()
137 }
138
139 fn format_name(&self) -> &str {
140 (**self).format_name()
141 }
142
143 fn metadata(&self) -> Option<DumpMetadata> {
144 (**self).metadata()
145 }
146}
147
148impl PhysicalMemoryProvider for std::sync::Arc<dyn PhysicalMemoryProvider> {
149 fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
150 (**self).read_phys(addr, buf)
151 }
152
153 fn ranges(&self) -> &[PhysicalRange] {
154 (**self).ranges()
155 }
156
157 fn total_size(&self) -> u64 {
158 (**self).total_size()
159 }
160
161 fn format_name(&self) -> &str {
162 (**self).format_name()
163 }
164
165 fn metadata(&self) -> Option<DumpMetadata> {
166 (**self).metadata()
167 }
168}
169
170pub trait FormatPlugin: Send + Sync {
172 fn name(&self) -> &str;
174
175 fn probe(&self, header: &[u8]) -> u8;
177
178 fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>>;
180}
181
182inventory::collect!(&'static dyn FormatPlugin);
183
184pub fn open_dump(path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
190 open_dump_inner(path, 20)
191}
192
193pub fn open_dump_with_raw_fallback(path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
200 open_dump_inner(path, 1)
201}
202
203fn open_dump_inner(path: &Path, min_fallback_score: u8) -> Result<Box<dyn PhysicalMemoryProvider>> {
204 use std::io::Read as _;
205 let mut file = std::fs::File::open(path)?;
206 let mut header = [0u8; 4096];
207 let n = file.read(&mut header)?;
208 let header = &header[..n];
209
210 let mut best: Option<(&dyn FormatPlugin, u8)> = None;
211 let mut ambiguous = false;
212
213 for plugin in inventory::iter::<&dyn FormatPlugin> {
214 let score = plugin.probe(header);
215 if score >= 80 {
216 return plugin.open(path);
217 }
218 if score >= 50 {
219 if let Some((_, prev_score)) = best {
220 if score >= prev_score {
221 if score == prev_score {
222 ambiguous = true;
223 } else {
224 ambiguous = false;
225 best = Some((*plugin, score));
226 }
227 }
228 } else {
229 best = Some((*plugin, score));
230 }
231 } else if score >= min_fallback_score && best.is_none() {
232 best = Some((*plugin, score));
233 }
234 }
235
236 if ambiguous {
237 return Err(Error::AmbiguousFormat);
238 }
239
240 match best {
241 Some((plugin, _)) => plugin.open(path),
242 None => Err(Error::UnknownFormat),
243 }
244}
245
246pub mod avml;
247pub mod elf_core;
248pub mod hiberfil;
249pub mod kdump;
250pub mod lime;
251pub mod raw;
252pub mod test_builders;
253pub mod vmware;
254pub mod win_crashdump;
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn physical_range_len() {
262 let r = PhysicalRange {
263 start: 0x1000,
264 end: 0x2000,
265 };
266 assert_eq!(r.len(), 0x1000);
267 }
268
269 #[test]
270 fn physical_range_empty() {
271 let r = PhysicalRange {
272 start: 0x1000,
273 end: 0x1000,
274 };
275 assert!(r.is_empty());
276 }
277
278 #[test]
279 fn physical_range_contains() {
280 let r = PhysicalRange {
281 start: 0x1000,
282 end: 0x2000,
283 };
284 assert!(r.contains_addr(0x1000));
285 assert!(r.contains_addr(0x1FFF));
286 assert!(!r.contains_addr(0x2000));
287 assert!(!r.contains_addr(0x0FFF));
288 }
289
290 #[test]
291 fn open_dump_lime() {
292 use crate::test_builders::LimeBuilder;
293 let dump = LimeBuilder::new().add_range(0, &[0xAA; 128]).build();
294 let dir = std::env::temp_dir().join("memf_test_lime");
295 std::fs::write(&dir, &dump).unwrap();
296 let provider = open_dump(&dir).unwrap();
297 assert_eq!(provider.format_name(), "LiME");
298 assert_eq!(provider.total_size(), 128);
299 std::fs::remove_file(&dir).ok();
300 }
301
302 #[test]
303 fn open_dump_avml() {
304 use crate::test_builders::AvmlBuilder;
305 let dump = AvmlBuilder::new().add_range(0, &[0xBB; 128]).build();
306 let dir = std::env::temp_dir().join("memf_test_avml");
307 std::fs::write(&dir, &dump).unwrap();
308 let provider = open_dump(&dir).unwrap();
309 assert_eq!(provider.format_name(), "AVML v2");
310 assert_eq!(provider.total_size(), 128);
311 std::fs::remove_file(&dir).ok();
312 }
313
314 #[test]
315 fn open_dump_unknown_is_error() {
316 let data = vec![0x00; 1024];
317 let dir = std::env::temp_dir().join("memf_test_raw");
318 std::fs::write(&dir, &data).unwrap();
319 let result = open_dump(&dir);
321 assert!(result.is_err());
322 std::fs::remove_file(&dir).ok();
323 }
324
325 #[test]
326 fn physical_range_zero_length() {
327 let r = PhysicalRange {
328 start: 0x5000,
329 end: 0x5000,
330 };
331 assert_eq!(r.len(), 0);
332 assert!(r.is_empty());
333 assert!(!r.contains_addr(0x5000));
334 }
335
336 #[test]
337 fn physical_range_saturating_sub() {
338 let r = PhysicalRange {
340 start: 0x2000,
341 end: 0x1000,
342 };
343 assert_eq!(r.len(), 0);
344 assert!(r.is_empty());
345 }
346
347 #[test]
348 fn error_io_from_impl() {
349 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
350 let err: Error = Error::from(io_err);
351 assert!(matches!(err, Error::Io(_)));
352 assert!(err.to_string().contains("file not found"));
353 }
354
355 #[test]
356 fn error_unknown_format_display() {
357 let err = Error::UnknownFormat;
358 assert_eq!(err.to_string(), "unknown dump format");
359 }
360
361 #[test]
362 fn error_ambiguous_format_display() {
363 let err = Error::AmbiguousFormat;
364 assert_eq!(
365 err.to_string(),
366 "ambiguous format: multiple plugins scored >= 50"
367 );
368 }
369
370 #[test]
371 fn error_corrupt_display() {
372 let err = Error::Corrupt("truncated header".into());
373 assert!(err.to_string().contains("truncated header"));
374 }
375
376 #[test]
377 fn error_decompression_display() {
378 let err = Error::Decompression("snappy failure".into());
379 assert!(err.to_string().contains("snappy failure"));
380 }
381
382 #[test]
383 fn open_dump_nonexistent_file() {
384 let result = open_dump(Path::new("/nonexistent/path/to/dump.lime"));
385 assert!(result.is_err());
386 let err = result.err().unwrap();
387 assert!(matches!(err, Error::Io(_)));
388 }
389
390 #[test]
391 fn dump_metadata_default_is_all_none() {
392 let m = DumpMetadata::default();
393 assert!(m.cr3.is_none());
394 assert!(m.machine_type.is_none());
395 assert!(m.os_version.is_none());
396 assert!(m.num_processors.is_none());
397 assert!(m.ps_active_process_head.is_none());
398 assert!(m.ps_loaded_module_list.is_none());
399 assert!(m.kd_debugger_data_block.is_none());
400 assert!(m.system_time.is_none());
401 assert!(m.dump_type.is_none());
402 }
403
404 #[test]
405 fn machine_type_variants() {
406 assert_ne!(MachineType::Amd64, MachineType::I386);
407 assert_ne!(MachineType::Amd64, MachineType::Aarch64);
408 assert_ne!(MachineType::I386, MachineType::Aarch64);
409 let a = MachineType::Amd64;
410 let b = a;
411 assert_eq!(a, b);
412 }
413
414 #[test]
415 fn metadata_default_method_returns_none() {
416 use crate::test_builders::LimeBuilder;
417 let dump = LimeBuilder::new().add_range(0, &[0xAA; 64]).build();
418 let provider = crate::lime::LimeProvider::from_bytes(&dump).unwrap();
419 assert!(provider.metadata().is_none());
420 }
421
422 #[test]
423 fn open_dump_crashdump() {
424 use crate::test_builders::CrashDumpBuilder;
425 let page = vec![0xAA; 4096];
426 let dump = CrashDumpBuilder::new().add_run(0, &page).build();
427 let path = std::env::temp_dir().join("memf_test_open_crashdump.dmp");
428 std::fs::write(&path, &dump).unwrap();
429 let provider = open_dump(&path).unwrap();
430 assert_eq!(provider.format_name(), "Windows Crash Dump");
431 assert_eq!(provider.total_size(), 4096);
432 let mut buf = [0u8; 2];
433 let n = provider.read_phys(0, &mut buf).unwrap();
434 assert_eq!(n, 2);
435 assert_eq!(buf, [0xAA, 0xAA]);
436 std::fs::remove_file(&path).ok();
437 }
438
439 #[test]
440 fn open_dump_hiberfil() {
441 use crate::test_builders::HiberfilBuilder;
442 let page = [0xBB; 4096];
443 let dump = HiberfilBuilder::new().add_page(0, &page).build();
444 let path = std::env::temp_dir().join("memf_test_open_hiberfil.sys");
445 std::fs::write(&path, &dump).unwrap();
446 let provider = open_dump(&path).unwrap();
447 assert_eq!(provider.format_name(), "Hiberfil.sys");
448 let mut buf = [0u8; 2];
449 let n = provider.read_phys(0, &mut buf).unwrap();
450 assert_eq!(n, 2);
451 assert_eq!(buf, [0xBB, 0xBB]);
452 std::fs::remove_file(&path).ok();
453 }
454
455 #[test]
456 fn open_dump_vmware() {
457 use crate::test_builders::VmwareStateBuilder;
458 let dump = VmwareStateBuilder::new()
459 .add_region(0, &[0xCC; 128])
460 .build();
461 let path = std::env::temp_dir().join("memf_test_open_vmware.vmss");
462 std::fs::write(&path, &dump).unwrap();
463 let provider = open_dump(&path).unwrap();
464 assert_eq!(provider.format_name(), "VMware State");
465 let mut buf = [0u8; 2];
466 let n = provider.read_phys(0, &mut buf).unwrap();
467 assert_eq!(n, 2);
468 assert_eq!(buf, [0xCC, 0xCC]);
469 std::fs::remove_file(&path).ok();
470 }
471
472 #[test]
473 fn open_dump_kdump() {
474 use crate::test_builders::KdumpBuilder;
475 let page = vec![0xDD; 4096];
476 let dump = KdumpBuilder::new()
477 .compression(0x04)
478 .add_page(0, &page)
479 .build();
480 let path = std::env::temp_dir().join("memf_test_open_kdump.dump");
481 std::fs::write(&path, &dump).unwrap();
482 let provider = open_dump(&path).unwrap();
483 assert_eq!(provider.format_name(), "kdump");
484 let mut buf = [0u8; 2];
485 let n = provider.read_phys(0, &mut buf).unwrap();
486 assert_eq!(n, 2);
487 assert_eq!(buf, [0xDD, 0xDD]);
488 std::fs::remove_file(&path).ok();
489 }
490
491 #[test]
492 fn metadata_returns_none_for_legacy_formats() {
493 use crate::test_builders::LimeBuilder;
494 let dump = LimeBuilder::new().add_range(0, &[0xAA; 64]).build();
495 let path = std::env::temp_dir().join("memf_test_meta_lime.lime");
496 std::fs::write(&path, &dump).unwrap();
497 let provider = open_dump(&path).unwrap();
498 assert!(provider.metadata().is_none());
499 std::fs::remove_file(&path).ok();
500 }
501
502 #[test]
503 fn box_dyn_provider_delegates_correctly() {
504 use crate::test_builders::LimeBuilder;
505 let dump = LimeBuilder::new().add_range(0x1000, &[0xAA; 128]).build();
506 let provider = crate::lime::LimeProvider::from_bytes(&dump).unwrap();
507 let boxed: Box<dyn PhysicalMemoryProvider> = Box::new(provider);
508
509 assert_eq!(boxed.format_name(), "LiME");
510 assert_eq!(boxed.total_size(), 128);
511 assert!(!boxed.ranges().is_empty());
512
513 let mut buf = [0u8; 4];
514 let n = boxed.read_phys(0x1000, &mut buf).unwrap();
515 assert_eq!(n, 4);
516 assert_eq!(buf, [0xAA; 4]);
517 }
518
519 #[test]
520 fn metadata_returns_some_for_crashdump() {
521 use crate::test_builders::CrashDumpBuilder;
522 let page = vec![0u8; 4096];
523 let dump = CrashDumpBuilder::new()
524 .cr3(0x1ab000)
525 .add_run(0, &page)
526 .build();
527 let path = std::env::temp_dir().join("memf_test_meta_crash.dmp");
528 std::fs::write(&path, &dump).unwrap();
529 let provider = open_dump(&path).unwrap();
530 let meta = provider
531 .metadata()
532 .expect("crash dump should have metadata");
533 assert_eq!(meta.cr3, Some(0x1ab000));
534 std::fs::remove_file(&path).ok();
535 }
536
537 #[test]
538 fn raw_fallback_accepts_plain_bytes() {
539 let data = vec![0x00; 1024];
540 let path = std::env::temp_dir().join("memf_test_raw_fallback");
541 std::fs::write(&path, &data).unwrap();
542 let result = open_dump_with_raw_fallback(&path);
544 assert!(result.is_ok());
545 let provider = result.unwrap();
546 assert_eq!(provider.format_name(), "Raw");
547 assert_eq!(provider.total_size(), 1024);
548 std::fs::remove_file(&path).ok();
549 }
550
551 #[test]
552 fn raw_fallback_still_detects_lime() {
553 use crate::test_builders::LimeBuilder;
554 let dump = LimeBuilder::new().add_range(0, &[0xAA; 128]).build();
555 let path = std::env::temp_dir().join("memf_test_raw_fallback_lime");
556 std::fs::write(&path, &dump).unwrap();
557 let provider = open_dump_with_raw_fallback(&path).unwrap();
559 assert_eq!(provider.format_name(), "LiME");
560 assert_eq!(provider.total_size(), 128);
561 std::fs::remove_file(&path).ok();
562 }
563
564 #[test]
571 fn physical_range_inverted_saturating_sub_spec_values() {
572 let r = PhysicalRange {
573 start: 100,
574 end: 50,
575 };
576 assert_eq!(r.len(), 0, "saturating_sub must clamp to 0, not overflow");
577 assert!(r.is_empty());
578 }
579
580 #[test]
583 fn total_size_default_impl_multi_range() {
584 use crate::test_builders::LimeBuilder;
585 let dump = LimeBuilder::new()
587 .add_range(0x0000, &[0xAA; 128])
588 .add_range(0x8000, &[0xBB; 64])
589 .build();
590 let provider = crate::lime::LimeProvider::from_bytes(&dump).unwrap();
591 assert_eq!(provider.ranges().len(), 2);
592 assert_eq!(provider.total_size(), 128 + 64);
594 }
595
596 #[test]
622 fn ambiguous_format_error_is_correct_variant_and_display() {
623 let err = Error::AmbiguousFormat;
624 assert!(
625 matches!(err, Error::AmbiguousFormat),
626 "variant must be AmbiguousFormat"
627 );
628 assert!(
629 err.to_string().contains("ambiguous"),
630 "display must mention 'ambiguous'"
631 );
632 }
633}