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