Skip to main content

memf_format/
test_builders.rs

1//! Synthetic dump builders for unit tests.
2//!
3//! These are only compiled when `cfg(test)` is active or when explicitly
4//! depended on by other test modules.
5//!
6//! This is test-fixture infrastructure (a `pub mod` so integration tests can
7//! reach it), so panic-on-failure via `unwrap`/`expect` is the intended
8//! behavior — a broken fixture should fail loudly, not silently degrade.
9#![allow(clippy::unwrap_used, clippy::expect_used)]
10
11/// Build a synthetic LiME dump byte-by-byte.
12///
13/// Each range is serialised as a 32-byte header followed by raw payload data.
14/// Header layout (all fields little-endian):
15/// - 0x00: magic  = 0x4C694D45 (4 bytes)
16/// - 0x04: version = 1          (4 bytes)
17/// - 0x08: s_addr               (8 bytes)
18/// - 0x10: e_addr (inclusive)   (8 bytes)
19/// - 0x18: reserved             (8 bytes zeros)
20#[derive(Default)]
21pub struct LimeBuilder {
22    ranges: Vec<(u64, Vec<u8>)>,
23}
24
25impl LimeBuilder {
26    /// Create an empty builder.
27    pub fn new() -> Self {
28        Self { ranges: Vec::new() }
29    }
30
31    /// Add a physical memory range starting at `start` with the given `data`.
32    pub fn add_range(mut self, start: u64, data: &[u8]) -> Self {
33        self.ranges.push((start, data.to_vec()));
34        self
35    }
36
37    /// Serialise all ranges into a complete LiME dump.
38    pub fn build(self) -> Vec<u8> {
39        const MAGIC: u32 = 0x4C694D45;
40        const VERSION: u32 = 1;
41
42        let mut out = Vec::new();
43        for (start, data) in &self.ranges {
44            let e_addr = start + data.len() as u64 - 1; // inclusive end
45            out.extend_from_slice(&MAGIC.to_le_bytes());
46            out.extend_from_slice(&VERSION.to_le_bytes());
47            out.extend_from_slice(&start.to_le_bytes());
48            out.extend_from_slice(&e_addr.to_le_bytes());
49            out.extend_from_slice(&0u64.to_le_bytes()); // reserved
50            out.extend_from_slice(data);
51        }
52        out
53    }
54}
55
56/// Build a synthetic AVML dump byte-by-byte.
57///
58/// Each range is serialised as a 32-byte header followed by a Snappy-compressed
59/// payload, then 8 trailing bytes encoding the uncompressed size as u64 LE.
60/// Header layout (all fields little-endian):
61/// - 0x00: magic   = 0x4C4D5641 (4 bytes)
62/// - 0x04: version = 2           (4 bytes)
63/// - 0x08: s_addr                (8 bytes)
64/// - 0x10: e_addr (exclusive)    (8 bytes)
65/// - 0x18: reserved              (8 bytes zeros)
66#[derive(Default)]
67pub struct AvmlBuilder {
68    ranges: Vec<(u64, Vec<u8>)>,
69}
70
71impl AvmlBuilder {
72    /// Create an empty builder.
73    pub fn new() -> Self {
74        Self { ranges: Vec::new() }
75    }
76
77    /// Add a physical memory range starting at `start` with the given `data`.
78    pub fn add_range(mut self, start: u64, data: &[u8]) -> Self {
79        self.ranges.push((start, data.to_vec()));
80        self
81    }
82
83    /// Serialise all ranges into a complete AVML dump.
84    pub fn build(self) -> Vec<u8> {
85        const MAGIC: u32 = 0x4C4D5641;
86        const VERSION: u32 = 2;
87
88        let mut out = Vec::new();
89        for (start, data) in &self.ranges {
90            let e_addr = start + data.len() as u64; // exclusive end
91            let uncompressed_size = data.len() as u64;
92
93            let mut encoder = snap::raw::Encoder::new();
94            let compressed = encoder.compress_vec(data).expect("snappy compress");
95
96            out.extend_from_slice(&MAGIC.to_le_bytes());
97            out.extend_from_slice(&VERSION.to_le_bytes());
98            out.extend_from_slice(&start.to_le_bytes());
99            out.extend_from_slice(&e_addr.to_le_bytes());
100            out.extend_from_slice(&0u64.to_le_bytes()); // reserved
101            out.extend_from_slice(&compressed);
102            out.extend_from_slice(&uncompressed_size.to_le_bytes()); // trailer
103        }
104        out
105    }
106}
107
108/// Build a synthetic Windows 64-bit crash dump (`_DUMP_HEADER64`).
109///
110/// Produces an 8192-byte header followed by physical memory page data.
111/// Supports both run-based (DumpType 0x01) and bitmap (DumpType 0x02/0x05) layouts.
112///
113/// Header layout (little-endian, key offsets):
114/// - 0x000: "PAGE" magic (u32 = 0x4547_4150)
115/// - 0x004: "DU64" signature (u32 = 0x3436_5544)
116/// - 0x010: DirectoryTableBase / CR3 (u64)
117/// - 0x020: PsLoadedModuleList (u64)
118/// - 0x028: PsActiveProcessHead (u64)
119/// - 0x030: MachineImageType (u32)
120/// - 0x034: NumberProcessors (u32)
121/// - 0x080: KdDebuggerDataBlock (u64)
122/// - 0x088: PhysicalMemoryBlockBuffer — NumberOfRuns(u32) + pad(u32) + NumberOfPages(u64) + Runs[]
123/// - 0xF98: DumpType (u32)
124/// - 0xFA8: SystemTime (u64)
125pub struct CrashDumpBuilder {
126    runs: Vec<(u64, Vec<u8>)>,
127    cr3: u64,
128    ps_active_process_head: u64,
129    ps_loaded_module_list: u64,
130    kd_debugger_data_block: u64,
131    machine_type: u32,
132    num_processors: u32,
133    dump_type: u32,
134    system_time: u64,
135}
136
137impl Default for CrashDumpBuilder {
138    fn default() -> Self {
139        Self {
140            runs: Vec::new(),
141            cr3: 0x0018_7000,
142            ps_active_process_head: 0xFFFFF802_1A2B3C40,
143            ps_loaded_module_list: 0xFFFFF802_1A2B3D60,
144            kd_debugger_data_block: 0xFFFFF802_1A000000,
145            machine_type: 0x8664, // AMD64
146            num_processors: 4,
147            dump_type: 0x01, // Full (run-based)
148            system_time: 0x01DA_5678_9ABC_DEF0,
149        }
150    }
151}
152
153impl CrashDumpBuilder {
154    /// Create a builder with sensible AMD64 defaults (DumpType = Full / run-based).
155    pub fn new() -> Self {
156        Self::default()
157    }
158
159    /// Add a physical memory run starting at `base_page` (PFN) with the given page `data`.
160    /// `data.len()` must be a multiple of 4096.
161    pub fn add_run(mut self, base_page: u64, data: &[u8]) -> Self {
162        assert!(
163            data.len() % 4096 == 0,
164            "run data length must be a multiple of 4096"
165        );
166        self.runs.push((base_page, data.to_vec()));
167        self
168    }
169
170    /// Set the CR3 / DirectoryTableBase value.
171    pub fn cr3(mut self, val: u64) -> Self {
172        self.cr3 = val;
173        self
174    }
175
176    /// Set the PsActiveProcessHead virtual address.
177    pub fn ps_active_process_head(mut self, val: u64) -> Self {
178        self.ps_active_process_head = val;
179        self
180    }
181
182    /// Set the PsLoadedModuleList virtual address.
183    pub fn ps_loaded_module_list(mut self, val: u64) -> Self {
184        self.ps_loaded_module_list = val;
185        self
186    }
187
188    /// Set the KdDebuggerDataBlock virtual address.
189    pub fn kd_debugger_data_block(mut self, val: u64) -> Self {
190        self.kd_debugger_data_block = val;
191        self
192    }
193
194    /// Set the MachineImageType (0x8664=AMD64, 0x014C=I386, 0xAA64=AArch64).
195    pub fn machine_type(mut self, val: u32) -> Self {
196        self.machine_type = val;
197        self
198    }
199
200    /// Set the number of processors.
201    pub fn num_processors(mut self, val: u32) -> Self {
202        self.num_processors = val;
203        self
204    }
205
206    /// Set the DumpType (0x01=Full, 0x02=Kernel/Bitmap, 0x05=Bitmap).
207    pub fn dump_type(mut self, val: u32) -> Self {
208        self.dump_type = val;
209        self
210    }
211
212    /// Set the SystemTime value.
213    pub fn system_time(mut self, val: u64) -> Self {
214        self.system_time = val;
215        self
216    }
217
218    /// Build the complete crash dump as a byte vector.
219    pub fn build(self) -> Vec<u8> {
220        const PAGE_MAGIC: u32 = 0x4547_4150; // "PAGE"
221        const DU64_SIG: u32 = 0x3436_5544; // "DU64"
222        const HEADER_SIZE: usize = 0x2000; // 8192 bytes
223        const PAGE_SIZE: usize = 4096;
224
225        let mut header = vec![0u8; HEADER_SIZE];
226
227        // 0x000: PAGE magic
228        header[0x000..0x004].copy_from_slice(&PAGE_MAGIC.to_le_bytes());
229        // 0x004: DU64 signature
230        header[0x004..0x008].copy_from_slice(&DU64_SIG.to_le_bytes());
231        // 0x010: CR3 / DirectoryTableBase
232        header[0x010..0x018].copy_from_slice(&self.cr3.to_le_bytes());
233        // 0x020: PsLoadedModuleList
234        header[0x020..0x028].copy_from_slice(&self.ps_loaded_module_list.to_le_bytes());
235        // 0x028: PsActiveProcessHead
236        header[0x028..0x030].copy_from_slice(&self.ps_active_process_head.to_le_bytes());
237        // 0x030: MachineImageType
238        header[0x030..0x034].copy_from_slice(&self.machine_type.to_le_bytes());
239        // 0x034: NumberProcessors
240        header[0x034..0x038].copy_from_slice(&self.num_processors.to_le_bytes());
241        // 0x080: KdDebuggerDataBlock
242        header[0x080..0x088].copy_from_slice(&self.kd_debugger_data_block.to_le_bytes());
243
244        // 0x088: PhysicalMemoryBlockBuffer
245        let num_runs = self.runs.len() as u32;
246        let total_pages: u64 = self
247            .runs
248            .iter()
249            .map(|(_, d)| (d.len() / PAGE_SIZE) as u64)
250            .sum();
251        // NumberOfRuns (u32) at 0x088
252        header[0x088..0x08C].copy_from_slice(&num_runs.to_le_bytes());
253        // Padding (u32) at 0x08C
254        header[0x08C..0x090].copy_from_slice(&0u32.to_le_bytes());
255        // NumberOfPages (u64) at 0x090
256        header[0x090..0x098].copy_from_slice(&total_pages.to_le_bytes());
257        // Runs[] starting at 0x098, each run is 16 bytes: base_page(u64) + page_count(u64)
258        for (i, (base_page, data)) in self.runs.iter().enumerate() {
259            let page_count = (data.len() / PAGE_SIZE) as u64;
260            let off = 0x098 + i * 16;
261            header[off..off + 8].copy_from_slice(&base_page.to_le_bytes());
262            header[off + 8..off + 16].copy_from_slice(&page_count.to_le_bytes());
263        }
264
265        // 0xF98: DumpType
266        header[0xF98..0xF9C].copy_from_slice(&self.dump_type.to_le_bytes());
267        // 0xFA8: SystemTime
268        header[0xFA8..0xFB0].copy_from_slice(&self.system_time.to_le_bytes());
269
270        let is_bitmap = self.dump_type == 0x02 || self.dump_type == 0x05;
271
272        if is_bitmap {
273            self.build_bitmap(header)
274        } else {
275            self.build_run_based(header)
276        }
277    }
278
279    /// Build run-based layout: data pages follow header sequentially at offset 0x2000.
280    fn build_run_based(self, mut out: Vec<u8>) -> Vec<u8> {
281        for (_, data) in &self.runs {
282            out.extend_from_slice(data);
283        }
284        out
285    }
286
287    /// Build bitmap layout: summary header + bitmap + data pages at offset 0x2000.
288    fn build_bitmap(self, mut out: Vec<u8>) -> Vec<u8> {
289        const DUMP_VALID: u32 = 0x504D_5544; // "DUMP"
290        const PAGE_SIZE: usize = 4096;
291
292        // Find the highest PFN to determine bitmap size.
293        let max_pfn: u64 = self
294            .runs
295            .iter()
296            .map(|(base, data)| base + (data.len() / PAGE_SIZE) as u64)
297            .max()
298            .unwrap_or(0);
299
300        // Bitmap: one bit per page up to max_pfn, rounded up to 8 bytes.
301        let bitmap_bits = max_pfn as usize;
302        let bitmap_bytes = bitmap_bits.div_ceil(8);
303        // Align bitmap_bytes up to multiple of 8.
304        let bitmap_bytes_aligned = (bitmap_bytes + 7) & !7;
305
306        let mut bitmap = vec![0u8; bitmap_bytes_aligned];
307        for (base_page, data) in &self.runs {
308            let page_count = data.len() / PAGE_SIZE;
309            for p in 0..page_count {
310                let pfn = *base_page as usize + p;
311                bitmap[pfn / 8] |= 1 << (pfn % 8);
312            }
313        }
314
315        // Summary header at 0x2000:
316        // ValidDump (u32) = "DUMP"
317        // HeaderSize (u32) = offset from 0x2000 to start of page data
318        // BitmapSize (u32) = bitmap size in bytes
319        // Pages (u32) = total number of set bits
320        let total_set_pages: u32 = self
321            .runs
322            .iter()
323            .map(|(_, d)| (d.len() / PAGE_SIZE) as u32)
324            .sum();
325        let summary_header_size: u32 = 16; // 4 fields * 4 bytes
326        let data_offset = summary_header_size as usize + bitmap_bytes_aligned;
327
328        out.extend_from_slice(&DUMP_VALID.to_le_bytes());
329        out.extend_from_slice(&(data_offset as u32).to_le_bytes());
330        out.extend_from_slice(&(bitmap_bytes_aligned as u32).to_le_bytes());
331        out.extend_from_slice(&total_set_pages.to_le_bytes());
332        out.extend_from_slice(&bitmap);
333
334        // Write page data in PFN order.
335        // Build a map from PFN to page data for ordered output.
336        let mut pfn_data: Vec<(u64, &[u8])> = Vec::new();
337        for (base_page, data) in &self.runs {
338            let page_count = data.len() / PAGE_SIZE;
339            for p in 0..page_count {
340                let pfn = base_page + p as u64;
341                let start = p * PAGE_SIZE;
342                pfn_data.push((pfn, &data[start..start + PAGE_SIZE]));
343            }
344        }
345        pfn_data.sort_by_key(|(pfn, _)| *pfn);
346        for (_, page) in &pfn_data {
347            out.extend_from_slice(page);
348        }
349
350        out
351    }
352}
353
354/// Build a synthetic ELF core dump for testing.
355#[derive(Default)]
356pub struct ElfCoreBuilder {
357    segments: Vec<(u64, Vec<u8>)>,
358}
359
360impl ElfCoreBuilder {
361    /// Create a new empty builder.
362    pub fn new() -> Self {
363        Self {
364            segments: Vec::new(),
365        }
366    }
367
368    /// Add a PT_LOAD segment at the given physical address with the given data.
369    pub fn add_segment(mut self, paddr: u64, data: &[u8]) -> Self {
370        self.segments.push((paddr, data.to_vec()));
371        self
372    }
373
374    /// Build the ELF core dump as a byte vector.
375    pub fn build(self) -> Vec<u8> {
376        let ehdr_size: usize = 64;
377        let phdr_size: usize = 56;
378        let phdr_count = self.segments.len();
379        let phdr_total = phdr_count * phdr_size;
380        let data_start = (ehdr_size + phdr_total).div_ceil(0x1000) * 0x1000;
381        let mut out = vec![0u8; data_start];
382
383        // ELF header
384        out[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
385        out[4] = 2; // ELFCLASS64
386        out[5] = 1; // ELFDATA2LSB
387        out[6] = 1; // EV_CURRENT
388        out[16..18].copy_from_slice(&4u16.to_le_bytes()); // ET_CORE
389        out[18..20].copy_from_slice(&62u16.to_le_bytes()); // EM_X86_64
390        out[20..24].copy_from_slice(&1u32.to_le_bytes()); // e_version
391        out[32..40].copy_from_slice(&(ehdr_size as u64).to_le_bytes()); // e_phoff
392        out[52..54].copy_from_slice(&(ehdr_size as u16).to_le_bytes()); // e_ehsize
393        out[54..56].copy_from_slice(&(phdr_size as u16).to_le_bytes()); // e_phentsize
394        out[56..58].copy_from_slice(&(phdr_count as u16).to_le_bytes()); // e_phnum
395
396        let mut current_offset = data_start;
397        for (i, (paddr, data)) in self.segments.iter().enumerate() {
398            let phdr_off = ehdr_size + i * phdr_size;
399            out[phdr_off..phdr_off + 4].copy_from_slice(&1u32.to_le_bytes()); // PT_LOAD
400            out[phdr_off + 4..phdr_off + 8].copy_from_slice(&6u32.to_le_bytes()); // PF_R|PF_W
401            out[phdr_off + 8..phdr_off + 16]
402                .copy_from_slice(&(current_offset as u64).to_le_bytes());
403            out[phdr_off + 24..phdr_off + 32].copy_from_slice(&paddr.to_le_bytes());
404            out[phdr_off + 32..phdr_off + 40].copy_from_slice(&(data.len() as u64).to_le_bytes());
405            out[phdr_off + 40..phdr_off + 48].copy_from_slice(&(data.len() as u64).to_le_bytes());
406            out[phdr_off + 48..phdr_off + 56].copy_from_slice(&0x1000u64.to_le_bytes());
407            out.resize(current_offset + data.len(), 0);
408            out[current_offset..current_offset + data.len()].copy_from_slice(data);
409            current_offset += data.len();
410        }
411        out
412    }
413}
414
415/// Build a synthetic VMware `.vmss`/`.vmsn` state file.
416///
417/// Produces the VMware group/tag binary format:
418/// - 12-byte file header: magic(u32=0xBED2BED0) + unknown(u32=0) + group_count(u32)
419/// - Group entries (80 bytes each): name(64 bytes null-terminated) + tags_offset(u64) + padding(8 bytes)
420/// - "memory" group with region tags containing paddr(u64) + data
421/// - Optional "cpu" group with CR3 tag
422///
423/// Tag format for memory regions:
424/// - flags: u8 (0x06 = large data with explicit size)
425/// - name_length: u8
426/// - name: bytes ("regionPPN", "regionBytes")
427/// - data_length: u32
428/// - payload: depends on tag name
429///
430/// Tag format for CPU CR3:
431/// - flags: u8 (0x46 = indexed + 8-byte data)
432/// - name_length: u8
433/// - name: "CR3"
434/// - index0: u8 (0 = CPU 0)
435/// - index1: u8 (3 = CR register 3)
436/// - value: u64
437///
438/// Tag terminator: flags byte = 0
439#[derive(Default)]
440pub struct VmwareStateBuilder {
441    memory_regions: Vec<(u64, Vec<u8>)>,
442    cr3: Option<u64>,
443}
444
445impl VmwareStateBuilder {
446    /// Create a new empty builder.
447    pub fn new() -> Self {
448        Self {
449            memory_regions: Vec::new(),
450            cr3: None,
451        }
452    }
453
454    /// Add a physical memory region at the given physical address.
455    pub fn add_region(mut self, paddr: u64, data: &[u8]) -> Self {
456        self.memory_regions.push((paddr, data.to_vec()));
457        self
458    }
459
460    /// Set the CR3 / DirectoryTableBase value (adds a "cpu" group).
461    pub fn cr3(mut self, cr3: u64) -> Self {
462        self.cr3 = Some(cr3);
463        self
464    }
465
466    /// Build the complete VMware state file as a byte vector.
467    pub fn build(self) -> Vec<u8> {
468        let group_count: u32 = if self.cr3.is_some() { 2 } else { 1 };
469
470        // Header: 12 bytes
471        let header_size = 12usize;
472        let group_entry_size = 80usize;
473        let groups_size = group_count as usize * group_entry_size;
474
475        let mut out = Vec::new();
476
477        // File header
478        out.extend_from_slice(&0xBED2BED0u32.to_le_bytes()); // magic
479        out.extend_from_slice(&0u32.to_le_bytes()); // unknown
480        out.extend_from_slice(&group_count.to_le_bytes()); // group_count
481
482        // Reserve space for group entries — we'll fill tags_offset later
483        let groups_start = out.len();
484        out.resize(header_size + groups_size, 0);
485
486        // --- "memory" group tags ---
487        let memory_tags_offset = out.len() as u64;
488
489        // Write region tags: for each region, emit regionPPN + regionBytes pair
490        for (paddr, data) in &self.memory_regions {
491            // regionPPN tag: flags=0x06, name="regionPPN", data=paddr as u64
492            let name = b"regionPPN";
493            out.push(0x06); // flags: large data with explicit size
494            out.push(name.len() as u8);
495            out.extend_from_slice(name);
496            out.extend_from_slice(&8u32.to_le_bytes()); // data_length = 8
497            out.extend_from_slice(&paddr.to_le_bytes());
498
499            // regionBytes tag: flags=0x06, name="regionBytes", data=raw bytes
500            let name = b"regionBytes";
501            out.push(0x06); // flags: large data with explicit size
502            out.push(name.len() as u8);
503            out.extend_from_slice(name);
504            let data_len = data.len() as u32;
505            out.extend_from_slice(&data_len.to_le_bytes()); // data_length
506            out.extend_from_slice(data);
507        }
508
509        // Tag terminator
510        out.push(0x00);
511
512        // Fill "memory" group entry
513        {
514            let entry_offset = groups_start;
515            let name = b"memory";
516            out[entry_offset..entry_offset + name.len()].copy_from_slice(name);
517            // name is null-terminated, rest of 64 bytes is already zero
518            let tags_off_pos = entry_offset + 64;
519            out[tags_off_pos..tags_off_pos + 8].copy_from_slice(&memory_tags_offset.to_le_bytes());
520            // padding 8 bytes already zero
521        }
522
523        // --- Optional "cpu" group ---
524        if let Some(cr3_val) = self.cr3 {
525            let cpu_tags_offset = out.len() as u64;
526
527            // CR3 tag: flags=0x46 (indexed + 8-byte data)
528            let name = b"CR3";
529            out.push(0x46); // flags
530            out.push(name.len() as u8);
531            out.extend_from_slice(name);
532            out.push(0x00); // index0 = CPU 0
533            out.push(0x03); // index1 = CR register 3
534            out.extend_from_slice(&cr3_val.to_le_bytes());
535
536            // Tag terminator
537            out.push(0x00);
538
539            // Fill "cpu" group entry (second entry)
540            let entry_offset = groups_start + group_entry_size;
541            let name = b"cpu";
542            out[entry_offset..entry_offset + name.len()].copy_from_slice(name);
543            let tags_off_pos = entry_offset + 64;
544            out[tags_off_pos..tags_off_pos + 8].copy_from_slice(&cpu_tags_offset.to_le_bytes());
545        }
546
547        out
548    }
549}
550
551/// Compress data using `lzxpress::data::compress`, falling back to a
552/// literal-only Xpress encoding if the library's round-trip is broken
553/// for the given input.
554///
555/// The literal-only format is: every 32 bytes of input get a 4-byte flags
556/// word (all zeros = all literals) followed by the 32 raw bytes.
557fn xpress_compress_safe(data: &[u8]) -> Vec<u8> {
558    // Try the library compressor first.
559    if let Ok(compressed) = lzxpress::data::compress(data) {
560        // Verify round-trip; the library has known issues with certain patterns.
561        if let Ok(decompressed) = lzxpress::data::decompress(&compressed) {
562            if decompressed == data {
563                return compressed;
564            }
565        }
566    }
567
568    // Fallback: produce literal-only Xpress output.
569    // Format: repeated blocks of [flags_u32_le=0x00000000][32 literal bytes].
570    // A flags word of 0 means all 32 bits are 0, so all 32 symbols are literals.
571    let mut out = Vec::with_capacity(data.len() + data.len() / 32 * 4 + 8);
572    let mut pos = 0;
573    while pos < data.len() {
574        let chunk_len = (data.len() - pos).min(32);
575        // flags = 0 means every bit is 0 = literal
576        out.extend_from_slice(&0u32.to_le_bytes());
577        out.extend_from_slice(&data[pos..pos + chunk_len]);
578        pos += chunk_len;
579    }
580    out
581}
582
583/// Build a synthetic Windows hibernation file (`hiberfil.sys`).
584///
585/// Produces a `PO_MEMORY_IMAGE` header with "hibr" magic, processor state
586/// page with CR3, a page table page with PFN entries, and Xpress LZ77
587/// compressed data blocks.
588///
589/// Layout:
590/// - Page 0 (offset 0x0000): PO\_MEMORY\_IMAGE header with "hibr" magic at 0x00,
591///   `LengthSelf` at 0x0C (256 = 64-bit), `FirstTablePage` at 0x68 (value 2).
592/// - Page 1 (offset 0x1000): Processor state page with CR3 at offset 0x28.
593/// - Page 2 (offset 0x2000): Page table -- array of PFN entries (u64),
594///   terminated by `0xFFFF_FFFF_FFFF_FFFF`.
595/// - After header pages: Xpress LZ77 compressed blocks.
596///   Block header: sig(8) + num\_pages\_minus\_1(1) + compressed\_size\_field(3 bytes)
597///   + padding to 0x20 + compressed data.
598pub struct HiberfilBuilder {
599    pages: Vec<(u64, [u8; 4096])>,
600    cr3: u64,
601}
602
603impl Default for HiberfilBuilder {
604    fn default() -> Self {
605        Self {
606            pages: Vec::new(),
607            cr3: 0x1ab000,
608        }
609    }
610}
611
612impl HiberfilBuilder {
613    /// Create a new builder with default CR3 (`0x1ab000`).
614    pub fn new() -> Self {
615        Self::default()
616    }
617
618    /// Set the CR3 / `DirectoryTableBase` value stored in the processor state page.
619    pub fn cr3(mut self, cr3: u64) -> Self {
620        self.cr3 = cr3;
621        self
622    }
623
624    /// Add a physical memory page at the given PFN.
625    pub fn add_page(mut self, pfn: u64, data: &[u8; 4096]) -> Self {
626        self.pages.push((pfn, *data));
627        self
628    }
629
630    /// Build the complete hibernation file as a byte vector.
631    pub fn build(self) -> Vec<u8> {
632        const PAGE_SIZE: usize = 4096;
633        const HIBR_MAGIC: u32 = 0x7262_6968; // "hibr" LE
634        const LENGTH_SELF_64: u32 = 256;
635        const XPRESS_SIG: [u8; 8] = [0x81, 0x81, b'x', b'p', b'r', b'e', b's', b's'];
636        const BLOCK_HEADER_SIZE: usize = 0x20;
637
638        let mut out = vec![0u8; 3 * PAGE_SIZE]; // pages 0, 1, 2
639
640        // --- Page 0: PO_MEMORY_IMAGE header ---
641        // Magic "hibr" at offset 0x00
642        out[0x00..0x04].copy_from_slice(&HIBR_MAGIC.to_le_bytes());
643        // LengthSelf at offset 0x0C: 256 indicates 64-bit
644        out[0x0C..0x10].copy_from_slice(&LENGTH_SELF_64.to_le_bytes());
645        // FirstTablePage at offset 0x68: page 2
646        out[0x68..0x70].copy_from_slice(&2u64.to_le_bytes());
647
648        // --- Page 1: Processor state ---
649        // CR3 at offset 0x28 within page 1
650        let cr3_offset = PAGE_SIZE + 0x28;
651        out[cr3_offset..cr3_offset + 8].copy_from_slice(&self.cr3.to_le_bytes());
652
653        // --- Page 2: Page table ---
654        // Array of PFN entries (u64), terminated by sentinel 0xFFFFFFFFFFFFFFFF
655        let table_base = 2 * PAGE_SIZE;
656        let mut table_offset = 0usize;
657        for (pfn, _) in &self.pages {
658            out[table_base + table_offset..table_base + table_offset + 8]
659                .copy_from_slice(&pfn.to_le_bytes());
660            table_offset += 8;
661        }
662        // Sentinel terminator
663        out[table_base + table_offset..table_base + table_offset + 8]
664            .copy_from_slice(&u64::MAX.to_le_bytes());
665
666        // --- Compressed data blocks ---
667        // Each page gets its own Xpress block.
668        for (_, page_data) in &self.pages {
669            let compressed = xpress_compress_safe(page_data);
670
671            // compressed_size_field = (compressed_len * 4) - 1, stored as 3 bytes LE
672            let compressed_size_field = (compressed.len() * 4 - 1) as u32;
673            let num_pages_minus_1: u8 = 0; // single page per block
674
675            let mut block = Vec::new();
676            // 8-byte signature
677            block.extend_from_slice(&XPRESS_SIG);
678            // 1 byte: num_pages_minus_1
679            block.push(num_pages_minus_1);
680            // 3 bytes: compressed_size_field (LE)
681            block.push(compressed_size_field as u8);
682            block.push((compressed_size_field >> 8) as u8);
683            block.push((compressed_size_field >> 16) as u8);
684            // Pad to BLOCK_HEADER_SIZE (0x20 = 32 bytes)
685            block.resize(BLOCK_HEADER_SIZE, 0);
686            // Compressed data
687            block.extend_from_slice(&compressed);
688
689            out.extend_from_slice(&block);
690        }
691
692        out
693    }
694}
695
696/// Build a synthetic kdump (makedumpfile) dump for testing.
697///
698/// Produces the `disk_dump_header` + `kdump_sub_header` + bitmaps + page
699/// descriptors + compressed page data layout used by makedumpfile and
700/// crash-utility.
701///
702/// File layout (block_size = 4096 by default):
703/// - Block 0: `disk_dump_header`
704/// - Block 1: `kdump_sub_header` (mostly zeros for test)
705/// - Blocks 2..2+N: 1st bitmap (valid PFNs)
706/// - Blocks 2+N..2+2N: 2nd bitmap (dumped PFNs)
707/// - After bitmaps: `page_desc[]` array (24 bytes each, block-aligned)
708/// - After descs: compressed page data
709///
710/// `disk_dump_header` (Block 0):
711/// - 0x00: signature "KDUMP   " (8 bytes, 3 trailing spaces)
712/// - 0x08: header_version = 6 (i32)
713/// - 0x0C: utsname (390 bytes = 6 fields * 65, zeros)
714/// - Aligned to 4 after utsname: block_size(i32) + sub_hdr_size(i32=1) +
715///   bitmap_blocks(u32) + max_mapnr(u32)
716///
717/// `page_desc` (24 bytes each):
718/// - offset: i64 (file offset of compressed data)
719/// - size: u32 (compressed size)
720/// - flags: u32 (compression method)
721/// - page_flags: u64 (kernel flags, 0 for test)
722pub struct KdumpBuilder {
723    pages: Vec<(u64, Vec<u8>)>,
724    compression: u32,
725    block_size: u32,
726}
727
728impl Default for KdumpBuilder {
729    fn default() -> Self {
730        Self {
731            pages: Vec::new(),
732            compression: 0x04, // snappy
733            block_size: 4096,
734        }
735    }
736}
737
738impl KdumpBuilder {
739    /// Create a new builder with defaults: block_size=4096, compression=snappy (0x04).
740    pub fn new() -> Self {
741        Self::default()
742    }
743
744    /// Set the block size (must be a power of 2, typically 4096).
745    pub fn block_size(mut self, bs: u32) -> Self {
746        self.block_size = bs;
747        self
748    }
749
750    /// Set the compression flags for page data.
751    /// - 0x00: uncompressed
752    /// - 0x01: zlib
753    /// - 0x04: snappy
754    /// - 0x20: zstd (stored as minimal zstd frame)
755    pub fn compression(mut self, flags: u32) -> Self {
756        self.compression = flags;
757        self
758    }
759
760    /// Add a physical page at the given PFN with `data` (must be exactly block_size bytes).
761    pub fn add_page(mut self, pfn: u64, data: &[u8]) -> Self {
762        self.pages.push((pfn, data.to_vec()));
763        self
764    }
765
766    /// Build the complete kdump file as a byte vector.
767    pub fn build(self) -> Vec<u8> {
768        let bs = self.block_size as usize;
769
770        // Determine max PFN for bitmap sizing.
771        let max_pfn = self
772            .pages
773            .iter()
774            .map(|(pfn, _)| *pfn)
775            .max()
776            .map_or(0, |p| p + 1);
777
778        // Bitmap: one bit per PFN up to max_pfn, padded to block_size.
779        let bitmap_bits = max_pfn as usize;
780        let bitmap_bytes_raw = bitmap_bits.div_ceil(8);
781        let bitmap_blocks = bitmap_bytes_raw.div_ceil(bs);
782        let bitmap_bytes = bitmap_blocks * bs;
783
784        // Build bitmaps: both bitmaps are identical (valid == dumped).
785        let mut bitmap = vec![0u8; bitmap_bytes];
786        for (pfn, _) in &self.pages {
787            let pfn = *pfn as usize;
788            if pfn / 8 < bitmap.len() {
789                bitmap[pfn / 8] |= 1 << (pfn % 8);
790            }
791        }
792
793        // Compress each page.
794        let mut compressed_pages: Vec<(u32, Vec<u8>)> = Vec::new(); // (flags, data)
795        for (_, page_data) in &self.pages {
796            let (flags, compressed) = self.compress_page(page_data);
797            compressed_pages.push((flags, compressed));
798        }
799
800        // Layout calculation:
801        // Block 0: disk_dump_header
802        // Block 1: kdump_sub_header
803        // Blocks 2..2+bitmap_blocks: 1st bitmap
804        // Blocks 2+bitmap_blocks..2+2*bitmap_blocks: 2nd bitmap
805        let desc_start_block = 2 + 2 * bitmap_blocks;
806        let desc_start = desc_start_block * bs;
807
808        // Sort pages by PFN for descriptor ordering.
809        let mut indexed_pages: Vec<(usize, u64)> = self
810            .pages
811            .iter()
812            .enumerate()
813            .map(|(i, (pfn, _))| (i, *pfn))
814            .collect();
815        indexed_pages.sort_by_key(|(_, pfn)| *pfn);
816
817        // page_desc array: 24 bytes each, block-aligned total.
818        let num_descs = indexed_pages.len();
819        let descs_raw_size = num_descs * 24;
820        let descs_padded = descs_raw_size.div_ceil(bs) * bs;
821        let data_start = desc_start + descs_padded;
822
823        // Compute file offsets for each page's compressed data.
824        let mut data_offsets: Vec<usize> = Vec::new();
825        let mut cur_offset = data_start;
826        for (orig_idx, _) in &indexed_pages {
827            data_offsets.push(cur_offset);
828            cur_offset += compressed_pages[*orig_idx].1.len();
829        }
830
831        // --- Assemble the file ---
832        let total_size = cur_offset;
833        let mut out = vec![0u8; total_size];
834
835        // Block 0: disk_dump_header
836        // Signature "KDUMP   " (8 bytes)
837        out[0x00..0x08].copy_from_slice(b"KDUMP   ");
838        // header_version = 6 (i32 LE)
839        out[0x08..0x0C].copy_from_slice(&6i32.to_le_bytes());
840        // utsname: 390 bytes of zeros (already zero)
841        // Aligned offset after utsname: (0x0C + 390 + 3) & !3 = 0x19C
842        let fields_off = (0x0C + 390 + 3) & !3; // 0x19C
843                                                // block_size (i32)
844        #[allow(clippy::cast_possible_wrap)]
845        let block_size_i32 = self.block_size as i32;
846        out[fields_off..fields_off + 4].copy_from_slice(&block_size_i32.to_le_bytes());
847        // sub_hdr_size (i32) = 1
848        out[fields_off + 4..fields_off + 8].copy_from_slice(&1i32.to_le_bytes());
849        // bitmap_blocks (u32)
850        out[fields_off + 8..fields_off + 12].copy_from_slice(&(bitmap_blocks as u32).to_le_bytes());
851        // max_mapnr (u32)
852        out[fields_off + 12..fields_off + 16].copy_from_slice(&(max_pfn as u32).to_le_bytes());
853
854        // Block 1: kdump_sub_header (all zeros, already done)
855
856        // Blocks 2..2+N: 1st bitmap
857        let bm1_start = 2 * bs;
858        out[bm1_start..bm1_start + bitmap.len()].copy_from_slice(&bitmap);
859
860        // Blocks 2+N..2+2N: 2nd bitmap (same as 1st)
861        let bm2_start = (2 + bitmap_blocks) * bs;
862        out[bm2_start..bm2_start + bitmap.len()].copy_from_slice(&bitmap);
863
864        // Page descriptors
865        for (desc_idx, (orig_idx, _)) in indexed_pages.iter().enumerate() {
866            let d_off = desc_start + desc_idx * 24;
867            let (flags, ref compressed) = compressed_pages[*orig_idx];
868            // offset: i64
869            out[d_off..d_off + 8].copy_from_slice(
870                &{
871                    #[allow(clippy::cast_possible_wrap)]
872                    let offset_i64 = data_offsets[desc_idx] as i64;
873                    offset_i64
874                }
875                .to_le_bytes(),
876            );
877            // size: u32
878            out[d_off + 8..d_off + 12].copy_from_slice(&(compressed.len() as u32).to_le_bytes());
879            // flags: u32
880            out[d_off + 12..d_off + 16].copy_from_slice(&flags.to_le_bytes());
881            // page_flags: u64 = 0 (already zero)
882        }
883
884        // Compressed page data
885        for (desc_idx, (orig_idx, _)) in indexed_pages.iter().enumerate() {
886            let offset = data_offsets[desc_idx];
887            let data = &compressed_pages[*orig_idx].1;
888            out[offset..offset + data.len()].copy_from_slice(data);
889        }
890
891        out
892    }
893
894    /// Compress a single page based on the configured compression method.
895    /// Returns (flags, compressed_data).
896    fn compress_page(&self, data: &[u8]) -> (u32, Vec<u8>) {
897        match self.compression {
898            0x00 => {
899                // Uncompressed: flags=0, data is raw
900                (0x00, data.to_vec())
901            }
902            0x01 => {
903                // Zlib
904                use std::io::Write;
905                let mut encoder =
906                    flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
907                encoder.write_all(data).expect("zlib compress write");
908                let compressed = encoder.finish().expect("zlib compress finish");
909                (0x01, compressed)
910            }
911            0x04 => {
912                // Snappy
913                let mut encoder = snap::raw::Encoder::new();
914                let compressed = encoder.compress_vec(data).expect("snappy compress");
915                (0x04, compressed)
916            }
917            0x20 => {
918                // Zstd: produce a minimal valid zstd frame.
919                let compressed = Self::zstd_compress_minimal(data);
920                (0x20, compressed)
921            }
922            _ => (0x00, data.to_vec()),
923        }
924    }
925
926    /// Produce a minimal valid zstd frame that stores raw (uncompressed) data.
927    ///
928    /// Zstd frame format (minimal raw block):
929    /// - 4-byte magic: 0xFD2FB528
930    /// - Frame header descriptor byte
931    /// - Window descriptor byte
932    /// - Block header: 3 bytes (last_block=1, block_type=raw, block_size)
933    /// - Raw data
934    fn zstd_compress_minimal(data: &[u8]) -> Vec<u8> {
935        let mut out = Vec::new();
936        // Magic number
937        out.extend_from_slice(&0xFD2FB528u32.to_le_bytes());
938        // Frame header descriptor: 0x00
939        // single_segment=0, so window descriptor is present
940        // no checksum, no dict_id, fcs_field_size=0
941        out.push(0x00);
942        // Window descriptor: exponent=0, mantissa=0 => window_size = 1 << 10 = 1024
943        out.push(0x00);
944        // Block header: 3 bytes LE
945        // bit 0: last_block = 1
946        // bits 2-1: block_type = 0 (raw)
947        // bits 23-3: block_size = data.len()
948        let block_header: u32 = 1 | ((data.len() as u32) << 3);
949        let bh_bytes = block_header.to_le_bytes();
950        out.extend_from_slice(&bh_bytes[..3]);
951        // Raw data
952        out.extend_from_slice(data);
953        out
954    }
955}
956
957/// Build a synthetic kdump (makedumpfile) dump for testing.
958///
959/// Produces the `disk_dump_header` + `kdump_sub_header` + bitmaps + page
960/// descriptors + compressed page data layout used by makedumpfile and
961/// crash-utility.
962#[cfg(test)]
963mod tests {
964    use super::*;
965    use crate::avml::AvmlProvider;
966    use crate::elf_core::ElfCoreProvider;
967    use crate::lime::LimeProvider;
968    use crate::PhysicalMemoryProvider;
969
970    // ─── LimeBuilder ─────────────────────────────────────────────────────────
971
972    #[test]
973    fn lime_builder_roundtrip() {
974        let data = vec![0xAB; 64];
975        let bytes = LimeBuilder::new().add_range(0x1000, &data).build();
976        let provider = LimeProvider::from_bytes(&bytes).unwrap();
977        assert_eq!(provider.ranges().len(), 1);
978        assert_eq!(provider.ranges()[0].start, 0x1000);
979        assert_eq!(provider.ranges()[0].end, 0x1000 + 64);
980
981        let mut buf = vec![0u8; 64];
982        let n = provider.read_phys(0x1000, &mut buf).unwrap();
983        assert_eq!(n, 64);
984        assert!(buf.iter().all(|&b| b == 0xAB));
985    }
986
987    #[test]
988    fn lime_builder_two_ranges() {
989        let bytes = LimeBuilder::new()
990            .add_range(0x0000, &[0x11; 32])
991            .add_range(0x8000, &[0x22; 48])
992            .build();
993        let provider = LimeProvider::from_bytes(&bytes).unwrap();
994        assert_eq!(provider.ranges().len(), 2);
995        assert_eq!(provider.total_size(), 32 + 48);
996
997        let mut buf = [0u8; 4];
998        let n = provider.read_phys(0x0000, &mut buf).unwrap();
999        assert_eq!(n, 4);
1000        assert_eq!(buf, [0x11; 4]);
1001
1002        let n = provider.read_phys(0x8000, &mut buf).unwrap();
1003        assert_eq!(n, 4);
1004        assert_eq!(buf, [0x22; 4]);
1005    }
1006
1007    #[test]
1008    fn lime_builder_empty_produces_parseable_bytes() {
1009        // Empty builder: zero ranges, produces empty (or near-empty) output
1010        let bytes = LimeBuilder::new().build();
1011        // Should parse without panic and yield zero ranges
1012        let provider = LimeProvider::from_bytes(&bytes).unwrap();
1013        assert_eq!(provider.ranges().len(), 0);
1014        assert_eq!(provider.total_size(), 0);
1015    }
1016
1017    // ─── AvmlBuilder ─────────────────────────────────────────────────────────
1018
1019    #[test]
1020    fn avml_builder_roundtrip() {
1021        let data = vec![0xCD; 128];
1022        let bytes = AvmlBuilder::new().add_range(0x2000, &data).build();
1023        let provider = AvmlProvider::from_bytes(&bytes).unwrap();
1024        assert_eq!(provider.ranges().len(), 1);
1025        assert_eq!(provider.ranges()[0].start, 0x2000);
1026        assert_eq!(provider.ranges()[0].end, 0x2000 + 128);
1027
1028        let mut buf = vec![0u8; 128];
1029        let n = provider.read_phys(0x2000, &mut buf).unwrap();
1030        assert_eq!(n, 128);
1031        assert!(buf.iter().all(|&b| b == 0xCD));
1032    }
1033
1034    #[test]
1035    fn avml_builder_two_ranges() {
1036        let bytes = AvmlBuilder::new()
1037            .add_range(0x0000, &[0xAA; 64])
1038            .add_range(0x4000, &[0xBB; 96])
1039            .build();
1040        let provider = AvmlProvider::from_bytes(&bytes).unwrap();
1041        assert_eq!(provider.ranges().len(), 2);
1042        assert_eq!(provider.total_size(), 64 + 96);
1043
1044        let mut buf = [0u8; 4];
1045        let n = provider.read_phys(0x0000, &mut buf).unwrap();
1046        assert_eq!(n, 4);
1047        assert_eq!(buf, [0xAA; 4]);
1048
1049        let n = provider.read_phys(0x4000, &mut buf).unwrap();
1050        assert_eq!(n, 4);
1051        assert_eq!(buf, [0xBB; 4]);
1052    }
1053
1054    // ─── ElfCoreBuilder ──────────────────────────────────────────────────────
1055
1056    #[test]
1057    fn elf_builder_roundtrip() {
1058        let data = vec![0xEF; 256];
1059        let bytes = ElfCoreBuilder::new().add_segment(0x3000, &data).build();
1060        let provider = ElfCoreProvider::from_bytes(bytes).unwrap();
1061        assert_eq!(provider.ranges().len(), 1);
1062        assert_eq!(provider.ranges()[0].start, 0x3000);
1063        assert_eq!(provider.ranges()[0].end, 0x3000 + 256);
1064
1065        let mut buf = vec![0u8; 256];
1066        let n = provider.read_phys(0x3000, &mut buf).unwrap();
1067        assert_eq!(n, 256);
1068        assert!(buf.iter().all(|&b| b == 0xEF));
1069    }
1070
1071    #[test]
1072    fn elf_builder_two_segments() {
1073        let bytes = ElfCoreBuilder::new()
1074            .add_segment(0x0000, &[0x55; 512])
1075            .add_segment(0x1000_0000, &[0x66; 128])
1076            .build();
1077        let provider = ElfCoreProvider::from_bytes(bytes).unwrap();
1078        assert_eq!(provider.ranges().len(), 2);
1079        assert_eq!(provider.total_size(), 512 + 128);
1080
1081        let mut buf = [0u8; 4];
1082        let n = provider.read_phys(0x0000, &mut buf).unwrap();
1083        assert_eq!(n, 4);
1084        assert_eq!(buf, [0x55; 4]);
1085
1086        let n = provider.read_phys(0x1000_0000, &mut buf).unwrap();
1087        assert_eq!(n, 4);
1088        assert_eq!(buf, [0x66; 4]);
1089    }
1090}