Skip to main content

memf_format/
win_crashdump.rs

1//! Windows crash dump (`.dmp`) format provider.
2//!
3//! Parses 64-bit Windows crash dumps with `_DUMP_HEADER64`.
4//! Supports run-based (DumpType 0x01) and bitmap (0x02/0x05) layouts.
5
6use std::path::Path;
7
8use crate::{
9    DumpMetadata, Error, FormatPlugin, MachineType, PhysicalMemoryProvider, PhysicalRange, Result,
10};
11
12/// PAGE magic: "PAGE" as little-endian u32 = 0x4547_4150.
13const PAGE_MAGIC: u32 = 0x4547_4150;
14/// DU64 signature: "DU64" as little-endian u32 = 0x3436_5544.
15const DU64_SIG: u32 = 0x3436_5544;
16/// Minimum header size for `_DUMP_HEADER64` (8192 bytes).
17const HEADER_SIZE: usize = 0x2000;
18/// Page size (4096 bytes).
19const PAGE_SIZE: u64 = 4096;
20/// "DUMP" as little-endian u32 for bitmap summary ValidDump field.
21const DUMP_VALID: u32 = 0x504D_5544;
22
23// Header field offsets.
24const OFF_MAGIC: usize = 0x000;
25const OFF_SIG: usize = 0x004;
26const OFF_CR3: usize = 0x010;
27const OFF_PS_LOADED_MODULE_LIST: usize = 0x020;
28const OFF_PS_ACTIVE_PROCESS_HEAD: usize = 0x028;
29const OFF_MACHINE_TYPE: usize = 0x030;
30const OFF_NUM_PROCESSORS: usize = 0x034;
31const OFF_KD_DEBUGGER_DATA_BLOCK: usize = 0x080;
32const OFF_PHYS_MEM_BLOCK: usize = 0x088;
33const OFF_DUMP_TYPE: usize = 0xF98;
34const OFF_SYSTEM_TIME: usize = 0xFA8;
35
36/// A physical memory run descriptor from the crash dump header.
37#[derive(Debug, Clone)]
38struct PhysMemRun {
39    /// Base page frame number (PFN).
40    base_page: u64,
41    /// Number of pages in this run.
42    page_count: u64,
43}
44
45/// Layout of physical memory data within the crash dump.
46#[derive(Debug)]
47enum CrashDumpLayout {
48    /// Run-based layout (DumpType 0x01): data pages stored sequentially after header.
49    RunBased {
50        /// Parsed runs from the header.
51        runs: Vec<PhysMemRun>,
52        /// Pre-computed file offset where each run's data begins.
53        run_file_offsets: Vec<u64>,
54    },
55    /// Bitmap layout (DumpType 0x02 or 0x05): bitmap indicates which PFNs are present.
56    Bitmap {
57        /// The bitmap bytes (one bit per PFN).
58        bitmap: Vec<u8>,
59        /// File offset where page data begins (after summary header + bitmap).
60        data_start: u64,
61    },
62}
63
64/// Provider that exposes physical memory from a Windows 64-bit crash dump.
65#[derive(Debug)]
66pub struct CrashDumpProvider {
67    data: Vec<u8>,
68    metadata: DumpMetadata,
69    layout: CrashDumpLayout,
70    ranges: Vec<PhysicalRange>,
71}
72
73/// Read a little-endian u32 from `data` at `offset`.
74fn read_u32(data: &[u8], offset: usize) -> crate::Result<u32> {
75    data.get(offset..offset + 4)
76        .and_then(|b| b.try_into().ok())
77        .map(u32::from_le_bytes)
78        .ok_or_else(|| {
79            crate::Error::Corrupt(format!("truncated header: need 4 bytes at offset {offset}"))
80        })
81}
82
83/// Read a little-endian u64 from `data` at `offset`.
84fn read_u64(data: &[u8], offset: usize) -> crate::Result<u64> {
85    data.get(offset..offset + 8)
86        .and_then(|b| b.try_into().ok())
87        .map(u64::from_le_bytes)
88        .ok_or_else(|| {
89            crate::Error::Corrupt(format!("truncated header: need 8 bytes at offset {offset}"))
90        })
91}
92
93/// Convert a MachineImageType u32 to a [`MachineType`].
94fn parse_machine_type(val: u32) -> Option<MachineType> {
95    match val {
96        0x8664 => Some(MachineType::Amd64),
97        0x014C => Some(MachineType::I386),
98        0xAA64 => Some(MachineType::Aarch64),
99        _ => None,
100    }
101}
102
103/// Convert a DumpType u32 to a human-readable label.
104fn dump_type_label(val: u32) -> &'static str {
105    match val {
106        0x01 => "Full",
107        0x02 => "Kernel",
108        0x05 => "Bitmap",
109        _ => "Unknown",
110    }
111}
112
113impl CrashDumpProvider {
114    /// Parse a crash dump from an in-memory byte slice.
115    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
116        if bytes.len() < HEADER_SIZE {
117            return Err(Error::Corrupt(format!(
118                "crash dump too small: {} bytes, need at least {HEADER_SIZE}",
119                bytes.len()
120            )));
121        }
122
123        // Validate magic.
124        let magic = read_u32(bytes, OFF_MAGIC)?;
125        let sig = read_u32(bytes, OFF_SIG)?;
126        if magic != PAGE_MAGIC || sig != DU64_SIG {
127            return Err(Error::Corrupt(format!(
128                "invalid crash dump magic: expected PAGE+DU64, got 0x{magic:08X}+0x{sig:08X}"
129            )));
130        }
131
132        // Extract metadata fields.
133        let cr3 = read_u64(bytes, OFF_CR3)?;
134        let ps_loaded_module_list = read_u64(bytes, OFF_PS_LOADED_MODULE_LIST)?;
135        let ps_active_process_head = read_u64(bytes, OFF_PS_ACTIVE_PROCESS_HEAD)?;
136        let machine_img_type = read_u32(bytes, OFF_MACHINE_TYPE)?;
137        let num_processors = read_u32(bytes, OFF_NUM_PROCESSORS)?;
138        let kd_debugger_data_block = read_u64(bytes, OFF_KD_DEBUGGER_DATA_BLOCK)?;
139        let dump_type_val = read_u32(bytes, OFF_DUMP_TYPE)?;
140        let system_time = read_u64(bytes, OFF_SYSTEM_TIME)?;
141
142        let metadata = DumpMetadata {
143            cr3: Some(cr3),
144            machine_type: parse_machine_type(machine_img_type),
145            os_version: None,
146            num_processors: Some(num_processors),
147            ps_active_process_head: Some(ps_active_process_head),
148            ps_loaded_module_list: Some(ps_loaded_module_list),
149            kd_debugger_data_block: Some(kd_debugger_data_block),
150            system_time: Some(system_time),
151            dump_type: Some(dump_type_label(dump_type_val).to_string()),
152        };
153
154        // Parse runs from PhysicalMemoryBlockBuffer at 0x088.
155        let num_runs = read_u32(bytes, OFF_PHYS_MEM_BLOCK)? as usize;
156        // _num_pages at 0x090 (skip padding at 0x08C)
157        let mut runs = Vec::with_capacity(num_runs);
158        for i in 0..num_runs {
159            let off = 0x098 + i * 16;
160            let base_page = read_u64(bytes, off)?;
161            let page_count = read_u64(bytes, off + 8)?;
162            runs.push(PhysMemRun {
163                base_page,
164                page_count,
165            });
166        }
167
168        // Build ranges from runs.
169        let ranges: Vec<PhysicalRange> = runs
170            .iter()
171            .map(|r| PhysicalRange {
172                start: r.base_page * PAGE_SIZE,
173                end: (r.base_page + r.page_count) * PAGE_SIZE,
174            })
175            .collect();
176
177        let is_bitmap = dump_type_val == 0x02 || dump_type_val == 0x05;
178        let layout = if is_bitmap {
179            Self::parse_bitmap_layout(bytes, HEADER_SIZE)?
180        } else {
181            Self::parse_run_layout(&runs)
182        };
183
184        Ok(Self {
185            data: bytes.to_vec(),
186            metadata,
187            layout,
188            ranges,
189        })
190    }
191
192    /// Parse a crash dump from a file path.
193    pub fn from_path(path: &Path) -> Result<Self> {
194        let data = std::fs::read(path)?;
195        Self::from_bytes(&data)
196    }
197
198    /// Build run-based layout: data starts at HEADER_SIZE, runs stored sequentially.
199    fn parse_run_layout(runs: &[PhysMemRun]) -> CrashDumpLayout {
200        let mut run_file_offsets = Vec::with_capacity(runs.len());
201        let mut offset = HEADER_SIZE as u64;
202        for run in runs {
203            run_file_offsets.push(offset);
204            offset += run.page_count * PAGE_SIZE;
205        }
206        CrashDumpLayout::RunBased {
207            runs: runs.to_vec(),
208            run_file_offsets,
209        }
210    }
211
212    /// Parse bitmap layout from the summary header at `summary_offset`.
213    fn parse_bitmap_layout(data: &[u8], summary_offset: usize) -> Result<CrashDumpLayout> {
214        if data.len() < summary_offset + 16 {
215            return Err(Error::Corrupt(
216                "crash dump too small for bitmap summary header".into(),
217            ));
218        }
219
220        let valid_dump = read_u32(data, summary_offset)?;
221        if valid_dump != DUMP_VALID {
222            return Err(Error::Corrupt(format!(
223                "invalid bitmap summary ValidDump: expected 0x{DUMP_VALID:08X}, got 0x{valid_dump:08X}"
224            )));
225        }
226
227        let header_size = read_u32(data, summary_offset + 4)? as usize;
228        let bitmap_size = read_u32(data, summary_offset + 8)? as usize;
229
230        // Bitmap starts right after the 16-byte summary header fields.
231        let bitmap_start = summary_offset + 16;
232        if data.len() < bitmap_start + bitmap_size {
233            return Err(Error::Corrupt("crash dump bitmap truncated".into()));
234        }
235
236        let bitmap = data[bitmap_start..bitmap_start + bitmap_size].to_vec();
237        let data_start = (summary_offset + header_size) as u64;
238
239        Ok(CrashDumpLayout::Bitmap { bitmap, data_start })
240    }
241
242    /// Read physical memory using run-based layout.
243    fn read_run_based(
244        &self,
245        addr: u64,
246        buf: &mut [u8],
247        runs: &[PhysMemRun],
248        run_file_offsets: &[u64],
249    ) -> Result<usize> {
250        let pfn = addr / PAGE_SIZE;
251        let page_offset = (addr % PAGE_SIZE) as usize;
252
253        for (i, run) in runs.iter().enumerate() {
254            if pfn >= run.base_page && pfn < run.base_page + run.page_count {
255                let pages_into_run = pfn - run.base_page;
256                let file_offset =
257                    run_file_offsets[i] + pages_into_run * PAGE_SIZE + page_offset as u64;
258                let remaining_in_run =
259                    ((run.page_count - pages_into_run) * PAGE_SIZE - page_offset as u64) as usize;
260                let to_read = buf.len().min(remaining_in_run);
261                let src = file_offset as usize;
262                if src + to_read > self.data.len() {
263                    return Err(Error::Corrupt("run data extends beyond file".into()));
264                }
265                buf[..to_read].copy_from_slice(&self.data[src..src + to_read]);
266                return Ok(to_read);
267            }
268        }
269
270        // Address not in any run — gap.
271        Ok(0)
272    }
273
274    /// Read physical memory using bitmap layout.
275    fn read_bitmap(
276        &self,
277        addr: u64,
278        buf: &mut [u8],
279        bitmap: &[u8],
280        data_start: u64,
281    ) -> Result<usize> {
282        let pfn = addr / PAGE_SIZE;
283        let page_offset = (addr % PAGE_SIZE) as usize;
284
285        // Check if this PFN's bit is set in the bitmap.
286        let byte_idx = pfn as usize / 8;
287        let bit_idx = pfn as usize % 8;
288        if byte_idx >= bitmap.len() || (bitmap[byte_idx] & (1 << bit_idx)) == 0 {
289            return Ok(0); // PFN not present.
290        }
291
292        // Count set bits before this PFN to find the page index in the data area.
293        let page_index = popcount_before(bitmap, pfn as usize);
294        let file_offset = data_start + page_index as u64 * PAGE_SIZE + page_offset as u64;
295        let remaining_in_page = (PAGE_SIZE as usize) - page_offset;
296        let to_read = buf.len().min(remaining_in_page);
297        let src = file_offset as usize;
298        if src + to_read > self.data.len() {
299            return Err(Error::Corrupt(
300                "bitmap page data extends beyond file".into(),
301            ));
302        }
303        buf[..to_read].copy_from_slice(&self.data[src..src + to_read]);
304        Ok(to_read)
305    }
306}
307
308/// Count the number of set bits in `bitmap` for all positions before `bit_pos`.
309fn popcount_before(bitmap: &[u8], bit_pos: usize) -> usize {
310    let full_bytes = bit_pos / 8;
311    let remaining_bits = bit_pos % 8;
312
313    let mut count: usize = 0;
314    for &byte in &bitmap[..full_bytes] {
315        count += byte.count_ones() as usize;
316    }
317    if remaining_bits > 0 && full_bytes < bitmap.len() {
318        // Mask off bits at positions >= remaining_bits.
319        let mask = (1u8 << remaining_bits) - 1;
320        count += (bitmap[full_bytes] & mask).count_ones() as usize;
321    }
322    count
323}
324
325impl PhysicalMemoryProvider for CrashDumpProvider {
326    fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
327        if buf.is_empty() {
328            return Ok(0);
329        }
330
331        match &self.layout {
332            CrashDumpLayout::RunBased {
333                runs,
334                run_file_offsets,
335            } => self.read_run_based(addr, buf, runs, run_file_offsets),
336            CrashDumpLayout::Bitmap { bitmap, data_start } => {
337                self.read_bitmap(addr, buf, bitmap, *data_start)
338            }
339        }
340    }
341
342    fn ranges(&self) -> &[PhysicalRange] {
343        &self.ranges
344    }
345
346    fn format_name(&self) -> &str {
347        "Windows Crash Dump"
348    }
349
350    fn metadata(&self) -> Option<DumpMetadata> {
351        Some(self.metadata.clone())
352    }
353}
354
355/// FormatPlugin implementation for Windows crash dumps.
356pub struct CrashDumpPlugin;
357
358impl FormatPlugin for CrashDumpPlugin {
359    fn name(&self) -> &str {
360        "Windows Crash Dump"
361    }
362
363    fn probe(&self, header: &[u8]) -> u8 {
364        if header.len() < 8 {
365            return 0;
366        }
367        let Ok(magic) = read_u32(header, 0) else {
368            return 0;
369        };
370        let Ok(sig) = read_u32(header, 4) else {
371            return 0;
372        };
373        if magic == PAGE_MAGIC && sig == DU64_SIG {
374            95
375        } else {
376            0
377        }
378    }
379
380    fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
381        Ok(Box::new(CrashDumpProvider::from_path(path)?))
382    }
383}
384
385inventory::submit!(&CrashDumpPlugin as &dyn FormatPlugin);
386
387#[cfg(test)]
388mod tests {
389    use crate::test_builders::CrashDumpBuilder;
390    use crate::{Error, MachineType, PhysicalMemoryProvider};
391
392    use super::{CrashDumpPlugin, CrashDumpProvider};
393    use crate::FormatPlugin;
394
395    const PAGE: usize = 4096;
396
397    #[test]
398    fn probe_crashdump_magic() {
399        let dump = CrashDumpBuilder::new().add_run(0, &[0xAA; PAGE]).build();
400        let plugin = CrashDumpPlugin;
401        assert_eq!(plugin.probe(&dump), 95);
402    }
403
404    #[test]
405    fn probe_non_crashdump() {
406        let zeros = vec![0u8; 64];
407        let plugin = CrashDumpPlugin;
408        assert_eq!(plugin.probe(&zeros), 0);
409    }
410
411    #[test]
412    fn probe_short_header_returns_zero() {
413        let plugin = CrashDumpPlugin;
414        assert_eq!(plugin.probe(&[0x50, 0x41, 0x47, 0x45, 0x44, 0x55, 0x36]), 0); // 7 bytes
415        assert_eq!(plugin.probe(&[]), 0);
416    }
417
418    #[test]
419    fn single_run_read() {
420        let mut page_data = vec![0u8; PAGE];
421        page_data[0] = 0xDE;
422        page_data[1] = 0xAD;
423        page_data[2] = 0xBE;
424        page_data[3] = 0xEF;
425        let dump = CrashDumpBuilder::new().add_run(0, &page_data).build();
426        let provider = CrashDumpProvider::from_bytes(&dump).unwrap();
427        let mut buf = [0u8; 4];
428        let n = provider.read_phys(0, &mut buf).unwrap();
429        assert_eq!(n, 4);
430        assert_eq!(buf, [0xDE, 0xAD, 0xBE, 0xEF]);
431    }
432
433    #[test]
434    fn multi_run_read() {
435        // Run 0: PFN 0 (1 page), Run 1: PFN 4 (1 page), gap at PFN 1-3.
436        let page_a = vec![0xAAu8; PAGE];
437        let page_b = vec![0xBBu8; PAGE];
438        let dump = CrashDumpBuilder::new()
439            .add_run(0, &page_a)
440            .add_run(4, &page_b)
441            .build();
442        let provider = CrashDumpProvider::from_bytes(&dump).unwrap();
443
444        let mut buf = [0u8; 2];
445        let n = provider.read_phys(0, &mut buf).unwrap();
446        assert_eq!(n, 2);
447        assert_eq!(buf, [0xAA, 0xAA]);
448
449        let n = provider.read_phys(4 * PAGE as u64, &mut buf).unwrap();
450        assert_eq!(n, 2);
451        assert_eq!(buf, [0xBB, 0xBB]);
452    }
453
454    #[test]
455    fn read_gap_returns_zero() {
456        let page_data = vec![0xCCu8; PAGE];
457        let dump = CrashDumpBuilder::new().add_run(2, &page_data).build();
458        let provider = CrashDumpProvider::from_bytes(&dump).unwrap();
459
460        // PFN 0 is not mapped (run starts at PFN 2).
461        let mut buf = [0xFFu8; 4];
462        let n = provider.read_phys(0, &mut buf).unwrap();
463        assert_eq!(n, 0);
464    }
465
466    #[test]
467    fn read_empty_buffer() {
468        let page_data = vec![0xAAu8; PAGE];
469        let dump = CrashDumpBuilder::new().add_run(0, &page_data).build();
470        let provider = CrashDumpProvider::from_bytes(&dump).unwrap();
471        let mut buf = [];
472        let n = provider.read_phys(0, &mut buf).unwrap();
473        assert_eq!(n, 0);
474    }
475
476    #[test]
477    fn metadata_extraction() {
478        let dump = CrashDumpBuilder::new()
479            .cr3(0x0018_7000)
480            .machine_type(0x8664)
481            .num_processors(4)
482            .dump_type(0x01)
483            .ps_active_process_head(0xFFFFF802_1A2B3C40)
484            .ps_loaded_module_list(0xFFFFF802_1A2B3D60)
485            .kd_debugger_data_block(0xFFFFF802_1A000000)
486            .system_time(0x01DA_5678_9ABC_DEF0)
487            .add_run(0, &[0u8; PAGE])
488            .build();
489        let provider = CrashDumpProvider::from_bytes(&dump).unwrap();
490        let meta = provider.metadata().expect("metadata should be Some");
491        assert_eq!(meta.cr3, Some(0x0018_7000));
492        assert_eq!(meta.machine_type, Some(MachineType::Amd64));
493        assert_eq!(meta.num_processors, Some(4));
494        assert_eq!(meta.dump_type.as_deref(), Some("Full"));
495        assert_eq!(meta.ps_active_process_head, Some(0xFFFFF802_1A2B3C40));
496        assert_eq!(meta.ps_loaded_module_list, Some(0xFFFFF802_1A2B3D60));
497        assert_eq!(meta.kd_debugger_data_block, Some(0xFFFFF802_1A000000));
498        assert_eq!(meta.system_time, Some(0x01DA_5678_9ABC_DEF0));
499    }
500
501    #[test]
502    fn plugin_name() {
503        let plugin = CrashDumpPlugin;
504        assert_eq!(plugin.name(), "Windows Crash Dump");
505    }
506
507    #[test]
508    fn builder_produces_valid_header() {
509        let dump = CrashDumpBuilder::new().add_run(0, &[0u8; PAGE]).build();
510        // Check PAGE magic at 0x000
511        let magic = u32::from_le_bytes(dump[0..4].try_into().unwrap());
512        assert_eq!(magic, 0x4547_4150);
513        // Check DU64 signature at 0x004
514        let sig = u32::from_le_bytes(dump[4..8].try_into().unwrap());
515        assert_eq!(sig, 0x3436_5544);
516        // Data starts at 0x2000 (8192)
517        assert!(dump.len() >= 0x2000 + PAGE);
518    }
519
520    #[test]
521    fn bitmap_single_page_read() {
522        let mut page_data = vec![0u8; PAGE];
523        page_data[0] = 0x42;
524        page_data[1] = 0x4D;
525        let dump = CrashDumpBuilder::new()
526            .dump_type(0x05)
527            .add_run(0, &page_data)
528            .build();
529        let provider = CrashDumpProvider::from_bytes(&dump).unwrap();
530        let mut buf = [0u8; 2];
531        let n = provider.read_phys(0, &mut buf).unwrap();
532        assert_eq!(n, 2);
533        assert_eq!(buf, [0x42, 0x4D]);
534    }
535
536    #[test]
537    fn bitmap_multi_run_with_gap() {
538        let page_a = vec![0xAAu8; PAGE];
539        let page_b = vec![0xBBu8; PAGE];
540        let dump = CrashDumpBuilder::new()
541            .dump_type(0x05)
542            .add_run(0, &page_a)
543            .add_run(4, &page_b)
544            .build();
545        let provider = CrashDumpProvider::from_bytes(&dump).unwrap();
546
547        let mut buf = [0u8; 2];
548        let n = provider.read_phys(0, &mut buf).unwrap();
549        assert_eq!(n, 2);
550        assert_eq!(buf, [0xAA, 0xAA]);
551
552        let n = provider.read_phys(4 * PAGE as u64, &mut buf).unwrap();
553        assert_eq!(n, 2);
554        assert_eq!(buf, [0xBB, 0xBB]);
555
556        // Gap at PFN 1-3 returns 0
557        let n = provider.read_phys(PAGE as u64, &mut buf).unwrap();
558        assert_eq!(n, 0);
559    }
560
561    #[test]
562    fn bitmap_popcount_correctness() {
563        // 3 contiguous pages at PFN 2, 3, 4 with DumpType 0x02.
564        let mut data = vec![0u8; PAGE * 3];
565        // Page 0 (PFN 2): fill with 0x11
566        data[0..PAGE].fill(0x11);
567        // Page 1 (PFN 3): fill with 0x22
568        data[PAGE..PAGE * 2].fill(0x22);
569        // Page 2 (PFN 4): fill with 0x33
570        data[PAGE * 2..PAGE * 3].fill(0x33);
571        let dump = CrashDumpBuilder::new()
572            .dump_type(0x02)
573            .add_run(2, &data)
574            .build();
575        let provider = CrashDumpProvider::from_bytes(&dump).unwrap();
576
577        let mut buf = [0u8; 1];
578        // PFN 2
579        let n = provider.read_phys(2 * PAGE as u64, &mut buf).unwrap();
580        assert_eq!(n, 1);
581        assert_eq!(buf[0], 0x11);
582        // PFN 3
583        let n = provider.read_phys(3 * PAGE as u64, &mut buf).unwrap();
584        assert_eq!(n, 1);
585        assert_eq!(buf[0], 0x22);
586        // PFN 4
587        let n = provider.read_phys(4 * PAGE as u64, &mut buf).unwrap();
588        assert_eq!(n, 1);
589        assert_eq!(buf[0], 0x33);
590    }
591
592    #[test]
593    fn from_path_roundtrip() {
594        let mut page_data = vec![0u8; PAGE];
595        page_data[0..4].copy_from_slice(&[0xCA, 0xFE, 0xBA, 0xBE]);
596        let dump = CrashDumpBuilder::new().add_run(0, &page_data).build();
597        let path = std::env::temp_dir().join("memf_test_crashdump_roundtrip.dmp");
598        std::fs::write(&path, &dump).unwrap();
599        let provider = CrashDumpProvider::from_path(&path).unwrap();
600        let mut buf = [0u8; 4];
601        let n = provider.read_phys(0, &mut buf).unwrap();
602        assert_eq!(n, 4);
603        assert_eq!(buf, [0xCA, 0xFE, 0xBA, 0xBE]);
604        std::fs::remove_file(&path).ok();
605    }
606
607    #[test]
608    fn corrupt_magic_errors() {
609        let mut dump = CrashDumpBuilder::new().add_run(0, &[0u8; PAGE]).build();
610        // Corrupt the PAGE magic
611        dump[0] = 0xFF;
612        let err = CrashDumpProvider::from_bytes(&dump).unwrap_err();
613        assert!(
614            matches!(err, Error::Corrupt(_)),
615            "expected Corrupt, got {err:?}"
616        );
617    }
618
619    #[test]
620    fn too_small_header_errors() {
621        let data = vec![0u8; 100];
622        let err = CrashDumpProvider::from_bytes(&data).unwrap_err();
623        assert!(
624            matches!(err, Error::Corrupt(_)),
625            "expected Corrupt, got {err:?}"
626        );
627    }
628
629    #[test]
630    fn from_bytes_empty_returns_error_not_panic() {
631        let result = CrashDumpProvider::from_bytes(&[]);
632        assert!(result.is_err(), "empty input must return Err");
633    }
634
635    #[test]
636    fn from_bytes_3_bytes_returns_error_not_panic() {
637        let result = CrashDumpProvider::from_bytes(&[0u8; 3]);
638        assert!(result.is_err(), "3 bytes is too short for any valid header");
639    }
640}