use std::fmt::{self, Debug, Display, Formatter};
use std::sync::LazyLock;
use std::{fs, io};
use regex_lite::Regex;
use thiserror::Error;
#[derive(Debug, PartialEq)]
pub struct MemoryRegion {
pub start_address: usize,
pub end_address: usize,
pub region_type: MemoryRegionType,
}
impl MemoryRegion {
#[must_use]
pub fn size(&self) -> usize {
self.end_address - self.start_address
}
}
impl Display for MemoryRegion {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"[{:#018x}-{:#018x}] ({:?})",
self.start_address, self.end_address, self.region_type
)
}
}
#[derive(Debug, PartialEq)]
pub enum MemoryRegionType {
Usable,
Reserved,
SoftReserved,
AcpiData,
AcpiNvs,
Unusable,
Persistent,
Unknown,
NonAcpi(String),
}
impl From<&str> for MemoryRegionType {
fn from(s: &str) -> Self {
match s {
"System RAM" => Self::Usable,
"Reserved" => Self::Reserved,
"Soft Reserved" => Self::SoftReserved,
"ACPI Tables" => Self::AcpiData,
"ACPI Non-volatile Storage" => Self::AcpiNvs,
"Unusable memory" => Self::Unusable,
"Persistent Memory" | "Persistent Memory (legacy)" => Self::Persistent,
"Unknown E820 type" => Self::Unknown,
_ => Self::NonAcpi(s.into()),
}
}
}
impl Display for MemoryRegionType {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
#[derive(Error, Debug, PartialEq)]
pub enum ParseError {
#[error("line of memory map is not in iomem format: '{0}'")]
InvalidFormat(String),
#[error("line of memory map has invalid start address: {0}")]
InvalidStartAddress(String),
#[error("line of memory map has invalid end address: {0}")]
InvalidEndAddress(String),
}
#[allow(clippy::module_name_repetitions)]
pub fn parse_proc_iomem() -> io::Result<Vec<MemoryRegion>> {
let contents = fs::read_to_string("/proc/iomem")?;
let memory_regions =
parse_iomem_map(&contents).expect("/proc/iomem should contain only valid lines");
Ok(memory_regions)
}
pub fn parse_iomem_map(content: &str) -> Result<Vec<MemoryRegion>, ParseError> {
let mut memory_regions = Vec::new();
for line in content.lines().filter(|l| !l.is_empty()) {
if line.starts_with(' ') {
continue;
}
let region = parse_iomem_map_line(line)?;
memory_regions.push(region);
}
Ok(memory_regions)
}
static IOMEM_MAP_LINE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(\w+)-(\w+) : (.+)").unwrap());
fn parse_iomem_map_line(line: &str) -> Result<MemoryRegion, ParseError> {
let Some((_full, [start_address, end_address, region_type])) = IOMEM_MAP_LINE_REGEX
.captures(line)
.map(|caps| caps.extract())
else {
return Err(ParseError::InvalidFormat(line.into()));
};
let Ok(start_address) = usize::from_str_radix(start_address, 16) else {
return Err(ParseError::InvalidStartAddress(start_address.into()));
};
let Ok(end_address) = usize::from_str_radix(end_address, 16) else {
return Err(ParseError::InvalidEndAddress(end_address.into()));
};
let memory_region = MemoryRegion {
start_address,
end_address,
region_type: MemoryRegionType::from(region_type),
};
Ok(memory_region)
}
#[cfg(test)]
#[allow(clippy::unreadable_literal)]
mod tests {
use super::*;
use indoc::indoc;
mod memory_region {
use super::*;
#[test]
fn size_returns_correct_size_of_region_in_bytes() {
let region = MemoryRegion {
start_address: 0x00100000,
end_address: 0x09bfffff,
region_type: MemoryRegionType::Usable,
};
assert_eq!(region.size(), 162_529_279);
}
#[test]
fn is_formatted_as_expected_human_readable_string() {
let region = MemoryRegion {
start_address: 0x00100000,
end_address: 0x09bfffff,
region_type: MemoryRegionType::Usable,
};
assert_eq!(
region.to_string(),
"[0x0000000000100000-0x0000000009bfffff] (Usable)"
);
}
}
mod parse_iomem_map {
use super::*;
const PROC_IOMEM_MAP: &str = indoc! {"
00000080-000000ff : System RAM
00000100-000001ff : Reserved
00000180-000001bf : PCI Bus 0000:00
000001c0-000001ff : System ROM
00000200-000003ff : Soft Reserved
00000400-000007ff : ACPI Tables
00000800-00000fff : ACPI Non-volatile Storage
00001000-00001fff : Unusable memory
00002000-00003fff : Persistent Memory
00004000-00007fff : Persistent Memory (legacy)
00008000-0000ffff : Unknown E820 type
00010000-0001ffff : PCI ECAM 0000
00010000-0001ffff : Reserved
00010000-0001ffff : pnp 00:00
"};
#[test]
fn returns_vector_of_expected_memory_regions() {
let regions = parse_iomem_map(PROC_IOMEM_MAP);
let expected_regions = vec![
(0x000080, MemoryRegionType::Usable),
(0x000100, MemoryRegionType::Reserved),
(0x000200, MemoryRegionType::SoftReserved),
(0x000400, MemoryRegionType::AcpiData),
(0x000800, MemoryRegionType::AcpiNvs),
(0x001000, MemoryRegionType::Unusable),
(0x002000, MemoryRegionType::Persistent),
(0x004000, MemoryRegionType::Persistent),
(0x008000, MemoryRegionType::Unknown),
(0x010000, MemoryRegionType::NonAcpi("PCI ECAM 0000".into())),
]
.into_iter()
.map(|(start_address, region_type)| MemoryRegion {
start_address,
end_address: start_address * 2 - 1,
region_type,
})
.collect();
assert_eq!(regions, Ok(expected_regions));
}
}
mod parse_iomem_map_line {
use super::*;
#[test]
fn returns_expected_memory_region_for_line_with_acpi_type() {
let result = parse_iomem_map_line("00100000-09bfffff : System RAM");
assert_eq!(
result,
Ok(MemoryRegion {
start_address: 0x00100000,
end_address: 0x09bfffff,
region_type: MemoryRegionType::Usable,
})
);
}
#[test]
fn returns_expected_memory_region_for_line_with_non_acpi_type() {
let result = parse_iomem_map_line("d0000000-f7ffffff : PCI Bus 0000:00");
assert_eq!(
result,
Ok(MemoryRegion {
start_address: 0xd0000000,
end_address: 0xf7ffffff,
region_type: MemoryRegionType::NonAcpi("PCI Bus 0000:00".into()),
})
);
}
#[test]
fn returns_invalid_format_error_if_line_is_not_in_iomem_format() {
let invalid_line = "This is not an iomem memory map line.";
assert_eq!(
parse_iomem_map_line(invalid_line),
Err(ParseError::InvalidFormat(invalid_line.into()))
);
}
#[test]
fn returns_invalid_start_address_error_if_start_address_is_not_hex() {
assert_eq!(
parse_iomem_map_line("0000yyyy-00000fff : Reserved"),
Err(ParseError::InvalidStartAddress("0000yyyy".into()))
);
}
#[test]
fn returns_invalid_end_address_error_if_end_address_is_not_hex() {
assert_eq!(
parse_iomem_map_line("00000000-0000zzzz : Reserved"),
Err(ParseError::InvalidEndAddress("0000zzzz".into()))
);
}
}
}