Skip to main content

consortium_cfg/
session.rs

1// Copyright 2026 Ethan Wu
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! Loads a `Consortium.toml` manifest and binds it to its chip-family database.
18
19use std::collections::HashMap;
20
21use consortium_cfg_common::config::{Config, MemoryRegion, Party, VisibleCore};
22use consortium_cfg_common::database::{ChipDatabase, HardwareDoorbellPeripheral};
23
24use consortium_data::DataError;
25
26use crate::{database, error::CfgError};
27
28/// A parsed manifest resolved against the database for its target chip.
29#[derive(Debug, Clone)]
30pub struct Session {
31    /// The parsed `Consortium.toml`.
32    pub config: Config,
33    /// The chip-family database selected by `[profile] chip`.
34    pub database: ChipDatabase,
35}
36
37impl Session {
38    /// Parses a `Consortium.toml` string, resolves its chip database, and
39    /// validates that the manifest only references resources the chip provides.
40    ///
41    /// # Errors
42    ///
43    /// - [`CfgError::Manifest`] if the TOML is malformed.
44    /// - [`DataError::UnknownChip`] / [`DataError::DatabaseParse`] if the chip
45    ///   cannot be resolved to a bundled database.
46    /// - [`CfgError::UnknownDoorbell`] / [`CfgError::OwnerNotVisible`] from
47    ///   [`Self::validate`].
48    pub fn from_toml(content: &str) -> Result<Self, CfgError> {
49        consortium_log::trace!("parsing Consortium.toml ({} bytes)", content.len());
50        let config: Config = toml::from_str(content).map_err(|err| {
51            consortium_log::trace!("manifest TOML parse failed: {}", err);
52            CfgError::Manifest(err.to_string())
53        })?;
54        consortium_log::trace!("resolving chip database for '{}'", config.profile.chip);
55        let database = database::load(&config.profile.chip)?;
56
57        let session = Self { config, database };
58        session.validate()?;
59        Ok(session)
60    }
61
62    /// Checks that every doorbell and peripheral owner named in the manifest
63    /// exists on the target chip, and that the shared-memory regions fit within
64    /// the chip's SRAM without overlapping.
65    pub fn validate(&self) -> Result<(), CfgError> {
66        let chip = &self.config.profile.chip;
67        consortium_log::trace!("validating manifest against chip '{}'", chip);
68
69        self.validate_memory()?;
70        self.validate_ipc()?;
71        self.validate_dbg()?;
72        // After IPC and dbg, so an unknown core or a doorbell party the chip
73        // lacks reports as that rather than as a window the core cannot reach.
74        self.validate_memory_access()?;
75        self.validate_remoteproc()?;
76
77        for (name, peripheral) in &self.config.peripheral {
78            consortium_log::trace!(
79                "validating peripheral '{}' owned by '{}'",
80                name,
81                peripheral.owner
82            );
83            if !self.is_core(peripheral.owner) {
84                consortium_log::trace!("peripheral '{}' owner not visible on chip", name);
85                return Err(CfgError::OwnerNotVisible(
86                    name.clone(),
87                    peripheral.owner.to_string(),
88                    chip.clone(),
89                ));
90            }
91
92            // A coprocessor may only own a peripheral its core can actually
93            // reach. The owning core's chip dump lists exactly those peripherals,
94            // so presence in that map is the accessibility filter. AP-owned
95            // peripherals are surfaced to Linux via the DTS transpilers instead,
96            // so they are not checked here. When no dump is bundled for the
97            // owning core, the filter cannot be applied and the peripheral is
98            // left unvalidated (it also gets no generated bring-up).
99            if let VisibleCore::Mcore(_) = peripheral.owner {
100                let core = peripheral.owner.to_string();
101                match crate::chipmap::load(chip, &core) {
102                    Ok(map) => {
103                        if !map.contains_peripheral(name) {
104                            consortium_log::trace!(
105                                "peripheral '{}' absent from chip map for core '{}'",
106                                name,
107                                core
108                            );
109                            return Err(CfgError::PeripheralNotOnCore {
110                                name: name.clone(),
111                                core,
112                                chip: chip.clone(),
113                            });
114                        }
115                    }
116                    Err(DataError::ChipMapUnavailable { .. }) => {
117                        consortium_log::trace!(
118                            "no chip map bundled for core '{}'; skipping peripheral accessibility check",
119                            core
120                        );
121                    }
122                    Err(err) => {
123                        consortium_log::trace!("chip map load failed for core '{}': {}", core, err);
124                        return Err(err.into());
125                    }
126                }
127            }
128        }
129
130        self.validate_pins()
131    }
132
133    /// Checks every `[peripheral.<name>] pin.<signal> = "<pad>"` against the
134    /// chip's signal map: the pad must be able to carry `<NAME>_<SIGNAL>`, must
135    /// be bonded out in the chip's package when `[profile] chip` names one, and
136    /// may be assigned only once across the manifest.
137    ///
138    /// Peripherals are checked in name order, so the first error reported is
139    /// stable. A chip with no bundled signal map is left unchecked.
140    fn validate_pins(&self) -> Result<(), CfgError> {
141        let chip = &self.config.profile.chip;
142        if self.config.peripheral.values().all(|p| p.pin.is_empty()) {
143            return Ok(());
144        }
145        let map = match crate::signal::load(chip) {
146            Ok(map) => map,
147            Err(DataError::SignalMapUnavailable(_)) => {
148                consortium_log::trace!("no signal map bundled for '{}'; pins unchecked", chip);
149                return Ok(());
150            }
151            Err(err) => return Err(err.into()),
152        };
153        let package = map.package_of(chip);
154
155        let mut names: Vec<&String> = self.config.peripheral.keys().collect();
156        names.sort();
157        let mut taken: HashMap<String, String> = HashMap::new();
158        for name in names {
159            let mut pins: Vec<(&String, &String)> =
160                self.config.peripheral[name].pin.iter().collect();
161            pins.sort();
162            for (signal, pad) in pins {
163                let function = format!("{name}_{signal}").to_ascii_uppercase();
164                let routes = map.signal_routes(name, signal);
165                if routes.is_empty() {
166                    return Err(CfgError::PinSignalUnknown {
167                        peripheral: name.clone(),
168                        signal: signal.clone(),
169                        function,
170                        chip: chip.clone(),
171                    });
172                }
173                if !routes.iter().any(|r| r.pad.eq_ignore_ascii_case(pad)) {
174                    let mut allowed: Vec<&str> = routes.iter().map(|r| r.pad.as_str()).collect();
175                    allowed.sort_unstable();
176                    allowed.dedup();
177                    return Err(CfgError::PinCannotCarry {
178                        peripheral: name.clone(),
179                        signal: signal.clone(),
180                        pad: pad.clone(),
181                        function,
182                        allowed: allowed.join(", "),
183                    });
184                }
185                if let Some(package) = package {
186                    let bonded = map
187                        .pad(pad)
188                        .and_then(|p| p.packages.as_ref())
189                        .is_none_or(|packages| packages.contains(&package.name));
190                    if !bonded {
191                        return Err(CfgError::PinNotInPackage {
192                            peripheral: name.clone(),
193                            signal: signal.clone(),
194                            pad: pad.clone(),
195                            package: package.name.clone(),
196                            chip: chip.clone(),
197                        });
198                    }
199                }
200                let owner = format!("{name}.{signal}");
201                if let Some(first) = taken.insert(pad.to_ascii_uppercase(), owner.clone()) {
202                    return Err(CfgError::PinConflict {
203                        pad: pad.clone(),
204                        first,
205                        second: owner,
206                    });
207                }
208            }
209        }
210        Ok(())
211    }
212
213    fn validate_remoteproc(&self) -> Result<(), CfgError> {
214        let layouts = self.lower_remoteproc()?;
215        if !layouts.is_empty() && self.config.dts.mode.is_overlay() {
216            return Err(CfgError::Manifest(
217                "remoteproc deployment requires `[dts] mode = \"merge\"`".into(),
218            ));
219        }
220        let shared =
221            self.config
222                .dbg
223                .cores
224                .iter()
225                .map(|(core, entry)| (format!("dbg.{core}"), entry.memory))
226                .chain(self.config.ipc.channels.iter().enumerate().filter_map(
227                    |(index, channel)| {
228                        channel
229                            .memory()
230                            .map(|memory| (format!("ipc.channel[{index}].config.memory"), memory))
231                    },
232                ));
233
234        for layout in layouts.values() {
235            let windows = [
236                ("vector".to_string(), layout.vector.window),
237                ("flash".to_string(), layout.flash),
238                ("shmem".to_string(), layout.shmem.window),
239                ("ram".to_string(), layout.ram),
240            ]
241            .into_iter()
242            .chain(
243                layout
244                    .regions
245                    .iter()
246                    .map(|region| (format!("regions.{}", region.name), region.window)),
247            );
248            for (window_name, window) in windows {
249                if !window.reserve {
250                    continue;
251                }
252                let window_end = window.end().expect("layout is validated");
253                for (name, region) in shared.clone() {
254                    let start = u64::from(region.base);
255                    let end = start + u64::from(region.length);
256                    if window.origin < end && start < window_end {
257                        return Err(CfgError::Manifest(format!(
258                            "[remoteproc.{}].{window_name} overlaps {name} in memory",
259                            layout.core
260                        )));
261                    }
262                }
263            }
264        }
265        Ok(())
266    }
267
268    /// Validates IPC doorbell references, party support, and channel allocation.
269    fn validate_ipc(&self) -> Result<(), CfgError> {
270        let chip = &self.config.profile.chip;
271        let mut used_channels: HashMap<(String, u32), Party> = HashMap::new();
272
273        for channel in &self.config.ipc.channels {
274            consortium_log::trace!(
275                "validating ipc channel '{}' on doorbell '{}'",
276                channel.name,
277                channel.doorbell
278            );
279
280            if let Some(memory) = channel.memory() {
281                let required =
282                    consortium_ipc_transport_memory::required_region_len(2, channel.mtu as usize);
283                if (memory.length as usize) < required {
284                    consortium_log::trace!(
285                        "ipc channel '{}' region length {:#x} is smaller than aligned layout {:#x}",
286                        channel.name,
287                        memory.length,
288                        required
289                    );
290                    return Err(CfgError::IpcRegionTooSmall {
291                        channel: channel.name.clone(),
292                        length: memory.length,
293                        mtu: channel.mtu,
294                        required,
295                    });
296                }
297            }
298
299            if channel.cache() {
300                let controller = [channel.secondary, channel.primary]
301                    .into_iter()
302                    .map(Party::core)
303                    .find(|core| *core != VisibleCore::Ap);
304                if controller
305                    != Some(VisibleCore::Mcore(
306                        consortium_cfg_common::config::McoreName::Cm7,
307                    ))
308                {
309                    let core = controller
310                        .map_or_else(|| VisibleCore::Ap.to_string(), |core| core.to_string());
311                    consortium_log::trace!(
312                        "ipc channel '{}' enables caching on unsupported core '{}'",
313                        channel.name,
314                        core
315                    );
316                    return Err(CfgError::IpcCacheUnsupported {
317                        channel: channel.name.clone(),
318                        core,
319                    });
320                }
321            }
322            let doorbell = self.doorbell(&channel.doorbell).ok_or_else(|| {
323                consortium_log::trace!("unknown doorbell '{}'", channel.doorbell);
324                self.unknown_doorbell(&channel.doorbell)
325            })?;
326
327            for (party, endpoint) in &channel.endpoints {
328                if !self.is_party(*party) {
329                    consortium_log::trace!("ipc endpoint party '{}' not visible on chip", party);
330                    return Err(CfgError::IpcEndpointNotVisible(
331                        channel.doorbell.clone(),
332                        party.to_string(),
333                        chip.clone(),
334                    ));
335                }
336
337                if !doorbell.parties.iter().any(|entry| entry.core == *party) {
338                    consortium_log::trace!("doorbell '{}' has no party '{}'", doorbell.name, party);
339                    return Err(self.doorbell_party_unavailable(doorbell, *party));
340                }
341
342                // An acknowledgement line is a doorbell channel like any other, so it goes
343                // through the same bounds and collision checks — that is what stops one
344                // from silently aliasing a data line or another endpoint's.
345                match (channel.ack(), endpoint.ack_chan) {
346                    (true, None) => {
347                        consortium_log::trace!(
348                            "ipc channel '{}' party '{}' sets config.ack without an ack_chan",
349                            channel.name,
350                            party
351                        );
352                        return Err(CfgError::IpcAckChannelMissing {
353                            channel: channel.name.clone(),
354                            party: party.to_string(),
355                        });
356                    }
357                    (false, Some(_)) => {
358                        consortium_log::trace!(
359                            "ipc channel '{}' party '{}' declares ack_chan without config.ack",
360                            channel.name,
361                            party
362                        );
363                        return Err(CfgError::IpcAckChannelUnexpected {
364                            channel: channel.name.clone(),
365                            party: party.to_string(),
366                        });
367                    }
368                    _ => {}
369                }
370
371                for chan in [Some(endpoint.chan), endpoint.ack_chan]
372                    .into_iter()
373                    .flatten()
374                {
375                    if chan >= doorbell.channels {
376                        consortium_log::trace!(
377                            "doorbell '{}' channel {} out of bounds ({} channels)",
378                            doorbell.name,
379                            chan,
380                            doorbell.channels
381                        );
382                        return Err(CfgError::DoorbellChannelUnavailable {
383                            doorbell: doorbell.name.clone(),
384                            channel: chan,
385                            channels: doorbell.channels,
386                            chip: chip.clone(),
387                            trm: self.database.trm.clone(),
388                            page: doorbell.channel_page,
389                        });
390                    }
391
392                    let key = (doorbell.name.to_ascii_lowercase(), chan);
393                    if let Some(first_party) = used_channels.insert(key, *party) {
394                        consortium_log::trace!(
395                            "doorbell '{}' channel {} claimed by both '{}' and '{}'",
396                            doorbell.name,
397                            chan,
398                            first_party,
399                            party
400                        );
401                        return Err(CfgError::DoorbellChannelDuplicate {
402                            doorbell: doorbell.name.clone(),
403                            channel: chan,
404                            first_party: first_party.to_string(),
405                            second_party: party.to_string(),
406                        });
407                    }
408                }
409            }
410        }
411
412        Ok(())
413    }
414
415    /// Validates `[dbg.<core>]` entries: each must name a coprocessor visible
416    /// on the chip. The application processor is always the host that decodes
417    /// the logs, so it can never be a logger and `[dbg.ap]` is rejected.
418    fn validate_dbg(&self) -> Result<(), CfgError> {
419        let chip = &self.config.profile.chip;
420
421        for core in self.config.dbg.cores.keys() {
422            consortium_log::trace!("validating dbg logger on core '{}'", core);
423            if *core == VisibleCore::Ap {
424                consortium_log::trace!("dbg core is the AP host, which cannot be a logger");
425                return Err(CfgError::DbgHostNotLoggable);
426            }
427
428            if !self.is_core(*core) {
429                consortium_log::trace!("dbg core '{}' not visible on chip", core);
430                return Err(CfgError::DbgCoreNotVisible {
431                    core: core.to_string(),
432                    chip: chip.clone(),
433                });
434            }
435        }
436
437        Ok(())
438    }
439
440    /// Every shared-memory window in the manifest, with its cache policy and the
441    /// cores that access it.
442    fn shared_windows(&self) -> Vec<(String, MemoryRegion, bool, Vec<VisibleCore>)> {
443        let mut regions = Vec::new();
444
445        // A debug ring is written by its core and drained by the AP.
446        for (core, entry) in &self.config.dbg.cores {
447            regions.push((
448                format!("dbg.{core}"),
449                entry.memory,
450                false,
451                vec![*core, VisibleCore::Ap],
452            ));
453        }
454
455        for (index, channel) in self.config.ipc.channels.iter().enumerate() {
456            if let Some(memory) = channel.memory() {
457                regions.push((
458                    format!("ipc.channel[{index}].config.memory"),
459                    memory,
460                    channel.cache(),
461                    vec![channel.primary.core(), channel.secondary.core()],
462                ));
463            }
464        }
465        regions
466    }
467
468    /// Validates shared-memory regions: each must have a non-zero length, lie
469    /// entirely within one of the chip's memory regions, and not overlap another.
470    fn validate_memory(&self) -> Result<(), CfgError> {
471        let regions = self.shared_windows();
472
473        for (name, region, cache, _) in &regions {
474            self.validate_memory_region(name, *region, *cache)?;
475        }
476
477        for (left_index, (left_name, left, _, _)) in regions.iter().enumerate() {
478            let left_end = left.base + left.length;
479            for (right_name, right, _, _) in regions.iter().skip(left_index + 1) {
480                let right_end = right.base + right.length;
481                if left.base < right_end && right.base < left_end {
482                    consortium_log::trace!(
483                        "shared-memory regions '{}' and '{}' overlap",
484                        left_name,
485                        right_name
486                    );
487                    return Err(CfgError::RegionOverlap {
488                        a: left_name.clone(),
489                        a_base: left.base,
490                        a_end: left_end,
491                        b: right_name.clone(),
492                        b_base: right.base,
493                        b_end: right_end,
494                    });
495                }
496            }
497        }
498
499        Ok(())
500    }
501
502    fn validate_memory_region(
503        &self,
504        name: &str,
505        region: MemoryRegion,
506        cache: bool,
507    ) -> Result<(), CfgError> {
508        consortium_log::trace!(
509            "validating region '{}' base {:#x} length {:#x}",
510            name,
511            region.base,
512            region.length
513        );
514        // Exclusive end address; a region that wraps past u64::MAX cannot
515        // fit in any SRAM window, so treat the overflow as out-of-bounds.
516        let Some(end) = region.base.checked_add(region.length) else {
517            consortium_log::trace!("region '{}' end address overflows", name);
518            return Err(self.region_out_of_bounds(name, region.base, region.base));
519        };
520
521        if region.length == 0 {
522            consortium_log::trace!("region '{}' has zero length", name);
523            return Err(CfgError::RegionEmpty {
524                region: name.to_string(),
525            });
526        }
527
528        if cache {
529            // Cache maintenance is destructive at cache-line granularity. Keep
530            // both boundaries on 64-byte lines: this covers AArch64's fixed
531            // sweep size and Cortex-M7's 32-byte SCB helper requirement.
532            let requirement = if !region.base.is_multiple_of(64) {
533                Some("base must be aligned to 64 bytes")
534            } else if !region.length.is_multiple_of(64) {
535                Some("size must be a multiple of 64 bytes")
536            } else {
537                None
538            };
539            if let Some(requirement) = requirement {
540                consortium_log::trace!("region '{}' not cache-line aligned: {}", name, requirement);
541                return Err(CfgError::RegionNotCacheAligned {
542                    region: name.to_string(),
543                    base: region.base,
544                    length: region.length,
545                    requirement,
546                });
547            }
548        } else {
549            // MPU constraints (ARMv7-M / ARMv8-M): a region's size must be a
550            // power of two of at least 32 bytes, and its base must be aligned
551            // to that size. Uncached controller mappings need an MPU entry.
552            let mpu_requirement = if !region.length.is_power_of_two() {
553                Some("size must be a power of two")
554            } else if region.length < 32 {
555                Some("size must be at least 32 bytes")
556            } else if !region.base.is_multiple_of(region.length) {
557                Some("base must be aligned to the region size")
558            } else {
559                None
560            };
561            if let Some(requirement) = mpu_requirement {
562                consortium_log::trace!("region '{}' not MPU-aligned: {}", name, requirement);
563                return Err(CfgError::RegionNotMpuAligned {
564                    region: name.to_string(),
565                    base: region.base,
566                    length: region.length,
567                    requirement,
568                });
569            }
570        }
571
572        let contained = self
573            .database
574            .memory
575            .regions
576            .iter()
577            .any(|window| window.contains(region.base, region.length));
578        if !contained {
579            consortium_log::trace!(
580                "region '{}' ({:#x}..{:#x}) not contained in any memory window",
581                name,
582                region.base,
583                end
584            );
585            return Err(self.region_out_of_bounds(name, region.base, end));
586        }
587
588        Ok(())
589    }
590
591    /// Validates that every core using a shared-memory window may access the
592    /// chip memory region holding it (`visible` in the chip database). On a
593    /// System-Manager-partitioned chip a window outside that grant bus-faults
594    /// on first touch.
595    fn validate_memory_access(&self) -> Result<(), CfgError> {
596        for (name, region, _, cores) in self.shared_windows() {
597            let Some(window) = self
598                .database
599                .memory
600                .regions
601                .iter()
602                .find(|window| window.contains(region.base, region.length))
603            else {
604                // `validate_memory` already rejected it.
605                continue;
606            };
607            self.validate_window_access(&name, window, &cores)?;
608        }
609        Ok(())
610    }
611
612    fn validate_window_access(
613        &self,
614        name: &str,
615        window: &consortium_cfg_common::database::MemoryRegion,
616        cores: &[VisibleCore],
617    ) -> Result<(), CfgError> {
618        if let Some(core) = cores.iter().find(|core| !window.visible.contains(core)) {
619            consortium_log::trace!(
620                "region '{}' lies in '{}', which core '{}' cannot access",
621                name,
622                window.name,
623                core
624            );
625            return Err(CfgError::RegionNotVisible {
626                region: name.to_string(),
627                window: window.name.clone(),
628                core: core.to_string(),
629                visible: window
630                    .visible
631                    .iter()
632                    .map(ToString::to_string)
633                    .collect::<Vec<_>>()
634                    .join(", "),
635                chip: self.config.profile.chip.clone(),
636            });
637        }
638
639        Ok(())
640    }
641
642    /// Builds a [`CfgError::RegionOutOfBounds`] annotated with the chip's TRM
643    /// URL and the memory-map page(s), so the user knows where to look.
644    fn region_out_of_bounds(&self, region: &str, base: u32, end: u32) -> CfgError {
645        let pages = self
646            .database
647            .memory
648            .trm_page
649            .iter()
650            .map(u32::to_string)
651            .collect::<Vec<_>>()
652            .join(", ");
653
654        CfgError::RegionOutOfBounds {
655            region: region.to_string(),
656            base,
657            end,
658            chip: self.config.profile.chip.clone(),
659            trm: self.database.trm.clone(),
660            pages,
661        }
662    }
663
664    /// Builds a [`CfgError::UnknownDoorbell`] listing the doorbells the chip
665    /// actually exposes, each with its TRM page.
666    fn unknown_doorbell(&self, name: &str) -> CfgError {
667        let available = self
668            .database
669            .peripheral
670            .doorbells
671            .iter()
672            .map(|entry| format!("{} (TRM p. {})", entry.name, entry.page))
673            .collect::<Vec<_>>()
674            .join(", ");
675
676        CfgError::UnknownDoorbell {
677            name: name.to_string(),
678            chip: self.config.profile.chip.clone(),
679            trm: self.database.trm.clone(),
680            available,
681        }
682    }
683
684    /// Finds a database doorbell by manifest name, ignoring ASCII case.
685    pub(crate) fn doorbell(&self, name: &str) -> Option<&HardwareDoorbellPeripheral> {
686        self.database
687            .peripheral
688            .doorbells
689            .iter()
690            .find(|entry| entry.name.eq_ignore_ascii_case(name))
691    }
692
693    /// Builds a [`CfgError::DoorbellPartyUnavailable`] with the configured
694    /// parties and the TRM page documenting party support.
695    fn doorbell_party_unavailable(
696        &self,
697        doorbell: &HardwareDoorbellPeripheral,
698        party: Party,
699    ) -> CfgError {
700        let available = doorbell
701            .parties
702            .iter()
703            .map(|entry| {
704                format!(
705                    "{} (TRM memory p. {}, IRQ p. {})",
706                    entry.core, entry.page_memory, entry.page_irq
707                )
708            })
709            .collect::<Vec<_>>()
710            .join(", ");
711
712        CfgError::DoorbellPartyUnavailable {
713            doorbell: doorbell.name.clone(),
714            party: party.to_string(),
715            chip: self.config.profile.chip.clone(),
716            available,
717            trm: self.database.trm.clone(),
718            page: doorbell.party_page,
719        }
720    }
721
722    /// Whether `core` is the application processor or one of the chip's
723    /// declared coprocessors.
724    fn is_core(&self, core: VisibleCore) -> bool {
725        match core {
726            VisibleCore::Ap => true,
727            VisibleCore::Mcore(name) => self
728                .database
729                .cp
730                .iter()
731                .any(|cp| cp.name.eq_ignore_ascii_case(name.as_str())),
732            VisibleCore::Rcore(name) => self
733                .database
734                .cp
735                .iter()
736                .any(|cp| cp.name.eq_ignore_ascii_case(name.as_str())),
737            VisibleCore::Rvcore(name) => self
738                .database
739                .cp
740                .iter()
741                .any(|cp| cp.name.eq_ignore_ascii_case(&name.to_string())),
742        }
743    }
744
745    /// Whether `party` names one of the chip's visible runtime endpoints.
746    fn is_party(&self, party: Party) -> bool {
747        self.is_core(party.core())
748    }
749}
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754
755    const MANIFEST: &str = include_str!("../Consortium.example.stm32mp257.toml");
756
757    fn cached_manifest(
758        chip: &str,
759        secondary: &str,
760        doorbell: &str,
761        base: u32,
762        length: u32,
763    ) -> String {
764        format!(
765            r#"
766                [profile]
767                chip = "{chip}"
768                shared = "./shared"
769                deploy_root = "/opt/consortium"
770
771                [[ipc.channel]]
772                name = "probe"
773                doorbell = "{doorbell}"
774                pattern = "memory"
775                primary = "linux"
776                secondary = "{secondary}"
777                party.linux = {{ chan = 0, type = "Ping" }}
778                party.{secondary} = {{ chan = 1, type = "Pong" }}
779                config.memory = {{ base = {base}, length = {length} }}
780                config.cache = true
781                mtu = 128
782            "#
783        )
784    }
785
786    #[test]
787    fn loads_example_manifest() {
788        let session = Session::from_toml(MANIFEST).expect("example manifest should load");
789
790        assert_eq!(session.config.profile.chip, "stm32mp257fak3");
791        assert_eq!(session.database.name, "STM32MP25");
792        assert_eq!(session.config.ipc.channels.len(), 2);
793        assert_eq!(session.config.ipc.channels[0].doorbell, "ipcc1");
794        assert_eq!(
795            session.config.ipc.channels[0].endpoints[&Party::AcoreNonSecure].chan,
796            0
797        );
798        assert_eq!(
799            session.config.ipc.channels[0].endpoints[&"cm33".parse::<Party>().unwrap()].r#type,
800            "MotionControl"
801        );
802        assert_eq!(
803            session.config.ipc.channels[0].primary,
804            Party::AcoreNonSecure
805        );
806        assert_eq!(
807            session.config.ipc.channels[0].secondary,
808            "cm33".parse::<Party>().unwrap()
809        );
810        assert_eq!(session.config.endpoints[&Party::AcoreNonSecure], "./app");
811        assert_eq!(
812            session
813                .config
814                .tee
815                .as_ref()
816                .unwrap()
817                .uuid
818                .to_string()
819                .to_uppercase(),
820            "48551DD5-74B2-4D24-BF9D-A09F02F5C56F"
821        );
822
823        // `[dbg] mode` flattens alongside the `[dbg.<core>]` tables.
824        assert_eq!(
825            session.config.dbg.mode,
826            consortium_cfg_common::config::DbgMode::Auto
827        );
828        let cm33_dbg = session.config.dbg.cores[&"cm33".parse::<VisibleCore>().unwrap()];
829        assert_eq!(cm33_dbg.mtu, 64);
830        assert_eq!(cm33_dbg.memory.length, 0x2000);
831
832        let i2c1 = &session.config.peripheral["i2c1"];
833        assert_eq!(i2c1.owner, VisibleCore::Ap);
834        assert_eq!(i2c1.pin["sda"], "PA2");
835
836        let spi1 = &session.config.peripheral["spi1"];
837        assert_eq!(spi1.owner, "cm33".parse().unwrap());
838    }
839
840    #[test]
841    fn rejects_unknown_doorbell() {
842        let manifest = r#"
843            [profile]
844            chip = "stm32mp257fak3"
845            shared = "./shared"
846            deploy_root = "/opt/consortium"
847
848            [[ipc.channel]]
849            name = "probe"
850            doorbell = "nope"
851            pattern = "memory"
852            primary = "linux"
853            secondary = "cm33"
854            party.linux = { chan = 0, type = "Ping" }
855            party.cm33 = { chan = 1, type = "Pong" }
856            config.memory = { base = 0x00040000, length = 0x00008000 }
857            mtu = 128
858        "#;
859
860        let err = Session::from_toml(manifest).unwrap_err();
861        assert!(matches!(err, CfgError::UnknownDoorbell { .. }));
862        // The hint lists real doorbells with their TRM pages.
863        assert!(err.to_string().contains("HSEM (TRM p. 1306)"));
864    }
865
866    #[test]
867    fn rejects_doorbell_party_unavailable() {
868        let manifest = r#"
869            [profile]
870            chip = "stm32mp257fak3"
871            shared = "./shared"
872            deploy_root = "/opt/consortium"
873
874            [[ipc.channel]]
875            name = "probe"
876            doorbell = "ipcc1"
877            pattern = "memory"
878            primary = "linux"
879            secondary = "cm0p"
880            party.cm0p = { chan = 0, type = "PowerMode" }
881            config.memory = { base = 0x00040000, length = 0x00008000 }
882            mtu = 128
883        "#;
884
885        let err = Session::from_toml(manifest).unwrap_err();
886        assert!(matches!(err, CfgError::DoorbellPartyUnavailable { .. }));
887        assert!(err.to_string().contains("Configured parties: linux"));
888        assert!(err.to_string().contains("TRM"));
889    }
890
891    #[test]
892    fn rejects_unavailable_doorbell_channel() {
893        let manifest = r#"
894            [profile]
895            chip = "stm32mp257fak3"
896            shared = "./shared"
897            deploy_root = "/opt/consortium"
898
899            [[ipc.channel]]
900            name = "probe"
901            doorbell = "ipcc1"
902            pattern = "memory"
903            primary = "linux"
904            secondary = "cm33"
905            party.linux = { chan = 32, type = "Ping" }
906            config.memory = { base = 0x00040000, length = 0x00008000 }
907            mtu = 128
908        "#;
909
910        // IPCC1 has 32 channels, so `chan = 32` is one past the last.
911        assert!(matches!(
912            Session::from_toml(manifest),
913            Err(CfgError::DoorbellChannelUnavailable { .. })
914        ));
915    }
916
917    #[test]
918    fn rejects_duplicate_doorbell_channel() {
919        let manifest = r#"
920            [profile]
921            chip = "stm32mp257fak3"
922            shared = "./shared"
923            deploy_root = "/opt/consortium"
924
925            [[ipc.channel]]
926            name = "probe"
927            doorbell = "ipcc1"
928            pattern = "memory"
929            primary = "linux"
930            secondary = "cm33"
931            party.linux = { chan = 0, type = "Ping" }
932            party.cm33 = { chan = 0, type = "Pong" }
933            config.memory = { base = 0x00040000, length = 0x00008000 }
934            mtu = 128
935        "#;
936
937        assert!(matches!(
938            Session::from_toml(manifest),
939            Err(CfgError::DoorbellChannelDuplicate { .. })
940        ));
941    }
942
943    /// IPC region `0x40_0000..0x48_0000` sits outside every SRAM window on
944    /// STM32MP25 (SRAM1 starts at `0x04_0000`, and the SRAM windows end at
945    /// `0x08_0000`), so it must be rejected.
946    #[test]
947    fn rejects_region_outside_sram() {
948        let manifest = r#"
949            [profile]
950            chip = "stm32mp257fak3"
951            shared = "./shared"
952            deploy_root = "/opt/consortium"
953
954            [[ipc.channel]]
955            name = "probe"
956            doorbell = "ipcc1"
957            pattern = "memory"
958            primary = "linux"
959            secondary = "cm33"
960            party.linux = { chan = 0, type = "Ping" }
961            config.memory = { base = 0x00400000, length = 0x00080000 }
962            mtu = 128
963        "#;
964
965        assert!(matches!(
966            Session::from_toml(manifest),
967            Err(CfgError::RegionOutOfBounds { .. })
968        ));
969    }
970
971    /// A non-power-of-two region length is not MPU-expressible.
972    #[test]
973    fn rejects_non_power_of_two_region() {
974        let manifest = r#"
975            [profile]
976            chip = "stm32mp257fak3"
977            shared = "./shared"
978            deploy_root = "/opt/consortium"
979
980            [[ipc.channel]]
981            name = "probe"
982            doorbell = "ipcc1"
983            pattern = "memory"
984            primary = "linux"
985            secondary = "cm33"
986            party.linux = { chan = 0, type = "Ping" }
987            config.memory = { base = 0x00400000, length = 0x00006000 }
988            mtu = 128
989        "#;
990
991        assert!(matches!(
992            Session::from_toml(manifest),
993            Err(CfgError::RegionNotMpuAligned { .. })
994        ));
995    }
996
997    /// Manifest with one acknowledgement-capable MU channel, parameterised so each test
998    /// can break exactly one thing about it.
999    fn imx95_ack_manifest(linux_ack: &str, cm7_ack: &str, ack: bool) -> String {
1000        format!(
1001            r#"
1002            [profile]
1003            chip = "mimx9596xvt"
1004            shared = "./shared"
1005            deploy_root = "/opt/consortium"
1006
1007            [[ipc.channel]]
1008            name = "sensor"
1009            doorbell = "mu7"
1010            pattern = "memory"
1011            primary = "linux"
1012            secondary = "cm7"
1013            party.linux = {{ chan = 6, {linux_ack} type = "SensorData" }}
1014            party.cm7 = {{ chan = 7, {cm7_ack} type = "MotionControl" }}
1015            config.memory = {{ base = 0x88300000, length = 0x4000 }}
1016            config.ack = {ack}
1017            mtu = 128
1018        "#
1019        )
1020    }
1021
1022    #[test]
1023    fn accepts_ack_channels_alongside_the_data_lines() {
1024        let manifest = imx95_ack_manifest("ack_chan = 0,", "ack_chan = 1,", true);
1025        Session::from_toml(&manifest).expect("distinct ack lines should be valid");
1026    }
1027
1028    /// Acknowledgement needs a line of its own, so opting in without naming one is a
1029    /// configuration error rather than a silent fallback.
1030    #[test]
1031    fn rejects_ack_without_an_ack_channel() {
1032        let manifest = imx95_ack_manifest("", "ack_chan = 1,", true);
1033        let err = Session::from_toml(&manifest).unwrap_err();
1034        assert!(matches!(err, CfgError::IpcAckChannelMissing { .. }));
1035        assert!(err.to_string().contains("ack_chan"));
1036    }
1037
1038    /// The mirror image: a line declared for a capability the channel never enables would
1039    /// silently reserve a doorbell channel that nothing rings.
1040    #[test]
1041    fn rejects_an_ack_channel_without_the_capability() {
1042        let manifest = imx95_ack_manifest("ack_chan = 0,", "ack_chan = 1,", false);
1043        assert!(matches!(
1044            Session::from_toml(&manifest),
1045            Err(CfgError::IpcAckChannelUnexpected { .. })
1046        ));
1047    }
1048
1049    /// An acknowledgement line is a doorbell channel like any other, so it goes through the
1050    /// same collision check — here it aliases the peer's data line, which would make the
1051    /// peer's receive half and this side's acknowledgement wait share one line.
1052    #[test]
1053    fn rejects_an_ack_channel_that_aliases_a_data_line() {
1054        let manifest = imx95_ack_manifest("ack_chan = 7,", "ack_chan = 1,", true);
1055        assert!(matches!(
1056            Session::from_toml(&manifest),
1057            Err(CfgError::DoorbellChannelDuplicate { channel: 7, .. })
1058        ));
1059    }
1060
1061    /// ...and through the same bounds check: MU exposes eight channels.
1062    #[test]
1063    fn rejects_an_out_of_range_ack_channel() {
1064        let manifest = imx95_ack_manifest("ack_chan = 0,", "ack_chan = 64,", true);
1065        assert!(matches!(
1066            Session::from_toml(&manifest),
1067            Err(CfgError::DoorbellChannelUnavailable { channel: 64, .. })
1068        ));
1069    }
1070
1071    #[test]
1072    fn accepts_cached_non_power_of_two_region() {
1073        let manifest = cached_manifest("mimx9596xvt", "cm7", "mu7", 0x8830_0000, 0x6000);
1074        Session::from_toml(&manifest).expect("cache-line-aligned cached region should be valid");
1075    }
1076
1077    #[test]
1078    fn rejects_region_too_small_for_aligned_slot_pair() {
1079        let manifest = cached_manifest("mimx9596xvt", "cm7", "mu7", 0x8830_0000, 0x200);
1080        let err = Session::from_toml(&manifest).unwrap_err();
1081        assert!(matches!(
1082            err,
1083            CfgError::IpcRegionTooSmall {
1084                length: 0x200,
1085                mtu: 128,
1086                required: 0x24c,
1087                ..
1088            }
1089        ));
1090        assert!(err.to_string().contains("bidirectional slot pair"));
1091    }
1092
1093    #[test]
1094    fn accepts_smallest_cache_aligned_region_for_slot_pair() {
1095        let manifest = cached_manifest("mimx9596xvt", "cm7", "mu7", 0x8830_0000, 0x280);
1096        Session::from_toml(&manifest)
1097            .expect("0x280 bytes contains both aligned 128-byte-MTU slots");
1098    }
1099
1100    #[test]
1101    fn rejects_cached_region_with_partial_cache_line() {
1102        for (base, length) in [(0x8830_0000, 0x4020), (0x8830_0020, 0x4000)] {
1103            let manifest = cached_manifest("mimx9596xvt", "cm7", "mu7", base, length);
1104            assert!(matches!(
1105                Session::from_toml(&manifest),
1106                Err(CfgError::RegionNotCacheAligned { .. })
1107            ));
1108        }
1109    }
1110
1111    #[test]
1112    fn rejects_cached_region_on_cacheless_controller() {
1113        let manifest = cached_manifest("mimx9352", "cm33", "mu1", 0x0048_8000, 0x4000);
1114        let err = Session::from_toml(&manifest).unwrap_err();
1115        assert!(matches!(err, CfgError::IpcCacheUnsupported { .. }));
1116        assert!(err.to_string().contains("cm33"));
1117    }
1118
1119    /// The i.MX 95 System Manager grants the M7 no OCRAM, so a channel placed there
1120    /// is rejected before it reaches the board, where it would bus-fault.
1121    #[test]
1122    fn rejects_a_region_the_controller_cannot_access() {
1123        let manifest = r#"
1124            [profile]
1125            chip = "mimx9596xvt"
1126            shared = "./shared"
1127            deploy_root = "/opt/consortium"
1128
1129            [[ipc.channel]]
1130            name = "probe"
1131            doorbell = "mu7"
1132            pattern = "memory"
1133            primary = "linux"
1134            secondary = "cm7"
1135            party.linux = { chan = 0, type = "Ping" }
1136            party.cm7 = { chan = 1, type = "Pong" }
1137            config.memory = { base = 0x004C0000, length = 0x4000 }
1138            mtu = 128
1139        "#;
1140        let err = Session::from_toml(manifest).unwrap_err();
1141        assert!(
1142            matches!(&err, CfgError::RegionNotVisible { window, core, .. }
1143                if window == "OCRAM" && core == "cm7"),
1144            "{err}"
1145        );
1146    }
1147
1148    /// A debug ring is drained by the AP, so it needs the AP's access as well as
1149    /// its own core's.
1150    #[test]
1151    fn rejects_a_dbg_ring_the_ap_cannot_drain() {
1152        let manifest = r#"
1153            [profile]
1154            chip = "mimx9596xvt"
1155            shared = "./shared"
1156            deploy_root = "/opt/consortium"
1157
1158            [dbg.cm7]
1159            memory = { base = 0x004C0000, length = 0x2000 }
1160            mtu = 64
1161        "#;
1162        assert!(matches!(
1163            Session::from_toml(manifest),
1164            Err(CfgError::RegionNotVisible { .. })
1165        ));
1166    }
1167
1168    /// A region whose base is not aligned to its (power-of-two) size is not
1169    /// MPU-expressible.
1170    #[test]
1171    fn rejects_misaligned_region() {
1172        // length 0x4000 (16 KiB) but base 0x402000 is only 8 KiB-aligned.
1173        let manifest = r#"
1174            [profile]
1175            chip = "stm32mp257fak3"
1176            shared = "./shared"
1177            deploy_root = "/opt/consortium"
1178
1179            [[ipc.channel]]
1180            name = "probe"
1181            doorbell = "ipcc1"
1182            pattern = "memory"
1183            primary = "linux"
1184            secondary = "cm33"
1185            party.linux = { chan = 0, type = "Ping" }
1186            config.memory = { base = 0x00402000, length = 0x00004000 }
1187            mtu = 128
1188        "#;
1189
1190        let err = Session::from_toml(manifest).unwrap_err();
1191        assert!(matches!(err, CfgError::RegionNotMpuAligned { .. }));
1192        assert!(err.to_string().contains("aligned"));
1193    }
1194
1195    /// A region smaller than the ARMv7-M minimum (32 bytes) is rejected.
1196    #[test]
1197    fn rejects_subminimal_region() {
1198        let manifest = r#"
1199            [profile]
1200            chip = "stm32mp257fak3"
1201            shared = "./shared"
1202            deploy_root = "/opt/consortium"
1203
1204            [[ipc.channel]]
1205            name = "probe"
1206            doorbell = "ipcc1"
1207            pattern = "memory"
1208            primary = "linux"
1209            secondary = "cm33"
1210            party.linux = { chan = 0, type = "Ping" }
1211            config.memory = { base = 0x00400000, length = 0x10 }
1212            mtu = 128
1213        "#;
1214
1215        assert!(matches!(
1216            Session::from_toml(manifest),
1217            Err(CfgError::RegionNotMpuAligned { .. })
1218        ));
1219    }
1220
1221    /// `[dbg.ap]` is rejected: the AP is always the host, never a logger.
1222    #[test]
1223    fn rejects_dbg_on_application_processor() {
1224        let manifest = r#"
1225            [profile]
1226            chip = "stm32mp257fak3"
1227            shared = "./shared"
1228            deploy_root = "/opt/consortium"
1229
1230            [dbg.ap]
1231            memory = { base = 0x00044000, length = 0x00002000 }
1232            mtu = 64
1233        "#;
1234
1235        assert!(matches!(
1236            Session::from_toml(manifest),
1237            Err(CfgError::DbgHostNotLoggable)
1238        ));
1239    }
1240
1241    /// A `[dbg.<core>]` table naming a core the chip does not expose is rejected.
1242    #[test]
1243    fn rejects_dbg_on_unknown_core() {
1244        let manifest = r#"
1245            [profile]
1246            chip = "stm32mp257fak3"
1247            shared = "./shared"
1248            deploy_root = "/opt/consortium"
1249
1250            [dbg.cm7]
1251            memory = { base = 0x00044000, length = 0x00002000 }
1252            mtu = 64
1253        "#;
1254
1255        assert!(matches!(
1256            Session::from_toml(manifest),
1257            Err(CfgError::DbgCoreNotVisible { .. })
1258        ));
1259    }
1260
1261    #[test]
1262    fn rejects_overlapping_regions() {
1263        let manifest = r#"
1264            [profile]
1265            chip = "stm32mp257fak3"
1266            shared = "./shared"
1267            deploy_root = "/opt/consortium"
1268
1269            [dbg.cm33]
1270            memory = { base = 0x00044000, length = 0x00002000 }
1271            mtu = 64
1272
1273            [[ipc.channel]]
1274            name = "probe"
1275            doorbell = "ipcc1"
1276            pattern = "memory"
1277            primary = "linux"
1278            secondary = "cm33"
1279            party.linux = { chan = 0, type = "Ping" }
1280            config.memory = { base = 0x00040000, length = 0x00008000 }
1281            mtu = 128
1282        "#;
1283
1284        assert!(matches!(
1285            Session::from_toml(manifest),
1286            Err(CfgError::RegionOverlap { .. })
1287        ));
1288    }
1289
1290    /// A coprocessor-owned peripheral present in that core's chip map validates.
1291    #[test]
1292    fn accepts_peripheral_present_on_core() {
1293        let manifest = r#"
1294            [profile]
1295            chip = "stm32mp257fak3"
1296            shared = "./shared"
1297            deploy_root = "/opt/consortium"
1298
1299            [[ipc.channel]]
1300            name = "probe"
1301            doorbell = "ipcc1"
1302            pattern = "memory"
1303            primary = "linux"
1304            secondary = "cm33"
1305            party.linux = { chan = 0, type = "Ping" }
1306            config.memory = { base = 0x00040000, length = 0x00002000 }
1307            mtu = 128
1308
1309            [peripheral.i2c1]
1310            owner = "cm33"
1311            clock = 64000000
1312            frequency = 100000
1313        "#;
1314
1315        assert!(Session::from_toml(manifest).is_ok());
1316    }
1317
1318    /// A coprocessor-owned peripheral absent from that core's chip map is
1319    /// rejected: the map is the accessibility filter.
1320    #[test]
1321    fn rejects_peripheral_absent_on_core() {
1322        let manifest = r#"
1323            [profile]
1324            chip = "stm32mp257fak3"
1325            shared = "./shared"
1326            deploy_root = "/opt/consortium"
1327
1328            [[ipc.channel]]
1329            name = "probe"
1330            doorbell = "ipcc1"
1331            pattern = "memory"
1332            primary = "linux"
1333            secondary = "cm33"
1334            party.linux = { chan = 0, type = "Ping" }
1335            config.memory = { base = 0x00040000, length = 0x00002000 }
1336            mtu = 128
1337
1338            [peripheral.nonexistent99]
1339            owner = "cm33"
1340        "#;
1341
1342        assert!(matches!(
1343            Session::from_toml(manifest),
1344            Err(CfgError::PeripheralNotOnCore { .. })
1345        ));
1346    }
1347
1348    const IMX952_MANIFEST: &str = include_str!("../Consortium.example.mimx952.toml");
1349
1350    /// The i.MX 952 example (CM7 peripherals only) loads against its own
1351    /// database, not i.MX 95's, and its pins validate against the i.MX 952
1352    /// signal map.
1353    #[test]
1354    fn loads_imx952_example_manifest() {
1355        let session = Session::from_toml(IMX952_MANIFEST).expect("i.MX 952 example should load");
1356        assert_eq!(session.database.name, "MIMX952");
1357        assert!(session.config.ipc.channels.is_empty());
1358    }
1359
1360    /// Until the i.MX 952 reference manual gives the MU party windows and the
1361    /// OCRAM map, a channel is refused rather than generated at guessed
1362    /// addresses. The chip declares neither an SRAM region nor a doorbell, and
1363    /// whichever check runs first rejects it.
1364    #[test]
1365    fn imx952_rejects_ipc_until_the_reference_manual() {
1366        let manifest = r#"
1367            [profile]
1368            chip = "mimx9529avzxn"
1369            shared = "./shared"
1370            deploy_root = "/opt/consortium"
1371
1372            [[ipc.channel]]
1373            name = "probe"
1374            doorbell = "mu7"
1375            pattern = "memory"
1376            primary = "linux"
1377            secondary = "cm7"
1378            party.linux = { chan = 0, type = "Ping" }
1379            party.cm7 = { chan = 1, type = "Pong" }
1380            config.memory = { base = 0x00480000, length = 0x4000 }
1381            mtu = 128
1382        "#;
1383        assert!(matches!(
1384            Session::from_toml(manifest),
1385            Err(CfgError::RegionOutOfBounds { .. } | CfgError::UnknownDoorbell { .. })
1386        ));
1387    }
1388
1389    /// With no SRAM region declared, a debug ring has nowhere valid to go.
1390    #[test]
1391    fn imx952_rejects_a_dbg_ring_without_sram() {
1392        let manifest = r#"
1393            [profile]
1394            chip = "mimx9529avzxn"
1395            shared = "./shared"
1396            deploy_root = "/opt/consortium"
1397
1398            [dbg.cm7]
1399            memory = { base = 0x00480000, length = 0x2000 }
1400            mtu = 64
1401        "#;
1402        assert!(matches!(
1403            Session::from_toml(manifest),
1404            Err(CfgError::RegionOutOfBounds { .. })
1405        ));
1406    }
1407
1408    const MP235_MANIFEST: &str = include_str!("../Consortium.example.stm32mp235.toml");
1409
1410    /// An STM32MP23 manifest with one CM33 channel and `extra` appended.
1411    fn mp235_manifest(doorbell: &str, secondary: &str, base: u32, extra: &str) -> String {
1412        format!(
1413            r#"
1414                [profile]
1415                chip = "stm32mp235fak3"
1416                shared = "./shared"
1417                deploy_root = "/opt/consortium"
1418
1419                [[ipc.channel]]
1420                name = "probe"
1421                doorbell = "{doorbell}"
1422                pattern = "memory"
1423                primary = "linux"
1424                secondary = "{secondary}"
1425                party.linux = {{ chan = 0, type = "Ping" }}
1426                party.{secondary} = {{ chan = 1, type = "Pong" }}
1427                config.memory = {{ base = {base}, length = 0x4000 }}
1428                mtu = 128
1429
1430                {extra}
1431            "#
1432        )
1433    }
1434
1435    #[test]
1436    fn loads_mp235_example_manifest() {
1437        let session = Session::from_toml(MP235_MANIFEST).expect("MP235 example should load");
1438        assert_eq!(session.database.name, "STM32MP23");
1439        assert_eq!(session.config.ipc.channels.len(), 1);
1440    }
1441
1442    /// IPCC2 is the MP25 CM0+'s mailbox; MP23 has no CM0+ and no IPCC2.
1443    #[test]
1444    fn mp235_rejects_ipcc2() {
1445        let manifest = mp235_manifest("ipcc2", "cm33", 0x0006_4000, "");
1446        assert!(matches!(
1447            Session::from_toml(&manifest),
1448            Err(CfgError::UnknownDoorbell { .. })
1449        ));
1450    }
1451
1452    #[test]
1453    fn mp235_rejects_a_cm0p_endpoint() {
1454        let manifest = mp235_manifest("ipcc1", "cm0p", 0x0006_4000, "");
1455        assert!(Session::from_toml(&manifest).is_err());
1456    }
1457
1458    /// I2C3 exists on MP25 but not MP23 (DS14634: 4x I2C, i.e. I2C1/2/7/8).
1459    #[test]
1460    fn mp235_rejects_an_mp25_only_peripheral() {
1461        let i2c = |name: &str| format!("[peripheral.{name}]\nowner = \"cm33\"");
1462        let absent = mp235_manifest("ipcc1", "cm33", 0x0006_4000, &i2c("i2c3"));
1463        assert!(matches!(
1464            Session::from_toml(&absent),
1465            Err(CfgError::PeripheralNotOnCore { .. })
1466        ));
1467        let present = mp235_manifest("ipcc1", "cm33", 0x0006_4000, &i2c("i2c8"));
1468        assert!(Session::from_toml(&present).is_ok());
1469    }
1470
1471    /// The 32 KB MP23 lacks against MP25 is LPSRAM1-3 (776 KB vs 808 KB).
1472    #[test]
1473    fn mp235_rejects_an_lpsram_window() {
1474        let manifest = mp235_manifest("ipcc1", "cm33", 0x000C_4000, "");
1475        assert!(Session::from_toml(&manifest).is_err());
1476    }
1477
1478    /// A manifest with only `[profile]` and the given peripheral tables.
1479    fn pins_manifest(chip: &str, peripherals: &str) -> String {
1480        format!(
1481            r#"
1482            [profile]
1483            chip = "{chip}"
1484            shared = "./shared"
1485            deploy_root = "/opt/consortium"
1486
1487            {peripherals}
1488            "#
1489        )
1490    }
1491
1492    /// STM32MP257FAK is the VFBGA424 part (the DK board's). I2C1_SDA reaches
1493    /// PA2 (AF10), PI1, and PJ10 (AF9), and PJ10 is bonded out only on TFBGA436.
1494    #[test]
1495    fn accepts_pins_the_pad_can_carry() {
1496        let manifest = pins_manifest(
1497            "stm32mp257fak3",
1498            "[peripheral.i2c1]\nowner = \"ap\"\npin.sda = \"PA2\"\npin.scl = \"pa3\"",
1499        );
1500        assert!(Session::from_toml(&manifest).is_ok());
1501    }
1502
1503    #[test]
1504    fn rejects_a_pad_that_cannot_carry_the_signal() {
1505        let manifest = pins_manifest(
1506            "stm32mp257fak3",
1507            "[peripheral.i2c1]\nowner = \"ap\"\npin.sda = \"PB0\"",
1508        );
1509        match Session::from_toml(&manifest) {
1510            Err(CfgError::PinCannotCarry { pad, allowed, .. }) => {
1511                assert_eq!(pad, "PB0");
1512                assert_eq!(allowed, "PA2, PI1, PJ10");
1513            }
1514            other => panic!("expected PinCannotCarry, got {other:?}"),
1515        }
1516    }
1517
1518    /// Pin keys are the vendor's signal names: NXP LPSPI has `sin`/`sout`,
1519    /// not `miso`/`mosi`.
1520    #[test]
1521    fn rejects_a_signal_the_peripheral_does_not_have() {
1522        let manifest = pins_manifest(
1523            "mimx9596xvt",
1524            "[peripheral.lpspi2]\nowner = \"ap\"\npin.miso = \"UART1_RXD\"",
1525        );
1526        assert!(matches!(
1527            Session::from_toml(&manifest),
1528            Err(CfgError::PinSignalUnknown { function, .. }) if function == "LPSPI2_MISO"
1529        ));
1530        let manifest = pins_manifest(
1531            "mimx9596xvt",
1532            "[peripheral.lpspi2]\nowner = \"ap\"\npin.sin = \"UART1_RXD\"",
1533        );
1534        assert!(Session::from_toml(&manifest).is_ok());
1535    }
1536
1537    /// The package comes from `[profile] chip`; a chip name too short to pick
1538    /// one leaves the package check off.
1539    #[test]
1540    fn rejects_a_pad_the_package_does_not_bond_out() {
1541        let peripheral = "[peripheral.i2c1]\nowner = \"ap\"\npin.sda = \"PJ10\"";
1542        match Session::from_toml(&pins_manifest("stm32mp257fak3", peripheral)) {
1543            Err(CfgError::PinNotInPackage { package, .. }) => assert_eq!(package, "VFBGA424"),
1544            other => panic!("expected PinNotInPackage, got {other:?}"),
1545        }
1546        assert!(Session::from_toml(&pins_manifest("stm32mp257fai3", peripheral)).is_ok());
1547        assert!(Session::from_toml(&pins_manifest("stm32mp257", peripheral)).is_ok());
1548    }
1549
1550    /// One pad, two signals: PE4 carries both SPI1_MOSI and FDCAN1_TX.
1551    #[test]
1552    fn rejects_a_pad_assigned_twice() {
1553        let manifest = pins_manifest(
1554            "stm32mp257fak3",
1555            "[peripheral.spi1]\nowner = \"ap\"\npin.mosi = \"PE4\"\n\n\
1556             [peripheral.fdcan1]\nowner = \"ap\"\npin.tx = \"PE4\"",
1557        );
1558        match Session::from_toml(&manifest) {
1559            Err(CfgError::PinConflict { first, second, .. }) => {
1560                assert_eq!(
1561                    (first.as_str(), second.as_str()),
1562                    ("fdcan1.tx", "spi1.mosi")
1563                );
1564            }
1565            other => panic!("expected PinConflict, got {other:?}"),
1566        }
1567    }
1568}