1use std::path::Path;
7
8use crate::{Error, FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};
9
10const AVML_MAGIC: u32 = 0x4C4D5641;
11const AVML_VERSION: u32 = 2;
12const HEADER_SIZE: usize = 32;
13
14fn le_u32(data: &[u8], off: usize) -> u32 {
16 data.get(off..off + 4)
17 .and_then(|s| s.try_into().ok())
18 .map_or(0, u32::from_le_bytes)
19}
20
21fn le_u64(data: &[u8], off: usize) -> u64 {
23 data.get(off..off + 8)
24 .and_then(|s| s.try_into().ok())
25 .map_or(0, u64::from_le_bytes)
26}
27
28#[derive(Debug)]
30struct AvmlBlock {
31 range: PhysicalRange,
32 data: Vec<u8>,
34}
35
36#[derive(Debug)]
41pub struct AvmlProvider {
42 blocks: Vec<AvmlBlock>,
43 ranges: Vec<PhysicalRange>,
45}
46
47impl AvmlProvider {
48 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
50 let blocks = parse_blocks(bytes)?;
51 let ranges = blocks.iter().map(|b| b.range.clone()).collect();
52 Ok(Self { blocks, ranges })
53 }
54
55 pub fn from_path(path: &Path) -> Result<Self> {
57 let bytes = std::fs::read(path)?;
58 Self::from_bytes(&bytes)
59 }
60}
61
62fn parse_blocks(data: &[u8]) -> Result<Vec<AvmlBlock>> {
64 let mut blocks = Vec::new();
65 let mut pos = 0usize;
66
67 while pos < data.len() {
68 if pos + HEADER_SIZE > data.len() {
70 return Err(Error::Corrupt(format!(
71 "truncated header at offset {pos:#x}"
72 )));
73 }
74
75 let magic = le_u32(data, pos);
76 let version = le_u32(data, pos + 4);
77 let start = le_u64(data, pos + 8);
78 let end = le_u64(data, pos + 16);
79 if magic != AVML_MAGIC {
82 return Err(Error::Corrupt(format!(
83 "bad magic {magic:#010x} at offset {pos:#x}"
84 )));
85 }
86 if version != AVML_VERSION {
87 return Err(Error::Corrupt(format!(
88 "unsupported AVML version {version} at offset {pos:#x}"
89 )));
90 }
91 if start >= end {
92 return Err(Error::Corrupt(format!(
93 "invalid range start={start:#x} end={end:#x} at offset {pos:#x}"
94 )));
95 }
96
97 let expected_uncompressed = end - start;
98
99 let payload_start = pos + HEADER_SIZE;
100
101 let search_end = (payload_start + expected_uncompressed as usize + 64).min(data.len());
102
103 if search_end < payload_start + 8 {
104 return Err(Error::Corrupt(format!(
105 "block at {pos:#x}: not enough data for trailer"
106 )));
107 }
108
109 let mut trailer_pos: Option<usize> = None;
110 let scan_start = payload_start;
111 let scan_end = search_end - 8;
112
113 let mut i = scan_start;
114 while i <= scan_end {
115 let val = le_u64(data, i);
116 if val == expected_uncompressed {
117 let compressed = &data[payload_start..i];
118 let mut decoder = snap::raw::Decoder::new();
119 match decoder.decompress_vec(compressed) {
120 Ok(decompressed) if decompressed.len() as u64 == expected_uncompressed => {
121 trailer_pos = Some(i);
122 let range = PhysicalRange { start, end };
123 blocks.push(AvmlBlock {
124 range,
125 data: decompressed,
126 });
127 pos = i + 8; break;
129 }
130 _ => {
131 i += 1;
132 continue;
133 }
134 }
135 }
136 i += 1;
137 }
138
139 if trailer_pos.is_none() {
140 return Err(Error::Corrupt(format!(
141 "block at {pos:#x}: could not locate valid Snappy trailer \
142 (expected uncompressed size {expected_uncompressed})"
143 )));
144 }
145 }
146
147 Ok(blocks)
148}
149
150impl PhysicalMemoryProvider for AvmlProvider {
151 fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
152 if buf.is_empty() {
153 return Ok(0);
154 }
155
156 for block in &self.blocks {
157 if block.range.contains_addr(addr) {
158 let offset_in_block = (addr - block.range.start) as usize;
159 let available = block.data.len().saturating_sub(offset_in_block);
160 let to_read = buf.len().min(available);
161 buf[..to_read]
162 .copy_from_slice(&block.data[offset_in_block..offset_in_block + to_read]);
163 return Ok(to_read);
164 }
165 }
166
167 Ok(0)
169 }
170
171 fn ranges(&self) -> &[PhysicalRange] {
172 &self.ranges
173 }
174
175 fn format_name(&self) -> &str {
176 "AVML v2"
177 }
178}
179
180pub struct AvmlPlugin;
182
183impl FormatPlugin for AvmlPlugin {
184 fn name(&self) -> &str {
185 "AVML v2"
186 }
187
188 fn probe(&self, header: &[u8]) -> u8 {
189 if header.len() < 8 {
190 return 0;
191 }
192 let magic = le_u32(header, 0);
193 let version = le_u32(header, 4);
194 if magic == AVML_MAGIC && version == AVML_VERSION {
195 90
196 } else {
197 0
198 }
199 }
200
201 fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
202 Ok(Box::new(AvmlProvider::from_path(path)?))
203 }
204}
205
206inventory::submit!(&AvmlPlugin as &dyn FormatPlugin);
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::test_builders::AvmlBuilder;
212
213 #[test]
217 fn probe_avml_magic() {
218 let data = AvmlBuilder::new().add_range(0x1000, &[0u8; 64]).build();
219 let plugin = AvmlPlugin;
220 assert_eq!(plugin.probe(&data), 90);
221 }
222
223 #[test]
227 fn probe_non_avml() {
228 let data = vec![0u8; 64];
229 let plugin = AvmlPlugin;
230 assert_eq!(plugin.probe(&data), 0);
231 }
232
233 #[test]
237 fn single_range_roundtrip() {
238 let payload: Vec<u8> = (0u8..=255).collect();
239 let dump = AvmlBuilder::new().add_range(0x1000, &payload).build();
240
241 let provider = AvmlProvider::from_bytes(&dump).expect("parse");
242
243 let ranges = provider.ranges();
245 assert_eq!(ranges.len(), 1);
246 assert_eq!(ranges[0].start, 0x1000);
247 assert_eq!(ranges[0].end, 0x1000 + 256);
248
249 assert_eq!(provider.total_size(), 256);
251
252 assert_eq!(provider.format_name(), "AVML v2");
254
255 let mut buf = vec![0u8; 256];
257 let n = provider.read_phys(0x1000, &mut buf).expect("read");
258 assert_eq!(n, 256);
259 assert_eq!(buf, payload);
260
261 let mut buf2 = vec![0u8; 4];
263 let n2 = provider.read_phys(0x1010, &mut buf2).expect("read mid");
264 assert_eq!(n2, 4);
265 assert_eq!(buf2, &payload[0x10..0x14]);
266 }
267
268 #[test]
272 fn two_ranges_roundtrip() {
273 let data_a = vec![0xAAu8; 256];
274 let data_b = vec![0xBBu8; 256];
275 let dump = AvmlBuilder::new()
276 .add_range(0x0000, &data_a)
277 .add_range(0x1000, &data_b)
278 .build();
279
280 let provider = AvmlProvider::from_bytes(&dump).expect("parse");
281
282 let ranges = provider.ranges();
283 assert_eq!(ranges.len(), 2);
284 assert_eq!(
285 ranges[0],
286 PhysicalRange {
287 start: 0x0000,
288 end: 0x0100
289 }
290 );
291 assert_eq!(
292 ranges[1],
293 PhysicalRange {
294 start: 0x1000,
295 end: 0x1100
296 }
297 );
298 assert_eq!(provider.total_size(), 512);
299
300 let mut buf = vec![0u8; 256];
301 let n = provider.read_phys(0x0000, &mut buf).expect("read a");
302 assert_eq!(n, 256);
303 assert_eq!(buf, data_a);
304
305 let n = provider.read_phys(0x1000, &mut buf).expect("read b");
306 assert_eq!(n, 256);
307 assert_eq!(buf, data_b);
308 }
309
310 #[test]
314 fn gap_returns_zero() {
315 let dump = AvmlBuilder::new().add_range(0x1000, &[0xCCu8; 256]).build();
316
317 let provider = AvmlProvider::from_bytes(&dump).expect("parse");
318
319 let mut buf = vec![0u8; 64];
320 let n = provider.read_phys(0x5000, &mut buf).expect("read gap");
321 assert_eq!(n, 0);
322 }
323
324 #[test]
325 fn from_path_roundtrip() {
326 let payload: Vec<u8> = (0u8..=127).collect();
327 let dump = AvmlBuilder::new().add_range(0x2000, &payload).build();
328 let path = std::env::temp_dir().join("memf_test_avml_from_path.avml");
329 std::fs::write(&path, &dump).unwrap();
330 let provider = AvmlProvider::from_path(&path).unwrap();
331 assert_eq!(provider.ranges().len(), 1);
332 assert_eq!(provider.total_size(), 128);
333 assert_eq!(provider.format_name(), "AVML v2");
334 let mut buf = [0u8; 4];
335 let n = provider.read_phys(0x2000, &mut buf).unwrap();
336 assert_eq!(n, 4);
337 assert_eq!(&buf, &[0, 1, 2, 3]);
338 std::fs::remove_file(&path).ok();
339 }
340
341 #[test]
342 fn plugin_name() {
343 let plugin = AvmlPlugin;
344 assert_eq!(plugin.name(), "AVML v2");
345 }
346
347 #[test]
348 fn probe_short_header_returns_zero() {
349 let plugin = AvmlPlugin;
350 assert_eq!(plugin.probe(&[0x41, 0x56, 0x4D]), 0); assert_eq!(plugin.probe(&[]), 0);
352 }
353
354 #[test]
355 fn read_phys_empty_buffer() {
356 let dump = AvmlBuilder::new().add_range(0x1000, &[0xBB; 64]).build();
357 let provider = AvmlProvider::from_bytes(&dump).expect("parse");
358 let mut buf = [];
359 let n = provider.read_phys(0x1000, &mut buf).unwrap();
360 assert_eq!(n, 0);
361 }
362
363 #[test]
370 fn corrupt_header_wrong_version_returns_error() {
371 let mut dump = AvmlBuilder::new().add_range(0x1000, &[0xAA; 64]).build();
372 dump[4..8].copy_from_slice(&99u32.to_le_bytes());
374 let result = AvmlProvider::from_bytes(&dump);
375 assert!(result.is_err(), "wrong version must return an error");
376 let err = result.unwrap_err();
377 assert!(
378 matches!(err, crate::Error::Corrupt(_)),
379 "error must be Corrupt, got: {err}"
380 );
381 assert!(
382 err.to_string().contains("99"),
383 "error message should mention the bad version number"
384 );
385 }
386
387 #[test]
390 fn corrupt_header_wrong_magic_returns_error() {
391 let mut dump = AvmlBuilder::new().add_range(0x1000, &[0xAA; 64]).build();
392 dump[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
394 let result = AvmlProvider::from_bytes(&dump);
395 assert!(result.is_err(), "wrong magic must return an error");
396 assert!(matches!(result.unwrap_err(), crate::Error::Corrupt(_)));
397 }
398
399 #[test]
402 fn truncated_header_returns_error() {
403 let partial: Vec<u8> = vec![
406 0x41, 0x56, 0x4D, 0x4C, 0x02, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x00, ];
411 assert!(
412 partial.len() < 32,
413 "test fixture must be shorter than a full header"
414 );
415 let result = AvmlProvider::from_bytes(&partial);
416 assert!(result.is_err(), "truncated input must return an error");
417 assert!(matches!(result.unwrap_err(), crate::Error::Corrupt(_)));
418 }
419
420 #[test]
424 fn snappy_compressed_block_roundtrip() {
425 let payload: Vec<u8> = (0u8..=255).cycle().take(512).collect();
427 let dump = AvmlBuilder::new().add_range(0x4000, &payload).build();
428
429 let provider = AvmlProvider::from_bytes(&dump).expect("parse snappy dump");
430 assert_eq!(provider.total_size(), 512);
431
432 let mut buf = vec![0u8; 512];
433 let n = provider.read_phys(0x4000, &mut buf).expect("read_phys");
434 assert_eq!(n, 512);
435 assert_eq!(buf, payload, "decompressed data must match original");
436 }
437}