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