Skip to main content

rusty_vipe/
mode.rs

1//! Compatibility mode resolution.
2//!
3//! Precedence ladder (FR-019, FR-020):
4//! 1. Explicit `--strict` / `--no-strict` flag wins over everything.
5//! 2. `RUSTY_VIPE_STRICT=1` env var (any truthy value).
6//! 3. `argv[0]` basename equals `vipe` (after `.exe` strip on Windows).
7//! 4. Default mode.
8
9use crate::CompatibilityMode;
10use std::ffi::OsStr;
11use std::path::Path;
12
13/// Resolve the compatibility mode from CLI flag, env var, and argv[0].
14pub fn resolve(
15    strict_flag: Option<bool>,
16    env_strict: Option<&OsStr>,
17    argv0: Option<&OsStr>,
18) -> CompatibilityMode {
19    if let Some(flag) = strict_flag {
20        return if flag {
21            CompatibilityMode::Strict
22        } else {
23            CompatibilityMode::Default
24        };
25    }
26    if let Some(value) = env_strict {
27        if env_var_is_truthy(value) {
28            return CompatibilityMode::Strict;
29        }
30    }
31    if let Some(arg0) = argv0 {
32        if argv0_implies_strict(arg0) {
33            return CompatibilityMode::Strict;
34        }
35    }
36    CompatibilityMode::Default
37}
38
39fn env_var_is_truthy(value: &OsStr) -> bool {
40    let Some(s) = value.to_str() else {
41        return false;
42    };
43    matches!(
44        s.trim().to_ascii_lowercase().as_str(),
45        "1" | "true" | "yes" | "on"
46    )
47}
48
49fn argv0_implies_strict(arg0: &OsStr) -> bool {
50    let Some(stem) = Path::new(arg0).file_stem() else {
51        return false;
52    };
53    stem == OsStr::new("vipe")
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn explicit_strict_flag_wins() {
62        assert_eq!(resolve(Some(true), None, None), CompatibilityMode::Strict);
63        assert_eq!(
64            resolve(Some(false), Some(OsStr::new("1")), Some(OsStr::new("vipe"))),
65            CompatibilityMode::Default,
66            "explicit --no-strict beats env and argv[0]"
67        );
68    }
69
70    #[test]
71    fn env_var_truthy_implies_strict() {
72        for v in ["1", "true", "yes", "on", "TRUE", " 1 ", "On"] {
73            assert_eq!(
74                resolve(None, Some(OsStr::new(v)), None),
75                CompatibilityMode::Strict,
76                "env value {v:?} should imply strict"
77            );
78        }
79    }
80
81    #[test]
82    fn env_var_falsy_does_not_imply_strict() {
83        for v in ["0", "false", "no", "off", ""] {
84            assert_eq!(
85                resolve(None, Some(OsStr::new(v)), None),
86                CompatibilityMode::Default,
87                "env value {v:?} should NOT imply strict"
88            );
89        }
90    }
91
92    #[test]
93    fn argv0_vipe_implies_strict() {
94        assert_eq!(
95            resolve(None, None, Some(OsStr::new("vipe"))),
96            CompatibilityMode::Strict
97        );
98        assert_eq!(
99            resolve(None, None, Some(OsStr::new("/usr/local/bin/vipe"))),
100            CompatibilityMode::Strict
101        );
102        assert_eq!(
103            resolve(None, None, Some(OsStr::new("vipe.exe"))),
104            CompatibilityMode::Strict,
105            "argv[0] = vipe.exe must imply strict (file_stem strips .exe)"
106        );
107    }
108
109    #[cfg(windows)]
110    #[test]
111    fn argv0_windows_backslash_path() {
112        assert_eq!(
113            resolve(None, None, Some(OsStr::new("C:\\bin\\vipe.exe"))),
114            CompatibilityMode::Strict
115        );
116    }
117
118    #[test]
119    fn argv0_rusty_vipe_does_not_imply_strict() {
120        assert_eq!(
121            resolve(None, None, Some(OsStr::new("rusty-vipe"))),
122            CompatibilityMode::Default
123        );
124        assert_eq!(
125            resolve(None, None, Some(OsStr::new("rusty-vipe.exe"))),
126            CompatibilityMode::Default
127        );
128    }
129
130    #[test]
131    fn default_when_nothing_set() {
132        assert_eq!(resolve(None, None, None), CompatibilityMode::Default);
133    }
134
135    // ──── T076: explicit precedence-ladder coverage ───────────────────────
136    //
137    // Ladder: `--strict` > `RUSTY_VIPE_STRICT` > argv[0]=`vipe` > Default
138    //
139    // Each rung must beat all lower rungs. The matrix tests below assert
140    // pairwise precedence at every boundary.
141
142    #[test]
143    fn ladder_strict_flag_beats_env_var() {
144        // --no-strict explicitly disables Strict even when RUSTY_VIPE_STRICT=1.
145        assert_eq!(
146            resolve(Some(false), Some(OsStr::new("1")), None),
147            CompatibilityMode::Default,
148            "ladder rung 1 (--no-strict) must beat rung 2 (env=1)"
149        );
150        // --strict explicitly enables Strict even when env says no.
151        assert_eq!(
152            resolve(Some(true), Some(OsStr::new("0")), None),
153            CompatibilityMode::Strict,
154            "ladder rung 1 (--strict) must beat rung 2 (env=0)"
155        );
156    }
157
158    #[test]
159    fn ladder_env_var_beats_argv0() {
160        // env=1 overrides argv[0]=rusty-vipe (which would otherwise be Default).
161        assert_eq!(
162            resolve(None, Some(OsStr::new("1")), Some(OsStr::new("rusty-vipe"))),
163            CompatibilityMode::Strict,
164            "ladder rung 2 (env=1) must beat rung 3 (argv0=rusty-vipe → Default)"
165        );
166        // env=0 (falsy) does NOT engage Strict, but it also doesn't VETO
167        // a lower rung — argv[0]=vipe can still engage Strict.
168        assert_eq!(
169            resolve(None, Some(OsStr::new("0")), Some(OsStr::new("vipe"))),
170            CompatibilityMode::Strict,
171            "rung 2 falsy is no-op; rung 3 (argv0=vipe) still engages Strict"
172        );
173    }
174
175    #[test]
176    fn ladder_argv0_beats_default() {
177        // argv[0]=vipe → Strict, no other signal needed.
178        assert_eq!(
179            resolve(None, None, Some(OsStr::new("vipe"))),
180            CompatibilityMode::Strict,
181            "ladder rung 3 (argv0=vipe) beats rung 4 (Default)"
182        );
183        // argv[0]=rusty-vipe → Default (rung 3 does not apply).
184        assert_eq!(
185            resolve(None, None, Some(OsStr::new("rusty-vipe"))),
186            CompatibilityMode::Default,
187            "rung 3 only fires for argv0=vipe; rusty-vipe falls to rung 4"
188        );
189    }
190}