memf-format 0.2.1

Physical memory dump format parsers for the memf forensics framework
Documentation
//! Raw (contiguous) memory dump format provider.
//!
//! The simplest possible format: a flat byte array representing contiguous
//! physical memory starting at address 0.

use std::path::Path;

use crate::{FormatPlugin, PhysicalMemoryProvider, PhysicalRange, Result};

/// Provider that exposes physical memory from a raw (flat) dump.
///
/// The entire file is treated as a single range `[0, data.len())`.
#[derive(Debug)]
pub struct RawProvider {
    data: Vec<u8>,
    /// Pre-extracted ranges for the `ranges()` slice return.
    ranges: Vec<PhysicalRange>,
}

impl RawProvider {
    /// Construct a `RawProvider` from an in-memory byte slice (infallible).
    pub fn from_bytes(bytes: &[u8]) -> Self {
        let data = bytes.to_vec();
        let ranges = if data.is_empty() {
            vec![]
        } else {
            vec![PhysicalRange {
                start: 0,
                end: data.len() as u64,
            }]
        };
        Self { data, ranges }
    }

    /// Construct a `RawProvider` by reading a file from the given path.
    pub fn from_path(path: &Path) -> Result<Self> {
        let data = std::fs::read(path)?;
        Ok(Self::from_bytes(&data))
    }
}

impl PhysicalMemoryProvider for RawProvider {
    fn read_phys(&self, addr: u64, buf: &mut [u8]) -> Result<usize> {
        if buf.is_empty() {
            return Ok(0);
        }

        let data_len = self.data.len() as u64;
        if addr >= data_len {
            return Ok(0);
        }

        let src_start = addr as usize;
        let available = self.data.len() - src_start;
        let to_read = buf.len().min(available);
        buf[..to_read].copy_from_slice(&self.data[src_start..src_start + to_read]);
        Ok(to_read)
    }

    fn ranges(&self) -> &[PhysicalRange] {
        &self.ranges
    }

    fn format_name(&self) -> &str {
        "Raw"
    }
}

/// FormatPlugin implementation for raw (flat) dumps.
///
/// This is the lowest-confidence fallback: any non-empty file can be treated
/// as a raw dump, so the probe returns 5 (not 0) for non-empty files.
pub struct RawPlugin;

impl FormatPlugin for RawPlugin {
    fn name(&self) -> &str {
        "Raw"
    }

    fn probe(&self, header: &[u8]) -> u8 {
        if header.is_empty() {
            0
        } else {
            5
        }
    }

    fn open(&self, path: &Path) -> Result<Box<dyn PhysicalMemoryProvider>> {
        Ok(Box::new(RawProvider::from_path(path)?))
    }
}

inventory::submit!(&RawPlugin as &dyn FormatPlugin);

#[cfg(test)]
mod tests {
    use super::*;

    // Test 1: probe returns 5 for non-empty, 0 for empty.
    #[test]
    fn probe_confidence() {
        let plugin = RawPlugin;
        assert_eq!(plugin.probe(&[0u8; 64]), 5);
        assert_eq!(plugin.probe(&[]), 0);
    }

    // Test 2: basic read from start.
    #[test]
    fn read_from_start() {
        let data: Vec<u8> = (0u8..=255).collect();
        let provider = RawProvider::from_bytes(&data);

        assert_eq!(provider.ranges().len(), 1);
        assert_eq!(provider.ranges()[0].start, 0);
        assert_eq!(provider.ranges()[0].end, 256);
        assert_eq!(provider.total_size(), 256);

        let mut buf = [0u8; 4];
        let n = provider.read_phys(0, &mut buf).unwrap();
        assert_eq!(n, 4);
        assert_eq!(&buf, &[0, 1, 2, 3]);
    }

    // Test 3: reading past end returns 0.
    #[test]
    fn read_past_end() {
        let data = vec![0xFFu8; 64];
        let provider = RawProvider::from_bytes(&data);

        let mut buf = [0u8; 4];
        let n = provider.read_phys(64, &mut buf).unwrap();
        assert_eq!(n, 0);

        let n2 = provider.read_phys(1000, &mut buf).unwrap();
        assert_eq!(n2, 0);
    }

    // Test 4: partial read when buffer extends past end.
    #[test]
    fn read_partial() {
        let data = vec![0xABu8; 10];
        let provider = RawProvider::from_bytes(&data);

        let mut buf = [0u8; 8];
        let n = provider.read_phys(6, &mut buf).unwrap();
        assert_eq!(n, 4); // only 4 bytes remain (10 - 6)
        assert_eq!(&buf[..4], &[0xABu8; 4]);
    }

    // Test 5: empty dump has no ranges and total_size 0.
    #[test]
    fn empty_dump() {
        let provider = RawProvider::from_bytes(&[]);
        assert_eq!(provider.ranges().len(), 0);
        assert_eq!(provider.total_size(), 0);
    }

    #[test]
    fn from_path_roundtrip() {
        let data: Vec<u8> = (0u8..=127).collect();
        let path = std::env::temp_dir().join("memf_test_raw_from_path.raw");
        std::fs::write(&path, &data).unwrap();
        let provider = RawProvider::from_path(&path).unwrap();
        assert_eq!(provider.ranges().len(), 1);
        assert_eq!(provider.total_size(), 128);
        assert_eq!(provider.format_name(), "Raw");
        let mut buf = [0u8; 4];
        let n = provider.read_phys(0, &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 = RawPlugin;
        assert_eq!(plugin.name(), "Raw");
    }

    #[test]
    fn read_phys_empty_buffer() {
        let data = vec![0xFFu8; 64];
        let provider = RawProvider::from_bytes(&data);
        let mut buf = [];
        let n = provider.read_phys(0, &mut buf).unwrap();
        assert_eq!(n, 0);
    }
}