use std::env;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) fn set_process_var<K, V>(key: K, value: V)
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
unsafe { env::set_var(key, value) }
}
#[cfg(test)]
pub(crate) fn remove_process_var<K>(key: K)
where
K: AsRef<OsStr>,
{
unsafe { env::remove_var(key) }
}
pub(crate) fn home_dir() -> PathBuf {
if cfg!(windows) {
env::var_os("USERPROFILE")
.filter(|value| !value.is_empty())
.or_else(|| env::var_os("HOME").filter(|value| !value.is_empty()))
} else {
env::var_os("HOME")
.filter(|value| !value.is_empty())
.or_else(|| env::var_os("USERPROFILE").filter(|value| !value.is_empty()))
}
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
}
pub(crate) fn resolve_command(command: &str) -> PathBuf {
let command_path = PathBuf::from(command);
if command_path.components().count() > 1 {
return command_path;
}
if let Some(path) = env::var_os("PATH") {
for directory in env::split_paths(&path) {
#[cfg(windows)]
if command_path.extension().is_none() {
let path_ext =
env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_string());
for extension in path_ext
.split(';')
.map(str::trim)
.filter(|extension| !extension.is_empty())
{
let candidate =
directory.join(format!("{command}{}", extension.to_ascii_lowercase()));
if candidate.is_file() {
return candidate;
}
}
}
let candidate = directory.join(command);
if candidate.is_file() {
return candidate;
}
}
}
if cfg!(target_os = "macos") && command == "brew" {
for candidate in ["/opt/homebrew/bin/brew", "/usr/local/bin/brew"] {
let candidate = PathBuf::from(candidate);
if candidate.is_file() {
return candidate;
}
}
}
command_path
}
pub(crate) fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0)
}
pub(crate) fn exe_name(bin: &str) -> String {
if cfg!(windows) && !bin.ends_with(".exe") {
format!("{bin}.exe")
} else {
bin.to_string()
}
}
pub(crate) fn valid_storage_id(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 160
&& value != "."
&& value != ".."
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'))
}
pub(crate) fn valid_sha256(value: &str) -> bool {
value.len() == 64
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
}
pub(crate) fn valid_config_id(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 96
&& value.bytes().enumerate().all(|(index, byte)| {
byte.is_ascii_lowercase()
|| byte.is_ascii_digit()
|| (byte == b'-'
&& index > 0
&& index + 1 < value.len()
&& value.as_bytes()[index - 1] != b'-')
})
}
pub(crate) fn source_name(source: &str) -> String {
source
.trim_end_matches(".git")
.replace('\\', "/")
.rsplit('/')
.next()
.filter(|name| !name.is_empty())
.unwrap_or("manual")
.to_string()
}
pub(crate) fn looks_like_git(source: &str) -> bool {
source.ends_with(".git")
|| source.starts_with("http://")
|| source.starts_with("https://")
|| source.starts_with("ssh://")
|| source.starts_with("git@")
}
pub(crate) fn shell_quote(path: &Path) -> String {
shell_quote_str(&path.display().to_string())
}
pub(crate) fn shell_quote_str(value: &str) -> String {
if cfg!(windows) {
format!("\"{}\"", value.replace('"', "\\\""))
} else {
format!("'{}'", value.replace('\'', "'\\''"))
}
}
pub(crate) fn first_line(value: String) -> String {
value.lines().next().unwrap_or("").trim().to_string()
}
pub(crate) fn fnv1a(value: &str) -> u64 {
let mut hash = 0xcbf29ce484222325u64;
for byte in value.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
pub(crate) fn unquote(value: &str) -> &str {
value
.strip_prefix('"')
.and_then(|value| value.strip_suffix('"'))
.unwrap_or(value)
}
#[cfg(test)]
mod tests {
use crate::execution::TEST_ENV_LOCK;
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
use crate::util::{home_dir, remove_process_var, set_process_var};
fn restore(name: &str, value: Option<OsString>) {
match value {
Some(value) => set_process_var(name, value),
None => remove_process_var(name),
}
}
#[test]
fn home_directory_uses_native_platform_variable_first() {
let _guard = TEST_ENV_LOCK.lock().unwrap();
let home = env::var_os("HOME");
let userprofile = env::var_os("USERPROFILE");
set_process_var("HOME", "/native-home");
set_process_var("USERPROFILE", "/windows-home");
assert_eq!(
home_dir(),
PathBuf::from(if cfg!(windows) {
"/windows-home"
} else {
"/native-home"
})
);
restore("HOME", home);
restore("USERPROFILE", userprofile);
}
}