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.");
}
}