zerodds_xml/
inheritance.rs1use crate::errors::XmlError;
17use alloc::collections::BTreeSet;
18use alloc::format;
19use alloc::string::{String, ToString};
20use alloc::vec::Vec;
21
22pub const MAX_INHERITANCE_DEPTH: usize = 32;
24
25pub fn resolve_chain<F>(name: &str, mut lookup: F) -> Result<Vec<String>, XmlError>
47where
48 F: FnMut(&str) -> Result<Option<String>, XmlError>,
49{
50 let mut visited: BTreeSet<String> = BTreeSet::new();
51 let mut chain: Vec<String> = Vec::new();
52 let mut current = name.to_string();
53
54 for _ in 0..MAX_INHERITANCE_DEPTH {
55 if !visited.insert(current.clone()) {
56 chain.push(current.clone());
58 let pretty = chain.join(" -> ");
59 return Err(XmlError::CircularInheritance(pretty));
60 }
61 chain.push(current.clone());
62
63 match lookup(¤t)? {
64 None => {
65 chain.reverse();
67 return Ok(chain);
68 }
69 Some(base) => {
70 current = base;
71 }
72 }
73 }
74
75 Err(XmlError::LimitExceeded(format!(
76 "base_name chain depth > {MAX_INHERITANCE_DEPTH}"
77 )))
78}
79
80#[cfg(test)]
81#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
82mod tests {
83 use super::*;
84 use alloc::collections::BTreeMap;
85 use alloc::vec;
86
87 fn make_lookup(
88 items: BTreeMap<&'static str, Option<&'static str>>,
89 ) -> impl FnMut(&str) -> Result<Option<String>, XmlError> {
90 move |name: &str| {
91 items
92 .get(name)
93 .copied()
94 .ok_or_else(|| XmlError::MissingRequiredElement(name.to_string()))
95 .map(|opt| opt.map(|s| s.to_string()))
96 }
97 }
98
99 #[test]
100 fn no_inheritance() {
101 let mut items: BTreeMap<&str, Option<&str>> = BTreeMap::new();
102 items.insert("A", None);
103 let chain = resolve_chain("A", make_lookup(items)).expect("ok");
104 assert_eq!(chain, vec!["A".to_string()]);
105 }
106
107 #[test]
108 fn three_level_chain() {
109 let mut items: BTreeMap<&str, Option<&str>> = BTreeMap::new();
111 items.insert("A", Some("B"));
112 items.insert("B", Some("C"));
113 items.insert("C", None);
114 let chain = resolve_chain("A", make_lookup(items)).expect("ok");
115 assert_eq!(
117 chain,
118 vec!["C".to_string(), "B".to_string(), "A".to_string()]
119 );
120 }
121
122 #[test]
123 fn two_node_cycle() {
124 let mut items: BTreeMap<&str, Option<&str>> = BTreeMap::new();
126 items.insert("A", Some("B"));
127 items.insert("B", Some("A"));
128 let err = resolve_chain("A", make_lookup(items)).expect_err("cycle");
129 match err {
130 XmlError::CircularInheritance(msg) => {
131 assert!(msg.contains("A -> B -> A") || msg.contains("A"));
132 }
133 other => panic!("unexpected error: {other:?}"),
134 }
135 }
136
137 #[test]
138 fn self_cycle() {
139 let mut items: BTreeMap<&str, Option<&str>> = BTreeMap::new();
140 items.insert("A", Some("A"));
141 let err = resolve_chain("A", make_lookup(items)).expect_err("self-cycle");
142 assert!(matches!(err, XmlError::CircularInheritance(_)));
143 }
144
145 #[test]
146 fn missing_base_propagates() {
147 let mut items: BTreeMap<&str, Option<&str>> = BTreeMap::new();
148 items.insert("A", Some("DOES_NOT_EXIST"));
149 let err = resolve_chain("A", make_lookup(items)).expect_err("missing");
150 assert!(matches!(err, XmlError::MissingRequiredElement(_)));
151 }
152
153 #[test]
154 fn depth_cap_enforced() {
155 let lookup = |name: &str| -> Result<Option<String>, XmlError> {
159 Ok(Some(format!("{name}x")))
161 };
162 let err = resolve_chain("A", lookup).expect_err("depth");
163 assert!(matches!(err, XmlError::LimitExceeded(_)));
164 }
165}