use include_dir::{Dir, include_dir};
use crate::error::DataError;
static DTS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/database/dts");
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BaseDts {
pub family_dir: &'static str,
pub entry: String,
}
impl BaseDts {
pub fn is_board(&self) -> bool {
self.entry.ends_with(".dts")
}
pub fn entry_path(&self) -> String {
format!("{}/{}", self.family_dir, self.entry)
}
pub fn contents(&self) -> Result<&'static str, DataError> {
read(&self.entry_path())
}
}
pub fn resolve_base(
chip: &str,
devkit: Option<&str>,
base_override: Option<&str>,
) -> Result<BaseDts, DataError> {
let family_dir = family_dir(chip).ok_or_else(|| DataError::UnknownChip(chip.to_string()))?;
if let Some(stem) = base_override.or(devkit) {
let entry =
match_stem(family_dir, stem).ok_or_else(|| DataError::UnknownDts(stem.to_string()))?;
return Ok(BaseDts { family_dir, entry });
}
let entry = generic_dtsi(chip).ok_or_else(|| DataError::UnknownChip(chip.to_string()))?;
if DTS.get_file(format!("{family_dir}/{entry}")).is_none() {
return Err(DataError::UnknownDts(entry));
}
Ok(BaseDts { family_dir, entry })
}
pub fn read(path: &str) -> Result<&'static str, DataError> {
DTS.get_file(path)
.and_then(|f| f.contents_utf8())
.ok_or_else(|| DataError::UnknownDts(path.to_string()))
}
pub fn local_include_closure(base: &BaseDts) -> Result<Vec<(String, String)>, DataError> {
let mut out: Vec<(String, String)> = Vec::new();
let mut seen: Vec<String> = Vec::new();
let mut queue: Vec<String> = vec![base.entry.clone()];
while let Some(name) = queue.pop() {
if seen.iter().any(|s| s == &name) {
continue;
}
seen.push(name.clone());
let path = format!("{}/{}", base.family_dir, name);
let Some(contents) = DTS.get_file(&path).and_then(|f| f.contents_utf8()) else {
continue;
};
for included in local_includes(contents) {
queue.push(included);
}
out.push((name, contents.to_string()));
}
Ok(out)
}
pub fn base_closure(
chip: &str,
devkit: Option<&str>,
base_override: Option<&str>,
) -> Result<Vec<(String, String)>, DataError> {
let base = resolve_base(chip, devkit, base_override)?;
local_include_closure(&base)
}
fn local_includes(source: &str) -> Vec<String> {
let mut out = Vec::new();
for line in source.lines() {
let trimmed = line.trim_start();
let Some(rest) = trimmed.strip_prefix("#include") else {
continue;
};
let rest = rest.trim_start();
if let Some(after) = rest.strip_prefix('"')
&& let Some(end) = after.find('"')
{
out.push(after[..end].to_string());
}
}
out
}
fn family_dir(chip: &str) -> Option<&'static str> {
let needle = normalize(chip);
if needle.starts_with("stm32mp2") {
Some("stm32mp2")
} else if needle.starts_with("mimx9") {
Some("imx9")
} else {
None
}
}
fn match_stem(family_dir: &str, stem: &str) -> Option<String> {
for ext in ["dts", "dtsi"] {
let name = format!("{stem}.{ext}");
if DTS.get_file(format!("{family_dir}/{name}")).is_some() {
return Some(name);
}
}
None
}
fn generic_dtsi(chip: &str) -> Option<String> {
let needle = normalize(chip);
if let Some(rest) = needle.strip_prefix("mimx9") {
let line = rest.chars().next()?;
return Some(format!("imx9{line}.dtsi"));
}
if needle.starts_with("stm32mp") {
let digits: String = needle
.chars()
.skip("stm32mp".len())
.take_while(|c| c.is_ascii_digit())
.collect();
if digits.len() >= 3 {
return Some(format!("stm32mp{}.dtsi", &digits[..3]));
}
}
None
}
fn normalize(chip: &str) -> String {
let lowered = chip.to_ascii_lowercase();
match lowered.strip_prefix("imx") {
Some(rest) => format!("mimx{rest}"),
None => lowered,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn devkit_stem_resolves_board_dts() {
let base = resolve_base("imx95", Some("imx95-15x15-frdm"), None).unwrap();
assert_eq!(base.family_dir, "imx9");
assert_eq!(base.entry, "imx95-15x15-frdm.dts");
assert!(base.is_board());
assert!(base.contents().unwrap().contains("/dts-v1/"));
}
#[test]
fn stm32_devkit_stem_resolves() {
let base = resolve_base("stm32mp257fak3", Some("stm32mp257f-dk"), None).unwrap();
assert_eq!(base.family_dir, "stm32mp2");
assert_eq!(base.entry, "stm32mp257f-dk.dts");
}
#[test]
fn no_devkit_falls_back_to_generic_dtsi() {
let imx = resolve_base("imx95", None, None).unwrap();
assert_eq!(imx.entry, "imx95.dtsi");
assert!(!imx.is_board());
let stm = resolve_base("stm32mp257fak3", None, None).unwrap();
assert_eq!(stm.entry, "stm32mp257.dtsi");
}
#[test]
fn base_override_wins_over_devkit() {
let base = resolve_base(
"stm32mp257fak3",
Some("stm32mp257f-dk"),
Some("stm32mp257f-ev1"),
)
.unwrap();
assert_eq!(base.entry, "stm32mp257f-ev1.dts");
}
#[test]
fn unknown_stem_errors() {
let err = resolve_base("imx95", Some("nope-not-a-board"), None).unwrap_err();
assert!(matches!(err, DataError::UnknownDts(_)));
}
#[test]
fn closure_includes_soc_dtsi_of_board() {
let base = resolve_base("stm32mp257fak3", Some("stm32mp257f-dk"), None).unwrap();
let closure = local_include_closure(&base).unwrap();
let names: Vec<&str> = closure.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"stm32mp257f-dk.dts"));
assert!(names.contains(&"stm32mp257.dtsi"));
}
}