cfgd_core/modules/
loader.rs1use std::collections::{HashMap, HashSet, VecDeque};
4use std::path::Path;
5
6use crate::PathDisplayExt;
7use crate::config::parse_module;
8use crate::errors::{ConfigError, ModuleError, Result};
9
10use super::LoadedModule;
11
12const MAX_MODULE_SIZE: u64 = 10 * 1024 * 1024; fn read_module_yaml_capped(module_yaml: &Path) -> Result<String> {
22 if let Ok(meta) = std::fs::metadata(module_yaml)
23 && meta.len() > MAX_MODULE_SIZE
24 {
25 return Err(ModuleError::InvalidSpec {
26 name: module_yaml.display_posix(),
27 message: format!(
28 "module file too large ({} bytes, max {})",
29 meta.len(),
30 MAX_MODULE_SIZE
31 ),
32 }
33 .into());
34 }
35
36 std::fs::read_to_string(module_yaml).map_err(|e| {
37 ConfigError::Invalid {
38 message: format!("cannot read module file {}: {e}", module_yaml.posix()),
39 }
40 .into()
41 })
42}
43
44pub fn load_modules(config_dir: &Path) -> Result<HashMap<String, LoadedModule>> {
47 let modules_dir = config_dir.join("modules");
48 if !modules_dir.is_dir() {
49 return Ok(HashMap::new());
50 }
51
52 let mut modules = HashMap::new();
53 let entries = std::fs::read_dir(&modules_dir).map_err(|e| ConfigError::Invalid {
54 message: format!("cannot read modules directory {}: {e}", modules_dir.posix()),
55 })?;
56
57 for entry in entries {
58 let entry = entry.map_err(|e| ConfigError::Invalid {
59 message: format!("cannot read modules directory entry: {e}"),
60 })?;
61 let path = entry.path();
62 if !path.is_dir() {
63 continue;
64 }
65
66 let module_yaml = path.join("module.yaml");
67 if !module_yaml.exists() {
68 continue;
69 }
70
71 let name = path
72 .file_name()
73 .and_then(|n| n.to_str())
74 .ok_or_else(|| ConfigError::Invalid {
75 message: format!("invalid module directory name: {}", path.posix()),
76 })?
77 .to_string();
78
79 let contents = read_module_yaml_capped(&module_yaml)?;
80
81 let doc = parse_module(&contents)?;
82
83 if doc.metadata.name != name {
84 return Err(ModuleError::InvalidSpec {
85 name: name.clone(),
86 message: format!(
87 "module directory '{}' does not match metadata.name '{}'",
88 name, doc.metadata.name
89 ),
90 }
91 .into());
92 }
93
94 modules.insert(
95 name.clone(),
96 LoadedModule {
97 name,
98 spec: doc.spec,
99 dir: path,
100 },
101 );
102 }
103
104 Ok(modules)
105}
106
107pub fn load_module(module_dir: &Path) -> Result<LoadedModule> {
109 let module_yaml = module_dir.join("module.yaml");
110 if !module_yaml.exists() {
111 let name = module_dir
112 .file_name()
113 .and_then(|n| n.to_str())
114 .ok_or_else(|| ModuleError::InvalidSpec {
115 name: module_dir.display_posix(),
116 message: "invalid module directory name".into(),
117 })?
118 .to_string();
119 return Err(ModuleError::NotFound { name }.into());
120 }
121
122 let contents = read_module_yaml_capped(&module_yaml)?;
123 let doc = parse_module(&contents)?;
124 let name = doc.metadata.name.clone();
125
126 Ok(LoadedModule {
127 name,
128 spec: doc.spec,
129 dir: module_dir.to_path_buf(),
130 })
131}
132
133pub fn resolve_dependency_order(
140 requested: &[String],
141 all_modules: &HashMap<String, LoadedModule>,
142) -> Result<Vec<String>> {
143 const MAX_MODULES: usize = 500;
145 const MAX_DEPENDENCY_DEPTH: usize = 50;
146
147 let mut needed: HashSet<String> = HashSet::new();
149 let mut queue: VecDeque<(String, usize)> = requested.iter().map(|r| (r.clone(), 0)).collect();
150
151 while let Some((name, depth)) = queue.pop_front() {
152 if needed.contains(&name) {
153 continue;
154 }
155
156 if depth > MAX_DEPENDENCY_DEPTH {
157 return Err(ModuleError::DependencyCycle {
158 chain: vec![format!(
159 "dependency depth exceeds {} (at '{}')",
160 MAX_DEPENDENCY_DEPTH, name
161 )],
162 }
163 .into());
164 }
165
166 if needed.len() >= MAX_MODULES {
167 return Err(ModuleError::DependencyCycle {
168 chain: vec![format!("total module count exceeds {} limit", MAX_MODULES)],
169 }
170 .into());
171 }
172
173 let module = all_modules
174 .get(&name)
175 .ok_or_else(|| ModuleError::NotFound { name: name.clone() })?;
176
177 needed.insert(name.clone());
178
179 for dep in &module.spec.depends {
180 if !all_modules.contains_key(dep) {
181 return Err(ModuleError::MissingDependency {
182 module: name.clone(),
183 dependency: dep.clone(),
184 }
185 .into());
186 }
187 if !needed.contains(dep) {
188 queue.push_back((dep.clone(), depth + 1));
189 }
190 }
191 }
192
193 let mut in_degree: HashMap<String, usize> = HashMap::new();
195 let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
196
197 for name in &needed {
198 in_degree.entry(name.clone()).or_insert(0);
199 let module = &all_modules[name];
200 for dep in &module.spec.depends {
201 if needed.contains(dep) {
202 *in_degree.entry(name.clone()).or_insert(0) += 1;
203 dependents
204 .entry(dep.clone())
205 .or_default()
206 .push(name.clone());
207 }
208 }
209 }
210
211 let mut queue: VecDeque<String> = in_degree
213 .iter()
214 .filter(|(_, deg)| **deg == 0)
215 .map(|(name, _)| name.clone())
216 .collect();
217
218 let mut sorted_initial: Vec<String> = queue.drain(..).collect();
220 sorted_initial.sort();
221 queue.extend(sorted_initial);
222
223 let mut order = Vec::new();
224
225 while let Some(name) = queue.pop_front() {
226 order.push(name.clone());
227
228 if let Some(deps) = dependents.get(&name) {
229 let mut next: Vec<String> = Vec::new();
230 for dep in deps {
231 if let Some(deg) = in_degree.get_mut(dep) {
232 *deg -= 1;
233 if *deg == 0 {
234 next.push(dep.clone());
235 }
236 }
237 }
238 next.sort();
240 queue.extend(next);
241 }
242 }
243
244 if order.len() != needed.len() {
245 let ordered: HashSet<&str> = order.iter().map(|s| s.as_str()).collect();
247 let in_cycle: Vec<String> = needed
248 .into_iter()
249 .filter(|n| !ordered.contains(n.as_str()))
250 .collect();
251 return Err(ModuleError::DependencyCycle { chain: in_cycle }.into());
252 }
253
254 Ok(order)
255}