1use include_dir::{Dir, include_dir};
26
27use crate::error::DataError;
28
29static DTS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/database/dts");
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct BaseDts {
35 pub family_dir: &'static str,
39 pub entry: String,
42}
43
44impl BaseDts {
45 pub fn is_board(&self) -> bool {
49 self.entry.ends_with(".dts")
50 }
51
52 pub fn entry_path(&self) -> String {
55 format!("{}/{}", self.family_dir, self.entry)
56 }
57
58 pub fn contents(&self) -> Result<&'static str, DataError> {
60 read(&self.entry_path())
61 }
62}
63
64pub fn resolve_base(
77 chip: &str,
78 devkit: Option<&str>,
79 base_override: Option<&str>,
80) -> Result<BaseDts, DataError> {
81 let family_dir = family_dir(chip).ok_or_else(|| DataError::UnknownChip(chip.to_string()))?;
82
83 if let Some(stem) = base_override.or(devkit) {
84 let entry =
85 match_stem(family_dir, stem).ok_or_else(|| DataError::UnknownDts(stem.to_string()))?;
86 return Ok(BaseDts { family_dir, entry });
87 }
88
89 let entry = generic_dtsi(chip).ok_or_else(|| DataError::UnknownChip(chip.to_string()))?;
90 if DTS.get_file(format!("{family_dir}/{entry}")).is_none() {
94 return Err(DataError::UnknownDts(entry));
95 }
96 Ok(BaseDts { family_dir, entry })
97}
98
99pub fn read(path: &str) -> Result<&'static str, DataError> {
102 DTS.get_file(path)
103 .and_then(|f| f.contents_utf8())
104 .ok_or_else(|| DataError::UnknownDts(path.to_string()))
105}
106
107pub fn local_include_closure(base: &BaseDts) -> Result<Vec<(String, String)>, DataError> {
116 let mut out: Vec<(String, String)> = Vec::new();
117 let mut seen: Vec<String> = Vec::new();
118 let mut queue: Vec<String> = vec![base.entry.clone()];
119
120 while let Some(name) = queue.pop() {
121 if seen.iter().any(|s| s == &name) {
122 continue;
123 }
124 seen.push(name.clone());
125 let path = format!("{}/{}", base.family_dir, name);
126 let Some(contents) = DTS.get_file(&path).and_then(|f| f.contents_utf8()) else {
127 continue;
130 };
131 for included in local_includes(contents) {
132 queue.push(included);
133 }
134 out.push((name, contents.to_string()));
135 }
136
137 Ok(out)
140}
141
142pub fn base_closure(
147 chip: &str,
148 devkit: Option<&str>,
149 base_override: Option<&str>,
150) -> Result<Vec<(String, String)>, DataError> {
151 let base = resolve_base(chip, devkit, base_override)?;
152 local_include_closure(&base)
153}
154
155fn local_includes(source: &str) -> Vec<String> {
157 let mut out = Vec::new();
158 for line in source.lines() {
159 let trimmed = line.trim_start();
160 let Some(rest) = trimmed.strip_prefix("#include") else {
161 continue;
162 };
163 let rest = rest.trim_start();
164 if let Some(after) = rest.strip_prefix('"')
166 && let Some(end) = after.find('"')
167 {
168 out.push(after[..end].to_string());
169 }
170 }
171 out
172}
173
174fn family_dir(chip: &str) -> Option<&'static str> {
176 let needle = normalize(chip);
177 if needle.starts_with("stm32mp2") {
178 Some("stm32mp2")
179 } else if needle.starts_with("mimx9") {
180 Some("imx9")
181 } else {
182 None
183 }
184}
185
186fn match_stem(family_dir: &str, stem: &str) -> Option<String> {
189 for ext in ["dts", "dtsi"] {
190 let name = format!("{stem}.{ext}");
191 if DTS.get_file(format!("{family_dir}/{name}")).is_some() {
192 return Some(name);
193 }
194 }
195 None
196}
197
198fn generic_dtsi(chip: &str) -> Option<String> {
205 let needle = normalize(chip);
206 if let Some(rest) = needle.strip_prefix("mimx9") {
207 let line = rest.chars().next()?;
209 return Some(format!("imx9{line}.dtsi"));
210 }
211 if needle.starts_with("stm32mp") {
212 let digits: String = needle
214 .chars()
215 .skip("stm32mp".len())
216 .take_while(|c| c.is_ascii_digit())
217 .collect();
218 if digits.len() >= 3 {
219 return Some(format!("stm32mp{}.dtsi", &digits[..3]));
220 }
221 }
222 None
223}
224
225fn normalize(chip: &str) -> String {
228 let lowered = chip.to_ascii_lowercase();
229 match lowered.strip_prefix("imx") {
230 Some(rest) => format!("mimx{rest}"),
231 None => lowered,
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn devkit_stem_resolves_board_dts() {
241 let base = resolve_base("imx95", Some("imx95-15x15-frdm"), None).unwrap();
242 assert_eq!(base.family_dir, "imx9");
243 assert_eq!(base.entry, "imx95-15x15-frdm.dts");
244 assert!(base.is_board());
245 assert!(base.contents().unwrap().contains("/dts-v1/"));
246 }
247
248 #[test]
249 fn stm32_devkit_stem_resolves() {
250 let base = resolve_base("stm32mp257fak3", Some("stm32mp257f-dk"), None).unwrap();
251 assert_eq!(base.family_dir, "stm32mp2");
252 assert_eq!(base.entry, "stm32mp257f-dk.dts");
253 }
254
255 #[test]
256 fn no_devkit_falls_back_to_generic_dtsi() {
257 let imx = resolve_base("imx95", None, None).unwrap();
258 assert_eq!(imx.entry, "imx95.dtsi");
259 assert!(!imx.is_board());
260
261 let stm = resolve_base("stm32mp257fak3", None, None).unwrap();
262 assert_eq!(stm.entry, "stm32mp257.dtsi");
263 }
264
265 #[test]
266 fn base_override_wins_over_devkit() {
267 let base = resolve_base(
268 "stm32mp257fak3",
269 Some("stm32mp257f-dk"),
270 Some("stm32mp257f-ev1"),
271 )
272 .unwrap();
273 assert_eq!(base.entry, "stm32mp257f-ev1.dts");
274 }
275
276 #[test]
277 fn unknown_stem_errors() {
278 let err = resolve_base("imx95", Some("nope-not-a-board"), None).unwrap_err();
279 assert!(matches!(err, DataError::UnknownDts(_)));
280 }
281
282 #[test]
283 fn closure_includes_soc_dtsi_of_board() {
284 let base = resolve_base("stm32mp257fak3", Some("stm32mp257f-dk"), None).unwrap();
285 let closure = local_include_closure(&base).unwrap();
286 let names: Vec<&str> = closure.iter().map(|(n, _)| n.as_str()).collect();
287 assert!(names.contains(&"stm32mp257f-dk.dts"));
288 assert!(names.contains(&"stm32mp257.dtsi"));
289 }
290}