1use std::fmt::{self, Debug, Display, Formatter};
67use std::sync::LazyLock;
68use std::{fs, io};
69
70use regex_lite::Regex;
71use thiserror::Error;
72
73#[derive(Debug, PartialEq)]
75pub struct MemoryRegion {
76 pub start_address: usize,
77 pub end_address: usize,
78 pub region_type: MemoryRegionType,
79}
80
81impl MemoryRegion {
82 #[must_use]
84 pub fn size(&self) -> usize {
85 self.end_address - self.start_address
86 }
87}
88
89impl Display for MemoryRegion {
90 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
91 write!(
92 f,
93 "[{:#018x}-{:#018x}] ({:?})",
94 self.start_address, self.end_address, self.region_type
95 )
96 }
97}
98
99#[derive(Debug, PartialEq)]
112pub enum MemoryRegionType {
113 Usable,
114 Reserved,
115 SoftReserved,
116 AcpiData,
117 AcpiNvs,
118 Unusable,
119 Persistent,
120 Unknown,
122
123 NonAcpi(String),
125}
126
127impl From<&str> for MemoryRegionType {
128 fn from(s: &str) -> Self {
135 match s {
136 "System RAM" => Self::Usable,
137 "Reserved" => Self::Reserved,
138 "Soft Reserved" => Self::SoftReserved,
139 "ACPI Tables" => Self::AcpiData,
140 "ACPI Non-volatile Storage" => Self::AcpiNvs,
141 "Unusable memory" => Self::Unusable,
142 "Persistent Memory" | "Persistent Memory (legacy)" => Self::Persistent,
143 "Unknown E820 type" => Self::Unknown,
144
145 _ => Self::NonAcpi(s.into()),
146 }
147 }
148}
149
150impl Display for MemoryRegionType {
151 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
152 write!(f, "{self:?}")
153 }
154}
155
156#[derive(Error, Debug, PartialEq)]
157pub enum ParseError {
158 #[error("line of memory map is not in iomem format: '{0}'")]
159 InvalidFormat(String),
160 #[error("line of memory map has invalid start address: {0}")]
161 InvalidStartAddress(String),
162 #[error("line of memory map has invalid end address: {0}")]
163 InvalidEndAddress(String),
164}
165
166#[allow(clippy::module_name_repetitions)]
178pub fn parse_proc_iomem() -> io::Result<Vec<MemoryRegion>> {
179 let contents = fs::read_to_string("/proc/iomem")?;
180 let memory_regions =
181 parse_iomem_map(&contents).expect("/proc/iomem should contain only valid lines");
182 Ok(memory_regions)
183}
184
185pub fn parse_iomem_map(content: &str) -> Result<Vec<MemoryRegion>, ParseError> {
191 let mut memory_regions = Vec::new();
192
193 for line in content.lines().filter(|l| !l.is_empty()) {
194 if line.starts_with(' ') {
198 continue;
199 }
200
201 let region = parse_iomem_map_line(line)?;
202 memory_regions.push(region);
203 }
204
205 Ok(memory_regions)
206}
207
208static IOMEM_MAP_LINE_REGEX: LazyLock<Regex> =
210 LazyLock::new(|| Regex::new(r"(\w+)-(\w+) : (.+)").unwrap());
211
212fn parse_iomem_map_line(line: &str) -> Result<MemoryRegion, ParseError> {
214 let Some((_full, [start_address, end_address, region_type])) = IOMEM_MAP_LINE_REGEX
215 .captures(line)
216 .map(|caps| caps.extract())
217 else {
218 return Err(ParseError::InvalidFormat(line.into()));
219 };
220
221 let Ok(start_address) = usize::from_str_radix(start_address, 16) else {
222 return Err(ParseError::InvalidStartAddress(start_address.into()));
223 };
224 let Ok(end_address) = usize::from_str_radix(end_address, 16) else {
225 return Err(ParseError::InvalidEndAddress(end_address.into()));
226 };
227
228 let memory_region = MemoryRegion {
229 start_address,
230 end_address,
231 region_type: MemoryRegionType::from(region_type),
232 };
233
234 Ok(memory_region)
235}
236
237#[cfg(test)]
238#[allow(clippy::unreadable_literal)]
239mod tests {
240 use super::*;
241 use indoc::indoc;
242
243 mod memory_region {
244 use super::*;
245
246 #[test]
247 fn size_returns_correct_size_of_region_in_bytes() {
248 let region = MemoryRegion {
249 start_address: 0x00100000,
250 end_address: 0x09bfffff,
251 region_type: MemoryRegionType::Usable,
252 };
253 assert_eq!(region.size(), 162_529_279);
254 }
255
256 #[test]
257 fn is_formatted_as_expected_human_readable_string() {
258 let region = MemoryRegion {
259 start_address: 0x00100000,
260 end_address: 0x09bfffff,
261 region_type: MemoryRegionType::Usable,
262 };
263 assert_eq!(
264 region.to_string(),
265 "[0x0000000000100000-0x0000000009bfffff] (Usable)"
266 );
267 }
268 }
269
270 mod parse_iomem_map {
271 use super::*;
272
273 const PROC_IOMEM_MAP: &str = indoc! {"
275 00000080-000000ff : System RAM
276 00000100-000001ff : Reserved
277 00000180-000001bf : PCI Bus 0000:00
278 000001c0-000001ff : System ROM
279 00000200-000003ff : Soft Reserved
280 00000400-000007ff : ACPI Tables
281 00000800-00000fff : ACPI Non-volatile Storage
282 00001000-00001fff : Unusable memory
283 00002000-00003fff : Persistent Memory
284 00004000-00007fff : Persistent Memory (legacy)
285 00008000-0000ffff : Unknown E820 type
286 00010000-0001ffff : PCI ECAM 0000
287 00010000-0001ffff : Reserved
288 00010000-0001ffff : pnp 00:00
289 "};
290
291 #[test]
292 fn returns_vector_of_expected_memory_regions() {
293 let regions = parse_iomem_map(PROC_IOMEM_MAP);
294 let expected_regions = vec![
295 (0x000080, MemoryRegionType::Usable),
296 (0x000100, MemoryRegionType::Reserved),
297 (0x000200, MemoryRegionType::SoftReserved),
298 (0x000400, MemoryRegionType::AcpiData),
299 (0x000800, MemoryRegionType::AcpiNvs),
300 (0x001000, MemoryRegionType::Unusable),
301 (0x002000, MemoryRegionType::Persistent),
302 (0x004000, MemoryRegionType::Persistent),
303 (0x008000, MemoryRegionType::Unknown),
304 (0x010000, MemoryRegionType::NonAcpi("PCI ECAM 0000".into())),
305 ]
306 .into_iter()
307 .map(|(start_address, region_type)| MemoryRegion {
308 start_address,
309 end_address: start_address * 2 - 1,
310 region_type,
311 })
312 .collect();
313
314 assert_eq!(regions, Ok(expected_regions));
315 }
316 }
317
318 mod parse_iomem_map_line {
319 use super::*;
320
321 #[test]
322 fn returns_expected_memory_region_for_line_with_acpi_type() {
323 let result = parse_iomem_map_line("00100000-09bfffff : System RAM");
324 assert_eq!(
325 result,
326 Ok(MemoryRegion {
327 start_address: 0x00100000,
328 end_address: 0x09bfffff,
329 region_type: MemoryRegionType::Usable,
330 })
331 );
332 }
333
334 #[test]
335 fn returns_expected_memory_region_for_line_with_non_acpi_type() {
336 let result = parse_iomem_map_line("d0000000-f7ffffff : PCI Bus 0000:00");
337 assert_eq!(
338 result,
339 Ok(MemoryRegion {
340 start_address: 0xd0000000,
341 end_address: 0xf7ffffff,
342 region_type: MemoryRegionType::NonAcpi("PCI Bus 0000:00".into()),
343 })
344 );
345 }
346
347 #[test]
348 fn returns_invalid_format_error_if_line_is_not_in_iomem_format() {
349 let invalid_line = "This is not an iomem memory map line.";
350 assert_eq!(
351 parse_iomem_map_line(invalid_line),
352 Err(ParseError::InvalidFormat(invalid_line.into()))
353 );
354 }
355
356 #[test]
357 fn returns_invalid_start_address_error_if_start_address_is_not_hex() {
358 assert_eq!(
359 parse_iomem_map_line("0000yyyy-00000fff : Reserved"),
360 Err(ParseError::InvalidStartAddress("0000yyyy".into()))
361 );
362 }
363
364 #[test]
365 fn returns_invalid_end_address_error_if_end_address_is_not_hex() {
366 assert_eq!(
367 parse_iomem_map_line("00000000-0000zzzz : Reserved"),
368 Err(ParseError::InvalidEndAddress("0000zzzz".into()))
369 );
370 }
371 }
372}