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