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