use crate::Result;
use crate::config::miserc;
use crate::env_diff::{EnvDiff, EnvDiffOperation, EnvDiffPatches, EnvMap};
use crate::file::replace_path;
use crate::shell::ShellType;
use crate::{cli::args::ToolArg, file::display_path};
use eyre::Context;
use indexmap::IndexSet;
use itertools::Itertools;
use log::LevelFilter;
pub use std::env::*;
use std::process;
use std::sync::LazyLock as Lazy;
use std::sync::RwLock;
use std::{
collections::{HashMap, HashSet},
ffi::OsStr,
sync::Mutex,
};
use std::{path::Path, string::ToString};
use std::{path::PathBuf, sync::atomic::AtomicBool};
pub static ARGS: RwLock<Vec<String>> = RwLock::new(vec![]);
pub static TOOL_ARGS: RwLock<Vec<ToolArg>> = RwLock::new(vec![]);
pub const MISE_INSTALL_VERSION_ENV_VAR: &str = "MISE_INSTALL_VERSION";
pub const MISE_TOOL_VERSION_ENV_VAR: &str = "MISE_TOOL_VERSION";
pub const NON_TOOL_VERSION_ENV_VARS: &[&str] =
&[MISE_INSTALL_VERSION_ENV_VAR, MISE_TOOL_VERSION_ENV_VAR];
#[cfg(unix)]
pub static SHELL: Lazy<String> = Lazy::new(|| var("SHELL").unwrap_or_else(|_| "sh".into()));
#[cfg(windows)]
pub static SHELL: Lazy<String> = Lazy::new(|| var("COMSPEC").unwrap_or_else(|_| "cmd.exe".into()));
pub static MISE_SHELL: Lazy<Option<ShellType>> =
Lazy::new(|| detect_shell(var("MISE_SHELL").ok(), var("SHELL").ok(), &SHELL));
fn detect_shell(
mise_shell: Option<String>,
shell_var: Option<String>,
fallback: &str,
) -> Option<ShellType> {
if let Some(s) = mise_shell {
return s.parse().ok();
}
if cfg!(windows)
&& let Some(st) = shell_var.and_then(|s| s.parse().ok())
{
return Some(st);
}
fallback.parse().ok()
}
#[cfg(unix)]
pub static SHELL_COMMAND_FLAG: &str = "-c";
#[cfg(windows)]
pub static SHELL_COMMAND_FLAG: &str = "/c";
#[cfg(test)]
pub static HOME: Lazy<PathBuf> =
Lazy::new(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test"));
#[cfg(not(test))]
pub static HOME: Lazy<PathBuf> = Lazy::new(|| {
homedir::my_home()
.ok()
.flatten()
.unwrap_or_else(|| PathBuf::from("/"))
});
pub static EDITOR: Lazy<String> =
Lazy::new(|| var("VISUAL").unwrap_or_else(|_| var("EDITOR").unwrap_or_else(|_| "nano".into())));
#[cfg(macos)]
pub static XDG_CACHE_HOME: Lazy<PathBuf> =
Lazy::new(|| var_path("XDG_CACHE_HOME").unwrap_or_else(|| HOME.join("Library/Caches")));
#[cfg(windows)]
pub static XDG_CACHE_HOME: Lazy<PathBuf> = Lazy::new(|| {
var_path("XDG_CACHE_HOME")
.or_else(|| var_path("TEMP"))
.unwrap_or_else(temp_dir)
});
#[cfg(all(not(windows), not(macos)))]
pub static XDG_CACHE_HOME: Lazy<PathBuf> =
Lazy::new(|| var_path("XDG_CACHE_HOME").unwrap_or_else(|| HOME.join(".cache")));
pub static XDG_CONFIG_HOME: Lazy<PathBuf> =
Lazy::new(|| var_path("XDG_CONFIG_HOME").unwrap_or_else(|| HOME.join(".config")));
#[cfg(unix)]
pub static XDG_DATA_HOME: Lazy<PathBuf> =
Lazy::new(|| var_path("XDG_DATA_HOME").unwrap_or_else(|| HOME.join(".local").join("share")));
#[cfg(windows)]
pub static XDG_DATA_HOME: Lazy<PathBuf> = Lazy::new(|| {
var_path("XDG_DATA_HOME")
.or(var_path("LOCALAPPDATA"))
.unwrap_or_else(|| HOME.join("AppData").join("Local"))
});
pub static XDG_STATE_HOME: Lazy<PathBuf> =
Lazy::new(|| var_path("XDG_STATE_HOME").unwrap_or_else(|| HOME.join(".local").join("state")));
#[cfg(windows)]
pub static LOCAL_APPDATA: Lazy<PathBuf> =
Lazy::new(|| var_path("LOCALAPPDATA").unwrap_or_else(|| HOME.join("AppData").join("Local")));
pub static MISE_FRIENDLY_ERROR: Lazy<bool> = Lazy::new(|| {
if var_is_true("MISE_FRIENDLY_ERROR") {
true
} else if var_is_false("MISE_FRIENDLY_ERROR") {
false
} else {
!cfg!(debug_assertions) && log::max_level() < log::LevelFilter::Debug
}
});
pub static MISE_TOOL_STUB: Lazy<bool> =
Lazy::new(|| ARGS.read().unwrap().get(1).map(|s| s.as_str()) == Some("tool-stub"));
pub static MISE_NO_CONFIG: Lazy<bool> = Lazy::new(|| var_is_true("MISE_NO_CONFIG"));
pub static MISE_NO_ENV: Lazy<bool> = Lazy::new(|| var_is_true("MISE_NO_ENV"));
pub static MISE_NO_HOOKS: Lazy<bool> = Lazy::new(|| var_is_true("MISE_NO_HOOKS"));
pub static MISE_PROGRESS_TRACE: Lazy<bool> = Lazy::new(|| var_is_true("MISE_PROGRESS_TRACE"));
pub static MISE_CACHE_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_CACHE_DIR").unwrap_or_else(|| XDG_CACHE_HOME.join("mise")));
pub static MISE_CONFIG_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_CONFIG_DIR").unwrap_or_else(|| XDG_CONFIG_HOME.join("mise")));
pub static MISE_DEFAULT_CONFIG_DIR: Lazy<PathBuf> = Lazy::new(|| XDG_CONFIG_HOME.join("mise"));
pub static MISE_CONFIG_DIR_OVERRIDDEN: Lazy<bool> = Lazy::new(|| {
var_path("MISE_CONFIG_DIR").is_some() && *MISE_CONFIG_DIR != *MISE_DEFAULT_CONFIG_DIR
});
pub static MISE_DATA_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_DATA_DIR").unwrap_or_else(|| XDG_DATA_HOME.join("mise")));
pub static MISE_STATE_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_STATE_DIR").unwrap_or_else(|| XDG_STATE_HOME.join("mise")));
pub static MISE_TMP_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_TMP_DIR").unwrap_or_else(|| temp_dir().join("mise")));
pub static MISE_SYSTEM_CONFIG_DIR: Lazy<PathBuf> = Lazy::new(|| {
var_path("MISE_SYSTEM_CONFIG_DIR")
.or_else(|| var_path("MISE_SYSTEM_DIR"))
.unwrap_or_else(|| PathBuf::from("/etc/mise"))
});
pub static MISE_INSTALLS_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_INSTALLS_DIR").unwrap_or_else(|| MISE_DATA_DIR.join("installs")));
pub static MISE_DOWNLOADS_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_DOWNLOADS_DIR").unwrap_or_else(|| MISE_DATA_DIR.join("downloads")));
pub static MISE_PLUGINS_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_PLUGINS_DIR").unwrap_or_else(|| MISE_DATA_DIR.join("plugins")));
pub static MISE_SHIMS_DIR: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_SHIMS_DIR").unwrap_or_else(|| MISE_DATA_DIR.join("shims")));
pub static MISE_SYSTEM_DATA_DIR: Lazy<PathBuf> = Lazy::new(|| {
var_path("MISE_SYSTEM_DATA_DIR").unwrap_or_else(|| PathBuf::from("/usr/local/share/mise"))
});
pub static MISE_SYSTEM_INSTALLS_DIR: Lazy<PathBuf> =
Lazy::new(|| MISE_SYSTEM_DATA_DIR.join("installs"));
static MISE_SHARED_INSTALL_DIRS_ENV: Lazy<Vec<PathBuf>> = Lazy::new(|| {
var_os("MISE_SHARED_INSTALL_DIRS")
.map(|v| {
std::env::split_paths(&v)
.filter(|p| !p.as_os_str().is_empty())
.map(replace_path)
.collect()
})
.unwrap_or_default()
});
pub fn shared_install_dirs() -> Vec<PathBuf> {
use crate::config::Settings;
let user_dirs = if let std::result::Result::Ok(settings) = Settings::try_get()
&& let Some(ref dirs) = settings.shared_install_dirs
&& !dirs.is_empty()
{
dirs.clone()
} else {
MISE_SHARED_INSTALL_DIRS_ENV.clone()
};
let system = &*MISE_SYSTEM_INSTALLS_DIR;
let mut result = Vec::new();
if system.is_dir() && *system != *MISE_INSTALLS_DIR {
result.push(system.clone());
}
result.extend(user_dirs);
result
}
pub fn shared_install_dirs_early() -> Vec<PathBuf> {
let system = &*MISE_SYSTEM_INSTALLS_DIR;
let mut result = Vec::new();
if system.is_dir() && *system != *MISE_INSTALLS_DIR {
result.push(system.clone());
}
result.extend(MISE_SHARED_INSTALL_DIRS_ENV.iter().cloned());
result
}
pub fn install_path_category(path: &Path) -> InstallPathCategory {
if path.starts_with(&*MISE_SYSTEM_INSTALLS_DIR) {
InstallPathCategory::System
} else if shared_install_dirs().iter().any(|d| path.starts_with(d)) {
InstallPathCategory::Shared
} else {
InstallPathCategory::Local
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InstallPathCategory {
Local,
System,
Shared,
}
pub fn find_in_shared_installs(
primary_path: PathBuf,
tool_dir_name: &str,
pathname: &str,
) -> PathBuf {
if !primary_path.exists() {
for shared_dir in shared_install_dirs() {
let shared_path = shared_dir.join(tool_dir_name).join(pathname);
if shared_path.exists() {
return shared_path;
}
}
}
primary_path
}
pub static MISE_DEFAULT_TOOL_VERSIONS_FILENAME: Lazy<String> = Lazy::new(|| {
var("MISE_DEFAULT_TOOL_VERSIONS_FILENAME")
.ok()
.or(MISE_OVERRIDE_TOOL_VERSIONS_FILENAMES
.as_ref()
.and_then(|v| v.first().cloned()))
.or(var("MISE_DEFAULT_TOOL_VERSIONS_FILENAME").ok())
.unwrap_or_else(|| ".tool-versions".into())
});
pub static MISE_DEFAULT_CONFIG_FILENAME: Lazy<String> = Lazy::new(|| {
var("MISE_DEFAULT_CONFIG_FILENAME")
.ok()
.or(MISE_OVERRIDE_CONFIG_FILENAMES.first().cloned())
.unwrap_or_else(|| "mise.toml".into())
});
pub static MISE_OVERRIDE_TOOL_VERSIONS_FILENAMES: Lazy<Option<IndexSet<String>>> =
Lazy::new(|| match var("MISE_OVERRIDE_TOOL_VERSIONS_FILENAMES") {
Ok(v) if v == "none" => Some([].into()),
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<IndexSet<String>> =
Lazy::new(|| match var("MISE_OVERRIDE_CONFIG_FILENAMES") {
Ok(v) => split_colon_list(&v),
Err(_) => miserc::get_override_config_filenames()
.map(|v| v.iter().cloned().collect())
.unwrap_or_default(),
});
pub static MISE_ENV: Lazy<Vec<String>> = Lazy::new(|| environment(&ARGS.read().unwrap()));
pub fn auto_env_setting() -> Option<bool> {
if var_is_true("MISE_AUTO_ENV") {
Some(true)
} else if var_is_false("MISE_AUTO_ENV") {
Some(false)
} else {
miserc::get_auto_env()
}
}
pub(crate) fn auto_env_default_for_version(v: &versions::Versioning) -> bool {
*v >= versions::Versioning::new("2027.6.0").unwrap()
}
pub fn platform_env_names() -> Vec<String> {
let mut names: Vec<String> = vec![];
for name in [
consts::FAMILY.to_string(),
crate::cli::version::OS.to_string(),
format!(
"{}-{}",
*crate::cli::version::OS,
*crate::cli::version::ARCH
),
] {
if !names.contains(&name) {
names.push(name);
}
}
names
}
pub static AUTO_ENV_NAMES: Lazy<Vec<String>> = Lazy::new(|| {
let enabled =
auto_env_setting().unwrap_or_else(|| auto_env_default_for_version(&crate::cli::version::V));
if !enabled {
return vec![];
}
platform_env_names()
.into_iter()
.filter(|name| !MISE_ENV.contains(name))
.collect()
});
pub static MISE_ENV_WITH_AUTO: Lazy<Vec<String>> = Lazy::new(|| {
AUTO_ENV_NAMES
.iter()
.chain(MISE_ENV.iter())
.cloned()
.collect()
});
pub static MISE_GLOBAL_CONFIG_FILE: Lazy<Option<PathBuf>> =
Lazy::new(|| var_path("MISE_GLOBAL_CONFIG_FILE").or_else(|| var_path("MISE_CONFIG_FILE")));
pub static MISE_GLOBAL_CONFIG_ROOT: Lazy<PathBuf> =
Lazy::new(|| var_path("MISE_GLOBAL_CONFIG_ROOT").unwrap_or_else(|| HOME.to_path_buf()));
pub static MISE_SYSTEM_CONFIG_FILE: Lazy<Option<PathBuf>> =
Lazy::new(|| var_path("MISE_SYSTEM_CONFIG_FILE"));
pub static MISE_IGNORED_CONFIG_PATHS: Lazy<Vec<PathBuf>> = Lazy::new(|| {
var_os("MISE_IGNORED_CONFIG_PATHS")
.map(|v| {
split_paths(&v)
.filter(|p| !p.as_os_str().is_empty())
.map(replace_path)
.collect()
})
.or_else(|| {
miserc::get_ignored_config_paths()
.map(|paths| paths.iter().cloned().map(replace_path).collect())
})
.unwrap_or_default()
});
pub static MISE_CEILING_PATHS: Lazy<HashSet<PathBuf>> = Lazy::new(|| {
var_os("MISE_CEILING_PATHS")
.map(|v| {
split_paths(&v)
.filter(|p| !p.as_os_str().is_empty())
.map(replace_path)
.collect()
})
.or_else(|| {
miserc::get_ceiling_paths()
.map(|paths| paths.iter().cloned().map(replace_path).collect())
})
.unwrap_or_default()
});
pub static MISE_USE_TOML: Lazy<bool> = Lazy::new(|| !var_is_false("MISE_USE_TOML"));
pub static MISE_LIST_ALL_VERSIONS: Lazy<bool> = Lazy::new(|| var_is_true("MISE_LIST_ALL_VERSIONS"));
pub static ARGV0: Lazy<String> = Lazy::new(|| ARGS.read().unwrap()[0].to_string());
pub static MISE_BIN_NAME: Lazy<&str> = Lazy::new(|| filename(&ARGV0));
pub static MISE_LOG_FILE: Lazy<Option<PathBuf>> = Lazy::new(|| var_path("MISE_LOG_FILE"));
pub static MISE_LOG_FILE_LEVEL: Lazy<Option<LevelFilter>> = Lazy::new(log_file_level);
fn find_in_tree(base: &Path, rels: &[&[&str]]) -> Option<PathBuf> {
for rel in rels {
let mut p = base.to_path_buf();
for part in *rel {
p = p.join(part);
}
if p.exists() {
return Some(p);
}
}
None
}
fn mise_install_base() -> Option<PathBuf> {
std::fs::canonicalize(&*MISE_BIN)
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
}
pub static MISE_SELF_UPDATE_INSTRUCTIONS: Lazy<Option<PathBuf>> = Lazy::new(|| {
if let Some(p) = var_path("MISE_SELF_UPDATE_INSTRUCTIONS") {
return Some(p);
}
let base = mise_install_base()?;
find_in_tree(
&base,
&[
&["lib", "mise-self-update-instructions.toml"],
&["lib", "mise", "mise-self-update-instructions.toml"],
&["lib64", "mise", "mise-self-update-instructions.toml"],
],
)
});
#[cfg(feature = "self_update")]
pub static MISE_SELF_UPDATE_AVAILABLE: Lazy<Option<bool>> = Lazy::new(|| {
if var_is_true("MISE_SELF_UPDATE_AVAILABLE") {
Some(true)
} else if var_is_false("MISE_SELF_UPDATE_AVAILABLE") {
Some(false)
} else {
None
}
});
#[cfg(feature = "self_update")]
pub static MISE_SELF_UPDATE_DISABLED_PATH: Lazy<Option<PathBuf>> = Lazy::new(|| {
let base = mise_install_base()?;
find_in_tree(
&base,
&[
&["lib", ".disable-self-update"],
&["lib", "mise", ".disable-self-update"],
&["lib64", "mise", ".disable-self-update"],
],
)
});
pub static MISE_LOG_HTTP: Lazy<bool> = Lazy::new(|| var_is_true("MISE_LOG_HTTP"));
pub static MISE_LOG_VERBOSE_DEPS: Lazy<bool> = Lazy::new(|| var_is_true("MISE_LOG_VERBOSE_DEPS"));
pub static __USAGE: Lazy<Option<String>> = Lazy::new(|| var("__USAGE").ok());
pub static __MISE_SHIM: Lazy<bool> = Lazy::new(|| var_is_true("__MISE_SHIM"));
pub const MISE_SHIM_PATH_ENV: &str = "__MISE_SHIM_PATH";
pub static MISE_SHIM_PATH: Lazy<RwLock<Option<PathBuf>>> =
Lazy::new(|| RwLock::new(var_path(MISE_SHIM_PATH_ENV)));
pub static IS_RUNNING_AS_SHIM: Lazy<bool> = Lazy::new(|| {
if cfg!(test) {
return false;
}
if *MISE_TOOL_STUB {
return true;
}
let bin_name = *MISE_BIN_NAME;
!is_mise_binary(bin_name)
});
pub fn is_mise_binary(bin_name: &str) -> bool {
let is_mise = |s: &str| {
if cfg!(windows) {
s.eq_ignore_ascii_case("mise")
} else {
s == "mise"
}
};
is_mise(bin_name)
|| bin_name
.split_once(['.', '-'])
.is_some_and(|(stem, _)| is_mise(stem))
}
#[cfg_attr(not(windows), allow(dead_code))]
pub const SELF_REPLACE_SUFFIXES: [&str; 2] = [".__selfdelete__.exe", ".__relocated__.exe"];
#[cfg_attr(not(windows), allow(dead_code))]
pub fn is_self_replace_helper(bin_name: &str, exe_stem: &str) -> bool {
let prefix = format!(".{exe_stem}.");
let matches = bin_name.get(..prefix.len()).is_some_and(|head| {
if cfg!(windows) {
head.eq_ignore_ascii_case(&prefix)
} else {
head == prefix
}
});
if !matches {
return false;
}
let rest = &bin_name[prefix.len()..];
SELF_REPLACE_SUFFIXES.iter().any(|suffix| {
rest.strip_suffix(suffix)
.is_some_and(is_self_replace_random_segment)
})
}
#[cfg_attr(not(windows), allow(dead_code))]
pub const SELF_REPLACE_RANDOM_LEN: usize = 32;
#[cfg_attr(not(windows), allow(dead_code))]
fn is_self_replace_random_segment(s: &str) -> bool {
s.len() == SELF_REPLACE_RANDOM_LEN && s.bytes().all(|b| b.is_ascii_lowercase())
}
#[cfg(windows)]
pub fn invoked_as_self_replace_helper() -> bool {
let Some(invoked) = std::env::args_os().next() else {
return false;
};
let Some(name) = Path::new(&invoked).file_name().and_then(|n| n.to_str()) else {
return false;
};
name.starts_with('.')
&& SELF_REPLACE_SUFFIXES.iter().any(|suffix| {
name.strip_suffix(suffix).is_some_and(|head| {
head.rsplit_once('.').is_some_and(|(stem, random)| {
!stem.is_empty() && is_self_replace_random_segment(random)
})
})
})
}
#[cfg(test)]
pub static TERM_WIDTH_OVERRIDE: Lazy<Option<usize>> = Lazy::new(|| None);
#[cfg(not(test))]
pub static TERM_WIDTH_OVERRIDE: Lazy<Option<usize>> = Lazy::new(|| {
for key in ["MISE_TERM_WIDTH", "COLUMNS"] {
if let Some(w) = var(key)
.ok()
.and_then(|v| v.trim().parse::<usize>().ok())
.filter(|w| *w > 0)
{
if key == "COLUMNS" {
debug!(
"overriding terminal width with COLUMNS={w}; set MISE_TERM_WIDTH to control this explicitly"
);
}
return Some(w);
}
}
None
});
#[cfg(test)]
pub static TERM_WIDTH: Lazy<usize> = Lazy::new(|| 80);
#[cfg(not(test))]
pub static TERM_WIDTH: Lazy<usize> = Lazy::new(|| {
if let Some(w) = *TERM_WIDTH_OVERRIDE {
return w;
}
terminal_size::terminal_size()
.map(|(w, _)| w.0 as usize)
.unwrap_or(80)
.max(80)
});
pub static MISE_BIN: Lazy<PathBuf> = Lazy::new(|| {
var_path("__MISE_BIN")
.or_else(|| current_exe().ok())
.unwrap_or_else(|| "mise".into())
});
pub static MISE_TIMINGS: Lazy<u8> = Lazy::new(|| var_u8("MISE_TIMINGS"));
pub static MISE_PID: Lazy<String> = Lazy::new(|| process::id().to_string());
pub static MISE_JOBS: Lazy<Option<usize>> =
Lazy::new(|| var("MISE_JOBS").ok().and_then(|v| v.parse::<usize>().ok()));
pub static __MISE_SCRIPT: Lazy<bool> = Lazy::new(|| var_is_true("__MISE_SCRIPT"));
pub static __MISE_DIFF: Lazy<EnvDiff> = Lazy::new(get_env_diff);
pub static __MISE_ORIG_PATH: Lazy<Option<String>> = Lazy::new(|| var("__MISE_ORIG_PATH").ok());
pub static __MISE_ZSH_PRECMD_RUN: Lazy<bool> = Lazy::new(|| !var_is_false("__MISE_ZSH_PRECMD_RUN"));
pub static LINUX_DISTRO: Lazy<Option<String>> = Lazy::new(linux_distro);
pub static PREFER_OFFLINE: Lazy<AtomicBool> =
Lazy::new(|| prefer_offline(&ARGS.read().unwrap()).into());
pub static REMOTE_FETCH_COMMAND: Lazy<AtomicBool> =
Lazy::new(|| remote_fetch_command(&ARGS.read().unwrap()).into());
pub static OFFLINE: Lazy<bool> = Lazy::new(|| offline(&ARGS.read().unwrap()));
pub static WARN_ON_MISSING_REQUIRED_ENV: Lazy<bool> =
Lazy::new(|| warn_on_missing_required_env(&ARGS.read().unwrap()));
pub static PRISTINE_ENV: Lazy<EnvMap> =
Lazy::new(|| get_pristine_env(&__MISE_DIFF, vars_safe().collect()));
pub static PATH_KEY: Lazy<String> =
Lazy::new(|| path_key_from_env(vars_os().filter_map(|(k, _)| k.into_string().ok())));
#[cfg(unix)]
fn path_key_from_env(_keys: impl IntoIterator<Item = String>) -> String {
"PATH".into()
}
#[cfg(windows)]
fn path_key_from_env(keys: impl IntoIterator<Item = String>) -> String {
keys.into_iter()
.find(|k| k.eq_ignore_ascii_case("PATH"))
.unwrap_or("PATH".into())
}
pub static PATH: Lazy<Vec<PathBuf>> = Lazy::new(|| match PRISTINE_ENV.get(&*PATH_KEY) {
Some(path) => split_paths(path).collect(),
None => vec![],
});
pub static PATH_NON_PRISTINE: Lazy<Vec<PathBuf>> = Lazy::new(|| match var(&*PATH_KEY) {
Ok(ref path) => split_paths(path).collect(),
Err(_) => vec![],
});
pub static DIRENV_DIFF: Lazy<Option<String>> = Lazy::new(|| var("DIRENV_DIFF").ok());
pub static GITHUB_TOKEN: Lazy<Option<String>> =
Lazy::new(|| get_token(&["MISE_GITHUB_TOKEN", "GITHUB_API_TOKEN", "GITHUB_TOKEN"]));
pub static MISE_GITHUB_ENTERPRISE_TOKEN: Lazy<Option<String>> =
Lazy::new(|| get_token(&["MISE_GITHUB_ENTERPRISE_TOKEN"]));
pub static GITLAB_TOKEN: Lazy<Option<String>> =
Lazy::new(|| get_token(&["MISE_GITLAB_TOKEN", "GITLAB_TOKEN"]));
pub static MISE_GITLAB_ENTERPRISE_TOKEN: Lazy<Option<String>> =
Lazy::new(|| get_token(&["MISE_GITLAB_ENTERPRISE_TOKEN"]));
pub static MISE_FORGEJO_ENTERPRISE_TOKEN: Lazy<Option<String>> =
Lazy::new(|| get_token(&["MISE_FORGEJO_ENTERPRISE_TOKEN"]));
pub static TEST_TRANCHE: Lazy<usize> = Lazy::new(|| var_u8("TEST_TRANCHE") as usize);
pub static TEST_TRANCHE_COUNT: Lazy<usize> = Lazy::new(|| var_u8("TEST_TRANCHE_COUNT") as usize);
pub static CLICOLOR_FORCE: Lazy<Option<bool>> =
Lazy::new(|| var("CLICOLOR_FORCE").ok().map(|v| v != "0"));
pub static CLICOLOR: Lazy<Option<bool>> = Lazy::new(|| {
if *CLICOLOR_FORCE == Some(true) {
Some(true)
} else if *NO_COLOR || var_is_false("MISE_COLOR") {
Some(false)
} else if let Ok(v) = var("CLICOLOR") {
Some(v != "0")
} else {
None
}
});
pub static NO_COLOR: Lazy<bool> = Lazy::new(|| var("NO_COLOR").is_ok_and(|v| !v.is_empty()));
pub static MISE_FORCE_PROGRESS: Lazy<bool> = Lazy::new(|| var_is_true("MISE_FORCE_PROGRESS"));
pub static PYENV_ROOT: Lazy<PathBuf> =
Lazy::new(|| var_path("PYENV_ROOT").unwrap_or_else(|| HOME.join(".pyenv")));
pub static UV_PYTHON_INSTALL_DIR: Lazy<PathBuf> = Lazy::new(|| {
var_path("UV_PYTHON_INSTALL_DIR").unwrap_or_else(|| XDG_DATA_HOME.join("uv").join("python"))
});
#[cfg(unix)]
pub const PATH_ENV_SEP: char = ':';
#[cfg(windows)]
pub const PATH_ENV_SEP: char = ';';
fn get_env_diff() -> EnvDiff {
let env = vars_safe().collect::<HashMap<_, _>>();
match env.get("__MISE_DIFF") {
Some(raw) => EnvDiff::deserialize(raw).unwrap_or_else(|err| {
warn!("Failed to deserialize __MISE_DIFF: {:#}", err);
EnvDiff::default()
}),
None => EnvDiff::default(),
}
}
fn var_u8(key: &str) -> u8 {
var(key)
.ok()
.and_then(|v| v.parse::<u8>().ok())
.unwrap_or_default()
}
pub(crate) fn var_is_true(key: &str) -> bool {
match var(key) {
Ok(v) => {
let v = v.to_lowercase();
v == "y" || v == "yes" || v == "true" || v == "1" || v == "on"
}
Err(_) => false,
}
}
fn var_is_false(key: &str) -> bool {
match var(key) {
Ok(v) => {
let v = v.to_lowercase();
v == "n" || v == "no" || v == "false" || v == "0" || v == "off"
}
Err(_) => false,
}
}
pub fn in_home_dir() -> bool {
current_dir().is_ok_and(|d| d == *HOME)
}
pub fn var_path(key: &str) -> Option<PathBuf> {
var_os(key)
.map(PathBuf::from)
.map(replace_path)
.filter(|p| !p.as_os_str().is_empty())
}
fn get_pristine_env(mise_diff: &EnvDiff, orig_env: EnvMap) -> EnvMap {
let patches = mise_diff.reverse().to_patches();
let mut env = apply_patches(&orig_env, &patches);
let path = match env.get(&*PATH_KEY) {
Some(path) => split_paths(path).collect(),
None => vec![],
};
let mut to_remove = mise_diff.path.iter().collect::<HashSet<_>>();
let path = path
.into_iter()
.filter(|p| !to_remove.remove(p))
.collect_vec();
env.insert(
PATH_KEY.to_string(),
join_paths(path).unwrap().to_string_lossy().to_string(),
);
env
}
fn apply_patches(env: &EnvMap, patches: &EnvDiffPatches) -> EnvMap {
let mut new_env = env.clone();
for patch in patches {
match patch {
EnvDiffOperation::Add(k, v) | EnvDiffOperation::Change(k, v) => {
new_env.insert(k.into(), v.into());
}
EnvDiffOperation::Remove(k) => {
new_env.remove(k);
}
}
}
new_env
}
fn offline(args: &[String]) -> bool {
if var_is_true("MISE_OFFLINE") {
return true;
}
args.iter()
.take_while(|a| *a != "--")
.any(|a| a == "--offline")
}
fn prefer_offline(args: &[String]) -> bool {
if var_is_true("MISE_PREFER_OFFLINE") {
return true;
}
let settings_args_end = first_non_global_arg_idx(args).unwrap_or(args.len());
if args[..settings_args_end]
.iter()
.any(|arg| arg == "--prefer-offline")
{
return true;
}
prefer_offline_command(args)
}
const PREFER_OFFLINE_COMMANDS: &[&str] = &[
"activate", "current", "direnv", "env", "exec", "hook-env", "ls", "where", "which", "x",
];
const REMOTE_FETCH_COMMANDS: &[&str] = &[
"lock",
"ls-remote",
"list-all",
"list-remote",
"outdated",
"upgrade",
"up",
];
fn first_non_global_arg_idx(args: &[String]) -> Option<usize> {
crate::cli::first_non_global_arg_idx_cached(args)
}
fn is_command(args: &[String], command_idx: Option<usize>, names: &[&str]) -> bool {
command_idx
.and_then(|idx| args.get(idx))
.map(|a| names.contains(&a.as_str()))
.unwrap_or_default()
}
fn prefer_offline_command(args: &[String]) -> bool {
is_command(
args,
first_non_global_arg_idx(args),
PREFER_OFFLINE_COMMANDS,
)
}
fn remote_fetch_command(args: &[String]) -> bool {
is_command(args, first_non_global_arg_idx(args), REMOTE_FETCH_COMMANDS)
}
fn warn_on_missing_required_env(args: &[String]) -> bool {
args.iter()
.take_while(|a| *a != "--")
.filter(|a| !a.starts_with('-'))
.nth(1)
.map(|a| {
[
"hook-env", ]
.contains(&a.as_str())
})
.unwrap_or_default()
}
fn environment(args: &[String]) -> Vec<String> {
let arg_defs = HashSet::from(["--profile", "-P", "--env", "-E"]);
let from_args = if *IS_RUNNING_AS_SHIM {
vec![]
} else {
let run_subcommands: HashSet<&str> = HashSet::from(["run", "r"]);
let mut values = Vec::new();
let mut it = args.iter().take_while(|a| a.as_str() != "--");
let mut in_run_subcommand = false;
while let Some(arg) = it.next() {
if arg.starts_with('-') {
if arg_defs.contains(arg.as_str()) {
if let Some(next) = it.next() {
values.push(next.to_string());
}
} else if let Some((prefix, rest)) = arg.split_at_checked(2)
&& !rest.starts_with('=')
&& arg_defs.contains(prefix)
{
values.push(rest.to_string());
} else if let Some((flag, value)) = arg.split_once('=') {
if arg_defs.contains(flag) {
values.push(value.to_string());
}
}
} else {
if in_run_subcommand {
break;
}
if run_subcommands.contains(arg.as_str()) {
in_run_subcommand = true;
}
}
}
values
.into_iter()
.flat_map(|s| {
s.split(',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect::<Vec<_>>()
})
.collect()
};
if !from_args.is_empty() {
return from_args;
}
var("MISE_ENV")
.ok()
.or_else(|| var("MISE_PROFILE").ok())
.or_else(|| var("MISE_ENVIRONMENT").ok())
.map(|s| {
s.split(',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
})
.or_else(|| miserc::get_env().cloned())
.unwrap_or_default()
}
fn log_file_level() -> Option<LevelFilter> {
let log_level = var("MISE_LOG_FILE_LEVEL").unwrap_or_default();
log_level.parse::<LevelFilter>().ok()
}
fn linux_distro() -> Option<String> {
crate::platform::linux_os_release().map(|release| release.id.clone())
}
fn split_colon_list(value: &str) -> IndexSet<String> {
value
.split(':')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect()
}
fn filename(path: &str) -> &str {
Path::new(path)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or(path)
}
fn get_token(keys: &[&str]) -> Option<String> {
keys.iter()
.find_map(|key| var(key).ok())
.filter(|v| !v.trim().is_empty())
}
pub fn is_activated() -> bool {
var("__MISE_DIFF").is_ok()
}
pub fn set_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(key: K, value: V) {
static MUTEX: Mutex<()> = Mutex::new(());
let _mutex = MUTEX.lock().unwrap();
unsafe {
std::env::set_var(key, value);
}
}
pub fn remove_var<K: AsRef<OsStr>>(key: K) {
static MUTEX: Mutex<()> = Mutex::new(());
let _mutex = MUTEX.lock().unwrap();
unsafe {
std::env::remove_var(key);
}
}
pub fn reset_env_cache_key() {
remove_var("__MISE_ENV_CACHE_KEY");
}
pub fn vars_safe() -> impl Iterator<Item = (String, String)> {
vars_os().filter_map(|(k, v)| {
let k_str = k.to_str()?;
let v_str = v.to_str()?;
Some((k_str.to_string(), v_str.to_string()))
})
}
pub fn args_safe() -> Vec<String> {
args_os().map(|a| a.to_string_lossy().to_string()).collect()
}
pub fn set_current_dir<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
trace!("cd {}", display_path(path));
std::env::set_current_dir(path)
.wrap_err_with(|| format!("failed to set current directory to {}", display_path(path)))?;
Ok(())
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use crate::config::Config;
use super::*;
#[tokio::test]
async fn test_apply_patches() {
let _config = Config::get().await.unwrap();
let mut env = EnvMap::new();
env.insert("foo".into(), "bar".into());
env.insert("baz".into(), "qux".into());
let patches = vec![
EnvDiffOperation::Add("foo".into(), "bar".into()),
EnvDiffOperation::Change("baz".into(), "qux".into()),
EnvDiffOperation::Remove("quux".into()),
];
let new_env = apply_patches(&env, &patches);
assert_eq!(new_env.len(), 2);
assert_eq!(new_env.get("foo").unwrap(), "bar");
assert_eq!(new_env.get("baz").unwrap(), "qux");
}
#[tokio::test]
async fn test_var_path() {
let _config = Config::get().await.unwrap();
set_var("MISE_TEST_PATH", "/foo/bar");
assert_eq!(
var_path("MISE_TEST_PATH").unwrap(),
PathBuf::from("/foo/bar")
);
remove_var("MISE_TEST_PATH");
}
#[tokio::test]
async fn test_var_path_treats_empty_as_unset() {
let _config = Config::get().await.unwrap();
set_var("MISE_TEST_EMPTY_PATH", "");
assert_eq!(var_path("MISE_TEST_EMPTY_PATH"), None);
remove_var("MISE_TEST_EMPTY_PATH");
}
#[cfg(unix)]
#[test]
fn test_vars_safe_skips_invalid_utf8() {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
let bad_value_key = "MISE_TEST_VARS_SAFE_BAD_VALUE";
let bad_key = OsString::from_vec(b"MISE_TEST_VARS_SAFE_BAD_KEY_\xff".to_vec());
let good_key = "MISE_TEST_VARS_SAFE_GOOD";
let mut guard = crate::test::EnvVarGuard::new();
guard
.set(bad_value_key, OsString::from_vec(vec![0xff]))
.set(&bad_key, "ok")
.set(good_key, "1");
assert!(vars_os().any(|(k, _)| k == bad_value_key));
assert!(vars_os().any(|(k, _)| k == bad_key));
let safe: Vec<(String, String)> = vars_safe().collect();
assert!(!safe.iter().any(|(k, _)| k == bad_value_key));
assert!(
!safe
.iter()
.any(|(k, _)| k.starts_with("MISE_TEST_VARS_SAFE_BAD_KEY_"))
);
assert!(safe.iter().any(|(k, v)| k == good_key && v == "1"));
}
#[test]
fn test_auto_env_default_for_version() {
let v = |s: &str| versions::Versioning::new(s).unwrap();
assert!(!auto_env_default_for_version(&v("2026.6.2")));
assert!(!auto_env_default_for_version(&v("2026.12.0")));
assert!(!auto_env_default_for_version(&v("2027.5.9")));
assert!(auto_env_default_for_version(&v("2027.6.0")));
assert!(auto_env_default_for_version(&v("2028.1.0")));
}
#[test]
fn test_remote_fetch_command_skips_global_option_values() {
let args = |args: &[&str]| {
args.iter()
.map(|arg| (*arg).to_string())
.collect::<Vec<_>>()
};
assert!(remote_fetch_command(&args(&[
"mise", "--cd", "/tmp", "lock"
])));
assert!(remote_fetch_command(&args(&[
"mise",
"--profile",
"development",
"ls-remote",
])));
assert!(remote_fetch_command(&args(&[
"mise",
"--cd=/tmp",
"--profile=development",
"outdated",
])));
}
#[test]
fn test_prefer_offline_command_skips_global_option_values() {
let args = |args: &[&str]| {
args.iter()
.map(|arg| (*arg).to_string())
.collect::<Vec<_>>()
};
assert!(prefer_offline_command(&args(&[
"mise", "--cd", "/tmp", "activate"
])));
assert!(prefer_offline_command(&args(&[
"mise",
"--profile",
"development",
"hook-env",
])));
assert!(prefer_offline_command(&args(&["mise", "-C/tmp", "env",])));
assert!(!prefer_offline_command(&args(&[
"mise", "--cd", "/tmp", "lock"
])));
assert!(prefer_offline(&args(&[
"mise",
"--cd",
"/tmp",
"--prefer-offline",
"lock",
])));
}
#[cfg(unix)]
#[test]
fn test_platform_env_names_unix() {
let names = platform_env_names();
assert_eq!(names.len(), 3);
assert_eq!(names[0], "unix");
assert_eq!(names[1], *crate::cli::version::OS);
assert_eq!(
names[2],
format!(
"{}-{}",
*crate::cli::version::OS,
*crate::cli::version::ARCH
)
);
}
#[cfg(windows)]
#[test]
fn test_platform_env_names_windows() {
let names = platform_env_names();
assert_eq!(
names,
vec![
"windows".to_string(),
format!("windows-{}", *crate::cli::version::ARCH)
]
);
}
#[cfg(unix)]
#[test]
fn test_path_key_from_env_uses_uppercase_path_on_unix() {
assert_eq!(
path_key_from_env(vec!["path".into(), "HOME".into()]),
"PATH"
);
assert_eq!(
path_key_from_env(vec!["Path".into(), "HOME".into()]),
"PATH"
);
}
#[cfg(windows)]
#[test]
fn test_path_key_from_env_preserves_windows_path_casing() {
assert_eq!(
path_key_from_env(vec!["Path".into(), "TEMP".into()]),
"Path"
);
assert_eq!(
path_key_from_env(vec!["TEMP".into(), "PATH".into()]),
"PATH"
);
assert_eq!(path_key_from_env(vec!["TEMP".into()]), "PATH");
}
#[test]
fn test_split_colon_list() {
let cases: Vec<(&str, Vec<&str>)> = vec![
("", vec![]), (":", vec![]), (":::", vec![]), ("mise.toml", vec!["mise.toml"]),
("a:b", vec!["a", "b"]),
(":a:b:", vec!["a", "b"]), ("a::b", vec!["a", "b"]), ];
for (input, expected) in cases {
let result = split_colon_list(input);
let expected: IndexSet<String> = expected.into_iter().map(|s| s.to_string()).collect();
assert_eq!(result, expected, "input: {input:?}");
}
}
#[test]
fn test_token_overwrite() {
remove_var("MISE_GITHUB_TOKEN");
remove_var("GITHUB_TOKEN");
remove_var("GITHUB_API_TOKEN");
set_var("MISE_GITHUB_TOKEN", "");
set_var("GITHUB_TOKEN", "invalid_token");
assert_eq!(
get_token(&["MISE_GITHUB_TOKEN", "GITHUB_TOKEN"]),
None,
"Empty token should overwrite other tokens"
);
assert_eq!(
get_token(&["GITHUB_API_TOKEN", "GITHUB_TOKEN"]),
Some("invalid_token".into()),
"Unset token should not overwrite other tokens"
);
remove_var("MISE_GITHUB_TOKEN");
remove_var("GITHUB_TOKEN");
remove_var("GITHUB_API_TOKEN");
}
#[test]
fn test_filename_takes_the_basename() {
assert_eq!(filename("C:/Users/alice/.cargo/bin/mise.EXE"), "mise.EXE");
assert_eq!(filename("/usr/local/bin/mise"), "mise");
assert_eq!(filename("/usr/local/bin/mise/"), "mise");
assert_eq!(filename("mise"), "mise");
assert_eq!(filename("mise.exe"), "mise.exe");
}
#[cfg(windows)]
#[test]
fn test_filename_splits_on_backslash_on_windows() {
assert_eq!(filename(r"C:\Users\alice\.cargo\bin\mise.EXE"), "mise.EXE");
assert_eq!(filename(r"C:\tools\mise\"), "mise");
}
#[cfg(unix)]
#[test]
fn test_filename_keeps_backslashes_on_unix() {
assert_eq!(
filename(r"C:\Users\alice\.cargo\bin\mise.EXE"),
r"C:\Users\alice\.cargo\bin\mise.EXE"
);
assert_eq!(filename(r"/opt/odd/weird\name"), r"weird\name");
}
#[test]
fn test_a_full_path_argv0_is_recognised_as_mise_itself() {
for argv0 in [
"C:/Users/alice/.cargo/bin/mise.EXE",
"/usr/local/bin/mise",
"mise",
] {
assert!(
is_mise_binary(filename(argv0)),
"argv[0] {argv0:?} should be recognised as mise, not a shim"
);
}
#[cfg(windows)]
assert!(
is_mise_binary(filename(r"C:\Users\alice\.cargo\bin\mise.exe")),
"a backslash argv[0] should be recognised as mise on Windows"
);
for argv0 in ["/home/alice/.local/share/mise/shims/node", "node.exe"] {
assert!(
!is_mise_binary(filename(argv0)),
"argv[0] {argv0:?} should still be a shim"
);
}
}
#[test]
fn test_is_mise_binary() {
assert!(is_mise_binary("mise"));
assert!(is_mise_binary("mise.exe"));
assert!(is_mise_binary("mise.cmd"));
assert!(is_mise_binary("mise-doctor"));
assert!(!is_mise_binary("node"));
assert!(!is_mise_binary("node.exe"));
assert!(!is_mise_binary("misex"));
assert!(!is_mise_binary("misex.exe"));
}
#[cfg(windows)]
#[test]
fn test_is_mise_binary_ignores_case_on_windows() {
assert!(is_mise_binary("MISE.EXE"));
assert!(is_mise_binary("Mise.exe"));
assert!(is_mise_binary("MISE"));
assert!(is_mise_binary("MISE-DOCTOR"));
assert!(!is_mise_binary("NODE.EXE"));
assert!(!is_mise_binary("MISEX.EXE"));
}
#[cfg(unix)]
#[test]
fn test_is_mise_binary_is_case_sensitive_on_unix() {
assert!(!is_mise_binary("MISE"));
assert!(!is_mise_binary("Mise"));
assert!(!is_mise_binary("MISE-DOCTOR"));
}
fn detect(mise_shell: Option<&str>, shell_var: Option<&str>, fallback: &str) -> String {
detect_shell(
mise_shell.map(str::to_string),
shell_var.map(str::to_string),
fallback,
)
.map(|st| st.to_string())
.unwrap_or_else(|| "(none)".to_string())
}
#[test]
fn mise_shell_wins_and_an_unparseable_one_is_still_the_answer() {
assert_eq!(detect(Some("zsh"), Some("/bin/bash"), "/bin/bash"), "zsh");
assert_eq!(
detect(Some("nonsense"), Some("/bin/bash"), "/bin/bash"),
"(none)"
);
}
#[cfg(windows)]
#[test]
fn shell_is_consulted_on_windows() {
for shell_var in [
r"C:\Program Files\Git\bin\bash.exe",
"/bin/bash.exe",
r"C:\msys64\usr\bin\zsh.exe",
] {
let expected = match shell_var.contains("zsh") {
true => "zsh",
false => "bash",
};
assert_eq!(
detect(None, Some(shell_var), r"C:\WINDOWS\system32\cmd.exe"),
expected,
"{shell_var}"
);
}
assert_eq!(detect(None, None, r"C:\WINDOWS\system32\cmd.exe"), "(none)");
}
#[cfg(unix)]
#[test]
fn the_fallback_is_the_only_second_source_on_unix() {
assert_eq!(detect(None, Some("/bin/zsh"), "/bin/bash"), "bash");
assert_eq!(detect(None, None, "/bin/zsh"), "zsh");
assert_eq!(detect(None, None, "sh"), "bash");
}
#[test]
fn a_self_replace_copy_of_this_binary_is_recognised() {
let rand = "qzcdgqhxhqwhzdwyqchsmdxcqouxxche";
assert!(is_self_replace_helper(
&format!(".mise.{rand}.__selfdelete__.exe"),
"mise"
));
assert!(is_self_replace_helper(
&format!(".mise.{rand}.__relocated__.exe"),
"mise"
));
assert!(is_self_replace_helper(
&format!(".mise-dev.{rand}.__selfdelete__.exe"),
"mise-dev"
));
}
#[test]
fn ordinary_names_and_other_applications_are_not() {
let rand = "qzcdgqhxhqwhzdwyqchsmdxcqouxxche";
assert!(!is_self_replace_helper("mise.exe", "mise"));
assert!(!is_self_replace_helper("node.exe", "mise"));
assert!(!is_self_replace_helper(
&format!(".node.{rand}.__selfdelete__.exe"),
"mise"
));
assert!(!is_self_replace_helper(".mise.something.exe", "mise"));
}
#[test]
fn a_name_that_only_resembles_a_generated_one_is_left_alone() {
let ok = "qzcdgqhxhqwhzdwyqchsmdxcqouxxche";
assert_eq!(ok.len(), SELF_REPLACE_RANDOM_LEN, "premise");
assert!(is_self_replace_helper(
&format!(".mise.{ok}.__selfdelete__.exe"),
"mise"
));
assert!(!is_self_replace_helper(".mise..__selfdelete__.exe", "mise"));
assert!(!is_self_replace_helper(".mise.__selfdelete__.exe", "mise"));
assert!(!is_self_replace_helper(
".mise.abc.__selfdelete__.exe",
"mise"
));
assert!(!is_self_replace_helper(
&format!(".mise.{ok}x.__selfdelete__.exe"),
"mise"
));
let digits = format!("{}1", &ok[..SELF_REPLACE_RANDOM_LEN - 1]);
assert!(!is_self_replace_helper(
&format!(".mise.{digits}.__selfdelete__.exe"),
"mise"
));
let upper = format!("{}A", &ok[..SELF_REPLACE_RANDOM_LEN - 1]);
assert!(!is_self_replace_helper(
&format!(".mise.{upper}.__selfdelete__.exe"),
"mise"
));
}
#[cfg(windows)]
#[test]
fn an_orphan_from_a_differently_cased_launch_is_still_ours() {
let rand = "qzcdgqhxhqwhzdwyqchsmdxcqouxxche";
for stem in ["mise", "MISE", "Mise"] {
for written in ["mise", "MISE", "Mise"] {
assert!(
is_self_replace_helper(&format!(".{written}.{rand}.__selfdelete__.exe"), stem),
"stem {stem} should claim an orphan written as {written}"
);
}
}
assert!(!is_self_replace_helper(
&format!(".NODE.{rand}.__selfdelete__.exe"),
"mise"
));
}
#[cfg(unix)]
#[test]
fn casing_still_separates_names_on_unix() {
let rand = "qzcdgqhxhqwhzdwyqchsmdxcqouxxche";
assert!(is_self_replace_helper(
&format!(".mise.{rand}.__selfdelete__.exe"),
"mise"
));
assert!(!is_self_replace_helper(
&format!(".MISE.{rand}.__selfdelete__.exe"),
"mise"
));
}
}