use std::path::Path;
use crate::{Error, FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};
const LIME_MAGIC: u32 = 0x4C694D45;
const LIME_VERSION: u32 = 1;
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 LimeRecord {
range: PhysicalRange,
data_offset: usize,
}
#[derive(Debug)]
pub struct LimeProvider {
data: Vec<u8>,
records: Vec<LimeRecord>,
ranges: Vec<PhysicalRange>,
}
impl LimeProvider {
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let data = bytes.to_vec();
let records = parse_records(&data)?;
let ranges = records.iter().map(|r| r.range.clone()).collect();
Ok(Self {
data,
records,
ranges,
})
}
pub fn from_path(path: &Path) -> Result<Self> {
let data = std::fs::read(path)?;
let records = parse_records(&data)?;
let ranges = records.iter().map(|r| r.range.clone()).collect();
Ok(Self {
data,
records,
ranges,
})
}
}
fn parse_records(data: &[u8]) -> Result<Vec<LimeRecord>> {
let mut records = Vec::new();
let mut pos = 0usize;
while pos < data.len() {
if data.len() - pos < HEADER_SIZE {
return Err(Error::Corrupt(format!(
"truncated header at offset {pos}: only {} bytes remain",
data.len() - pos
)));
}
let magic = le_u32(data, pos);
if magic != LIME_MAGIC {
return Err(Error::Corrupt(format!(
"bad magic at offset {pos}: expected 0x{LIME_MAGIC:08X}, got 0x{magic:08X}"
)));
}
let version = le_u32(data, pos + 4);
if version != LIME_VERSION {
return Err(Error::Corrupt(format!(
"unsupported LiME version {version} at offset {pos}"
)));
}
let s_addr = le_u64(data, pos + 8);
let e_addr = le_u64(data, pos + 16);
if s_addr > e_addr {
return Err(Error::Corrupt(format!(
"record at offset {pos}: s_addr 0x{s_addr:016X} > e_addr 0x{e_addr:016X}"
)));
}
let payload_len = (e_addr - s_addr + 1) as usize;
let data_offset = pos + HEADER_SIZE;
if data.len() - data_offset < payload_len {
return Err(Error::Corrupt(format!(
"record at offset {pos}: payload truncated (need {payload_len}, have {})",
data.len() - data_offset
)));
}
records.push(LimeRecord {
range: PhysicalRange {
start: s_addr,
end: e_addr + 1, },
data_offset,
});
pos = data_offset + payload_len;
}
Ok(records)
}
impl PhysicalMemoryProvider for LimeProvider {
fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
if buf.is_empty() {
return Ok(0);
}
for record in &self.records {
if record.range.contains_addr(addr) {
let offset_in_range = (addr - record.range.start) as usize;
let available = (record.range.end - addr) as usize;
let to_read = buf.len().min(available);
let src_start = record.data_offset + offset_in_range;
buf[..to_read].copy_from_slice(&self.data[src_start..src_start + to_read]);
return Ok(to_read);
}
}
Ok(0)
}
fn ranges(&self) -> &[PhysicalRange] {
&self.ranges
}
fn format_name(&self) -> &str {
"LiME"
}
}
pub struct LimePlugin;
impl FormatPlugin for LimePlugin {
fn name(&self) -> &str {
"LiME"
}
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 == LIME_MAGIC && version == LIME_VERSION {
90
} else {
0
}
}
fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
Ok(Box::new(LimeProvider::from_path(path)?))
}
}
inventory::submit!(&LimePlugin as &dyn FormatPlugin);
#[cfg(test)]
mod tests {
use super::*;
use crate::test_builders::LimeBuilder;
fn parse(bytes: &[u8]) -> Result<LimeProvider> {
LimeProvider::from_bytes(bytes)
}
#[test]
fn probe_lime_magic() {
let dump = LimeBuilder::new().add_range(0x1000, &[0u8; 0x1000]).build();
let plugin = LimePlugin;
assert_eq!(plugin.probe(&dump), 90);
}
#[test]
fn probe_non_lime() {
let zeros = vec![0u8; 64];
let plugin = LimePlugin;
assert_eq!(plugin.probe(&zeros), 0);
}
#[test]
fn single_range() {
let data: Vec<u8> = (0u8..=255).collect();
let dump = LimeBuilder::new().add_range(0x1000, &data).build();
let provider = parse(&dump).unwrap();
assert_eq!(provider.ranges().len(), 1);
assert_eq!(provider.ranges()[0].start, 0x1000);
assert_eq!(provider.ranges()[0].end, 0x1100);
assert_eq!(provider.total_size(), 256);
assert_eq!(provider.format_name(), "LiME");
let mut buf = [0u8; 4];
let n = provider.read_phys(0x1000, &mut buf).unwrap();
assert_eq!(n, 4);
assert_eq!(&buf, &[0, 1, 2, 3]);
}
#[test]
fn two_ranges() {
let data_a = vec![0xAAu8; 0x2000];
let data_b = vec![0xBBu8; 0x1000];
let dump = LimeBuilder::new()
.add_range(0x0000, &data_a)
.add_range(0x4000, &data_b)
.build();
let provider = parse(&dump).unwrap();
assert_eq!(provider.ranges().len(), 2);
assert_eq!(provider.total_size(), 0x3000);
let mut buf = [0u8; 2];
let n = provider.read_phys(0x0000, &mut buf).unwrap();
assert_eq!(n, 2);
assert_eq!(buf, [0xAA, 0xAA]);
let n = provider.read_phys(0x4000, &mut buf).unwrap();
assert_eq!(n, 2);
assert_eq!(buf, [0xBB, 0xBB]);
}
#[test]
fn read_gap_returns_zero() {
let data = vec![0xCCu8; 0x1000];
let dump = LimeBuilder::new().add_range(0x1000, &data).build();
let provider = parse(&dump).unwrap();
let mut buf = [0xFFu8; 4];
let n = provider.read_phys(0x0000, &mut buf).unwrap();
assert_eq!(n, 0);
}
#[test]
fn corrupt_magic_errors() {
let mut dump = LimeBuilder::new().add_range(0x1000, &[0u8; 64]).build();
dump[0] = 0xFF;
let err = parse(&dump).unwrap_err();
assert!(
matches!(err, Error::Corrupt(_)),
"expected Corrupt, got {err:?}"
);
}
#[test]
fn truncated_header_errors() {
let truncated = vec![0x45u8, 0x4D, 0x69, 0x4C]; let err = parse(&truncated).unwrap_err();
assert!(
matches!(err, Error::Corrupt(_)),
"expected Corrupt, got {err:?}"
);
}
#[test]
fn from_path_roundtrip() {
let data: Vec<u8> = (0u8..=127).collect();
let dump = LimeBuilder::new().add_range(0x2000, &data).build();
let path = std::env::temp_dir().join("memf_test_lime_from_path.lime");
std::fs::write(&path, &dump).unwrap();
let provider = LimeProvider::from_path(&path).unwrap();
assert_eq!(provider.ranges().len(), 1);
assert_eq!(provider.total_size(), 128);
assert_eq!(provider.format_name(), "LiME");
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 = LimePlugin;
assert_eq!(plugin.name(), "LiME");
}
#[test]
fn probe_short_header_returns_zero() {
let plugin = LimePlugin;
assert_eq!(plugin.probe(&[0x45, 0x4D, 0x69]), 0); assert_eq!(plugin.probe(&[]), 0);
}
#[test]
fn read_phys_empty_buffer() {
let dump = LimeBuilder::new().add_range(0x1000, &[0xAA; 64]).build();
let provider = parse(&dump).unwrap();
let mut buf = [];
let n = provider.read_phys(0x1000, &mut buf).unwrap();
assert_eq!(n, 0);
}
}