use std::ffi::{OsStr, OsString};
use std::io::IsTerminal;
use std::process::ExitStatus;
pub struct RootCommand {
pub program: OsString,
pub args: Vec<OsString>,
pub env: Vec<(OsString, OsString)>,
}
#[derive(Debug)]
pub enum EscalationError {
NoGraphicalEscalator,
Spawn(std::io::Error),
}
impl std::fmt::Display for EscalationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EscalationError::NoGraphicalEscalator => f.write_str(
"cannot escalate: no controlling terminal, no GUI session, and no \
non-interactive escalation (passwordless sudo / polkit) available — \
run in a terminal, as root, or configure passwordless sudo",
),
EscalationError::Spawn(e) => write!(f, "failed to launch privilege escalation: {e}"),
}
}
}
impl std::error::Error for EscalationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
EscalationError::Spawn(e) => Some(e),
EscalationError::NoGraphicalEscalator => None,
}
}
}
#[must_use]
pub fn shell_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
#[must_use]
pub fn applescript_quote(s: &str) -> String {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
pub fn root_command<S: AsRef<OsStr>>(
argv: &[S],
interactive: bool,
) -> Result<RootCommand, EscalationError> {
let argv: Vec<OsString> = argv.iter().map(|s| s.as_ref().to_os_string()).collect();
#[cfg(unix)]
{
if interactive {
let mut args = vec![OsString::from("-E")];
args.extend(argv);
return Ok(RootCommand {
program: OsString::from("sudo"),
args,
env: Vec::new(),
});
}
#[cfg(target_os = "macos")]
{
if macos_gui_session_available() {
return Ok(macos_sudo(&argv).unwrap_or_else(|| macos_osascript(&argv)));
}
if sudo_noninteractive_available() {
return Ok(sudo_noninteractive(&argv));
}
Err(EscalationError::NoGraphicalEscalator)
}
#[cfg(not(target_os = "macos"))]
{
linux_graphical(&argv)
}
}
#[cfg(windows)]
{
let _ = interactive; Ok(windows_runas(&argv))
}
#[cfg(not(any(unix, windows)))]
{
let _ = (interactive, argv);
Err(EscalationError::NoGraphicalEscalator)
}
}
pub fn run_as_root<S: AsRef<OsStr>>(argv: &[S]) -> Result<ExitStatus, EscalationError> {
let cmd = root_command(argv, std::io::stdin().is_terminal())?;
std::process::Command::new(&cmd.program)
.args(&cmd.args)
.envs(cmd.env.iter().map(|(k, v)| (k, v)))
.status()
.map_err(EscalationError::Spawn)
}
#[cfg(target_os = "macos")]
fn macos_sudo(argv: &[OsString]) -> Option<RootCommand> {
if !on_path("sudo") {
return None;
}
let askpass = macos_askpass().ok()?;
let mut args = vec![OsString::from("-A"), OsString::from("-E")];
args.extend(argv.iter().cloned());
Some(RootCommand {
program: OsString::from("sudo"),
args,
env: vec![(OsString::from("SUDO_ASKPASS"), askpass)],
})
}
#[cfg(target_os = "macos")]
pub fn macos_askpass() -> std::io::Result<OsString> {
if let Some(existing) = std::env::var_os("SUDO_ASKPASS") {
if !existing.is_empty() && std::path::Path::new(&existing).is_file() {
return Ok(existing);
}
}
use std::os::unix::fs::PermissionsExt as _;
let dir = crate::ZLayerDirs::default_data_dir();
std::fs::create_dir_all(&dir)?;
let path = dir.join("askpass.sh");
const SCRIPT: &str = "#!/bin/sh\nexec osascript \
-e 'display dialog \"ZLayer needs administrator access…\" default answer \"\" with hidden answer with title \"ZLayer\"' \
-e 'text returned of result'\n";
std::fs::write(&path, SCRIPT)?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700))?;
Ok(path.into_os_string())
}
#[cfg(target_os = "macos")]
#[must_use]
pub fn macos_gui_session_available() -> bool {
let uid = unsafe { libc::getuid() };
std::process::Command::new("launchctl")
.args(["print", &format!("gui/{uid}")])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[cfg(target_os = "macos")]
fn forwarded_env() -> Vec<(String, String)> {
let mut env: Vec<(String, String)> = std::env::vars()
.filter(|(k, _)| k.starts_with("ZLAYER_"))
.collect();
if let Ok(path) = std::env::var("PATH") {
env.push((String::from("PATH"), path));
}
env
}
#[cfg(target_os = "macos")]
fn macos_osascript(argv: &[OsString]) -> RootCommand {
use std::fmt::Write as _;
let mut script = String::new();
for (key, value) in forwarded_env() {
let _ = write!(script, "export {key}={}; ", shell_quote(&value));
}
script.push_str("exec");
for arg in argv {
script.push(' ');
script.push_str(&shell_quote(&arg.to_string_lossy()));
}
let apple = format!(
"do shell script {} with administrator privileges",
applescript_quote(&script)
);
RootCommand {
program: OsString::from("/usr/bin/osascript"),
args: vec![OsString::from("-e"), OsString::from(apple)],
env: Vec::new(),
}
}
#[cfg(all(unix, not(target_os = "macos")))]
fn linux_graphical(argv: &[OsString]) -> Result<RootCommand, EscalationError> {
let have_askpass_env = std::env::var_os("SUDO_ASKPASS").is_some();
let graphical = has_graphical_display();
if graphical && on_path("pkexec") {
return Ok(RootCommand {
program: OsString::from("pkexec"),
args: argv.to_vec(),
env: Vec::new(),
});
}
if have_askpass_env || (graphical && known_askpass_present()) {
let mut args = vec![OsString::from("-A"), OsString::from("-E")];
args.extend(argv.iter().cloned());
return Ok(RootCommand {
program: OsString::from("sudo"),
args,
env: Vec::new(),
});
}
if sudo_noninteractive_available() {
return Ok(sudo_noninteractive(argv));
}
Err(EscalationError::NoGraphicalEscalator)
}
#[cfg(all(unix, not(target_os = "macos")))]
fn has_graphical_display() -> bool {
let nonempty = |k: &str| std::env::var_os(k).is_some_and(|v| !v.is_empty());
nonempty("DISPLAY") || nonempty("WAYLAND_DISPLAY")
}
#[cfg(unix)]
fn sudo_noninteractive_available() -> bool {
on_path("sudo")
&& std::process::Command::new("sudo")
.args(["-n", "true"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[cfg(unix)]
fn sudo_noninteractive(argv: &[OsString]) -> RootCommand {
let mut args = vec![OsString::from("-n"), OsString::from("-E")];
args.extend(argv.iter().cloned());
RootCommand {
program: OsString::from("sudo"),
args,
env: Vec::new(),
}
}
#[cfg(unix)]
fn on_path(name: &str) -> bool {
let Some(paths) = std::env::var_os("PATH") else {
return false;
};
std::env::split_paths(&paths).any(|dir| dir.join(name).is_file())
}
#[cfg(all(unix, not(target_os = "macos")))]
fn known_askpass_present() -> bool {
const CANDIDATES: &[&str] = &[
"/usr/bin/ssh-askpass",
"/usr/libexec/openssh/ssh-askpass",
"/usr/lib/ssh/ssh-askpass",
];
CANDIDATES.iter().any(|p| std::path::Path::new(p).exists())
}
#[cfg(windows)]
fn windows_runas(argv: &[OsString]) -> RootCommand {
let file = argv
.first()
.map(|p| p.to_string_lossy().replace('\'', "''"))
.unwrap_or_default();
let arg_list = argv[1..]
.iter()
.map(|a| format!("'{}'", a.to_string_lossy().replace('\'', "''")))
.collect::<Vec<_>>()
.join(",");
let ps = if arg_list.is_empty() {
format!(
"$p = Start-Process -FilePath '{file}' -Verb RunAs -WindowStyle Hidden -Wait -PassThru; exit $p.ExitCode"
)
} else {
format!(
"$p = Start-Process -FilePath '{file}' -ArgumentList {arg_list} -Verb RunAs -WindowStyle Hidden -Wait -PassThru; exit $p.ExitCode"
)
};
RootCommand {
program: OsString::from("powershell"),
args: vec![
OsString::from("-NoProfile"),
OsString::from("-NonInteractive"),
OsString::from("-Command"),
OsString::from(ps),
],
env: Vec::new(),
}
}