use std::fs::File;
use std::io::{self, Read, Seek};
use heapless::HistoryBuffer;
use thiserror::Error;
use crate::iomem::{MemoryRegion, MemoryRegionType, parse_proc_iomem};
use crate::reader::SkippingBufReader;
#[derive(PartialEq)]
pub struct AgesaVersion {
pub version_string: String,
pub absolute_address: usize,
pub surrounding_region: MemoryRegion,
}
impl AgesaVersion {
#[must_use]
pub fn offset_in_region(&self) -> usize {
self.absolute_address - self.surrounding_region.start_address
}
}
#[derive(Error, Debug)]
pub enum SearchError {
#[error("could not open `/dev/mem`")]
DevMemUnopenable(#[source] io::Error),
#[error("could not read memory map from `/proc/iomem`")]
IomemUnreadable(#[source] io::Error),
#[error("could not read byte in physical memory from `/dev/mem`")]
ByteUnreadable(#[source] io::Error),
}
pub type SearchResult = Result<Option<AgesaVersion>, SearchError>;
pub fn find_agesa_version() -> SearchResult {
let possible_regions =
get_reserved_regions_in_extended_memory().map_err(SearchError::IomemUnreadable)?;
for region in possible_regions {
log::info!("Searching memory region: {region}");
let maybe_found_version = find_agesa_version_in_memory_region(region)?;
if maybe_found_version.is_some() {
return Ok(maybe_found_version);
}
}
Ok(None)
}
pub fn find_agesa_version_in_memory_region(region: MemoryRegion) -> SearchResult {
let file = File::open("/dev/mem").map_err(SearchError::DevMemUnopenable)?;
let buf_reader = SkippingBufReader::new(file, region.start_address, Some(region.end_address));
if let Some((version_string, absolute_address)) = find_agesa_version_in_reader(buf_reader)? {
return Ok(Some(AgesaVersion {
version_string,
absolute_address,
surrounding_region: region,
}));
}
Ok(None)
}
enum SearchState {
Searching,
SignatureFound,
VersionStartFound,
}
const SIGNATURE_V9: &[u8] = b"AGESA!V";
const SIGNATURE_LENGTH: usize = SIGNATURE_V9.len();
const SIGNATURE_V5: &[u8; SIGNATURE_LENGTH] = b"!!AGESA";
pub fn find_agesa_version_in_reader<R: Read + Seek>(
mut buf_reader: SkippingBufReader<R>,
) -> Result<Option<(String, usize)>, SearchError> {
let mut version_string = Vec::new();
let mut search_state = SearchState::Searching;
let mut search_window: HistoryBuffer<u8, SIGNATURE_LENGTH> = HistoryBuffer::new();
let mut buffer = [0; 1024];
loop {
let bytes_read = buf_reader
.read(&mut buffer)
.map_err(SearchError::ByteUnreadable)?;
if bytes_read == 0 {
break;
}
for (i, &byte) in buffer[..bytes_read].iter().enumerate() {
match search_state {
SearchState::Searching => {
search_window.write(byte);
if search_window.oldest_ordered().eq(SIGNATURE_V9) {
search_state = SearchState::SignatureFound;
} else if search_window.oldest_ordered().eq(SIGNATURE_V5) {
search_state = SearchState::VersionStartFound;
}
}
SearchState::SignatureFound => {
if byte == b'\0' {
search_state = SearchState::VersionStartFound;
}
}
SearchState::VersionStartFound if byte == b'\0' => {
let trimmed_version = version_string.trim_ascii_start();
let current_offset = buf_reader.position_in_file() - (bytes_read - i);
let version_offset = current_offset - trimmed_version.len();
return Ok(Some((
String::from_utf8_lossy(trimmed_version).into(),
version_offset,
)));
}
SearchState::VersionStartFound => {
version_string.push(byte);
}
}
}
}
Ok(None)
}
const EXTENDED_MEM_START: usize = 0x0000_0000_0010_0000;
pub fn get_reserved_regions_in_extended_memory() -> io::Result<Vec<MemoryRegion>> {
let all_regions = parse_proc_iomem()?;
let reserved_high_mem_regions: Vec<MemoryRegion> = all_regions
.into_iter()
.filter(|r| r.region_type == MemoryRegionType::Reserved)
.filter(|r| r.start_address >= EXTENDED_MEM_START)
.collect();
Ok(reserved_high_mem_regions)
}
#[cfg(test)]
mod tests {
use super::*;
mod found_version {
use super::*;
#[test]
fn offset_in_region_returns_expected_offset() {
let version = AgesaVersion {
version_string: "CezannePI-FP6 1.0.1.1".into(),
absolute_address: 20,
surrounding_region: MemoryRegion {
start_address: 5,
end_address: 100,
region_type: MemoryRegionType::Reserved,
},
};
assert_eq!(version.offset_in_region(), 15);
}
}
mod find_agesa_version_in_reader {
use super::*;
use indoc::formatdoc;
use rstest::rstest;
use std::io::Cursor;
#[rstest]
#[case::agesa_v9_signature(
"AGESA!V9\0CezannePI-FP6 1.0.1.1\0",
"CezannePI-FP6 1.0.1.1",
37
)]
#[case::agesa_v5_signature_arch2008(
"!!AGESA KaveriPI V1.1.0.7 \0",
"KaveriPI V1.1.0.7 ",
36
)]
#[case::agesa_v5_signature_alternative(
"!!!AGESAKaveriPI V1.1.0.7 \0",
"KaveriPI V1.1.0.7 ",
36
)]
fn returns_expected_version_string_and_absolute_address(
#[case] version_in_memory: String,
#[case] expected_version_string: String,
#[case] expected_absolute_address: usize,
) {
let file = Cursor::new(formatdoc! {"
PreceedingUnrelated\0Bytes%p
{version_in_memory}
\0SubsequentUnrelatedBytes\0
"});
let buf_reader = SkippingBufReader::new(file, 0, None);
let result = find_agesa_version_in_reader(buf_reader).unwrap();
assert_eq!(
result,
Some((expected_version_string, expected_absolute_address))
);
}
#[test]
fn returns_none_if_no_agesa_signature_is_present() {
let file = Cursor::new(b"AESA!V9\0CezannePI-FP6 1.0.1.1\0");
let buf_reader = SkippingBufReader::new(file, 0, None);
let result = find_agesa_version_in_reader(buf_reader).unwrap();
assert_eq!(result, None);
}
#[test]
fn returns_none_if_version_string_does_not_end() {
let file = Cursor::new(b"AGESA!V9\0CezannePI-FP6 1.0.1.1");
let buf_reader = SkippingBufReader::new(file, 0, None);
let result = find_agesa_version_in_reader(buf_reader).unwrap();
assert_eq!(result, None);
}
}
}