1use std::collections::HashMap;
4use std::path::Path;
5
6use crate::Result;
7
8pub trait PagefileSource: Send + Sync {
10 fn pagefile_number(&self) -> u8;
12
13 fn read_page(&self, page_offset: u64) -> Result<Option<[u8; 4096]>>;
16}
17
18pub struct PagefileProvider {
23 mmap: memmap2::Mmap,
24 pagefile_num: u8,
25 page_count: u64,
26}
27
28impl PagefileProvider {
29 #[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; const SM_HEADER_SIZE: usize = 20;
63const REGION_ENTRY_SIZE: usize = 24;
64
65#[derive(Debug)]
67pub struct SwapfileProvider {
68 mmap: memmap2::Mmap,
69 index: HashMap<u64, (u64, u32)>,
71}
72
73impl SwapfileProvider {
74 #[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 }
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(); 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 let mut data = vec![0u8; 0x3000];
257
258 data[0] = 0x53; data[1] = 0x4D; data[2..4].copy_from_slice(&1u16.to_le_bytes()); data[4..8].copy_from_slice(&0x1000u32.to_le_bytes()); data[8..16].copy_from_slice(&0x1000u64.to_le_bytes()); data[16..20].copy_from_slice(&1u32.to_le_bytes()); let region_off = 0x1000usize;
268 data[region_off..region_off + 8].copy_from_slice(&5u64.to_le_bytes()); data[region_off + 8..region_off + 16].copy_from_slice(&0x1800u64.to_le_bytes()); data[region_off + 16..region_off + 20].copy_from_slice(&1u32.to_le_bytes()); data[region_off + 20..region_off + 24].copy_from_slice(&0x1000u32.to_le_bytes()); data.resize(0x2800, 0); 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 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}