1use std::collections::HashMap;
4use std::path::Path;
5
6use crate::Result;
7
8fn 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
15fn 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
22pub trait PagefileSource: Send + Sync {
24 fn pagefile_number(&self) -> u8;
26
27 fn read_page(&self, page_offset: u64) -> Result<Option<[u8; 4096]>>;
30}
31
32pub struct PagefileProvider {
37 mmap: memmap2::Mmap,
38 pagefile_num: u8,
39 page_count: u64,
40}
41
42impl PagefileProvider {
43 #[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; const SM_HEADER_SIZE: usize = 20;
77const REGION_ENTRY_SIZE: usize = 24;
78
79#[derive(Debug)]
81pub struct SwapfileProvider {
82 mmap: memmap2::Mmap,
83 index: HashMap<u64, (u64, u32)>,
85}
86
87impl SwapfileProvider {
88 #[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 }
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(); 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 let mut data = vec![0u8; 0x3000];
258
259 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;
269 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;
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 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}