Skip to main content

envx_secure/commands/
diff.rs

1//! Semantic diff between two `.env` files.
2//!
3//! Values for keys that match common sensitive patterns (`SECRET`, `KEY`,
4//! `TOKEN`, `PASSWORD`, `PASS`, `PWD`) are redacted with bullet characters so
5//! secrets are never echoed to the terminal.
6
7use crate::parser;
8use anyhow::Result;
9use owo_colors::OwoColorize;
10use std::path::Path;
11
12/// Key substrings that trigger value redaction.
13const SENSITIVE_PATTERNS: &[&str] = &["SECRET", "KEY", "TOKEN", "PASSWORD", "PASS", "PWD"];
14
15fn is_sensitive(key: &str) -> bool {
16    let upper = key.to_uppercase();
17    SENSITIVE_PATTERNS.iter().any(|p| upper.contains(p))
18}
19
20fn redact(value: &str) -> String {
21    "•".repeat(value.len().max(7))
22}
23
24/// Compare `file_a` and `file_b` and print a coloured semantic diff.
25///
26/// Keys present in both files with equal values are shown dimmed.  Changed
27/// values are shown as `- old` / `+ new` lines (or redacted for sensitive
28/// keys).  Keys only in one file are shown as removed (`-`) or added (`+`).
29///
30/// # Exit behaviour
31///
32/// Calls `std::process::exit(1)` when any difference is found so the
33/// function can be used as a CI gate.
34///
35/// # Errors
36///
37/// Returns an error if either file cannot be parsed.
38pub fn run(file_a: &Path, file_b: &Path) -> Result<()> {
39    let map_a = parser::parse(file_a)?;
40    let map_b = parser::parse(file_b)?;
41
42    println!("{}", format!("--- {}", file_a.display()).bold());
43    println!("{}", format!("+++ {}", file_b.display()).bold());
44
45    let mut differences = false;
46
47    // Collect all keys preserving order: A first, then new keys in B
48    let mut all_keys: Vec<&str> = map_a.keys().map(String::as_str).collect();
49    for k in map_b.keys() {
50        if !map_a.contains_key(k.as_str()) {
51            all_keys.push(k.as_str());
52        }
53    }
54
55    for key in all_keys {
56        match (map_a.get(key), map_b.get(key)) {
57            (Some(va), Some(vb)) if va == vb => {
58                println!("  {}", format!("{key}={va}").dimmed());
59            }
60            (Some(va), Some(vb)) => {
61                differences = true;
62                if is_sensitive(key) {
63                    println!(
64                        "{}",
65                        format!("~ {key}={} → {}", redact(va), redact(vb)).yellow()
66                    );
67                } else {
68                    println!("{}", format!("- {key}={va}").red());
69                    println!("{}", format!("+ {key}={vb}").green());
70                }
71            }
72            (Some(va), None) => {
73                differences = true;
74                println!("{}", format!("- {key}={va}").red());
75            }
76            (None, Some(vb)) => {
77                differences = true;
78                println!("{}", format!("+ {key}={vb}").green());
79            }
80            (None, None) => unreachable!(),
81        }
82    }
83
84    if differences {
85        std::process::exit(1);
86    }
87
88    Ok(())
89}