use std::collections::HashMap;
use consortium_cfg_common::config::{Config, MemoryRegion, Party, VisibleCore};
use consortium_cfg_common::database::{ChipDatabase, HardwareDoorbellPeripheral};
use consortium_data::DataError;
use crate::{database, error::CfgError};
#[derive(Debug, Clone)]
pub struct Session {
pub config: Config,
pub database: ChipDatabase,
}
impl Session {
pub fn from_toml(content: &str) -> Result<Self, CfgError> {
consortium_log::trace!("parsing Consortium.toml ({} bytes)", content.len());
let config: Config = toml::from_str(content).map_err(|err| {
consortium_log::trace!("manifest TOML parse failed: {}", err);
CfgError::Manifest(err.to_string())
})?;
consortium_log::trace!("resolving chip database for '{}'", config.profile.chip);
let database = database::load(&config.profile.chip)?;
let session = Self { config, database };
session.validate()?;
Ok(session)
}
pub fn validate(&self) -> Result<(), CfgError> {
let chip = &self.config.profile.chip;
consortium_log::trace!("validating manifest against chip '{}'", chip);
self.validate_memory()?;
self.validate_ipc()?;
self.validate_dbg()?;
self.validate_remoteproc()?;
for (name, peripheral) in &self.config.peripheral {
consortium_log::trace!(
"validating peripheral '{}' owned by '{}'",
name,
peripheral.owner
);
if !self.is_core(peripheral.owner) {
consortium_log::trace!("peripheral '{}' owner not visible on chip", name);
return Err(CfgError::OwnerNotVisible(
name.clone(),
peripheral.owner.to_string(),
chip.clone(),
));
}
if let VisibleCore::Mcore(_) = peripheral.owner {
let core = peripheral.owner.to_string();
match crate::chipmap::load(chip, &core) {
Ok(map) => {
if !map.contains_peripheral(name) {
consortium_log::trace!(
"peripheral '{}' absent from chip map for core '{}'",
name,
core
);
return Err(CfgError::PeripheralNotOnCore {
name: name.clone(),
core,
chip: chip.clone(),
});
}
}
Err(DataError::ChipMapUnavailable { .. }) => {
consortium_log::trace!(
"no chip map bundled for core '{}'; skipping peripheral accessibility check",
core
);
}
Err(err) => {
consortium_log::trace!("chip map load failed for core '{}': {}", core, err);
return Err(err.into());
}
}
}
}
Ok(())
}
fn validate_remoteproc(&self) -> Result<(), CfgError> {
let layouts = self.lower_remoteproc()?;
if !layouts.is_empty() && self.config.dts.mode.is_overlay() {
return Err(CfgError::Manifest(
"remoteproc deployment requires `[dts] mode = \"merge\"`".into(),
));
}
let shared =
self.config
.dbg
.cores
.iter()
.map(|(core, entry)| (format!("dbg.{core}"), entry.memory))
.chain(self.config.ipc.channels.iter().enumerate().filter_map(
|(index, channel)| {
channel
.memory()
.map(|memory| (format!("ipc.channel[{index}].config.memory"), memory))
},
));
for layout in layouts.values() {
let windows = [
("vector".to_string(), layout.vector.window),
("flash".to_string(), layout.flash),
("shmem".to_string(), layout.shmem.window),
("ram".to_string(), layout.ram),
]
.into_iter()
.chain(
layout
.regions
.iter()
.map(|region| (format!("regions.{}", region.name), region.window)),
);
for (window_name, window) in windows {
if !window.reserve {
continue;
}
let window_end = window.end().expect("layout is validated");
for (name, region) in shared.clone() {
let start = u64::from(region.base);
let end = start + u64::from(region.length);
if window.origin < end && start < window_end {
return Err(CfgError::Manifest(format!(
"[remoteproc.{}].{window_name} overlaps {name} in memory",
layout.core
)));
}
}
}
}
Ok(())
}
fn validate_ipc(&self) -> Result<(), CfgError> {
let chip = &self.config.profile.chip;
let mut used_channels: HashMap<(String, u32), Party> = HashMap::new();
for channel in &self.config.ipc.channels {
consortium_log::trace!(
"validating ipc channel '{}' on doorbell '{}'",
channel.name,
channel.doorbell
);
if let Some(memory) = channel.memory() {
let required =
consortium_ipc_transport_memory::required_region_len(2, channel.mtu as usize);
if (memory.length as usize) < required {
consortium_log::trace!(
"ipc channel '{}' region length {:#x} is smaller than aligned layout {:#x}",
channel.name,
memory.length,
required
);
return Err(CfgError::IpcRegionTooSmall {
channel: channel.name.clone(),
length: memory.length,
mtu: channel.mtu,
required,
});
}
}
if channel.cache() {
let controller = [channel.secondary, channel.primary]
.into_iter()
.map(Party::core)
.find(|core| *core != VisibleCore::Ap);
if controller
!= Some(VisibleCore::Mcore(
consortium_cfg_common::config::McoreName::Cm7,
))
{
let core = controller
.map_or_else(|| VisibleCore::Ap.to_string(), |core| core.to_string());
consortium_log::trace!(
"ipc channel '{}' enables caching on unsupported core '{}'",
channel.name,
core
);
return Err(CfgError::IpcCacheUnsupported {
channel: channel.name.clone(),
core,
});
}
}
let doorbell = self.doorbell(&channel.doorbell).ok_or_else(|| {
consortium_log::trace!("unknown doorbell '{}'", channel.doorbell);
self.unknown_doorbell(&channel.doorbell)
})?;
for (party, endpoint) in &channel.endpoints {
if !self.is_party(*party) {
consortium_log::trace!("ipc endpoint party '{}' not visible on chip", party);
return Err(CfgError::IpcEndpointNotVisible(
channel.doorbell.clone(),
party.to_string(),
chip.clone(),
));
}
if !doorbell.parties.iter().any(|entry| entry.core == *party) {
consortium_log::trace!("doorbell '{}' has no party '{}'", doorbell.name, party);
return Err(self.doorbell_party_unavailable(doorbell, *party));
}
match (channel.ack(), endpoint.ack_chan) {
(true, None) => {
consortium_log::trace!(
"ipc channel '{}' party '{}' sets config.ack without an ack_chan",
channel.name,
party
);
return Err(CfgError::IpcAckChannelMissing {
channel: channel.name.clone(),
party: party.to_string(),
});
}
(false, Some(_)) => {
consortium_log::trace!(
"ipc channel '{}' party '{}' declares ack_chan without config.ack",
channel.name,
party
);
return Err(CfgError::IpcAckChannelUnexpected {
channel: channel.name.clone(),
party: party.to_string(),
});
}
_ => {}
}
for chan in [Some(endpoint.chan), endpoint.ack_chan]
.into_iter()
.flatten()
{
if chan >= doorbell.channels {
consortium_log::trace!(
"doorbell '{}' channel {} out of bounds ({} channels)",
doorbell.name,
chan,
doorbell.channels
);
return Err(CfgError::DoorbellChannelUnavailable {
doorbell: doorbell.name.clone(),
channel: chan,
channels: doorbell.channels,
chip: chip.clone(),
trm: self.database.trm.clone(),
page: doorbell.channel_page,
});
}
let key = (doorbell.name.to_ascii_lowercase(), chan);
if let Some(first_party) = used_channels.insert(key, *party) {
consortium_log::trace!(
"doorbell '{}' channel {} claimed by both '{}' and '{}'",
doorbell.name,
chan,
first_party,
party
);
return Err(CfgError::DoorbellChannelDuplicate {
doorbell: doorbell.name.clone(),
channel: chan,
first_party: first_party.to_string(),
second_party: party.to_string(),
});
}
}
}
}
Ok(())
}
fn validate_dbg(&self) -> Result<(), CfgError> {
let chip = &self.config.profile.chip;
for core in self.config.dbg.cores.keys() {
consortium_log::trace!("validating dbg logger on core '{}'", core);
if *core == VisibleCore::Ap {
consortium_log::trace!("dbg core is the AP host, which cannot be a logger");
return Err(CfgError::DbgHostNotLoggable);
}
if !self.is_core(*core) {
consortium_log::trace!("dbg core '{}' not visible on chip", core);
return Err(CfgError::DbgCoreNotVisible {
core: core.to_string(),
chip: chip.clone(),
});
}
}
Ok(())
}
fn validate_memory(&self) -> Result<(), CfgError> {
let mut regions = Vec::new();
for (core, entry) in &self.config.dbg.cores {
regions.push((format!("dbg.{core}"), entry.memory, false));
}
for (index, channel) in self.config.ipc.channels.iter().enumerate() {
if let Some(memory) = channel.memory() {
regions.push((
format!("ipc.channel[{index}].config.memory"),
memory,
channel.cache(),
));
}
}
for (name, region, cache) in ®ions {
self.validate_memory_region(name, *region, *cache)?;
}
for (left_index, (left_name, left, _)) in regions.iter().enumerate() {
let left_end = left.base + left.length;
for (right_name, right, _) in regions.iter().skip(left_index + 1) {
let right_end = right.base + right.length;
if left.base < right_end && right.base < left_end {
consortium_log::trace!(
"shared-memory regions '{}' and '{}' overlap",
left_name,
right_name
);
return Err(CfgError::RegionOverlap {
a: left_name.clone(),
a_base: left.base,
a_end: left_end,
b: right_name.clone(),
b_base: right.base,
b_end: right_end,
});
}
}
}
Ok(())
}
fn validate_memory_region(
&self,
name: &str,
region: MemoryRegion,
cache: bool,
) -> Result<(), CfgError> {
consortium_log::trace!(
"validating region '{}' base {:#x} length {:#x}",
name,
region.base,
region.length
);
let Some(end) = region.base.checked_add(region.length) else {
consortium_log::trace!("region '{}' end address overflows", name);
return Err(self.region_out_of_bounds(name, region.base, region.base));
};
if region.length == 0 {
consortium_log::trace!("region '{}' has zero length", name);
return Err(CfgError::RegionEmpty {
region: name.to_string(),
});
}
if cache {
let requirement = if !region.base.is_multiple_of(64) {
Some("base must be aligned to 64 bytes")
} else if !region.length.is_multiple_of(64) {
Some("size must be a multiple of 64 bytes")
} else {
None
};
if let Some(requirement) = requirement {
consortium_log::trace!("region '{}' not cache-line aligned: {}", name, requirement);
return Err(CfgError::RegionNotCacheAligned {
region: name.to_string(),
base: region.base,
length: region.length,
requirement,
});
}
} else {
let mpu_requirement = if !region.length.is_power_of_two() {
Some("size must be a power of two")
} else if region.length < 32 {
Some("size must be at least 32 bytes")
} else if !region.base.is_multiple_of(region.length) {
Some("base must be aligned to the region size")
} else {
None
};
if let Some(requirement) = mpu_requirement {
consortium_log::trace!("region '{}' not MPU-aligned: {}", name, requirement);
return Err(CfgError::RegionNotMpuAligned {
region: name.to_string(),
base: region.base,
length: region.length,
requirement,
});
}
}
let contained = self.database.memory.regions.iter().any(|sram| {
let sram_base = sram.base;
let sram_end = sram_base + sram.length;
region.base >= sram_base && end <= sram_end
});
if !contained {
consortium_log::trace!(
"region '{}' ({:#x}..{:#x}) not contained in any SRAM window",
name,
region.base,
end
);
return Err(self.region_out_of_bounds(name, region.base, end));
}
Ok(())
}
fn region_out_of_bounds(&self, region: &str, base: u32, end: u32) -> CfgError {
let pages = self
.database
.memory
.trm_page
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(", ");
CfgError::RegionOutOfBounds {
region: region.to_string(),
base,
end,
chip: self.config.profile.chip.clone(),
trm: self.database.trm.clone(),
pages,
}
}
fn unknown_doorbell(&self, name: &str) -> CfgError {
let available = self
.database
.peripheral
.doorbells
.iter()
.map(|entry| format!("{} (TRM p. {})", entry.name, entry.page))
.collect::<Vec<_>>()
.join(", ");
CfgError::UnknownDoorbell {
name: name.to_string(),
chip: self.config.profile.chip.clone(),
trm: self.database.trm.clone(),
available,
}
}
pub(crate) fn doorbell(&self, name: &str) -> Option<&HardwareDoorbellPeripheral> {
self.database
.peripheral
.doorbells
.iter()
.find(|entry| entry.name.eq_ignore_ascii_case(name))
}
fn doorbell_party_unavailable(
&self,
doorbell: &HardwareDoorbellPeripheral,
party: Party,
) -> CfgError {
let available = doorbell
.parties
.iter()
.map(|entry| {
format!(
"{} (TRM memory p. {}, IRQ p. {})",
entry.core, entry.page_memory, entry.page_irq
)
})
.collect::<Vec<_>>()
.join(", ");
CfgError::DoorbellPartyUnavailable {
doorbell: doorbell.name.clone(),
party: party.to_string(),
chip: self.config.profile.chip.clone(),
available,
trm: self.database.trm.clone(),
page: doorbell.party_page,
}
}
fn is_core(&self, core: VisibleCore) -> bool {
match core {
VisibleCore::Ap => true,
VisibleCore::Mcore(name) => self
.database
.cp
.iter()
.any(|cp| cp.name.eq_ignore_ascii_case(name.as_str())),
VisibleCore::Rcore(name) => self
.database
.cp
.iter()
.any(|cp| cp.name.eq_ignore_ascii_case(name.as_str())),
VisibleCore::Rvcore(name) => self
.database
.cp
.iter()
.any(|cp| cp.name.eq_ignore_ascii_case(&name.to_string())),
}
}
fn is_party(&self, party: Party) -> bool {
self.is_core(party.core())
}
}
#[cfg(test)]
mod tests {
use super::*;
const MANIFEST: &str = include_str!("../Consortium.example.stm32mp257.toml");
fn imx95_cached_manifest(secondary: &str, doorbell: &str, base: u32, length: u32) -> String {
format!(
r#"
[profile]
chip = "mimx9596xvt"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "{doorbell}"
pattern = "memory"
primary = "linux"
secondary = "{secondary}"
party.linux = {{ chan = 0, type = "Ping" }}
party.{secondary} = {{ chan = 1, type = "Pong" }}
config.memory = {{ base = {base}, length = {length} }}
config.cache = true
mtu = 128
"#
)
}
#[test]
fn loads_example_manifest() {
let session = Session::from_toml(MANIFEST).expect("example manifest should load");
assert_eq!(session.config.profile.chip, "stm32mp257fak3");
assert_eq!(session.database.name, "STM32MP25");
assert_eq!(session.config.ipc.channels.len(), 2);
assert_eq!(session.config.ipc.channels[0].doorbell, "ipcc1");
assert_eq!(
session.config.ipc.channels[0].endpoints[&Party::AcoreNonSecure].chan,
0
);
assert_eq!(
session.config.ipc.channels[0].endpoints[&"cm33".parse::<Party>().unwrap()].r#type,
"MotionControl"
);
assert_eq!(
session.config.ipc.channels[0].primary,
Party::AcoreNonSecure
);
assert_eq!(
session.config.ipc.channels[0].secondary,
"cm33".parse::<Party>().unwrap()
);
assert_eq!(session.config.endpoints[&Party::AcoreNonSecure], "./app");
assert_eq!(
session
.config
.tee
.as_ref()
.unwrap()
.uuid
.to_string()
.to_uppercase(),
"48551DD5-74B2-4D24-BF9D-A09F02F5C56F"
);
assert_eq!(
session.config.dbg.mode,
consortium_cfg_common::config::DbgMode::Auto
);
let cm33_dbg = session.config.dbg.cores[&"cm33".parse::<VisibleCore>().unwrap()];
assert_eq!(cm33_dbg.mtu, 64);
assert_eq!(cm33_dbg.memory.length, 0x2000);
let i2c1 = &session.config.peripheral["i2c1"];
assert_eq!(i2c1.owner, VisibleCore::Ap);
assert_eq!(i2c1.pin["sda"], "PB0");
let spi1 = &session.config.peripheral["spi1"];
assert_eq!(spi1.owner, "cm33".parse().unwrap());
}
#[test]
fn rejects_unknown_doorbell() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "nope"
pattern = "memory"
primary = "linux"
secondary = "cm33"
party.linux = { chan = 0, type = "Ping" }
party.cm33 = { chan = 1, type = "Pong" }
config.memory = { base = 0x00040000, length = 0x00008000 }
mtu = 128
"#;
let err = Session::from_toml(manifest).unwrap_err();
assert!(matches!(err, CfgError::UnknownDoorbell { .. }));
assert!(err.to_string().contains("HSEM (TRM p. 1306)"));
}
#[test]
fn rejects_doorbell_party_unavailable() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "ipcc1"
pattern = "memory"
primary = "linux"
secondary = "cm0p"
party.cm0p = { chan = 0, type = "PowerMode" }
config.memory = { base = 0x00040000, length = 0x00008000 }
mtu = 128
"#;
let err = Session::from_toml(manifest).unwrap_err();
assert!(matches!(err, CfgError::DoorbellPartyUnavailable { .. }));
assert!(err.to_string().contains("Configured parties: linux"));
assert!(err.to_string().contains("TRM"));
}
#[test]
fn rejects_unavailable_doorbell_channel() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "ipcc1"
pattern = "memory"
primary = "linux"
secondary = "cm33"
party.linux = { chan = 32, type = "Ping" }
config.memory = { base = 0x00040000, length = 0x00008000 }
mtu = 128
"#;
assert!(matches!(
Session::from_toml(manifest),
Err(CfgError::DoorbellChannelUnavailable { .. })
));
}
#[test]
fn rejects_duplicate_doorbell_channel() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "ipcc1"
pattern = "memory"
primary = "linux"
secondary = "cm33"
party.linux = { chan = 0, type = "Ping" }
party.cm33 = { chan = 0, type = "Pong" }
config.memory = { base = 0x00040000, length = 0x00008000 }
mtu = 128
"#;
assert!(matches!(
Session::from_toml(manifest),
Err(CfgError::DoorbellChannelDuplicate { .. })
));
}
#[test]
fn rejects_region_outside_sram() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "ipcc1"
pattern = "memory"
primary = "linux"
secondary = "cm33"
party.linux = { chan = 0, type = "Ping" }
config.memory = { base = 0x00400000, length = 0x00080000 }
mtu = 128
"#;
assert!(matches!(
Session::from_toml(manifest),
Err(CfgError::RegionOutOfBounds { .. })
));
}
#[test]
fn rejects_non_power_of_two_region() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "ipcc1"
pattern = "memory"
primary = "linux"
secondary = "cm33"
party.linux = { chan = 0, type = "Ping" }
config.memory = { base = 0x00400000, length = 0x00006000 }
mtu = 128
"#;
assert!(matches!(
Session::from_toml(manifest),
Err(CfgError::RegionNotMpuAligned { .. })
));
}
fn imx95_ack_manifest(linux_ack: &str, cm7_ack: &str, ack: bool) -> String {
format!(
r#"
[profile]
chip = "mimx9596xvt"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "sensor"
doorbell = "mu7"
pattern = "memory"
primary = "linux"
secondary = "cm7"
party.linux = {{ chan = 6, {linux_ack} type = "SensorData" }}
party.cm7 = {{ chan = 7, {cm7_ack} type = "MotionControl" }}
config.memory = {{ base = 0x00488000, length = 0x4000 }}
config.ack = {ack}
mtu = 128
"#
)
}
#[test]
fn accepts_ack_channels_alongside_the_data_lines() {
let manifest = imx95_ack_manifest("ack_chan = 0,", "ack_chan = 1,", true);
Session::from_toml(&manifest).expect("distinct ack lines should be valid");
}
#[test]
fn rejects_ack_without_an_ack_channel() {
let manifest = imx95_ack_manifest("", "ack_chan = 1,", true);
let err = Session::from_toml(&manifest).unwrap_err();
assert!(matches!(err, CfgError::IpcAckChannelMissing { .. }));
assert!(err.to_string().contains("ack_chan"));
}
#[test]
fn rejects_an_ack_channel_without_the_capability() {
let manifest = imx95_ack_manifest("ack_chan = 0,", "ack_chan = 1,", false);
assert!(matches!(
Session::from_toml(&manifest),
Err(CfgError::IpcAckChannelUnexpected { .. })
));
}
#[test]
fn rejects_an_ack_channel_that_aliases_a_data_line() {
let manifest = imx95_ack_manifest("ack_chan = 7,", "ack_chan = 1,", true);
assert!(matches!(
Session::from_toml(&manifest),
Err(CfgError::DoorbellChannelDuplicate { channel: 7, .. })
));
}
#[test]
fn rejects_an_out_of_range_ack_channel() {
let manifest = imx95_ack_manifest("ack_chan = 0,", "ack_chan = 64,", true);
assert!(matches!(
Session::from_toml(&manifest),
Err(CfgError::DoorbellChannelUnavailable { channel: 64, .. })
));
}
#[test]
fn accepts_cached_non_power_of_two_region() {
let manifest = imx95_cached_manifest("cm7", "mu7", 0x0048_8000, 0x6000);
Session::from_toml(&manifest).expect("cache-line-aligned cached region should be valid");
}
#[test]
fn rejects_region_too_small_for_aligned_slot_pair() {
let manifest = imx95_cached_manifest("cm7", "mu7", 0x0048_8000, 0x200);
let err = Session::from_toml(&manifest).unwrap_err();
assert!(matches!(
err,
CfgError::IpcRegionTooSmall {
length: 0x200,
mtu: 128,
required: 0x24c,
..
}
));
assert!(err.to_string().contains("bidirectional slot pair"));
}
#[test]
fn accepts_smallest_cache_aligned_region_for_slot_pair() {
let manifest = imx95_cached_manifest("cm7", "mu7", 0x0048_8000, 0x280);
Session::from_toml(&manifest)
.expect("0x280 bytes contains both aligned 128-byte-MTU slots");
}
#[test]
fn rejects_cached_region_with_partial_cache_line() {
for (base, length) in [(0x0048_8000, 0x4020), (0x0048_8020, 0x4000)] {
let manifest = imx95_cached_manifest("cm7", "mu7", base, length);
assert!(matches!(
Session::from_toml(&manifest),
Err(CfgError::RegionNotCacheAligned { .. })
));
}
}
#[test]
fn rejects_cached_region_on_cacheless_controller() {
let manifest = imx95_cached_manifest("cm33", "mu1", 0x0048_8000, 0x4000);
let err = Session::from_toml(&manifest).unwrap_err();
assert!(matches!(err, CfgError::IpcCacheUnsupported { .. }));
assert!(err.to_string().contains("cm33"));
}
#[test]
fn rejects_misaligned_region() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "ipcc1"
pattern = "memory"
primary = "linux"
secondary = "cm33"
party.linux = { chan = 0, type = "Ping" }
config.memory = { base = 0x00402000, length = 0x00004000 }
mtu = 128
"#;
let err = Session::from_toml(manifest).unwrap_err();
assert!(matches!(err, CfgError::RegionNotMpuAligned { .. }));
assert!(err.to_string().contains("aligned"));
}
#[test]
fn rejects_subminimal_region() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "ipcc1"
pattern = "memory"
primary = "linux"
secondary = "cm33"
party.linux = { chan = 0, type = "Ping" }
config.memory = { base = 0x00400000, length = 0x10 }
mtu = 128
"#;
assert!(matches!(
Session::from_toml(manifest),
Err(CfgError::RegionNotMpuAligned { .. })
));
}
#[test]
fn rejects_dbg_on_application_processor() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[dbg.ap]
memory = { base = 0x00044000, length = 0x00002000 }
mtu = 64
"#;
assert!(matches!(
Session::from_toml(manifest),
Err(CfgError::DbgHostNotLoggable)
));
}
#[test]
fn rejects_dbg_on_unknown_core() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[dbg.cm7]
memory = { base = 0x00044000, length = 0x00002000 }
mtu = 64
"#;
assert!(matches!(
Session::from_toml(manifest),
Err(CfgError::DbgCoreNotVisible { .. })
));
}
#[test]
fn rejects_overlapping_regions() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[dbg.cm33]
memory = { base = 0x00044000, length = 0x00002000 }
mtu = 64
[[ipc.channel]]
name = "probe"
doorbell = "ipcc1"
pattern = "memory"
primary = "linux"
secondary = "cm33"
party.linux = { chan = 0, type = "Ping" }
config.memory = { base = 0x00040000, length = 0x00008000 }
mtu = 128
"#;
assert!(matches!(
Session::from_toml(manifest),
Err(CfgError::RegionOverlap { .. })
));
}
#[test]
fn accepts_peripheral_present_on_core() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "ipcc1"
pattern = "memory"
primary = "linux"
secondary = "cm33"
party.linux = { chan = 0, type = "Ping" }
config.memory = { base = 0x00040000, length = 0x00002000 }
mtu = 128
[peripheral.i2c1]
owner = "cm33"
clock = 64000000
frequency = 100000
"#;
assert!(Session::from_toml(manifest).is_ok());
}
#[test]
fn rejects_peripheral_absent_on_core() {
let manifest = r#"
[profile]
chip = "stm32mp257fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "ipcc1"
pattern = "memory"
primary = "linux"
secondary = "cm33"
party.linux = { chan = 0, type = "Ping" }
config.memory = { base = 0x00040000, length = 0x00002000 }
mtu = 128
[peripheral.nonexistent99]
owner = "cm33"
"#;
assert!(matches!(
Session::from_toml(manifest),
Err(CfgError::PeripheralNotOnCore { .. })
));
}
const MP235_MANIFEST: &str = include_str!("../Consortium.example.stm32mp235.toml");
fn mp235_manifest(doorbell: &str, secondary: &str, base: u32, extra: &str) -> String {
format!(
r#"
[profile]
chip = "stm32mp235fak3"
shared = "./shared"
deploy_root = "/opt/consortium"
[[ipc.channel]]
name = "probe"
doorbell = "{doorbell}"
pattern = "memory"
primary = "linux"
secondary = "{secondary}"
party.linux = {{ chan = 0, type = "Ping" }}
party.{secondary} = {{ chan = 1, type = "Pong" }}
config.memory = {{ base = {base}, length = 0x4000 }}
mtu = 128
{extra}
"#
)
}
#[test]
fn loads_mp235_example_manifest() {
let session = Session::from_toml(MP235_MANIFEST).expect("MP235 example should load");
assert_eq!(session.database.name, "STM32MP23");
assert_eq!(session.config.ipc.channels.len(), 1);
}
#[test]
fn mp235_rejects_ipcc2() {
let manifest = mp235_manifest("ipcc2", "cm33", 0x0006_4000, "");
assert!(matches!(
Session::from_toml(&manifest),
Err(CfgError::UnknownDoorbell { .. })
));
}
#[test]
fn mp235_rejects_a_cm0p_endpoint() {
let manifest = mp235_manifest("ipcc1", "cm0p", 0x0006_4000, "");
assert!(Session::from_toml(&manifest).is_err());
}
#[test]
fn mp235_rejects_an_mp25_only_peripheral() {
let i2c = |name: &str| format!("[peripheral.{name}]\nowner = \"cm33\"");
let absent = mp235_manifest("ipcc1", "cm33", 0x0006_4000, &i2c("i2c3"));
assert!(matches!(
Session::from_toml(&absent),
Err(CfgError::PeripheralNotOnCore { .. })
));
let present = mp235_manifest("ipcc1", "cm33", 0x0006_4000, &i2c("i2c8"));
assert!(Session::from_toml(&present).is_ok());
}
#[test]
fn mp235_rejects_an_lpsram_window() {
let manifest = mp235_manifest("ipcc1", "cm33", 0x000C_4000, "");
assert!(Session::from_toml(&manifest).is_err());
}
}