Skip to main content

rusty_sponge/
mode.rs

1//! Compatibility mode resolution.
2//!
3//! Precedence ladder (FR-021, AD-010, HINT-004):
4//! 1. Explicit `--strict` flag wins over everything.
5//! 2. `RUSTY_SPONGE_STRICT=1` env var (any truthy value).
6//! 3. `argv[0]` basename equals `sponge` (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].
14///
15/// `strict_flag` is the post-parse value of `--strict`/`--no-strict` if set.
16/// `env_strict` is the value of `$RUSTY_SPONGE_STRICT`.
17/// `argv0` is the executable name (the first argv element, as the OS provided it).
18pub fn resolve(
19    strict_flag: Option<bool>,
20    env_strict: Option<&OsStr>,
21    argv0: Option<&OsStr>,
22) -> CompatibilityMode {
23    // (1) Explicit flag wins.
24    if let Some(flag) = strict_flag {
25        return if flag {
26            CompatibilityMode::Strict
27        } else {
28            CompatibilityMode::Default
29        };
30    }
31    // (2) Env var.
32    if let Some(value) = env_strict {
33        if env_var_is_truthy(value) {
34            return CompatibilityMode::Strict;
35        }
36    }
37    // (3) argv[0] basename match (with .exe strip on Windows).
38    if let Some(arg0) = argv0 {
39        if argv0_implies_strict(arg0) {
40            return CompatibilityMode::Strict;
41        }
42    }
43    // (4) Default.
44    CompatibilityMode::Default
45}
46
47/// Returns true for env-var values commonly meaning "yes" (1, true, yes, on).
48/// Case-insensitive; whitespace-trimmed via OsStr conversion.
49fn env_var_is_truthy(value: &OsStr) -> bool {
50    let Some(s) = value.to_str() else {
51        return false;
52    };
53    matches!(
54        s.trim().to_ascii_lowercase().as_str(),
55        "1" | "true" | "yes" | "on"
56    )
57}
58
59/// Returns true if argv[0]'s basename (with `.exe` suffix stripped on Windows)
60/// equals `sponge`.
61fn argv0_implies_strict(arg0: &OsStr) -> bool {
62    // Use file_stem to strip the extension (.exe on Windows) per HINT-004.
63    let Some(stem) = Path::new(arg0).file_stem() else {
64        return false;
65    };
66    stem == OsStr::new("sponge")
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn explicit_strict_flag_wins() {
75        assert_eq!(resolve(Some(true), None, None), CompatibilityMode::Strict);
76        assert_eq!(
77            resolve(
78                Some(false),
79                Some(OsStr::new("1")),
80                Some(OsStr::new("sponge"))
81            ),
82            CompatibilityMode::Default,
83            "explicit --no-strict beats env and argv[0]"
84        );
85    }
86
87    #[test]
88    fn env_var_truthy_implies_strict() {
89        for v in ["1", "true", "yes", "on", "TRUE", " 1 ", "On"] {
90            assert_eq!(
91                resolve(None, Some(OsStr::new(v)), None),
92                CompatibilityMode::Strict,
93                "env value {v:?} should imply strict"
94            );
95        }
96    }
97
98    #[test]
99    fn env_var_falsy_does_not_imply_strict() {
100        for v in ["0", "false", "no", "off", ""] {
101            assert_eq!(
102                resolve(None, Some(OsStr::new(v)), None),
103                CompatibilityMode::Default,
104                "env value {v:?} should NOT imply strict"
105            );
106        }
107    }
108
109    #[test]
110    fn argv0_sponge_implies_strict() {
111        assert_eq!(
112            resolve(None, None, Some(OsStr::new("sponge"))),
113            CompatibilityMode::Strict
114        );
115        assert_eq!(
116            resolve(None, None, Some(OsStr::new("/usr/local/bin/sponge"))),
117            CompatibilityMode::Strict
118        );
119        // Windows-style .exe suffix without a path. file_stem() strips it on
120        // every platform.
121        assert_eq!(
122            resolve(None, None, Some(OsStr::new("sponge.exe"))),
123            CompatibilityMode::Strict,
124            "argv[0] = sponge.exe must imply strict per HINT-004"
125        );
126    }
127
128    #[cfg(windows)]
129    #[test]
130    fn argv0_sponge_implies_strict_windows_backslash_path() {
131        // Windows-only: backslash is the path separator. On Linux/macOS, Path
132        // treats `\` as a regular filename character so file_stem() would
133        // return `"C:\bin\sponge"` and the comparison would fail.
134        assert_eq!(
135            resolve(None, None, Some(OsStr::new("C:\\bin\\sponge.exe"))),
136            CompatibilityMode::Strict
137        );
138    }
139
140    #[test]
141    fn argv0_rusty_sponge_does_not_imply_strict() {
142        assert_eq!(
143            resolve(None, None, Some(OsStr::new("rusty-sponge"))),
144            CompatibilityMode::Default
145        );
146        assert_eq!(
147            resolve(None, None, Some(OsStr::new("rusty-sponge.exe"))),
148            CompatibilityMode::Default
149        );
150    }
151
152    #[test]
153    fn default_when_nothing_set() {
154        assert_eq!(resolve(None, None, None), CompatibilityMode::Default);
155    }
156}