use std::time::Duration;
#[cfg(unix)]
use crate::{run, RunOptions};
#[cfg(unix)]
const PATH_BEGIN: &str = "__RIGHTKIT_PATH_BEGIN__";
#[cfg(unix)]
const PATH_END: &str = "__RIGHTKIT_PATH_END__";
#[cfg(unix)]
const ENV_BEGIN: &str = "__RIGHTKIT_ENV_BEGIN__";
#[cfg(unix)]
const ENV_END: &str = "__RIGHTKIT_ENV_END__";
#[cfg(unix)]
const OUTPUT_CAP: usize = 256 * 1024;
pub fn path(timeout: Duration) -> Option<String> {
#[cfg(unix)]
{
let script =
format!("printf '%s' '{PATH_BEGIN}'; printf '%s' \"$PATH\"; printf '%s' '{PATH_END}'");
let output = run(shell(), ["-lic", script.as_str()], &capped(timeout)).ok()?;
if output.timed_out || output.stdout.truncated {
return None;
}
let text = String::from_utf8_lossy(&output.stdout.bytes);
let (_, rest) = text.split_once(PATH_BEGIN)?;
let (value, _) = rest.split_once(PATH_END)?;
(!value.is_empty()).then(|| value.to_string())
}
#[cfg(not(unix))]
{
let _ = timeout;
None
}
}
pub fn env_values(names: &[String], timeout: Duration) -> Vec<(String, String)> {
#[cfg(unix)]
{
let wanted: Vec<&str> = names
.iter()
.map(String::as_str)
.filter(|name| is_identifier(name))
.collect();
if wanted.is_empty() {
return Vec::new();
}
let mut script = format!("printf '%s' '{ENV_BEGIN}'");
for name in &wanted {
script.push_str(&format!("; printf '%s\\0' \"${{{name}-}}\""));
}
script.push_str(&format!("; printf '%s' '{ENV_END}'"));
let Ok(output) = run(shell(), ["-lic", script.as_str()], &capped(timeout)) else {
return Vec::new();
};
if output.timed_out || output.stdout.truncated {
return Vec::new();
}
let text = String::from_utf8_lossy(&output.stdout.bytes);
let Some((_, rest)) = text.split_once(ENV_BEGIN) else {
return Vec::new();
};
let Some((body, _)) = rest.split_once(ENV_END) else {
return Vec::new();
};
body.split('\0')
.zip(wanted)
.filter(|(value, _)| !value.is_empty())
.map(|(value, name)| (name.to_string(), value.to_string()))
.collect()
}
#[cfg(not(unix))]
{
let _ = (names, timeout);
Vec::new()
}
}
#[cfg(unix)]
fn capped(timeout: Duration) -> RunOptions {
RunOptions::new(timeout)
.stdout_cap(OUTPUT_CAP)
.stderr_cap(0)
.no_window(true)
}
#[cfg(unix)]
fn shell() -> std::path::PathBuf {
use std::path::PathBuf;
let from_env = std::env::var_os("SHELL")
.map(PathBuf::from)
.filter(|shell| {
shell.is_absolute()
&& shell.is_file()
&& shell
.to_str()
.is_some_and(|text| !text.chars().any(char::is_control))
});
from_env.unwrap_or_else(|| {
["/bin/zsh", "/bin/sh"]
.iter()
.map(PathBuf::from)
.find(|candidate| candidate.is_file())
.unwrap_or_else(|| PathBuf::from("/bin/sh"))
})
}
#[cfg(unix)]
fn is_identifier(name: &str) -> bool {
let mut chars = name.chars();
chars
.next()
.is_some_and(|first| first.is_ascii_alphabetic() || first == '_')
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
#[test]
fn only_plain_identifiers_reach_the_script() {
assert!(is_identifier("PATH"));
assert!(is_identifier("_X1"));
assert!(!is_identifier("1X"));
assert!(!is_identifier("A;rm"));
assert!(!is_identifier("A B"));
assert!(!is_identifier(""));
}
#[test]
fn a_nonexistent_shell_falls_back_to_a_real_one() {
let shell = shell();
assert!(shell.is_absolute());
assert!(shell.is_file());
}
#[test]
fn env_values_skips_invalid_names_and_returns_nothing_for_none_valid() {
assert!(env_values(&["not valid".to_string()], Duration::from_secs(1)).is_empty());
}
}