1use crate::config::model::Config;
2use crate::error::{ConfigError, Result};
3use std::path::{Path, PathBuf};
4
5pub fn validate_config(config: &Config) -> Result<()> {
6 if config.specs.is_empty() {
8 return Err(ConfigError::NoSpecDefined.into());
9 }
10
11 let mut seen_names = std::collections::HashSet::new();
14 for spec in &config.specs {
15 if seen_names.contains(&spec.name) {
16 return Err(ConfigError::DuplicateSpecName {
17 name: spec.name.clone(),
18 }
19 .into());
20 }
21 seen_names.insert(&spec.name);
22
23 if spec.name.is_empty() {
25 return Err(ConfigError::InvalidSpecName {
26 name: spec.name.clone(),
27 }
28 .into());
29 }
30
31 if !spec
33 .name
34 .chars()
35 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
36 {
37 return Err(ConfigError::InvalidSpecName {
38 name: spec.name.clone(),
39 }
40 .into());
41 }
42
43 if spec.path.is_empty() {
45 return Err(ConfigError::Invalid {
46 message: format!("Spec '{}' has an empty path", spec.name),
47 }
48 .into());
49 }
50
51 let schemas_output = PathBuf::from(&spec.schemas.output);
53 if schemas_output.is_absolute() {
54 validate_safe_path(&schemas_output)?;
55 }
56
57 let apis_output = PathBuf::from(&spec.apis.output);
59 if apis_output.is_absolute() {
60 validate_safe_path(&apis_output)?;
61 }
62
63 if spec.apis.style != "fetch" {
65 return Err(ConfigError::Invalid {
66 message: format!(
67 "Unsupported API style for spec '{}': {}. Only 'fetch' is supported.",
68 spec.name, spec.apis.style
69 ),
70 }
71 .into());
72 }
73 }
74
75 let root_dir = PathBuf::from(&config.root_dir);
77 if root_dir.is_absolute() && !root_dir.exists() {
78 return Err(ConfigError::Invalid {
79 message: format!("Root directory does not exist: {}", config.root_dir),
80 }
81 .into());
82 }
83
84 Ok(())
85}
86
87fn validate_safe_path(path: &Path) -> Result<()> {
88 let path_str = path.to_string_lossy();
90
91 if path_str.contains("/etc/")
92 || path_str.contains("/usr/")
93 || path_str.contains("/bin/")
94 || path_str.contains("/sbin/")
95 || path_str.contains("/var/")
96 || path_str.contains("/opt/")
97 || path_str == "/"
98 || path_str == "/root"
99 {
100 return Err(ConfigError::InvalidOutputDirectory {
101 path: path_str.to_string(),
102 }
103 .into());
104 }
105
106 Ok(())
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::config::model::Config;
113
114 #[test]
115 fn test_validate_config_valid() {
116 let mut config = Config::default();
117 config.specs = vec![crate::config::model::SpecEntry {
118 name: "test".to_string(),
119 path: "test.yaml".to_string(),
120 schemas: crate::config::model::SchemasConfig::default(),
121 apis: crate::config::model::ApisConfig::default(),
122 hooks: None,
123 modules: crate::config::model::ModulesConfig::default(),
124 }];
125 assert!(validate_config(&config).is_ok());
126 }
127
128 #[test]
129 fn test_validate_config_invalid_style() {
130 let apis = crate::config::model::ApisConfig {
131 style: "invalid".to_string(),
132 ..Default::default()
133 };
134 let config = Config {
135 specs: vec![crate::config::model::SpecEntry {
136 name: "test".to_string(),
137 path: "test.yaml".to_string(),
138 schemas: crate::config::model::SchemasConfig::default(),
139 apis,
140 hooks: None,
141 modules: crate::config::model::ModulesConfig::default(),
142 }],
143 ..Default::default()
144 };
145
146 let result = validate_config(&config);
147 assert!(result.is_err());
148 let error = result.unwrap_err();
149 assert!(error.to_string().contains("Unsupported API style"));
150 }
151
152 #[test]
153 fn test_validate_safe_path_etc() {
154 let path = PathBuf::from("/etc/test");
155 let result = validate_safe_path(&path);
156 assert!(result.is_err());
157 }
158
159 #[test]
160 fn test_validate_safe_path_usr() {
161 let path = PathBuf::from("/usr/test");
162 let result = validate_safe_path(&path);
163 assert!(result.is_err());
164 }
165
166 #[test]
167 fn test_validate_safe_path_bin() {
168 let path = PathBuf::from("/bin/test");
169 let result = validate_safe_path(&path);
170 assert!(result.is_err());
171 }
172
173 #[test]
174 fn test_validate_safe_path_root() {
175 let path = PathBuf::from("/");
176 let result = validate_safe_path(&path);
177 assert!(result.is_err());
178 }
179
180 #[test]
181 fn test_validate_safe_path_valid() {
182 let path = PathBuf::from("/home/user/project");
183 let result = validate_safe_path(&path);
184 assert!(result.is_ok());
185 }
186
187 #[test]
188 fn test_validate_config_absolute_paths() {
189 let schemas = crate::config::model::SchemasConfig {
190 output: "/home/user/schemas".to_string(),
191 ..Default::default()
192 };
193 let apis = crate::config::model::ApisConfig {
194 output: "/home/user/apis".to_string(),
195 ..Default::default()
196 };
197 let config = Config {
198 specs: vec![crate::config::model::SpecEntry {
199 name: "test".to_string(),
200 path: "test.yaml".to_string(),
201 schemas,
202 apis,
203 hooks: None,
204 modules: crate::config::model::ModulesConfig::default(),
205 }],
206 ..Default::default()
207 };
208
209 let result = validate_config(&config);
210 assert!(result.is_ok());
211 }
212
213 #[test]
214 fn test_validate_config_unsafe_schemas_path() {
215 let schemas = crate::config::model::SchemasConfig {
216 output: "/etc/schemas".to_string(),
217 ..Default::default()
218 };
219 let config = Config {
220 specs: vec![crate::config::model::SpecEntry {
221 name: "test".to_string(),
222 path: "test.yaml".to_string(),
223 schemas,
224 apis: crate::config::model::ApisConfig::default(),
225 hooks: None,
226 modules: crate::config::model::ModulesConfig::default(),
227 }],
228 ..Default::default()
229 };
230
231 let result = validate_config(&config);
232 assert!(result.is_err());
233 }
234
235 #[test]
236 fn test_validate_config_unsafe_apis_path() {
237 let apis = crate::config::model::ApisConfig {
238 output: "/usr/apis".to_string(),
239 ..Default::default()
240 };
241 let config = Config {
242 specs: vec![crate::config::model::SpecEntry {
243 name: "test".to_string(),
244 path: "test.yaml".to_string(),
245 schemas: crate::config::model::SchemasConfig::default(),
246 apis,
247 hooks: None,
248 modules: crate::config::model::ModulesConfig::default(),
249 }],
250 ..Default::default()
251 };
252
253 let result = validate_config(&config);
254 assert!(result.is_err());
255 }
256
257 #[test]
258 fn test_validate_config_no_spec_defined() {
259 let config = Config::default();
260 let result = validate_config(&config);
262 assert!(result.is_err());
263 let error = result.unwrap_err();
264 assert!(error.to_string().contains("No specs are defined"));
265 }
266
267 #[test]
268 fn test_validate_config_empty_specs_array() {
269 let config = Config {
270 specs: vec![],
271 ..Default::default()
272 };
273
274 let result = validate_config(&config);
275 assert!(result.is_err());
276 let error = result.unwrap_err();
277 assert!(error.to_string().contains("No specs are defined"));
278 }
279
280 #[test]
281 fn test_validate_config_duplicate_spec_names() {
282 let mut config = Config::default();
283 config.specs = vec![
284 crate::config::model::SpecEntry {
285 name: "auth".to_string(),
286 path: "specs/auth.yaml".to_string(),
287 schemas: crate::config::model::SchemasConfig::default(),
288 apis: crate::config::model::ApisConfig::default(),
289 hooks: None,
290 modules: crate::config::model::ModulesConfig::default(),
291 },
292 crate::config::model::SpecEntry {
293 name: "auth".to_string(),
294 path: "specs/auth2.yaml".to_string(),
295 schemas: crate::config::model::SchemasConfig::default(),
296 apis: crate::config::model::ApisConfig::default(),
297 hooks: None,
298 modules: crate::config::model::ModulesConfig::default(),
299 },
300 ];
301
302 let result = validate_config(&config);
303 assert!(result.is_err());
304 let error = result.unwrap_err();
305 assert!(error.to_string().contains("Duplicate spec name"));
306 }
307
308 #[test]
309 fn test_validate_config_invalid_spec_name() {
310 let mut config = Config::default();
311 config.specs = vec![crate::config::model::SpecEntry {
312 name: "invalid name".to_string(), path: "specs/auth.yaml".to_string(),
314 schemas: crate::config::model::SchemasConfig::default(),
315 apis: crate::config::model::ApisConfig::default(),
316 hooks: None,
317 modules: crate::config::model::ModulesConfig::default(),
318 }];
319
320 let result = validate_config(&config);
321 assert!(result.is_err());
322 let error = result.unwrap_err();
323 assert!(error.to_string().contains("Invalid spec name"));
324 }
325
326 #[test]
327 fn test_validate_config_empty_spec_name() {
328 let mut config = Config::default();
329 config.specs = vec![crate::config::model::SpecEntry {
330 name: "".to_string(),
331 path: "specs/auth.yaml".to_string(),
332 schemas: crate::config::model::SchemasConfig::default(),
333 apis: crate::config::model::ApisConfig::default(),
334 hooks: None,
335 modules: crate::config::model::ModulesConfig::default(),
336 }];
337
338 let result = validate_config(&config);
339 assert!(result.is_err());
340 let error = result.unwrap_err();
341 assert!(error.to_string().contains("Invalid spec name"));
342 }
343
344 #[test]
345 fn test_validate_config_empty_spec_path() {
346 let mut config = Config::default();
347 config.specs = vec![crate::config::model::SpecEntry {
348 name: "auth".to_string(),
349 path: "".to_string(),
350 schemas: crate::config::model::SchemasConfig::default(),
351 apis: crate::config::model::ApisConfig::default(),
352 hooks: None,
353 modules: crate::config::model::ModulesConfig::default(),
354 }];
355
356 let result = validate_config(&config);
357 assert!(result.is_err());
358 let error = result.unwrap_err();
359 assert!(error.to_string().contains("empty path"));
360 }
361
362 #[test]
363 fn test_validate_config_valid_multi_spec() {
364 let mut config = Config::default();
365 config.specs = vec![
366 crate::config::model::SpecEntry {
367 name: "auth".to_string(),
368 path: "specs/auth.yaml".to_string(),
369 schemas: crate::config::model::SchemasConfig::default(),
370 apis: crate::config::model::ApisConfig::default(),
371 hooks: None,
372 modules: crate::config::model::ModulesConfig::default(),
373 },
374 crate::config::model::SpecEntry {
375 name: "orders".to_string(),
376 path: "specs/orders.json".to_string(),
377 schemas: crate::config::model::SchemasConfig::default(),
378 apis: crate::config::model::ApisConfig::default(),
379 hooks: None,
380 modules: crate::config::model::ModulesConfig::default(),
381 },
382 ];
383
384 let result = validate_config(&config);
385 assert!(result.is_ok());
386 }
387
388 #[test]
389 fn test_validate_config_valid_single_spec() {
390 let mut config = Config::default();
391 config.specs = vec![crate::config::model::SpecEntry {
392 name: "default".to_string(),
393 path: "openapi.json".to_string(),
394 schemas: crate::config::model::SchemasConfig::default(),
395 apis: crate::config::model::ApisConfig::default(),
396 hooks: None,
397 modules: crate::config::model::ModulesConfig::default(),
398 }];
399
400 let result = validate_config(&config);
401 assert!(result.is_ok());
402 }
403}