rwmem 0.1.2

rwmem is a Rust library to read from / write to / search on memory of a process.
Documentation
// Copyright (c) 2026 Eray Erdin
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

// TODO: add docs

use std::{fs, io, num, path};

pub struct MappingPermissions {
    pub(crate) read: bool,
    write: bool,
    execute: bool,
    private: bool,
    shared: bool,
}

pub struct MappingDevice {
    major: String,
    minor: String,
}

pub enum MappingPointer {
    Path(path::PathBuf),
    Marker(String),
}

pub struct Mapping {
    pub(crate) start: usize,
    pub(crate) end: usize,
    pub(crate) permissions: MappingPermissions,
    offset: usize,
    device: MappingDevice,
    inode: usize,
    pointer: Option<MappingPointer>,
}

impl std::str::FromStr for Mapping {
    type Err = MappingExtError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let parts: Vec<&str> = s
            .trim()
            .split(" ")
            .filter(|a| *a != "")
            .map(|a| a.trim())
            .collect();

        if parts.len() < 5 {
            return Err(MappingExtError::InvalidMappingLine(s.into()));
        }

        let addr_range_part = parts[0];
        let ranges: Vec<&str> = addr_range_part.split("-").collect();
        let start: usize = usize::from_str_radix(ranges[0], 16)?;
        let end: usize = usize::from_str_radix(ranges[1], 16)?;

        let permissions_part = parts[1];
        if permissions_part.len() < 4 {
            return Err(MappingExtError::InvalidMappingLine(s.into()));
        }
        let permissions = MappingPermissions {
            read: permissions_part.contains('r'),
            write: permissions_part.contains('w'),
            execute: permissions_part.contains('x'),
            private: permissions_part.contains('p'),
            shared: permissions_part.contains('s'),
        };

        let offset_part = parts[2];
        let offset: usize = usize::from_str_radix(offset_part, 16)?;

        let device_part = parts[3];
        let device_definition: Vec<&str> = device_part.split(':').collect();
        let major: String = device_definition[0].into();
        let minor: String = device_definition[1].into();
        if major.len() != 2 || minor.len() != 2 {
            return Err(MappingExtError::InvalidMappingLine(s.into()));
        }
        let device = MappingDevice { major, minor };

        let inode_part = parts[4];
        let inode: usize = inode_part.parse()?;

        let pointer = if let Some(pointer_part) = parts.get(5) {
            if pointer_part.contains('[') {
                Some(MappingPointer::Marker((*pointer_part).into()))
            } else {
                Some(MappingPointer::Path(pointer_part.into()))
            }
        } else {
            None
        };

        Ok(Mapping {
            start,
            end,
            permissions,
            offset,
            device,
            inode,
            pointer,
        })
    }
}

pub trait MappingExt {
    fn mappings(&self) -> MappingExtResult<Vec<Mapping>>;
}

#[cfg(target_os = "linux")]
impl MappingExt for crate::process::Process {
    fn mappings(&self) -> MappingExtResult<Vec<Mapping>> {
        use std::str::FromStr;

        let mapspath = path::PathBuf::from(format!("/proc/{}/maps", self.pid));
        let content = fs::read_to_string(mapspath)?;
        let lines = content.lines();

        let mappings: MappingExtResult<Vec<Mapping>> = lines.map(Mapping::from_str).collect();

        mappings
    }
}

#[derive(Debug, Error)]
pub enum MappingExtError {
    #[error("An IO error occured while reading the process mapping. {0} {0:?}")]
    IO(io::Error),
    #[error("Process mapping file contains an invalid line: `{0}`")]
    InvalidMappingLine(String),
    #[error("Mapping line contains invalid address definition. {0} {0:?}")]
    InvalidAddressDefinition(num::ParseIntError),
}

impl From<io::Error> for MappingExtError {
    fn from(value: io::Error) -> Self {
        Self::IO(value)
    }
}

impl From<num::ParseIntError> for MappingExtError {
    fn from(value: num::ParseIntError) -> Self {
        Self::InvalidAddressDefinition(value)
    }
}

pub type MappingExtResult<T> = Result<T, MappingExtError>;

#[cfg(test)]
mod tests {
    use std::{
        env,
        io::{self, BufRead},
        process,
    };

    use crate::process::Process;

    use super::*;

    struct DummyProcess {
        child: process::Child,
        pub pid: i32,
    }

    impl Drop for DummyProcess {
        fn drop(&mut self) {
            let _ = self.child.kill();
            let _ = self.child.wait();
        }
    }

    #[fixture]
    fn dummy_process() -> DummyProcess {
        let cwd = env::current_dir().expect("Could not get the current working directory.");
        let dummy_process_dir = cwd.join("dummy_process");

        let build_status = process::Command::new("cargo")
            .arg("build")
            .current_dir(&dummy_process_dir)
            .status()
            .expect("Failed to run dummy_process compilation.");
        assert!(build_status.success(), "Failed to build dummy_process");

        let bin_path = dummy_process_dir.join("target/debug/dummy_process");
        let mut child = process::Command::new(bin_path)
            .stdout(process::Stdio::piped())
            .spawn()
            .expect("Could not spawn dummy_process.");

        let port = {
            let stdout = child.stdout.as_mut().expect("Failed to get stdout.");
            let reader = io::BufReader::new(stdout);
            let port_line = reader
                .lines()
                .next()
                .expect("Port line does not exist.")
                .expect("Could not read port line of stdout.");
            port_line
                .trim()
                .split(":")
                .last()
                .expect("Could not read port part from stdout.")
                .parse::<u16>()
                .expect("Could not parse port from stdout.")
        };

        let pid = {
            let body = reqwest::blocking::get(format!("http://127.0.0.1:{port}/pid"))
                .expect("Could not request PID of dummy process.");
            body.text()
                .expect("Could not decode the charset of PID of dummy process.")
                .parse::<i32>()
                .expect("Could not parse PID of dummy process")
        };

        DummyProcess { child, pid }
    }

    #[rstest]
    fn test_presence(dummy_process: DummyProcess) {
        let process = Process::try_new(dummy_process.pid).expect("Could not initialize Process");
        let maps = process.mappings().expect("Could not parse mappings.");
        assert!(!maps.is_empty(), "Process mapping is empty.");
    }
}