use std::path::Path;
use crate::{Error, FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};
const AVML_MAGIC: u32 = 0x4C4D5641;
const AVML_VERSION: u32 = 2;
const HEADER_SIZE: usize = 32;
fn le_u32(data: &[u8], off: usize) -> u32 {
data.get(off..off + 4)
.and_then(|s| s.try_into().ok())
.map_or(0, u32::from_le_bytes)
}
fn le_u64(data: &[u8], off: usize) -> u64 {
data.get(off..off + 8)
.and_then(|s| s.try_into().ok())
.map_or(0, u64::from_le_bytes)
}
#[derive(Debug)]
struct AvmlBlock {
range: PhysicalRange,
data: Vec<u8>,
}
#[derive(Debug)]
pub struct AvmlProvider {
blocks: Vec<AvmlBlock>,
ranges: Vec<PhysicalRange>,
}
impl AvmlProvider {
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let blocks = parse_blocks(bytes)?;
let ranges = blocks.iter().map(|b| b.range.clone()).collect();
Ok(Self { blocks, ranges })
}
pub fn from_path(path: &Path) -> Result<Self> {
let bytes = std::fs::read(path)?;
Self::from_bytes(&bytes)
}
}
fn parse_blocks(data: &[u8]) -> Result<Vec<AvmlBlock>> {
let mut blocks = Vec::new();
let mut pos = 0usize;
while pos < data.len() {
if pos + HEADER_SIZE > data.len() {
return Err(Error::Corrupt(format!(
"truncated header at offset {pos:#x}"
)));
}
let magic = le_u32(data, pos);
let version = le_u32(data, pos + 4);
let start = le_u64(data, pos + 8);
let end = le_u64(data, pos + 16);
if magic != AVML_MAGIC {
return Err(Error::Corrupt(format!(
"bad magic {magic:#010x} at offset {pos:#x}"
)));
}
if version != AVML_VERSION {
return Err(Error::Corrupt(format!(
"unsupported AVML version {version} at offset {pos:#x}"
)));
}
if start >= end {
return Err(Error::Corrupt(format!(
"invalid range start={start:#x} end={end:#x} at offset {pos:#x}"
)));
}
let expected_uncompressed = end - start;
let payload_start = pos + HEADER_SIZE;
let search_end = (payload_start + expected_uncompressed as usize + 64).min(data.len());
if search_end < payload_start + 8 {
return Err(Error::Corrupt(format!(
"block at {pos:#x}: not enough data for trailer"
)));
}
let mut trailer_pos: Option<usize> = None;
let scan_start = payload_start;
let scan_end = search_end - 8;
let mut i = scan_start;
while i <= scan_end {
let val = le_u64(data, i);
if val == expected_uncompressed {
let compressed = &data[payload_start..i];
let mut decoder = snap::raw::Decoder::new();
match decoder.decompress_vec(compressed) {
Ok(decompressed) if decompressed.len() as u64 == expected_uncompressed => {
trailer_pos = Some(i);
let range = PhysicalRange { start, end };
blocks.push(AvmlBlock {
range,
data: decompressed,
});
pos = i + 8; break;
}
_ => {
i += 1;
continue;
}
}
}
i += 1;
}
if trailer_pos.is_none() {
return Err(Error::Corrupt(format!(
"block at {pos:#x}: could not locate valid Snappy trailer \
(expected uncompressed size {expected_uncompressed})"
)));
}
}
Ok(blocks)
}
impl PhysicalMemoryProvider for AvmlProvider {
fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
if buf.is_empty() {
return Ok(0);
}
for block in &self.blocks {
if block.range.contains_addr(addr) {
let offset_in_block = (addr - block.range.start) as usize;
let available = block.data.len().saturating_sub(offset_in_block);
let to_read = buf.len().min(available);
buf[..to_read]
.copy_from_slice(&block.data[offset_in_block..offset_in_block + to_read]);
return Ok(to_read);
}
}
Ok(0)
}
fn ranges(&self) -> &[PhysicalRange] {
&self.ranges
}
fn format_name(&self) -> &str {
"AVML v2"
}
}
pub struct AvmlPlugin;
impl FormatPlugin for AvmlPlugin {
fn name(&self) -> &str {
"AVML v2"
}
fn probe(&self, header: &[u8]) -> u8 {
if header.len() < 8 {
return 0;
}
let magic = le_u32(header, 0);
let version = le_u32(header, 4);
if magic == AVML_MAGIC && version == AVML_VERSION {
90
} else {
0
}
}
fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
Ok(Box::new(AvmlProvider::from_path(path)?))
}
}
inventory::submit!(&AvmlPlugin as &dyn FormatPlugin);
#[cfg(test)]
mod tests {
use super::*;
use crate::test_builders::AvmlBuilder;
#[test]
fn probe_avml_magic() {
let data = AvmlBuilder::new().add_range(0x1000, &[0u8; 64]).build();
let plugin = AvmlPlugin;
assert_eq!(plugin.probe(&data), 90);
}
#[test]
fn probe_non_avml() {
let data = vec![0u8; 64];
let plugin = AvmlPlugin;
assert_eq!(plugin.probe(&data), 0);
}
#[test]
fn single_range_roundtrip() {
let payload: Vec<u8> = (0u8..=255).collect();
let dump = AvmlBuilder::new().add_range(0x1000, &payload).build();
let provider = AvmlProvider::from_bytes(&dump).expect("parse");
let ranges = provider.ranges();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start, 0x1000);
assert_eq!(ranges[0].end, 0x1000 + 256);
assert_eq!(provider.total_size(), 256);
assert_eq!(provider.format_name(), "AVML v2");
let mut buf = vec![0u8; 256];
let n = provider.read_phys(0x1000, &mut buf).expect("read");
assert_eq!(n, 256);
assert_eq!(buf, payload);
let mut buf2 = vec![0u8; 4];
let n2 = provider.read_phys(0x1010, &mut buf2).expect("read mid");
assert_eq!(n2, 4);
assert_eq!(buf2, &payload[0x10..0x14]);
}
#[test]
fn two_ranges_roundtrip() {
let data_a = vec![0xAAu8; 256];
let data_b = vec![0xBBu8; 256];
let dump = AvmlBuilder::new()
.add_range(0x0000, &data_a)
.add_range(0x1000, &data_b)
.build();
let provider = AvmlProvider::from_bytes(&dump).expect("parse");
let ranges = provider.ranges();
assert_eq!(ranges.len(), 2);
assert_eq!(
ranges[0],
PhysicalRange {
start: 0x0000,
end: 0x0100
}
);
assert_eq!(
ranges[1],
PhysicalRange {
start: 0x1000,
end: 0x1100
}
);
assert_eq!(provider.total_size(), 512);
let mut buf = vec![0u8; 256];
let n = provider.read_phys(0x0000, &mut buf).expect("read a");
assert_eq!(n, 256);
assert_eq!(buf, data_a);
let n = provider.read_phys(0x1000, &mut buf).expect("read b");
assert_eq!(n, 256);
assert_eq!(buf, data_b);
}
#[test]
fn gap_returns_zero() {
let dump = AvmlBuilder::new().add_range(0x1000, &[0xCCu8; 256]).build();
let provider = AvmlProvider::from_bytes(&dump).expect("parse");
let mut buf = vec![0u8; 64];
let n = provider.read_phys(0x5000, &mut buf).expect("read gap");
assert_eq!(n, 0);
}
#[test]
fn from_path_roundtrip() {
let payload: Vec<u8> = (0u8..=127).collect();
let dump = AvmlBuilder::new().add_range(0x2000, &payload).build();
let path = std::env::temp_dir().join("memf_test_avml_from_path.avml");
std::fs::write(&path, &dump).unwrap();
let provider = AvmlProvider::from_path(&path).unwrap();
assert_eq!(provider.ranges().len(), 1);
assert_eq!(provider.total_size(), 128);
assert_eq!(provider.format_name(), "AVML v2");
let mut buf = [0u8; 4];
let n = provider.read_phys(0x2000, &mut buf).unwrap();
assert_eq!(n, 4);
assert_eq!(&buf, &[0, 1, 2, 3]);
std::fs::remove_file(&path).ok();
}
#[test]
fn plugin_name() {
let plugin = AvmlPlugin;
assert_eq!(plugin.name(), "AVML v2");
}
#[test]
fn probe_short_header_returns_zero() {
let plugin = AvmlPlugin;
assert_eq!(plugin.probe(&[0x41, 0x56, 0x4D]), 0); assert_eq!(plugin.probe(&[]), 0);
}
#[test]
fn read_phys_empty_buffer() {
let dump = AvmlBuilder::new().add_range(0x1000, &[0xBB; 64]).build();
let provider = AvmlProvider::from_bytes(&dump).expect("parse");
let mut buf = [];
let n = provider.read_phys(0x1000, &mut buf).unwrap();
assert_eq!(n, 0);
}
#[test]
fn corrupt_header_wrong_version_returns_error() {
let mut dump = AvmlBuilder::new().add_range(0x1000, &[0xAA; 64]).build();
dump[4..8].copy_from_slice(&99u32.to_le_bytes());
let result = AvmlProvider::from_bytes(&dump);
assert!(result.is_err(), "wrong version must return an error");
let err = result.unwrap_err();
assert!(
matches!(err, crate::Error::Corrupt(_)),
"error must be Corrupt, got: {err}"
);
assert!(
err.to_string().contains("99"),
"error message should mention the bad version number"
);
}
#[test]
fn corrupt_header_wrong_magic_returns_error() {
let mut dump = AvmlBuilder::new().add_range(0x1000, &[0xAA; 64]).build();
dump[0..4].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
let result = AvmlProvider::from_bytes(&dump);
assert!(result.is_err(), "wrong magic must return an error");
assert!(matches!(result.unwrap_err(), crate::Error::Corrupt(_)));
}
#[test]
fn truncated_header_returns_error() {
let partial: Vec<u8> = vec![
0x41, 0x56, 0x4D, 0x4C, 0x02, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x10, 0x00, 0x00, ];
assert!(
partial.len() < 32,
"test fixture must be shorter than a full header"
);
let result = AvmlProvider::from_bytes(&partial);
assert!(result.is_err(), "truncated input must return an error");
assert!(matches!(result.unwrap_err(), crate::Error::Corrupt(_)));
}
#[test]
fn snappy_compressed_block_roundtrip() {
let payload: Vec<u8> = (0u8..=255).cycle().take(512).collect();
let dump = AvmlBuilder::new().add_range(0x4000, &payload).build();
let provider = AvmlProvider::from_bytes(&dump).expect("parse snappy dump");
assert_eq!(provider.total_size(), 512);
let mut buf = vec![0u8; 512];
let n = provider.read_phys(0x4000, &mut buf).expect("read_phys");
assert_eq!(n, 512);
assert_eq!(buf, payload, "decompressed data must match original");
}
}