Skip to main content

memf_core/
pagefile.rs

1//! Pagefile and swapfile sources for resolving paged-out memory.
2
3use std::collections::HashMap;
4use std::path::Path;
5
6use crate::Result;
7
8/// A source of paged-out memory pages (pagefile.sys, swapfile.sys, etc.).
9pub trait PagefileSource: Send + Sync {
10    /// Which pagefile number this source handles (0 = pagefile.sys, 1-15 = secondary).
11    fn pagefile_number(&self) -> u8;
12
13    /// Read a 4KB page at the given page offset.
14    /// Returns `Ok(None)` if the offset is beyond the file's page count.
15    fn read_page(&self, page_offset: u64) -> Result<Option<[u8; 4096]>>;
16}
17
18/// Provider for Windows pagefile.sys — a flat file of 4KB pages.
19///
20/// pagefile.sys has no headers and no compression. Each page occupies
21/// exactly 4096 bytes at offset `page_index * 0x1000`.
22pub struct PagefileProvider {
23    mmap: memmap2::Mmap,
24    pagefile_num: u8,
25    page_count: u64,
26}
27
28impl PagefileProvider {
29    /// Open a pagefile and memory-map it.
30    #[allow(unsafe_code)]
31    pub fn open(path: &Path, pagefile_num: u8) -> Result<Self> {
32        let file = std::fs::File::open(path)
33            .map_err(|e| crate::Error::Physical(memf_format::Error::Io(e)))?;
34        let mmap = unsafe { memmap2::MmapOptions::new().map(&file) }
35            .map_err(|e| crate::Error::Physical(memf_format::Error::Io(e)))?;
36        let page_count = mmap.len() as u64 / 0x1000;
37        Ok(Self {
38            mmap,
39            pagefile_num,
40            page_count,
41        })
42    }
43}
44
45impl PagefileSource for PagefileProvider {
46    fn pagefile_number(&self) -> u8 {
47        self.pagefile_num
48    }
49
50    fn read_page(&self, page_offset: u64) -> Result<Option<[u8; 4096]>> {
51        if page_offset >= self.page_count {
52            return Ok(None);
53        }
54        let byte_offset = page_offset as usize * 0x1000;
55        let mut page = [0u8; 4096];
56        page.copy_from_slice(&self.mmap[byte_offset..byte_offset + 4096]);
57        Ok(Some(page))
58    }
59}
60
61const SM_MAGIC: u16 = 0x4D53; // "SM" in little-endian: 'S'=0x53 at byte 0, 'M'=0x4D at byte 1
62const SM_HEADER_SIZE: usize = 20;
63const REGION_ENTRY_SIZE: usize = 24;
64
65/// Provider for Windows swapfile.sys — SM header format with optional Xpress compression.
66#[derive(Debug)]
67pub struct SwapfileProvider {
68    mmap: memmap2::Mmap,
69    /// Maps page offset -> (file_offset, compressed_size).
70    index: HashMap<u64, (u64, u32)>,
71}
72
73impl SwapfileProvider {
74    /// Open a swapfile.sys and parse its SM header to build the page index.
75    #[allow(unsafe_code)]
76    pub fn open(path: &Path) -> Result<Self> {
77        let file = std::fs::File::open(path)
78            .map_err(|e| crate::Error::Physical(memf_format::Error::Io(e)))?;
79        let mmap = unsafe { memmap2::MmapOptions::new().map(&file) }
80            .map_err(|e| crate::Error::Physical(memf_format::Error::Io(e)))?;
81
82        if mmap.len() < SM_HEADER_SIZE {
83            return Err(crate::Error::Physical(memf_format::Error::Corrupt(
84                "swapfile too small for SM header".into(),
85            )));
86        }
87
88        let magic = u16::from_le_bytes([mmap[0], mmap[1]]);
89        if magic != SM_MAGIC {
90            return Err(crate::Error::Physical(memf_format::Error::Corrupt(
91                format!("invalid SM magic: expected 0x4D53, got {magic:#06X}"),
92            )));
93        }
94
95        let region_table_offset = u64::from_le_bytes(mmap[8..16].try_into().unwrap()) as usize;
96        let region_count = u32::from_le_bytes(mmap[16..20].try_into().unwrap()) as usize;
97
98        let mut index = HashMap::new();
99
100        for i in 0..region_count {
101            let entry_offset = region_table_offset + i * REGION_ENTRY_SIZE;
102            if entry_offset + REGION_ENTRY_SIZE > mmap.len() {
103                return Err(crate::Error::Physical(memf_format::Error::Corrupt(
104                    format!("SM region entry {i} at offset {entry_offset:#x} truncated"),
105                )));
106            }
107
108            let page_offset =
109                u64::from_le_bytes(mmap[entry_offset..entry_offset + 8].try_into().unwrap());
110            let file_offset = u64::from_le_bytes(
111                mmap[entry_offset + 8..entry_offset + 16]
112                    .try_into()
113                    .unwrap(),
114            );
115            let page_count = u32::from_le_bytes(
116                mmap[entry_offset + 16..entry_offset + 20]
117                    .try_into()
118                    .unwrap(),
119            );
120            let compressed_size = u32::from_le_bytes(
121                mmap[entry_offset + 20..entry_offset + 24]
122                    .try_into()
123                    .unwrap(),
124            );
125
126            for p in 0..u64::from(page_count) {
127                let fo = file_offset + p * u64::from(compressed_size);
128                index.insert(page_offset + p, (fo, compressed_size));
129            }
130        }
131
132        Ok(Self { mmap, index })
133    }
134}
135
136impl PagefileSource for SwapfileProvider {
137    fn pagefile_number(&self) -> u8 {
138        2 // Windows convention for swapfile virtual store
139    }
140
141    fn read_page(&self, page_offset: u64) -> Result<Option<[u8; 4096]>> {
142        let Some(&(file_offset, compressed_size)) = self.index.get(&page_offset) else {
143            return Ok(None);
144        };
145
146        let fo = file_offset as usize;
147        let cs = compressed_size as usize;
148
149        if fo + cs > self.mmap.len() {
150            return Err(crate::Error::Physical(memf_format::Error::Corrupt(
151                format!(
152                    "swapfile page at offset {page_offset:#x}: data at {fo:#x}+{cs:#x} beyond file"
153                ),
154            )));
155        }
156
157        if compressed_size == 0x1000 {
158            let mut page = [0u8; 4096];
159            page.copy_from_slice(&self.mmap[fo..fo + 4096]);
160            Ok(Some(page))
161        } else {
162            let compressed_data = &self.mmap[fo..fo + cs];
163            let decompressed = lzxpress::data::decompress(compressed_data).map_err(|e| {
164                crate::Error::Physical(memf_format::Error::Decompression(format!(
165                    "swapfile xpress decompress at page {page_offset:#x}: {e:?}"
166                )))
167            })?;
168            if decompressed.len() < 4096 {
169                return Err(crate::Error::Physical(memf_format::Error::Corrupt(
170                    format!(
171                        "swapfile decompressed page {page_offset:#x}: {} bytes (expected 4096)",
172                        decompressed.len()
173                    ),
174                )));
175            }
176            let mut page = [0u8; 4096];
177            page.copy_from_slice(&decompressed[..4096]);
178            Ok(Some(page))
179        }
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use std::io::Write;
187
188    fn create_temp_pagefile(num_pages: usize) -> (tempfile::NamedTempFile, Vec<[u8; 4096]>) {
189        let mut file = tempfile::NamedTempFile::new().unwrap();
190        let mut pages = Vec::new();
191        for i in 0..num_pages {
192            let mut page = [0u8; 4096];
193            page[0..4].copy_from_slice(&(i as u32).to_le_bytes());
194            page[4] = 0xFF;
195            file.write_all(&page).unwrap();
196            pages.push(page);
197        }
198        file.flush().unwrap();
199        (file, pages)
200    }
201
202    #[test]
203    fn pagefile_provider_open_and_read() {
204        let (file, pages) = create_temp_pagefile(4);
205        let provider = PagefileProvider::open(file.path(), 0).unwrap();
206        assert_eq!(provider.pagefile_number(), 0);
207
208        let page = provider.read_page(0).unwrap().unwrap();
209        assert_eq!(page, pages[0]);
210
211        let page2 = provider.read_page(2).unwrap().unwrap();
212        assert_eq!(page2, pages[2]);
213    }
214
215    #[test]
216    fn pagefile_provider_out_of_range() {
217        let (file, _pages) = create_temp_pagefile(4);
218        let provider = PagefileProvider::open(file.path(), 0).unwrap();
219        assert!(provider.read_page(4).unwrap().is_none());
220        assert!(provider.read_page(9999).unwrap().is_none());
221    }
222
223    #[test]
224    fn pagefile_provider_number() {
225        let (file, _) = create_temp_pagefile(1);
226        let provider = PagefileProvider::open(file.path(), 3).unwrap();
227        assert_eq!(provider.pagefile_number(), 3);
228    }
229
230    #[test]
231    fn swapfile_provider_invalid_magic() {
232        let mut file = tempfile::NamedTempFile::new().unwrap();
233        file.write_all(&[0x00; 4096]).unwrap();
234        file.flush().unwrap();
235        let result = SwapfileProvider::open(file.path());
236        assert!(result.is_err());
237        let msg = result.unwrap_err().to_string();
238        assert!(
239            msg.contains("SM") || msg.contains("magic"),
240            "error should mention SM magic: {msg}"
241        );
242    }
243
244    #[test]
245    fn swapfile_provider_too_small() {
246        let mut file = tempfile::NamedTempFile::new().unwrap();
247        file.write_all(&[0x53, 0x4D]).unwrap(); // "SM" but too short
248        file.flush().unwrap();
249        let result = SwapfileProvider::open(file.path());
250        assert!(result.is_err());
251    }
252
253    #[test]
254    fn swapfile_provider_valid_sm_header() {
255        // Build a synthetic SM swapfile with one uncompressed page
256        let mut data = vec![0u8; 0x3000];
257
258        // SM header at offset 0
259        data[0] = 0x53; // 'S'
260        data[1] = 0x4D; // 'M'
261        data[2..4].copy_from_slice(&1u16.to_le_bytes()); // version = 1
262        data[4..8].copy_from_slice(&0x1000u32.to_le_bytes()); // page_size
263        data[8..16].copy_from_slice(&0x1000u64.to_le_bytes()); // region_table_offset
264        data[16..20].copy_from_slice(&1u32.to_le_bytes()); // region_count
265
266        // Region entry at offset 0x1000:
267        let region_off = 0x1000usize;
268        data[region_off..region_off + 8].copy_from_slice(&5u64.to_le_bytes()); // page_offset = 5
269        data[region_off + 8..region_off + 16].copy_from_slice(&0x1800u64.to_le_bytes()); // file_offset
270        data[region_off + 16..region_off + 20].copy_from_slice(&1u32.to_le_bytes()); // page_count
271        data[region_off + 20..region_off + 24].copy_from_slice(&0x1000u32.to_le_bytes()); // compressed_size (uncompressed)
272
273        // Page data at file offset 0x1800
274        data.resize(0x2800, 0); // ensure enough space: 0x1800 + 0x1000 = 0x2800
275        data[0x1800] = 0x42;
276        data[0x1801] = 0x43;
277        for i in 2..4096 {
278            data[0x1800 + i] = 0xAB;
279        }
280
281        let mut file = tempfile::NamedTempFile::new().unwrap();
282        file.write_all(&data).unwrap();
283        file.flush().unwrap();
284
285        let provider = SwapfileProvider::open(file.path()).unwrap();
286        assert_eq!(provider.pagefile_number(), 2);
287
288        let page = provider.read_page(5).unwrap().unwrap();
289        assert_eq!(page[0], 0x42);
290        assert_eq!(page[1], 0x43);
291        assert_eq!(page[2], 0xAB);
292
293        assert!(provider.read_page(99).unwrap().is_none());
294    }
295
296    #[test]
297    fn swapfile_provider_compressed_page() {
298        let mut original_page = [0u8; 4096];
299        original_page[0..4].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
300        for i in (4..4096).step_by(4) {
301            original_page[i..i + 4].copy_from_slice(&[0x01, 0x02, 0x03, 0x04]);
302        }
303
304        let compressed = lzxpress::data::compress(&original_page).unwrap();
305        assert!(compressed.len() < 4096, "compressed should be smaller");
306
307        let mut data = vec![0u8; 0x3000 + compressed.len()];
308
309        // SM header
310        data[0] = 0x53;
311        data[1] = 0x4D;
312        data[2..4].copy_from_slice(&1u16.to_le_bytes());
313        data[4..8].copy_from_slice(&0x1000u32.to_le_bytes());
314        data[8..16].copy_from_slice(&0x1000u64.to_le_bytes());
315        data[16..20].copy_from_slice(&1u32.to_le_bytes());
316
317        let region_off = 0x1000usize;
318        data[region_off..region_off + 8].copy_from_slice(&7u64.to_le_bytes());
319        data[region_off + 8..region_off + 16].copy_from_slice(&0x1800u64.to_le_bytes());
320        data[region_off + 16..region_off + 20].copy_from_slice(&1u32.to_le_bytes());
321        data[region_off + 20..region_off + 24]
322            .copy_from_slice(&(compressed.len() as u32).to_le_bytes());
323
324        data[0x1800..0x1800 + compressed.len()].copy_from_slice(&compressed);
325
326        let mut file = tempfile::NamedTempFile::new().unwrap();
327        file.write_all(&data).unwrap();
328        file.flush().unwrap();
329
330        let provider = SwapfileProvider::open(file.path()).unwrap();
331        let page = provider.read_page(7).unwrap().unwrap();
332        assert_eq!(page, original_page);
333    }
334}