1use crate::EnvVar;
2use ahash::AHashMap as HashMap;
3use std::collections::HashSet;
4use std::path::Path;
5
6#[derive(Debug)]
7pub struct ValidationResult {
8 pub valid: bool,
9 pub errors: Vec<String>,
10 pub warnings: Vec<String>,
11}
12
13pub struct Analyzer {
14 vars: Vec<EnvVar>,
15}
16
17impl Analyzer {
18 #[must_use]
19 pub const fn new(vars: Vec<EnvVar>) -> Self {
20 Self { vars }
21 }
22
23 pub fn find_duplicates(&self) -> HashMap<String, Vec<&EnvVar>> {
24 let mut duplicates = HashMap::new();
25 let mut seen = HashMap::new();
26
27 for var in &self.vars {
28 seen.entry(var.name.to_uppercase()).or_insert_with(Vec::new).push(var);
29 }
30
31 for (name, vars) in seen {
32 if vars.len() > 1 {
33 duplicates.insert(name, vars);
34 }
35 }
36
37 duplicates
38 }
39
40 #[must_use]
41 pub fn validate_all(&self) -> HashMap<String, ValidationResult> {
42 let mut results = HashMap::new();
43
44 for var in &self.vars {
45 let mut errors = Vec::new();
46 let mut warnings = Vec::new();
47
48 if var.name.is_empty() {
50 errors.push("Variable name is empty".to_string());
51 }
52
53 if var.name.contains(' ') {
54 errors.push("Variable name contains spaces".to_string());
55 }
56
57 if var.name.starts_with(|c: char| c.is_numeric()) {
58 errors.push("Variable name starts with a number".to_string());
59 }
60
61 if var.name.to_uppercase().ends_with("PATH") {
63 let path_analyzer = PathAnalyzer::new(&var.value);
64 let path_result = path_analyzer.analyze();
65 errors.extend(path_result.errors);
66 warnings.extend(path_result.warnings);
67 }
68
69 let valid = errors.is_empty();
70 results.insert(
71 var.name.clone(),
72 ValidationResult {
73 valid,
74 errors,
75 warnings,
76 },
77 );
78 }
79
80 results
81 }
82
83 #[must_use]
84 pub fn find_unused(&self) -> Vec<&EnvVar> {
85 self.vars
87 .iter()
88 .filter(|v| {
89 v.name.starts_with("OLD_")
91 || v.name.starts_with("BACKUP_")
92 || v.name.ends_with("_OLD")
93 || v.name.ends_with("_BACKUP")
94 })
95 .collect()
96 }
97
98 #[must_use]
99 pub fn analyze_dependencies(&self) -> HashMap<String, Vec<String>> {
100 let mut deps = HashMap::new();
101
102 for var in &self.vars {
103 let mut var_deps = Vec::new();
104
105 for other in &self.vars {
107 if var.name != other.name && !other.name.is_empty() {
108 let pattern_windows = format!("%{}%", other.name);
110 let pattern_unix_braces = format!("${{{}}}", other.name);
112
113 if var.value.contains(&pattern_windows) || var.value.contains(&pattern_unix_braces) {
114 var_deps.push(other.name.clone());
115 } else {
116 let unix_pattern = format!("${}", other.name);
119 if let Some(pos) = var.value.find(&unix_pattern) {
121 let next_pos = pos + unix_pattern.len();
122 if next_pos == var.value.len()
123 || !var
124 .value
125 .chars()
126 .nth(next_pos)
127 .is_some_and(|c| c.is_alphanumeric() || c == '_')
128 {
129 var_deps.push(other.name.clone());
130 }
131 }
132 }
133 }
134 }
135
136 var_deps.sort();
138 var_deps.dedup();
139
140 if !var_deps.is_empty() {
141 deps.insert(var.name.clone(), var_deps);
142 }
143 }
144
145 deps
146 }
147}
148
149pub struct PathAnalyzer {
150 paths: Vec<String>,
151}
152
153impl PathAnalyzer {
154 #[must_use]
155 pub fn new(path_value: &str) -> Self {
156 let separator = if cfg!(windows) { ';' } else { ':' };
157 let paths = path_value
158 .split(separator)
159 .map(std::string::ToString::to_string)
160 .collect();
161
162 Self { paths }
163 }
164
165 #[must_use]
166 pub fn analyze(&self) -> ValidationResult {
167 let mut errors = Vec::new();
168 let mut warnings = Vec::new();
169 let mut seen = HashSet::new();
170
171 for path_str in &self.paths {
172 if path_str.is_empty() {
173 warnings.push("Empty path entry found".to_string());
174 continue;
175 }
176
177 if !seen.insert(path_str.to_lowercase()) {
179 warnings.push(format!("Duplicate path entry: {path_str}"));
180 }
181
182 let path = Path::new(path_str);
184 if !path.exists() {
185 errors.push(format!("Path does not exist: {path_str}"));
186 } else if !path.is_dir() {
187 errors.push(format!("Path is not a directory: {path_str}"));
188 }
189
190 if path_str.contains("..") {
192 warnings.push(format!("Path contains relative parent reference: {path_str}"));
193 }
194
195 #[cfg(windows)]
196 {
197 if path_str.contains('/') {
198 warnings.push(format!("Path uses Unix-style separators on Windows: {path_str}"));
199 }
200 }
201
202 #[cfg(unix)]
203 {
204 if path_str.contains('\\') {
205 warnings.push(format!("Path uses Windows-style separators on Unix: {path_str}"));
206 }
207 }
208 }
209
210 ValidationResult {
211 valid: errors.is_empty(),
212 errors,
213 warnings,
214 }
215 }
216
217 #[must_use]
218 pub fn get_duplicates(&self) -> Vec<String> {
219 let mut seen = HashSet::new();
220 let mut duplicates = Vec::new();
221
222 for path in &self.paths {
223 let normalized = path.to_lowercase();
224 if !seen.insert(normalized.clone()) {
225 duplicates.push(path.clone());
226 }
227 }
228
229 duplicates
230 }
231
232 #[must_use]
233 pub fn get_invalid(&self) -> Vec<String> {
234 self.paths.iter().filter(|p| !Path::new(p).exists()).cloned().collect()
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::{EnvVar, EnvVarSource};
242 use chrono::Utc;
243 use std::fs;
244 use tempfile::TempDir;
245
246 fn create_test_var(name: &str, value: &str) -> EnvVar {
248 EnvVar {
249 name: name.to_string(),
250 value: value.to_string(),
251 source: EnvVarSource::User,
252 modified: Utc::now(),
253 original_value: None,
254 }
255 }
256
257 fn create_test_vars() -> Vec<EnvVar> {
259 vec![
260 create_test_var("PATH", "/usr/bin:/usr/local/bin"),
261 create_test_var("HOME", "/home/user"),
262 create_test_var("JAVA_HOME", "/usr/lib/jvm/java-11"),
263 create_test_var("PYTHON_PATH", "/usr/bin/python"),
264 create_test_var("OLD_PATH", "/old/path"),
265 create_test_var("BACKUP_HOME", "/backup/home"),
266 create_test_var("API_KEY", "secret123"),
267 create_test_var("DATABASE_URL", "postgres://localhost:5432/db"),
268 create_test_var("APP_CONFIG", "${HOME}/config:${JAVA_HOME}/conf"),
269 create_test_var("FULL_PATH", "%PATH%;%JAVA_HOME%\\bin"),
270 ]
271 }
272
273 #[test]
274 fn test_analyzer_new() {
275 let vars = create_test_vars();
276 let analyzer = Analyzer::new(vars.clone());
277 assert_eq!(analyzer.vars.len(), vars.len());
278 }
279
280 #[test]
281 fn test_find_duplicates_no_duplicates() {
282 let vars = create_test_vars();
283 let analyzer = Analyzer::new(vars);
284
285 let duplicates = analyzer.find_duplicates();
286 assert!(duplicates.is_empty());
287 }
288
289 #[test]
290 fn test_find_duplicates_with_case_variations() {
291 let vars = vec![
292 create_test_var("PATH", "/usr/bin"),
293 create_test_var("Path", "/usr/local/bin"),
294 create_test_var("path", "/bin"),
295 create_test_var("HOME", "/home/user"),
296 create_test_var("home", "/home/user2"),
297 ];
298
299 let analyzer = Analyzer::new(vars);
300 let duplicates = analyzer.find_duplicates();
301
302 assert_eq!(duplicates.len(), 2); assert_eq!(duplicates.get("PATH").unwrap().len(), 3);
304 assert_eq!(duplicates.get("HOME").unwrap().len(), 2);
305 }
306
307 #[test]
308 fn test_validate_all_valid_variables() {
309 let vars = vec![
310 create_test_var("VALID_VAR", "value"),
311 create_test_var("ANOTHER_VAR", "another value"),
312 create_test_var("_UNDERSCORE_START", "value"),
313 ];
314
315 let analyzer = Analyzer::new(vars);
316 let results = analyzer.validate_all();
317
318 for (_, result) in results {
319 assert!(result.valid);
320 assert!(result.errors.is_empty());
321 }
322 }
323
324 #[test]
325 fn test_validate_all_invalid_names() {
326 let vars = vec![
327 create_test_var("", "empty name"),
328 create_test_var("SPACE IN NAME", "value"),
329 create_test_var("123_STARTS_WITH_NUMBER", "value"),
330 create_test_var("VALID_NAME", "value"),
331 ];
332
333 let analyzer = Analyzer::new(vars);
334 let results = analyzer.validate_all();
335
336 let empty_result = results.get("").unwrap();
338 assert!(!empty_result.valid);
339 assert!(empty_result.errors.iter().any(|e| e.contains("empty")));
340
341 let space_result = results.get("SPACE IN NAME").unwrap();
343 assert!(!space_result.valid);
344 assert!(space_result.errors.iter().any(|e| e.contains("spaces")));
345
346 let number_result = results.get("123_STARTS_WITH_NUMBER").unwrap();
348 assert!(!number_result.valid);
349 assert!(number_result.errors.iter().any(|e| e.contains("number")));
350
351 let valid_result = results.get("VALID_NAME").unwrap();
353 assert!(valid_result.valid);
354 }
355
356 #[test]
357 fn test_validate_path_variables() {
358 let temp_dir = TempDir::new().unwrap();
360 let valid_path = temp_dir.path().to_str().unwrap();
361 let invalid_path = "/nonexistent/path/that/does/not/exist";
362
363 let separator = if cfg!(windows) { ";" } else { ":" };
364 let path_value = format!("{valid_path}{separator}{invalid_path}");
365
366 let vars = vec![
367 create_test_var("CUSTOM_PATH", &path_value),
368 create_test_var("EMPTY_PATH", &format!("{valid_path}{separator}")),
369 ];
370
371 let analyzer = Analyzer::new(vars);
372 let results = analyzer.validate_all();
373
374 let custom_result = results.get("CUSTOM_PATH").unwrap();
376 assert!(!custom_result.valid);
377 assert!(custom_result.errors.iter().any(|e| e.contains("does not exist")));
378
379 let empty_result = results.get("EMPTY_PATH").unwrap();
381 assert!(empty_result.warnings.iter().any(|w| w.contains("Empty path entry")));
382 }
383
384 #[test]
385 fn test_find_unused() {
386 let vars = vec![
387 create_test_var("ACTIVE_VAR", "value"),
388 create_test_var("OLD_CONFIG", "old value"),
389 create_test_var("BACKUP_PATH", "backup"),
390 create_test_var("DATA_OLD", "old data"),
391 create_test_var("CONFIG_BACKUP", "backup config"),
392 create_test_var("CURRENT_VAR", "current"),
393 ];
394
395 let analyzer = Analyzer::new(vars);
396 let unused = analyzer.find_unused();
397
398 assert_eq!(unused.len(), 4);
399 let unused_names: Vec<&str> = unused.iter().map(|v| v.name.as_str()).collect();
400 assert!(unused_names.contains(&"OLD_CONFIG"));
401 assert!(unused_names.contains(&"BACKUP_PATH"));
402 assert!(unused_names.contains(&"DATA_OLD"));
403 assert!(unused_names.contains(&"CONFIG_BACKUP"));
404 assert!(!unused_names.contains(&"ACTIVE_VAR"));
405 assert!(!unused_names.contains(&"CURRENT_VAR"));
406 }
407
408 #[test]
409 fn test_analyze_dependencies_no_deps() {
410 let vars = vec![
411 create_test_var("VAR1", "value1"),
412 create_test_var("VAR2", "value2"),
413 create_test_var("VAR3", "value3"),
414 ];
415
416 let analyzer = Analyzer::new(vars);
417 let deps = analyzer.analyze_dependencies();
418 assert!(deps.is_empty());
419 }
420
421 #[test]
422 fn test_analyze_dependencies_with_references() {
423 let vars = vec![
424 create_test_var("HOME", "/home/user"),
425 create_test_var("JAVA_HOME", "/usr/lib/jvm/java"),
426 create_test_var("CONFIG_PATH", "${HOME}/config"),
427 create_test_var("JAVA_BIN", "${JAVA_HOME}/bin"),
428 create_test_var("FULL_PATH", "${HOME}/bin:${JAVA_HOME}/bin"),
429 create_test_var("WINDOWS_PATH", "%HOME%;%JAVA_HOME%"),
430 ];
431
432 let analyzer = Analyzer::new(vars);
433 let deps = analyzer.analyze_dependencies();
434
435 assert!(deps.contains_key("CONFIG_PATH"));
437 assert_eq!(deps.get("CONFIG_PATH").unwrap(), &vec!["HOME".to_string()]);
438
439 assert!(deps.contains_key("JAVA_BIN"));
441 assert_eq!(deps.get("JAVA_BIN").unwrap(), &vec!["JAVA_HOME".to_string()]);
442
443 assert!(deps.contains_key("FULL_PATH"));
445 let full_path_deps = deps.get("FULL_PATH").unwrap();
446 assert_eq!(full_path_deps.len(), 2);
447 assert!(full_path_deps.contains(&"HOME".to_string()));
448 assert!(full_path_deps.contains(&"JAVA_HOME".to_string()));
449
450 assert!(deps.contains_key("WINDOWS_PATH"));
452 let windows_deps = deps.get("WINDOWS_PATH").unwrap();
453 assert_eq!(windows_deps.len(), 2);
454 }
455
456 #[test]
457 fn test_path_analyzer_new() {
458 let path_value = if cfg!(windows) {
459 "C:\\Windows;C:\\Program Files;C:\\Users"
460 } else {
461 "/usr/bin:/usr/local/bin:/home/user/bin"
462 };
463
464 let analyzer = PathAnalyzer::new(path_value);
465 assert_eq!(analyzer.paths.len(), 3);
466 }
467
468 #[test]
469 fn test_path_analyzer_empty_entries() {
470 let separator = if cfg!(windows) { ";" } else { ":" };
471 let path_value = format!("/path1{separator}{separator}/path2");
472
473 let analyzer = PathAnalyzer::new(&path_value);
474 let result = analyzer.analyze();
475
476 assert!(result.warnings.iter().any(|w| w.contains("Empty path entry")));
477 }
478
479 #[test]
480 fn test_path_analyzer_duplicate_detection() {
481 let separator = if cfg!(windows) { ";" } else { ":" };
482 let path_value = format!("/path1{separator}/path2{separator}/path1{separator}/PATH1");
483
484 let analyzer = PathAnalyzer::new(&path_value);
485 let result = analyzer.analyze();
486
487 assert!(result.warnings.iter().any(|w| w.contains("Duplicate")));
488
489 let duplicates = analyzer.get_duplicates();
490 assert!(!duplicates.is_empty());
491 }
492
493 #[test]
494 fn test_path_analyzer_invalid_paths() {
495 let path_value = if cfg!(windows) {
496 "C:\\NonExistent;C:\\AlsoNonExistent"
497 } else {
498 "/nonexistent:/also/nonexistent"
499 };
500
501 let analyzer = PathAnalyzer::new(path_value);
502 let result = analyzer.analyze();
503
504 assert!(!result.valid);
505 assert!(result.errors.iter().any(|e| e.contains("does not exist")));
506
507 let invalid = analyzer.get_invalid();
508 assert_eq!(invalid.len(), 2);
509 }
510
511 #[test]
512 fn test_path_analyzer_relative_paths() {
513 let separator = if cfg!(windows) { ";" } else { ":" };
514 let path_value = format!("/absolute/path{separator}../relative/path");
515
516 let analyzer = PathAnalyzer::new(&path_value);
517 let result = analyzer.analyze();
518
519 assert!(result.warnings.iter().any(|w| w.contains("relative parent reference")));
520 }
521
522 #[test]
523 #[cfg(windows)]
524 fn test_path_analyzer_wrong_separators_windows() {
525 let path_value = "C:\\Windows;/unix/style/path";
526
527 let analyzer = PathAnalyzer::new(path_value);
528 let result = analyzer.analyze();
529
530 assert!(result.warnings.iter().any(|w| w.contains("Unix-style separators")));
531 }
532
533 #[test]
534 #[cfg(unix)]
535 fn test_path_analyzer_wrong_separators_unix() {
536 let path_value = "/usr/bin:C:\\Windows\\Style\\Path";
537
538 let analyzer = PathAnalyzer::new(path_value);
539 let result = analyzer.analyze();
540
541 assert!(result.warnings.iter().any(|w| w.contains("Windows-style separators")));
542 }
543
544 #[test]
545 fn test_path_analyzer_file_not_directory() {
546 let temp_dir = TempDir::new().unwrap();
548 let temp_file = temp_dir.path().join("test.txt");
549 fs::write(&temp_file, "test").unwrap();
550
551 let path_value = temp_file.to_str().unwrap();
552 let analyzer = PathAnalyzer::new(path_value);
553 let result = analyzer.analyze();
554
555 assert!(!result.valid);
556 assert!(result.errors.iter().any(|e| e.contains("not a directory")));
557 }
558
559 #[test]
560 fn test_complex_validation_scenario() {
561 let temp_dir = TempDir::new().unwrap();
562 let valid_path = temp_dir.path().to_str().unwrap();
563 let separator = if cfg!(windows) { ";" } else { ":" };
564
565 let vars = vec![
566 create_test_var("", "empty name"),
567 create_test_var("SPACE NAME", "value"),
568 create_test_var("123START", "value"),
569 create_test_var("VALID_PATH", valid_path),
570 create_test_var("INVALID_PATH", "/nonexistent"),
571 create_test_var("MIXED_PATH", &format!("{valid_path}{separator}/nonexistent")),
572 create_test_var("OLD_VAR", "old value"),
573 create_test_var("REF_VAR", "${VALID_PATH}/subdir"),
574 ];
575
576 let analyzer = Analyzer::new(vars);
577
578 let validation_results = analyzer.validate_all();
580 assert!(!validation_results.get("").unwrap().valid);
581 assert!(!validation_results.get("SPACE NAME").unwrap().valid);
582 assert!(!validation_results.get("123START").unwrap().valid);
583 assert!(validation_results.get("VALID_PATH").unwrap().valid);
584 assert!(!validation_results.get("INVALID_PATH").unwrap().valid);
585 assert!(!validation_results.get("MIXED_PATH").unwrap().valid);
586
587 let unused = analyzer.find_unused();
589 assert!(unused.iter().any(|v| v.name == "OLD_VAR"));
590
591 let deps = analyzer.analyze_dependencies();
593 assert!(deps.contains_key("REF_VAR"));
594 assert_eq!(deps.get("REF_VAR").unwrap(), &vec!["VALID_PATH".to_string()]);
595 }
596
597 #[test]
598 fn test_validation_result_structure() {
599 let result = ValidationResult {
600 valid: false,
601 errors: vec!["Error 1".to_string(), "Error 2".to_string()],
602 warnings: vec!["Warning 1".to_string()],
603 };
604
605 assert!(!result.valid);
606 assert_eq!(result.errors.len(), 2);
607 assert_eq!(result.warnings.len(), 1);
608 }
609
610 #[test]
611 fn test_case_insensitive_duplicate_detection() {
612 let vars = vec![
613 create_test_var("path", "/lower"),
614 create_test_var("PATH", "/upper"),
615 create_test_var("Path", "/mixed"),
616 create_test_var("pAtH", "/weird"),
617 ];
618
619 let analyzer = Analyzer::new(vars);
620 let duplicates = analyzer.find_duplicates();
621
622 assert_eq!(duplicates.len(), 1);
623 assert!(duplicates.contains_key("PATH"));
624 assert_eq!(duplicates.get("PATH").unwrap().len(), 4);
625 }
626
627 #[test]
628 fn test_empty_analyzer() {
629 let analyzer = Analyzer::new(vec![]);
630
631 assert!(analyzer.find_duplicates().is_empty());
632 assert!(analyzer.validate_all().is_empty());
633 assert!(analyzer.find_unused().is_empty());
634 assert!(analyzer.analyze_dependencies().is_empty());
635 }
636
637 #[test]
638 fn test_circular_dependencies() {
639 let vars = vec![
640 create_test_var("VAR_A", "${VAR_B}/a"),
641 create_test_var("VAR_B", "${VAR_C}/b"),
642 create_test_var("VAR_C", "${VAR_A}/c"),
643 ];
644
645 let analyzer = Analyzer::new(vars);
646 let deps = analyzer.analyze_dependencies();
647
648 assert!(deps.contains_key("VAR_A"));
649 assert!(deps.contains_key("VAR_B"));
650 assert!(deps.contains_key("VAR_C"));
651 assert_eq!(deps.get("VAR_A").unwrap(), &vec!["VAR_B".to_string()]);
652 assert_eq!(deps.get("VAR_B").unwrap(), &vec!["VAR_C".to_string()]);
653 assert_eq!(deps.get("VAR_C").unwrap(), &vec!["VAR_A".to_string()]);
654 }
655
656 #[test]
657 fn test_multiple_dependency_formats() {
658 let vars = vec![
659 create_test_var("BASE", "/base"),
660 create_test_var("DEP1", "$BASE/path"), create_test_var("DEP2", "${BASE}/path"), create_test_var("DEP3", "%BASE%\\path"), create_test_var("MULTI", "${BASE}:$BASE:%BASE%"), ];
665
666 let analyzer = Analyzer::new(vars);
667 let deps = analyzer.analyze_dependencies();
668
669 assert!(deps.contains_key("DEP1"));
670 assert!(deps.contains_key("DEP2"));
671 assert!(deps.contains_key("DEP3"));
672 assert!(deps.contains_key("MULTI"));
673
674 assert_eq!(deps.get("DEP1").unwrap(), &vec!["BASE".to_string()]);
676 assert_eq!(deps.get("DEP2").unwrap(), &vec!["BASE".to_string()]);
677 assert_eq!(deps.get("DEP3").unwrap(), &vec!["BASE".to_string()]);
678 assert_eq!(deps.get("MULTI").unwrap(), &vec!["BASE".to_string()]);
679 }
680}