diff --git a/e2e/config/test_override_config_filenames_empty b/e2e/config/test_override_config_filenames_empty new file mode 100755 index 0000000000..425b44ec72 --- /dev/null +++ b/e2e/config/test_override_config_filenames_empty @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Regression test: MISE_OVERRIDE_CONFIG_FILENAMES set to empty string +# should not cause a panic. An empty value should be treated the same +# as the variable being unset (i.e., use default config filenames). +# +# Background: split(':') on "" produces [""], a set with one empty-string +# entry. This non-empty set bypasses the default config filename list and +# injects "" as a config filename, which eventually causes a panic in +# config_root() during Config::load. +# +# See: https://github.com/jdx/mise/pull/9076 + +echo 'tools.dummy = "1"' >mise.toml + +# Empty string — should behave like unset (use mise.toml) +MISE_OVERRIDE_CONFIG_FILENAMES="" assert_succeed "mise ls" + +# Colon-only — split produces ["", ""], both empty, should behave like unset +MISE_OVERRIDE_CONFIG_FILENAMES=":" assert_succeed "mise ls" + +# Trailing/leading colons — should ignore empty segments +echo 'tools.dummy = "2"' >mise.ci.toml +MISE_OVERRIDE_CONFIG_FILENAMES=":mise.ci.toml:" assert "mise ls dummy" "dummy 2.0.0 (missing) ~/workdir/mise.ci.toml 2" diff --git a/src/env.rs b/src/env.rs index c18af75726..79b33ed072 100644 --- a/src/env.rs +++ b/src/env.rs @@ -244,14 +244,14 @@ pub static MISE_DEFAULT_CONFIG_FILENAME: Lazy = Lazy::new(|| { pub static MISE_OVERRIDE_TOOL_VERSIONS_FILENAMES: Lazy>> = Lazy::new(|| match var("MISE_OVERRIDE_TOOL_VERSIONS_FILENAMES") { Ok(v) if v == "none" => Some([].into()), - Ok(v) => Some(v.split(':').map(|s| s.to_string()).collect()), + Ok(v) => Some(split_colon_list(&v)), Err(_) => { miserc::get_override_tool_versions_filenames().map(|v| v.iter().cloned().collect()) } }); pub static MISE_OVERRIDE_CONFIG_FILENAMES: Lazy> = Lazy::new(|| match var("MISE_OVERRIDE_CONFIG_FILENAMES") { - Ok(v) => v.split(':').map(|s| s.to_string()).collect(), + Ok(v) => split_colon_list(&v), Err(_) => miserc::get_override_config_filenames() .map(|v| v.iter().cloned().collect()) .unwrap_or_default(), @@ -738,6 +738,18 @@ fn linux_glibc_version() -> Option<(u32, u32)> { None } +/// Split a colon-separated string into a set, filtering empty segments. +/// Empty segments arise from empty strings, leading/trailing colons, or +/// consecutive colons — all of which should be ignored rather than +/// injected as empty paths into config discovery. +fn split_colon_list(value: &str) -> IndexSet { + value + .split(':') + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect() +} + fn filename(path: &str) -> &str { path.rsplit_once(path::MAIN_SEPARATOR_STR) .map(|(_, file)| file) @@ -834,6 +846,24 @@ mod tests { remove_var("MISE_TEST_PATH"); } + #[test] + fn test_split_colon_list() { + let cases: Vec<(&str, Vec<&str>)> = vec![ + ("", vec![]), // empty string — was causing panic + (":", vec![]), // colon only + (":::", vec![]), // multiple colons + ("mise.toml", vec!["mise.toml"]), + ("a:b", vec!["a", "b"]), + (":a:b:", vec!["a", "b"]), // leading/trailing colons + ("a::b", vec!["a", "b"]), // consecutive colons + ]; + for (input, expected) in cases { + let result = split_colon_list(input); + let expected: IndexSet = expected.into_iter().map(|s| s.to_string()).collect(); + assert_eq!(result, expected, "input: {input:?}"); + } + } + #[test] fn test_token_overwrite() { // Clean up any existing environment variables that might interfere