Files
MercuryToolbox/crates/msudo/src/lib.rs
T

2314 lines
78 KiB
Rust

//! The `msudo` command launches Windows processes with elevated identities.
use std::ffi::{OsStr, OsString};
use std::fmt::Write as _;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use common::{
CliError, CommonArgs, ExitCode, RenderMode, parse_color_choice, parse_format_choice,
print_json, print_quick_help_error, print_structured, print_text,
};
use lexopt::prelude::{Long, Short, Value as ArgValue};
use serde::{Deserialize, Serialize};
use windowsupport::{
LaunchIdentity, LaunchRequest, LaunchResult, PrivilegeMode, ProcessPriority, ShowWindowMode,
TokenIntegrity, TokenStatus,
};
const HELP: &str = "\
Launch Windows commands as admin, SYSTEM, or TrustedInstaller with Mercury-friendly output.
Windows only. This tool is intentionally high risk. `--user system` and `--user trustedinstaller`
require `--dangerous`.
Usage:
msudo [OPTIONS] [--] <COMMAND...>
msudo run [OPTIONS] [--] <COMMAND...>
msudo status [OPTIONS]
msudo [OPTIONS] --shell <PRESET>
Options:
--format <FORMAT> Structured output format: text, json, toon
--json Shortcut for --format json
--toon Shortcut for --format toon
--color <WHEN> Control ANSI color output: auto, never
--quiet Suppress non-essential status output
--user <IDENTITY> Target identity: current-process, current-user, admin, system, trustedinstaller
--dangerous Required for system or trustedinstaller launches
--shell <PRESET> Shell preset: cmd, powershell, pwsh, wsl, git-bash, mingw, msys2, cygwin, yori, tcc, nu
--integrity <LEVEL> Integrity: untrusted, low, medium, medium-plus, high, system
--privileges <MODE> Privilege policy: default, enable-all, disable-all
--priority <CLASS> Priority: idle, below-normal, normal, above-normal, high, realtime
--show-window <MODE> Window mode: default, hidden, normal, minimized, maximized
--session <ID> Target session id for duplicated tokens
--current-directory <PATH> Working directory for the launched process
--same-console Reuse the current console in the foreground and wait for the child
--new-window Force a new window for the launched process
--wait Wait for the launched process and return its exit code
-h, --help Show this help text
-V, --version Show the command version
Examples:
msudo status --json | ConvertFrom-Json
msudo --user admin -- cmd /d /c whoami
msudo --user system --dangerous -- cmd /d /c whoami
msudo --same-console --user system --dangerous --shell powershell
msudo --shell pwsh
";
const STATUS_HELP: &str = "\
Show the current token state, runas availability, and shell preset resolution.
Usage:
msudo status [OPTIONS]
Options:
--format <FORMAT> Structured output format: text, json, toon
--json Shortcut for --format json
--toon Shortcut for --format toon
--quiet Suppress shell inventory in text output
--color <WHEN> Control ANSI color output: auto, never
-h, --help Show this help text
-V, --version Show the command version
Examples:
msudo status --json | ConvertFrom-Json
";
const RUN_HELP: &str = "\
Run a command or shell with the requested elevated identity.
Usage:
msudo run [OPTIONS] [--] <COMMAND...>
msudo [OPTIONS] [--] <COMMAND...>
msudo [OPTIONS] --shell <PRESET>
Options:
--format <FORMAT> Structured output format: text, json, toon
--json Emit JSON launch results instead of compact text output
--toon Shortcut for --format toon
--quiet Suppress non-essential text output
--color <WHEN> Control ANSI color output: auto, never
--user <IDENTITY> Target identity: current-process, current-user, admin, system, trustedinstaller
--dangerous Required for system or trustedinstaller launches
--shell <PRESET> Shell preset: cmd, powershell, pwsh, wsl, git-bash, mingw, msys2, cygwin, yori, tcc, nu
--integrity <LEVEL> Integrity: untrusted, low, medium, medium-plus, high, system
--privileges <MODE> Privilege policy: default, enable-all, disable-all
--priority <CLASS> Priority: idle, below-normal, normal, above-normal, high, realtime
--show-window <MODE> Window mode: default, hidden, normal, minimized, maximized
--session <ID> Target session id for duplicated tokens
--current-directory <PATH> Working directory for the launched process
--same-console Reuse the current console in the foreground and wait for the child
--new-window Force a new window for the launched process
--wait Wait for the launched process and return its exit code
-h, --help Show this help text
-V, --version Show the command version
Examples:
msudo --user admin -- cmd /d /c whoami
msudo --user system --dangerous -- cmd /d /c whoami
msudo --same-console --user system --dangerous --shell powershell
msudo --shell pwsh
Interactive shells open in a new window by default; use --same-console to keep them in the
current terminal and wait in the foreground.
";
const RELAY_SUBCOMMAND: &str = "__relay";
static EXIT_CODE_PATH_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParseOutcome {
Help,
Version,
Run,
}
#[derive(Debug, Clone)]
struct Cli {
common: CommonArgs,
command: CommandMode,
}
#[derive(Debug, Clone)]
enum CommandMode {
Help(&'static str),
Status,
Run(RunCli),
Relay(RelayCli),
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
struct RunCli {
user: LaunchIdentity,
dangerous: bool,
shell: Option<ShellPreset>,
integrity: Option<TokenIntegrity>,
privileges: PrivilegeMode,
priority: ProcessPriority,
show_window: ShowWindowMode,
session: Option<u32>,
current_directory: Option<String>,
same_console: bool,
new_window: bool,
wait: bool,
command: Vec<String>,
}
#[derive(Debug, Clone)]
struct RelayCli {
run: RunCli,
exit_code_path: PathBuf,
stdout_path: Option<PathBuf>,
stderr_path: Option<PathBuf>,
console_pid: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct RelayExitStatus {
ok: bool,
exit_code: Option<i32>,
error: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
enum ShellPreset {
#[serde(rename = "cmd")]
Cmd,
#[serde(rename = "powershell")]
PowerShell,
#[serde(rename = "pwsh")]
Pwsh,
#[serde(rename = "wsl")]
Wsl,
#[serde(rename = "git-bash")]
GitBash,
#[serde(rename = "mingw")]
Mingw,
#[serde(rename = "msys2")]
Msys2,
#[serde(rename = "cygwin")]
Cygwin,
#[serde(rename = "yori")]
Yori,
#[serde(rename = "tcc")]
Tcc,
#[serde(rename = "nu")]
Nu,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
enum ShellResolutionSource {
KnownLocation,
Path,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct ShellStatus {
preset: ShellPreset,
available: bool,
executable: Option<String>,
resolution_source: Option<ShellResolutionSource>,
error: Option<String>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct StatusReport {
ok: bool,
host: String,
supports_runas: bool,
is_elevated: bool,
is_admin_member: bool,
integrity: TokenIntegrity,
current_user: Option<String>,
session_id: Option<u32>,
active_session_id: Option<u32>,
can_admin: bool,
can_current_user: bool,
can_system: bool,
can_trustedinstaller: bool,
trustedinstaller_installed: bool,
trustedinstaller_running: bool,
shells: Vec<ShellStatus>,
}
#[derive(Debug)]
struct RelayLaunch {
arguments: Vec<String>,
exit_path: PathBuf,
stdout_path: Option<PathBuf>,
stderr_path: Option<PathBuf>,
}
/// Parses CLI arguments and returns a process exit code.
#[must_use]
pub fn main_entry() -> i32 {
match parse_cli_from(std::env::args_os()) {
Ok((ParseOutcome::Help, cli)) => {
if let CommandMode::Help(help) = cli.command {
print!("{help}");
} else {
print!("{HELP}");
}
ExitCode::Success.as_i32()
}
Ok((ParseOutcome::Version, _)) => {
println!("msudo {}", env!("CARGO_PKG_VERSION"));
ExitCode::Success.as_i32()
}
Ok((ParseOutcome::Run, cli)) => match run(&cli) {
Ok(code) => code,
Err(error) => {
print_quick_help_error(&error, HELP);
error.exit_code().as_i32()
}
},
Err(error) => {
print_quick_help_error(&error, HELP);
error.exit_code().as_i32()
}
}
}
fn run(cli: &Cli) -> Result<i32, CliError> {
let provider = WindowsSudoProvider;
run_with_provider(cli, &provider)
}
fn run_with_provider(cli: &Cli, provider: &dyn SudoProvider) -> Result<i32, CliError> {
match &cli.command {
CommandMode::Help(help) => {
print!("{help}");
Ok(ExitCode::Success.as_i32())
}
CommandMode::Status => run_status(cli.common, provider),
CommandMode::Run(run_cli) => run_launch(cli.common.render_mode(), run_cli, provider),
CommandMode::Relay(relay) => run_relay(relay, provider),
}
}
fn run_status(common: CommonArgs, provider: &dyn SudoProvider) -> Result<i32, CliError> {
let status = provider.status()?;
let report = build_status_report(status);
match common.render_mode() {
RenderMode::Json => print_json(&report)?,
RenderMode::Toon => print_structured(&report, RenderMode::Toon)?,
RenderMode::Text => print_text(render_status_text(&report, common.quiet))?,
}
Ok(ExitCode::Success.as_i32())
}
fn run_launch(
render_mode: RenderMode,
run_cli: &RunCli,
provider: &dyn SudoProvider,
) -> Result<i32, CliError> {
validate_run_cli(run_cli)?;
let status = provider.status()?;
let prepared = prepare_launch(run_cli)?;
if !status.is_elevated && launch_requires_elevated_relay(run_cli) {
let relay = build_relay_arguments(run_cli);
let result = provider.elevate_current_process(
&relay.arguments,
effective_wait(run_cli),
run_cli.show_window,
)?;
if effective_wait(run_cli) {
let exit_code = read_relay_exit_code(&relay.exit_path)?;
replay_relay_output(relay.stdout_path.as_deref(), relay.stderr_path.as_deref())?;
return Ok(exit_code);
}
return Ok(exit_code_from_launch(result));
}
let result = provider.launch_request(&prepared)?;
if matches!(render_mode, RenderMode::Json) {
print_json(&result)?;
}
Ok(exit_code_from_launch(result))
}
const fn launch_requires_elevated_relay(run_cli: &RunCli) -> bool {
match run_cli.user {
LaunchIdentity::CurrentProcess => false,
LaunchIdentity::CurrentUser => {
run_cli.session.is_some()
|| run_cli.integrity.is_some()
|| !matches!(run_cli.privileges, PrivilegeMode::Default)
}
LaunchIdentity::Admin | LaunchIdentity::System | LaunchIdentity::TrustedInstaller => true,
}
}
fn run_relay(relay: &RelayCli, provider: &dyn SudoProvider) -> Result<i32, CliError> {
validate_run_cli(&relay.run)?;
if relay.run.same_console {
let console_pid = relay.console_pid.ok_or_else(|| {
CliError::usage("internal same-console relay requires --console-pid <PID>")
})?;
provider.attach_parent_console(console_pid)?;
}
let mut prepared = prepare_launch(&relay.run)?;
prepared.stdout_path = relay
.stdout_path
.as_ref()
.map(|path| path.to_string_lossy().into_owned());
prepared.stderr_path = relay
.stderr_path
.as_ref()
.map(|path| path.to_string_lossy().into_owned());
match provider.launch_request(&prepared) {
Ok(result) => {
let exit_code = exit_code_from_launch(result);
write_relay_exit_status(
&relay.exit_code_path,
&RelayExitStatus {
ok: true,
exit_code: Some(exit_code),
error: None,
},
)?;
Ok(exit_code)
}
Err(error) => {
write_relay_exit_status(
&relay.exit_code_path,
&RelayExitStatus {
ok: false,
exit_code: None,
error: Some(error.to_string()),
},
)?;
Err(error)
}
}
}
fn exit_code_from_launch(result: LaunchResult) -> i32 {
result
.exit_code
.unwrap_or_else(|| ExitCode::Success.as_i32())
}
fn build_status_report(status: TokenStatus) -> StatusReport {
StatusReport {
ok: true,
host: std::env::var("COMPUTERNAME").unwrap_or_else(|_| "localhost".to_string()),
supports_runas: true,
is_elevated: status.is_elevated,
is_admin_member: status.is_admin_member,
integrity: status.integrity,
current_user: status.current_user,
session_id: status.session_id,
active_session_id: status.active_session_id,
can_admin: status.can_admin,
can_current_user: status.can_current_user,
can_system: status.can_system,
can_trustedinstaller: status.can_trustedinstaller,
trustedinstaller_installed: status.trustedinstaller_installed,
trustedinstaller_running: status.trustedinstaller_running,
shells: ShellPreset::all()
.into_iter()
.map(resolve_shell_status)
.collect(),
}
}
fn render_status_text(report: &StatusReport, quiet: bool) -> String {
let mut rendered = String::new();
let _ = writeln!(
rendered,
"host={} elevated={} admin_member={} integrity={:?}",
report.host, report.is_elevated, report.is_admin_member, report.integrity
);
let _ = writeln!(
rendered,
"runas={} current_user={} system={} trustedinstaller={} session={} active_session={}",
report.supports_runas,
report.can_current_user,
report.can_system,
report.can_trustedinstaller,
format_optional_u32(report.session_id),
format_optional_u32(report.active_session_id)
);
if !quiet {
for shell in &report.shells {
let _ = writeln!(
rendered,
"shell={} available={} executable={} source={}",
shell.preset.name(),
shell.available,
shell.executable.as_deref().unwrap_or("-"),
shell
.resolution_source
.map_or("-", shell_resolution_source_name),
);
}
}
rendered
}
fn validate_run_cli(run_cli: &RunCli) -> Result<(), CliError> {
if matches!(
run_cli.user,
LaunchIdentity::System | LaunchIdentity::TrustedInstaller
) && !run_cli.dangerous
{
return Err(CliError::usage(
"--user system and --user trustedinstaller require --dangerous",
));
}
if run_cli.command.is_empty() && run_cli.shell.is_none() {
return Err(CliError::usage(
"provide a command after -- or select --shell <preset>",
));
}
if run_cli.same_console && run_cli.new_window {
return Err(CliError::usage(
"--same-console cannot be combined with --new-window",
));
}
if matches!(run_cli.user, LaunchIdentity::CurrentProcess)
&& (run_cli.integrity.is_some()
|| !matches!(run_cli.privileges, PrivilegeMode::Default)
|| run_cli.session.is_some())
{
return Err(CliError::usage(
"--user current-process cannot be combined with token shaping options",
));
}
Ok(())
}
fn prepare_launch(run_cli: &RunCli) -> Result<LaunchRequest, CliError> {
let interactive_shell = is_interactive_shell(run_cli);
let current_directory = effective_current_directory(run_cli);
let prepared_command = if run_cli.command.is_empty() {
prepare_interactive_shell(run_cli.shell.expect("shell checked in validate"))?
} else if let Some(preset) = run_cli.shell {
prepare_shell_wrapped_command(preset, &run_cli.command)?
} else {
prepare_direct_command(
&run_cli.command[0],
&run_cli.command[1..],
run_cli.user,
std::env::var_os("PATH").as_deref(),
std::env::var_os("PATHEXT").as_deref(),
)?
};
let new_window = effective_new_window(run_cli, interactive_shell);
Ok(LaunchRequest {
program: prepared_command.program,
args: prepared_command.args,
current_directory,
stdout_path: None,
stderr_path: None,
identity: run_cli.user,
privileges: run_cli.privileges,
integrity: run_cli.integrity,
priority: run_cli.priority,
show_window: run_cli.show_window,
session: run_cli.session,
same_console: run_cli.same_console,
new_window,
wait: effective_wait(run_cli),
})
}
fn is_interactive_shell(run_cli: &RunCli) -> bool {
run_cli.command.is_empty() && run_cli.shell.is_some()
}
const fn effective_wait(run_cli: &RunCli) -> bool {
run_cli.wait || run_cli.same_console
}
fn effective_current_directory(run_cli: &RunCli) -> Option<String> {
run_cli.current_directory.clone().or_else(|| {
std::env::current_dir()
.ok()
.map(|path| path.to_string_lossy().into_owned())
})
}
const fn effective_new_window(run_cli: &RunCli, interactive_shell: bool) -> bool {
if run_cli.same_console {
false
} else {
run_cli.new_window || interactive_shell
}
}
fn build_relay_arguments(run_cli: &RunCli) -> RelayLaunch {
let exit_path = unique_exit_code_path();
let (stdout_path, stderr_path) = if effective_wait(run_cli) && !is_interactive_shell(run_cli) {
(Some(unique_exit_code_path()), Some(unique_exit_code_path()))
} else {
(None, None)
};
let mut arguments = vec![
RELAY_SUBCOMMAND.to_string(),
"--user".to_string(),
launch_identity_name(run_cli.user).to_string(),
"--exit-code-path".to_string(),
exit_path.display().to_string(),
];
if run_cli.dangerous {
arguments.push("--dangerous".to_string());
}
if let Some(shell) = run_cli.shell {
arguments.push("--shell".to_string());
arguments.push(shell.name().to_string());
}
if let Some(integrity) = run_cli.integrity {
arguments.push("--integrity".to_string());
arguments.push(token_integrity_name(integrity).to_string());
}
if !matches!(run_cli.privileges, PrivilegeMode::Default) {
arguments.push("--privileges".to_string());
arguments.push(privilege_mode_name(run_cli.privileges).to_string());
}
if !matches!(run_cli.priority, ProcessPriority::Normal) {
arguments.push("--priority".to_string());
arguments.push(priority_name(run_cli.priority).to_string());
}
if !matches!(run_cli.show_window, ShowWindowMode::Default) {
arguments.push("--show-window".to_string());
arguments.push(show_window_name(run_cli.show_window).to_string());
}
if let Some(session) = run_cli.session {
arguments.push("--session".to_string());
arguments.push(session.to_string());
}
if let Some(current_directory) = effective_current_directory(run_cli) {
arguments.push("--current-directory".to_string());
arguments.push(current_directory);
}
if run_cli.same_console {
arguments.push("--same-console".to_string());
arguments.push("--console-pid".to_string());
arguments.push(std::process::id().to_string());
}
if let Some(path) = &stdout_path {
arguments.push("--stdout-path".to_string());
arguments.push(path.display().to_string());
}
if let Some(path) = &stderr_path {
arguments.push("--stderr-path".to_string());
arguments.push(path.display().to_string());
}
if run_cli.new_window {
arguments.push("--new-window".to_string());
}
if effective_wait(run_cli) {
arguments.push("--wait".to_string());
}
if !run_cli.command.is_empty() {
arguments.push("--".to_string());
arguments.extend(run_cli.command.iter().cloned());
}
RelayLaunch {
arguments,
exit_path,
stdout_path,
stderr_path,
}
}
fn unique_exit_code_path() -> PathBuf {
let temp_dir = std::env::temp_dir();
for _ in 0..32 {
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |value| value.as_nanos());
let counter = EXIT_CODE_PATH_COUNTER.fetch_add(1, Ordering::Relaxed);
let candidate = temp_dir.join(format!(
"msudo-exit-{}-{unique}-{counter}.json",
std::process::id()
));
if !candidate.exists() {
return candidate;
}
}
let counter = EXIT_CODE_PATH_COUNTER.fetch_add(1, Ordering::Relaxed);
temp_dir.join(format!(
"msudo-exit-fallback-{}-{counter}.json",
std::process::id()
))
}
fn write_relay_exit_status(path: &Path, status: &RelayExitStatus) -> Result<(), CliError> {
let payload = serde_json::to_string(status).map_err(|error| {
CliError::runtime(format!("failed to encode relay exit status: {error}"))
})?;
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
.map_err(|error| {
CliError::runtime(format!(
"failed to create relay exit status at {}: {error}",
path.display()
))
})?;
file.write_all(payload.as_bytes()).map_err(|error| {
CliError::runtime(format!(
"failed to write relay exit status to {}: {error}",
path.display()
))
})
}
fn read_relay_exit_code(path: &Path) -> Result<i32, CliError> {
let deadline = Instant::now() + Duration::from_secs(2);
let content = loop {
match fs::read_to_string(path) {
Ok(content) => break content,
Err(error)
if Instant::now() < deadline
&& matches!(
error.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied
) =>
{
std::thread::sleep(Duration::from_millis(25));
}
Err(error) => {
return Err(CliError::runtime(format!(
"failed to read relay exit code from {}: {error}",
path.display()
)));
}
}
};
let _ = fs::remove_file(path);
if let Ok(status) = serde_json::from_str::<RelayExitStatus>(content.trim()) {
if status.ok {
return status.exit_code.ok_or_else(|| {
CliError::runtime("relay exit status was missing an exit code".to_string())
});
}
return Err(CliError::runtime(status.error.unwrap_or_else(|| {
"privileged relay failed without an error message".to_string()
})));
}
content.trim().parse::<i32>().map_err(|error| {
CliError::runtime(format!(
"failed to parse relay exit status from {}: {error}",
path.display()
))
})
}
fn replay_relay_output(
stdout_path: Option<&Path>,
stderr_path: Option<&Path>,
) -> Result<(), CliError> {
if let Some(path) = stdout_path {
replay_one_relay_output(path, false)?;
}
if let Some(path) = stderr_path {
replay_one_relay_output(path, true)?;
}
Ok(())
}
fn replay_one_relay_output(path: &Path, stderr: bool) -> Result<(), CliError> {
let bytes = fs::read(path).map_err(|error| {
CliError::runtime(format!(
"failed to read relay output from {}: {error}",
path.display()
))
})?;
let _ = fs::remove_file(path);
if stderr {
std::io::stderr().write_all(&bytes).map_err(|error| {
CliError::runtime(format!("failed to replay relay stderr: {error}"))
})?;
std::io::stderr()
.flush()
.map_err(|error| CliError::runtime(format!("failed to flush relay stderr: {error}")))?;
} else {
std::io::stdout().write_all(&bytes).map_err(|error| {
CliError::runtime(format!("failed to replay relay stdout: {error}"))
})?;
std::io::stdout()
.flush()
.map_err(|error| CliError::runtime(format!("failed to flush relay stdout: {error}")))?;
}
Ok(())
}
fn prepare_interactive_shell(shell: ShellPreset) -> Result<PreparedCommand, CliError> {
let resolution = resolve_shell(shell)?;
Ok(PreparedCommand {
program: resolution.executable,
args: resolution.default_args,
})
}
fn prepare_shell_wrapped_command(
shell: ShellPreset,
command: &[String],
) -> Result<PreparedCommand, CliError> {
let resolution = resolve_shell(shell)?;
let shell_command = quote_for_shell(shell, command)?;
let args = match shell {
ShellPreset::Cmd | ShellPreset::Tcc | ShellPreset::Yori => {
let mut args = resolution.default_args;
args.push("/d".to_string());
args.push("/s".to_string());
args.push("/c".to_string());
args.push(shell_command);
args
}
ShellPreset::PowerShell | ShellPreset::Pwsh => {
let mut args = resolution.default_args;
args.push("-Command".to_string());
args.push(shell_command);
args
}
ShellPreset::Wsl => {
let mut args = resolution.default_args;
args.push("--".to_string());
args.extend(command.iter().cloned());
args
}
ShellPreset::GitBash | ShellPreset::Mingw | ShellPreset::Msys2 | ShellPreset::Cygwin => {
let mut args = resolution.default_args;
args.push("-lc".to_string());
args.push(shell_command);
args
}
ShellPreset::Nu => {
let mut args = resolution.default_args;
args.push("-c".to_string());
args.push(shell_command);
args
}
};
Ok(PreparedCommand {
program: resolution.executable,
args,
})
}
fn prepare_direct_command(
program: &str,
args: &[String],
identity: LaunchIdentity,
path: Option<&OsStr>,
pathext: Option<&OsStr>,
) -> Result<PreparedCommand, CliError> {
let resolution = resolve_direct_command_from_path(program, path, pathext);
validate_direct_command_policy(program, identity, resolution.source)?;
Ok(PreparedCommand {
program: resolution.executable,
args: args.to_vec(),
})
}
fn quote_for_shell(shell: ShellPreset, values: &[String]) -> Result<String, CliError> {
match shell {
ShellPreset::PowerShell | ShellPreset::Pwsh => Ok(quote_powershell_invocation(values)),
ShellPreset::Cmd | ShellPreset::Tcc | ShellPreset::Yori => Ok(values
.iter()
.map(|value| quote_windows_style(value))
.collect::<Result<Vec<_>, _>>()?
.join(" ")),
_ => Ok(values
.iter()
.map(|value| quote_posix_style(value))
.collect::<Vec<_>>()
.join(" ")),
}
}
fn quote_powershell_invocation(values: &[String]) -> String {
let Some((program, args)) = values.split_first() else {
return "& ''".to_string();
};
let program = quote_powershell_string(program);
if args.is_empty() {
return format!("& {program}");
}
let args = args
.iter()
.map(|value| quote_powershell_string(value))
.collect::<Vec<_>>()
.join(", ");
format!("& {program} @({args})")
}
fn quote_powershell_string(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
fn quote_windows_style(value: &str) -> Result<String, CliError> {
if let Some(character) = value
.chars()
.find(|character| matches!(character, '%' | '`' | '\r' | '\n'))
{
return Err(CliError::usage(format!(
"unsafe shell metacharacter {character:?} is not allowed with cmd-style shells"
)));
}
if value.is_empty() {
return Ok("\"\"".to_string());
}
if !value.contains([' ', '\t', '"', '&', '|', '^', '>', '<']) {
return Ok(value.to_string());
}
Ok(format!("\"{}\"", value.replace('"', "\\\"")))
}
fn quote_posix_style(value: &str) -> String {
if value.is_empty() {
return "''".to_string();
}
if !value.contains([
' ', '\t', '\'', '"', '$', '|', '&', ';', '>', '<', '`', '\r', '\n', '%',
]) {
return value.to_string();
}
format!("'{}'", value.replace('\'', "'\"'\"'"))
}
fn resolve_shell_status(preset: ShellPreset) -> ShellStatus {
match preset.try_resolve() {
Ok(resolution) => ShellStatus {
preset,
available: true,
executable: Some(resolution.executable),
resolution_source: Some(resolution.source),
error: None,
},
Err(error) => ShellStatus {
preset,
available: false,
executable: None,
resolution_source: None,
error: Some(error),
},
}
}
#[derive(Debug, Clone)]
struct PreparedCommand {
program: String,
args: Vec<String>,
}
#[derive(Debug, Clone)]
struct ResolvedShell {
executable: String,
default_args: Vec<String>,
source: ShellResolutionSource,
}
trait SudoProvider {
fn status(&self) -> Result<TokenStatus, CliError>;
fn elevate_current_process(
&self,
arguments: &[String],
wait: bool,
show_window: ShowWindowMode,
) -> Result<LaunchResult, CliError>;
fn attach_parent_console(&self, parent_pid: u32) -> Result<(), CliError>;
fn launch_request(&self, request: &LaunchRequest) -> Result<LaunchResult, CliError>;
}
struct WindowsSudoProvider;
impl SudoProvider for WindowsSudoProvider {
fn status(&self) -> Result<TokenStatus, CliError> {
windowsupport::current_token_status().map_err(|error| CliError::runtime(error.to_string()))
}
fn elevate_current_process(
&self,
arguments: &[String],
wait: bool,
show_window: ShowWindowMode,
) -> Result<LaunchResult, CliError> {
windowsupport::elevate_current_process(arguments, wait, show_window)
.map_err(|error| CliError::runtime(error.to_string()))
}
fn attach_parent_console(&self, parent_pid: u32) -> Result<(), CliError> {
windowsupport::attach_parent_console(parent_pid)
.map_err(|error| CliError::runtime(error.to_string()))
}
fn launch_request(&self, request: &LaunchRequest) -> Result<LaunchResult, CliError> {
windowsupport::launch_request(request).map_err(|error| CliError::runtime(error.to_string()))
}
}
impl ShellPreset {
const fn all() -> [Self; 11] {
[
Self::Cmd,
Self::PowerShell,
Self::Pwsh,
Self::Wsl,
Self::GitBash,
Self::Mingw,
Self::Msys2,
Self::Cygwin,
Self::Yori,
Self::Tcc,
Self::Nu,
]
}
const fn name(self) -> &'static str {
match self {
Self::Cmd => "cmd",
Self::PowerShell => "powershell",
Self::Pwsh => "pwsh",
Self::Wsl => "wsl",
Self::GitBash => "git-bash",
Self::Mingw => "mingw",
Self::Msys2 => "msys2",
Self::Cygwin => "cygwin",
Self::Yori => "yori",
Self::Tcc => "tcc",
Self::Nu => "nu",
}
}
fn try_resolve(self) -> Result<ResolvedShell, String> {
resolve_shell_from_path(
self,
std::env::var_os("PATH").as_deref(),
std::env::var_os("PATHEXT").as_deref(),
)
}
}
fn default_shell_args(shell: ShellPreset) -> Vec<String> {
match shell {
ShellPreset::PowerShell | ShellPreset::Pwsh => vec!["-NoProfile".to_string()],
_ => Vec::new(),
}
}
const fn shell_candidates(shell: ShellPreset) -> &'static [&'static str] {
match shell {
ShellPreset::Cmd => &["cmd.exe", "cmd"],
ShellPreset::PowerShell => &["powershell.exe", "powershell"],
ShellPreset::Pwsh => &["pwsh.exe", "pwsh"],
ShellPreset::Wsl => &["wsl.exe", "wsl"],
ShellPreset::GitBash => &["bash.exe", "bash", "git-bash.exe"],
ShellPreset::Mingw | ShellPreset::Msys2 | ShellPreset::Cygwin => &[],
ShellPreset::Yori => &["yori.exe", "yori"],
ShellPreset::Tcc => &["tcc.exe", "tcc"],
ShellPreset::Nu => &["nu.exe", "nu"],
}
}
const fn shell_known_locations(shell: ShellPreset) -> &'static [&'static str] {
match shell {
ShellPreset::Cmd => &["C:\\Windows\\System32\\cmd.exe"],
ShellPreset::PowerShell => {
&["C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"]
}
ShellPreset::Pwsh => &[
"C:\\Program Files\\PowerShell\\7\\pwsh.exe",
"C:\\Program Files (x86)\\PowerShell\\7\\pwsh.exe",
],
ShellPreset::Wsl => &["C:\\Windows\\System32\\wsl.exe"],
ShellPreset::GitBash => &[
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
"C:\\Program Files\\Git\\bin\\bash.exe",
"C:\\Program Files\\Git\\git-bash.exe",
],
ShellPreset::Mingw | ShellPreset::Msys2 => &["C:\\msys64\\usr\\bin\\bash.exe"],
ShellPreset::Cygwin => &["C:\\cygwin64\\bin\\bash.exe"],
ShellPreset::Yori => &["C:\\Program Files\\Yori\\yori.exe"],
ShellPreset::Tcc => &["C:\\Program Files\\JPSoft\\TCC.exe"],
ShellPreset::Nu => &[
"C:\\Program Files\\Nushell\\bin\\nu.exe",
"C:\\Program Files\\nu\\bin\\nu.exe",
],
}
}
fn shell_resolution_candidates(shell: ShellPreset) -> Vec<&'static str> {
let known = shell_known_locations(shell);
let path = shell_candidates(shell);
let mut values = Vec::with_capacity(known.len() + path.len());
values.extend_from_slice(known);
values.extend_from_slice(path);
values
}
fn resolve_shell(shell: ShellPreset) -> Result<ResolvedShell, CliError> {
shell.try_resolve().map_err(CliError::runtime)
}
fn resolve_shell_from_path(
shell: ShellPreset,
path: Option<&OsStr>,
pathext: Option<&OsStr>,
) -> Result<ResolvedShell, String> {
for candidate in shell_resolution_candidates(shell) {
if let Some(resolution) = find_executable(candidate, path, pathext) {
return Ok(ResolvedShell {
executable: resolution.executable,
default_args: default_shell_args(shell),
source: resolution.source,
});
}
}
Err(format!("no executable found for {}", shell.name()))
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ResolvedExecutable {
executable: String,
source: ShellResolutionSource,
}
fn find_executable(
candidate: &str,
path: Option<&OsStr>,
pathext: Option<&OsStr>,
) -> Option<ResolvedExecutable> {
let candidate_path = Path::new(candidate);
if candidate_path.components().count() > 1 && candidate_path.exists() {
return Some(ResolvedExecutable {
executable: candidate_path.display().to_string(),
source: ShellResolutionSource::KnownLocation,
});
}
find_executable_in_path(candidate, path?, pathext).map(|executable| ResolvedExecutable {
executable,
source: ShellResolutionSource::Path,
})
}
fn find_executable_in_path(
candidate: &str,
path: &OsStr,
pathext: Option<&OsStr>,
) -> Option<String> {
let pathext = pathext.map_or_else(
|| vec![".exe".to_string(), ".cmd".to_string(), ".bat".to_string()],
|value| {
value
.to_string_lossy()
.split(';')
.map(str::to_ascii_lowercase)
.collect::<Vec<_>>()
},
);
for directory in std::env::split_paths(path) {
let direct = directory.join(candidate);
if direct.exists() {
return Some(direct.display().to_string());
}
if direct.extension().is_none() {
for extension in &pathext {
let path = directory.join(format!("{candidate}{extension}"));
if path.exists() {
return Some(path.display().to_string());
}
}
}
}
None
}
fn resolve_direct_command_from_path(
program: &str,
path: Option<&OsStr>,
pathext: Option<&OsStr>,
) -> ResolvedExecutable {
let program_path = Path::new(program);
if program_path.components().count() > 1 {
return ResolvedExecutable {
executable: program.to_string(),
source: ShellResolutionSource::KnownLocation,
};
}
for candidate in direct_command_known_locations(program) {
let candidate_path = Path::new(candidate);
if candidate_path.exists() {
return ResolvedExecutable {
executable: candidate_path.display().to_string(),
source: ShellResolutionSource::KnownLocation,
};
}
}
find_executable_in_path(program, path.unwrap_or_else(|| OsStr::new("")), pathext).map_or_else(
|| ResolvedExecutable {
executable: program.to_string(),
source: ShellResolutionSource::Path,
},
|executable| ResolvedExecutable {
executable,
source: ShellResolutionSource::Path,
},
)
}
fn direct_command_known_locations(program: &str) -> &'static [&'static str] {
match program.to_ascii_lowercase().as_str() {
"cmd" | "cmd.exe" => shell_known_locations(ShellPreset::Cmd),
"powershell" | "powershell.exe" => shell_known_locations(ShellPreset::PowerShell),
"pwsh" | "pwsh.exe" => shell_known_locations(ShellPreset::Pwsh),
"wsl" | "wsl.exe" => shell_known_locations(ShellPreset::Wsl),
_ => &[],
}
}
fn validate_direct_command_policy(
program: &str,
identity: LaunchIdentity,
source: ShellResolutionSource,
) -> Result<(), CliError> {
if matches!(
identity,
LaunchIdentity::Admin | LaunchIdentity::System | LaunchIdentity::TrustedInstaller
) && matches!(source, ShellResolutionSource::Path)
{
return Err(CliError::usage(format!(
"{identity:?} launch refuses PATH-resolved bare command '{program}'; use an explicit path or --shell <preset>"
)));
}
Ok(())
}
const fn shell_resolution_source_name(source: ShellResolutionSource) -> &'static str {
match source {
ShellResolutionSource::KnownLocation => "known-location",
ShellResolutionSource::Path => "path",
}
}
fn format_optional_u32(value: Option<u32>) -> String {
value.map_or_else(|| "-".to_string(), |item| item.to_string())
}
const fn launch_identity_name(identity: LaunchIdentity) -> &'static str {
match identity {
LaunchIdentity::CurrentProcess => "current-process",
LaunchIdentity::CurrentUser => "current-user",
LaunchIdentity::Admin => "admin",
LaunchIdentity::System => "system",
LaunchIdentity::TrustedInstaller => "trustedinstaller",
}
}
const fn token_integrity_name(level: TokenIntegrity) -> &'static str {
match level {
TokenIntegrity::Untrusted => "untrusted",
TokenIntegrity::Low => "low",
TokenIntegrity::Medium => "medium",
TokenIntegrity::MediumPlus => "medium-plus",
TokenIntegrity::High => "high",
TokenIntegrity::System => "system",
TokenIntegrity::Unknown => "unknown",
}
}
const fn privilege_mode_name(mode: PrivilegeMode) -> &'static str {
match mode {
PrivilegeMode::Default => "default",
PrivilegeMode::EnableAll => "enable-all",
PrivilegeMode::DisableAll => "disable-all",
}
}
const fn priority_name(priority: ProcessPriority) -> &'static str {
match priority {
ProcessPriority::Idle => "idle",
ProcessPriority::BelowNormal => "below-normal",
ProcessPriority::Normal => "normal",
ProcessPriority::AboveNormal => "above-normal",
ProcessPriority::High => "high",
ProcessPriority::Realtime => "realtime",
}
}
const fn show_window_name(mode: ShowWindowMode) -> &'static str {
match mode {
ShowWindowMode::Default => "default",
ShowWindowMode::Hidden => "hidden",
ShowWindowMode::Normal => "normal",
ShowWindowMode::Minimized => "minimized",
ShowWindowMode::Maximized => "maximized",
}
}
#[allow(clippy::too_many_lines)]
fn parse_cli_from<I, T>(args: I) -> Result<(ParseOutcome, Cli), CliError>
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
let mut parser = lexopt::Parser::from_iter(args);
let mut common = CommonArgs::default();
let mut run = RunCli {
user: LaunchIdentity::Admin,
dangerous: false,
shell: None,
integrity: None,
privileges: PrivilegeMode::Default,
priority: ProcessPriority::Normal,
show_window: ShowWindowMode::Default,
session: None,
current_directory: None,
same_console: false,
new_window: false,
wait: false,
command: Vec::new(),
};
let mut mode = None::<String>;
let mut relay_exit_code_path = None::<PathBuf>;
let mut relay_stdout_path = None::<PathBuf>;
let mut relay_stderr_path = None::<PathBuf>;
let mut relay_console_pid = None::<u32>;
while let Some(argument) = parser
.next()
.map_err(|error| CliError::usage(error.to_string()))?
{
match argument {
Long("help") | Short('h') => {
let help = match mode.as_deref() {
Some("status") => STATUS_HELP,
Some("run" | RELAY_SUBCOMMAND) => RUN_HELP,
_ => HELP,
};
return Ok((
ParseOutcome::Help,
Cli {
common,
command: CommandMode::Help(help),
},
));
}
Long("version") | Short('V') => {
return Ok((
ParseOutcome::Version,
Cli {
common,
command: CommandMode::Status,
},
));
}
Long("json") => common.set_render_mode(RenderMode::Json),
Long("toon") => common.set_render_mode(RenderMode::Toon),
Long("format") => {
let value = parser_value_string(&mut parser, "--format")?;
common.set_render_mode(parse_format_choice(&value)?);
}
Long("quiet") => common.quiet = true,
Long("color") => {
common.color = parse_color_choice(&parser_value_string(&mut parser, "--color")?)?;
}
Long("user") => {
run.user = parse_launch_identity(&parser_value_string(&mut parser, "--user")?)?;
}
Long("dangerous") => run.dangerous = true,
Long("shell") => {
run.shell = Some(parse_shell_preset(&parser_value_string(
&mut parser,
"--shell",
)?)?);
}
Long("integrity") => {
run.integrity = Some(parse_token_integrity(&parser_value_string(
&mut parser,
"--integrity",
)?)?);
}
Long("privileges") => {
run.privileges =
parse_privilege_mode(&parser_value_string(&mut parser, "--privileges")?)?;
}
Long("priority") => {
run.priority = parse_priority(&parser_value_string(&mut parser, "--priority")?)?;
}
Long("show-window") => {
run.show_window =
parse_show_window_mode(&parser_value_string(&mut parser, "--show-window")?)?;
}
Long("session") => {
run.session = Some(parse_u32_flag(
"--session",
&parser_value_string(&mut parser, "--session")?,
)?);
}
Long("current-directory") => {
run.current_directory =
Some(parser_value_string(&mut parser, "--current-directory")?);
}
Long("same-console") => run.same_console = true,
Long("new-window") => run.new_window = true,
Long("wait") => run.wait = true,
Long("exit-code-path") => {
relay_exit_code_path = Some(PathBuf::from(parser_value_string(
&mut parser,
"--exit-code-path",
)?));
}
Long("stdout-path") => {
relay_stdout_path = Some(PathBuf::from(parser_value_string(
&mut parser,
"--stdout-path",
)?));
}
Long("stderr-path") => {
relay_stderr_path = Some(PathBuf::from(parser_value_string(
&mut parser,
"--stderr-path",
)?));
}
Long("console-pid") => {
relay_console_pid = Some(parse_u32_flag(
"--console-pid",
&parser_value_string(&mut parser, "--console-pid")?,
)?);
}
ArgValue(value) => {
let token = os_to_utf8(value, "subcommand")?;
if mode.is_none() && matches!(token.as_str(), "status" | "run" | RELAY_SUBCOMMAND) {
mode = Some(token);
continue;
}
run.command = collect_command_values(&mut parser, token)?;
break;
}
_ => {
return Err(CliError::usage(
"unsupported argument; use --help to see available options",
));
}
}
}
let command = match mode.as_deref() {
Some("status") => CommandMode::Status,
Some("run") => {
if run.command.is_empty() && run.shell.is_none() {
return Err(CliError::usage(
"provide a command after -- or select --shell <preset>",
));
}
CommandMode::Run(run)
}
None => {
if run.command.is_empty() && run.shell.is_none() {
CommandMode::Help(HELP)
} else {
CommandMode::Run(run)
}
}
Some(RELAY_SUBCOMMAND) => CommandMode::Relay(RelayCli {
run,
exit_code_path: relay_exit_code_path.ok_or_else(|| {
CliError::usage("internal relay requires --exit-code-path <PATH>")
})?,
stdout_path: relay_stdout_path,
stderr_path: relay_stderr_path,
console_pid: relay_console_pid,
}),
Some(other) => {
return Err(CliError::usage(format!(
"unknown subcommand '{other}'; expected status or run"
)));
}
};
Ok((ParseOutcome::Run, Cli { common, command }))
}
fn collect_command_values(
parser: &mut lexopt::Parser,
first: String,
) -> Result<Vec<String>, CliError> {
let mut values = vec![first];
while let Some(argument) = parser
.next()
.map_err(|error| CliError::usage(error.to_string()))?
{
match argument {
ArgValue(value) => values.push(os_to_utf8(value, "command argument")?),
_ => {
return Err(CliError::usage(
"command arguments must appear after -- and cannot include extra flags",
));
}
}
}
Ok(values)
}
fn parse_launch_identity(value: &str) -> Result<LaunchIdentity, CliError> {
match value.to_ascii_lowercase().as_str() {
"admin" => Ok(LaunchIdentity::Admin),
"current-process" | "self" => Ok(LaunchIdentity::CurrentProcess),
"current-user" | "user" => Ok(LaunchIdentity::CurrentUser),
"system" => Ok(LaunchIdentity::System),
"trustedinstaller" => Ok(LaunchIdentity::TrustedInstaller),
_ => Err(CliError::usage(format!(
"invalid --user value '{value}'; expected current-process, current-user, admin, system, or trustedinstaller"
))),
}
}
fn parse_shell_preset(value: &str) -> Result<ShellPreset, CliError> {
match value {
"cmd" => Ok(ShellPreset::Cmd),
"powershell" => Ok(ShellPreset::PowerShell),
"pwsh" => Ok(ShellPreset::Pwsh),
"wsl" => Ok(ShellPreset::Wsl),
"git-bash" => Ok(ShellPreset::GitBash),
"mingw" => Ok(ShellPreset::Mingw),
"msys2" => Ok(ShellPreset::Msys2),
"cygwin" => Ok(ShellPreset::Cygwin),
"yori" => Ok(ShellPreset::Yori),
"tcc" => Ok(ShellPreset::Tcc),
"nu" => Ok(ShellPreset::Nu),
other => Err(CliError::usage(format!("invalid --shell value '{other}'"))),
}
}
fn parse_token_integrity(value: &str) -> Result<TokenIntegrity, CliError> {
match value.to_ascii_lowercase().as_str() {
"untrusted" => Ok(TokenIntegrity::Untrusted),
"low" => Ok(TokenIntegrity::Low),
"medium" => Ok(TokenIntegrity::Medium),
"medium-plus" => Ok(TokenIntegrity::MediumPlus),
"high" => Ok(TokenIntegrity::High),
"system" => Ok(TokenIntegrity::System),
_ => Err(CliError::usage(format!(
"invalid --integrity value '{value}'"
))),
}
}
fn parse_privilege_mode(value: &str) -> Result<PrivilegeMode, CliError> {
match value.to_ascii_lowercase().as_str() {
"default" => Ok(PrivilegeMode::Default),
"enable-all" => Ok(PrivilegeMode::EnableAll),
"disable-all" => Ok(PrivilegeMode::DisableAll),
_ => Err(CliError::usage(format!(
"invalid --privileges value '{value}'"
))),
}
}
fn parse_priority(value: &str) -> Result<ProcessPriority, CliError> {
match value.to_ascii_lowercase().as_str() {
"idle" => Ok(ProcessPriority::Idle),
"below-normal" | "belownormal" => Ok(ProcessPriority::BelowNormal),
"normal" => Ok(ProcessPriority::Normal),
"above-normal" | "abovenormal" => Ok(ProcessPriority::AboveNormal),
"high" => Ok(ProcessPriority::High),
"realtime" | "real-time" => Ok(ProcessPriority::Realtime),
_ => Err(CliError::usage(format!(
"invalid --priority value '{value}'"
))),
}
}
fn parse_show_window_mode(value: &str) -> Result<ShowWindowMode, CliError> {
match value.to_ascii_lowercase().as_str() {
"default" => Ok(ShowWindowMode::Default),
"hidden" | "hide" => Ok(ShowWindowMode::Hidden),
"normal" | "show" => Ok(ShowWindowMode::Normal),
"minimized" | "minimize" => Ok(ShowWindowMode::Minimized),
"maximized" | "maximize" => Ok(ShowWindowMode::Maximized),
_ => Err(CliError::usage(format!(
"invalid --show-window value '{value}'"
))),
}
}
fn parse_u32_flag(flag: &str, value: &str) -> Result<u32, CliError> {
value
.parse::<u32>()
.map_err(|error| CliError::usage(format!("invalid {flag} value '{value}': {error}")))
}
fn parser_value_string(parser: &mut lexopt::Parser, flag: &str) -> Result<String, CliError> {
let value = parser
.value()
.map_err(|error| CliError::usage(error.to_string()))?;
os_to_utf8(value, flag)
}
fn os_to_utf8(value: OsString, context: &str) -> Result<String, CliError> {
value.into_string().map_err(|invalid| {
CliError::usage(format!(
"{context} expects UTF-8 text, got '{}'",
invalid.to_string_lossy()
))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone)]
struct MockProvider {
status: TokenStatus,
elevate_result: LaunchResult,
launch_result: LaunchResult,
elevated_arguments: Vec<String>,
attached_console_pids: Vec<u32>,
launched: Vec<LaunchRequest>,
relay_exit_code_to_write: Option<i32>,
elevate_wait: Option<bool>,
}
impl MockProvider {
fn new() -> Self {
Self {
status: TokenStatus {
is_elevated: true,
is_admin_member: true,
integrity: TokenIntegrity::High,
current_user: Some("TEST\\User".to_string()),
session_id: Some(1),
active_session_id: Some(1),
can_admin: true,
can_current_user: true,
can_system: true,
can_trustedinstaller: true,
trustedinstaller_installed: true,
trustedinstaller_running: false,
},
elevate_result: LaunchResult {
pid: Some(100),
exit_code: Some(0),
identity: LaunchIdentity::Admin,
},
launch_result: LaunchResult {
pid: Some(101),
exit_code: Some(0),
identity: LaunchIdentity::Admin,
},
elevated_arguments: Vec::new(),
attached_console_pids: Vec::new(),
launched: Vec::new(),
relay_exit_code_to_write: None,
elevate_wait: None,
}
}
}
impl SudoProvider for std::cell::RefCell<MockProvider> {
fn status(&self) -> Result<TokenStatus, CliError> {
Ok(self.borrow().status.clone())
}
fn elevate_current_process(
&self,
arguments: &[String],
wait: bool,
_show_window: ShowWindowMode,
) -> Result<LaunchResult, CliError> {
let mut state = self.borrow_mut();
state.elevated_arguments = arguments.to_vec();
state.elevate_wait = Some(wait);
if let Some(exit_code) = state.relay_exit_code_to_write {
let exit_path = arguments
.windows(2)
.find(|pair| pair[0] == "--exit-code-path")
.map(|pair| PathBuf::from(&pair[1]))
.expect("relay exit path");
fs::write(&exit_path, exit_code.to_string())
.expect("write relay exit code for test");
for relay_path in arguments
.windows(2)
.filter(|pair| pair[0] == "--stdout-path" || pair[0] == "--stderr-path")
.map(|pair| PathBuf::from(&pair[1]))
{
fs::write(&relay_path, b"").expect("write relay output for test");
}
}
Ok(state.elevate_result)
}
fn attach_parent_console(&self, parent_pid: u32) -> Result<(), CliError> {
self.borrow_mut().attached_console_pids.push(parent_pid);
Ok(())
}
fn launch_request(&self, request: &LaunchRequest) -> Result<LaunchResult, CliError> {
self.borrow_mut().launched.push(request.clone());
Ok(self.borrow().launch_result)
}
}
#[test]
fn parse_status_subcommand_and_json_flag() {
let (outcome, cli) =
parse_cli_from(["msudo", "status", "--json"]).expect("cli should parse");
assert!(matches!(outcome, ParseOutcome::Run));
assert!(cli.common.json);
assert!(matches!(cli.command, CommandMode::Status));
}
#[test]
fn parse_default_command_mode_accepts_bare_payload() {
let (_, cli) = parse_cli_from([
"msudo", "--user", "admin", "--", "cmd", "/d", "/c", "whoami",
])
.expect("cli should parse");
let CommandMode::Run(run) = cli.command else {
panic!("expected run mode");
};
assert_eq!(run.command[0], "cmd");
assert_eq!(run.command[3], "whoami");
}
#[test]
fn parse_shell_preset_and_window_flags() {
let (_, cli) = parse_cli_from([
"msudo",
"--shell",
"pwsh",
"--new-window",
"--priority",
"high",
])
.expect("cli should parse");
let CommandMode::Run(run) = cli.command else {
panic!("expected run mode");
};
assert_eq!(run.shell, Some(ShellPreset::Pwsh));
assert!(run.new_window);
assert_eq!(run.priority, ProcessPriority::High);
}
#[test]
fn parse_same_console_flag() {
let (_, cli) = parse_cli_from(["msudo", "--same-console", "--shell", "powershell"])
.expect("cli should parse");
let CommandMode::Run(run) = cli.command else {
panic!("expected run mode");
};
assert!(run.same_console);
}
#[test]
fn parse_current_identity_user_modes() {
let (_, current_user) = parse_cli_from(["msudo", "--user", "current-user", "--", "cmd"])
.expect("current user parses");
let CommandMode::Run(run) = current_user.command else {
panic!("expected run mode");
};
assert_eq!(run.user, LaunchIdentity::CurrentUser);
let (_, current_process) =
parse_cli_from(["msudo", "--user", "current-process", "--", "cmd"])
.expect("current process parses");
let CommandMode::Run(run) = current_process.command else {
panic!("expected run mode");
};
assert_eq!(run.user, LaunchIdentity::CurrentProcess);
}
#[test]
fn validate_requires_dangerous_for_system_and_trustedinstaller() {
let mut run = sample_run();
run.user = LaunchIdentity::System;
assert!(validate_run_cli(&run).is_err());
run.user = LaunchIdentity::TrustedInstaller;
assert!(validate_run_cli(&run).is_err());
run.dangerous = true;
assert!(validate_run_cli(&run).is_ok());
}
#[test]
fn validate_rejects_conflicting_console_mode_flags() {
let mut run = sample_run();
run.same_console = true;
run.new_window = true;
let error = validate_run_cli(&run).expect_err("validation should fail");
assert!(
error
.to_string()
.contains("--same-console cannot be combined with --new-window")
);
}
#[test]
fn status_report_contains_required_stable_fields() {
let report = build_status_report(TokenStatus {
is_elevated: false,
is_admin_member: true,
integrity: TokenIntegrity::Medium,
current_user: Some("User".to_string()),
session_id: Some(1),
active_session_id: Some(1),
can_admin: true,
can_current_user: true,
can_system: false,
can_trustedinstaller: false,
trustedinstaller_installed: true,
trustedinstaller_running: false,
});
let json = serde_json::to_string(&report).expect("json");
assert!(json.contains("\"ok\":true"));
assert!(json.contains("\"supports_runas\":true"));
assert!(json.contains("\"host\":"));
assert!(json.contains("\"is_elevated\":false"));
}
#[test]
fn render_status_text_uses_cli_shell_names() {
let rendered = render_status_text(
&StatusReport {
ok: true,
host: "localhost".to_string(),
supports_runas: true,
is_elevated: true,
is_admin_member: true,
integrity: TokenIntegrity::High,
current_user: Some("User".to_string()),
session_id: Some(1),
active_session_id: Some(1),
can_admin: true,
can_current_user: true,
can_system: true,
can_trustedinstaller: true,
trustedinstaller_installed: true,
trustedinstaller_running: false,
shells: vec![ShellStatus {
preset: ShellPreset::GitBash,
available: true,
executable: Some("C:\\Program Files\\Git\\usr\\bin\\bash.exe".to_string()),
resolution_source: Some(ShellResolutionSource::KnownLocation),
error: None,
}],
},
false,
);
assert!(rendered.contains("shell=git-bash"));
assert!(!rendered.contains("shell=GitBash"));
}
#[test]
fn quiet_status_text_omits_shell_inventory() {
let rendered = render_status_text(
&StatusReport {
ok: true,
host: "localhost".to_string(),
supports_runas: true,
is_elevated: true,
is_admin_member: true,
integrity: TokenIntegrity::High,
current_user: Some("User".to_string()),
session_id: Some(1),
active_session_id: Some(1),
can_admin: true,
can_current_user: true,
can_system: true,
can_trustedinstaller: true,
trustedinstaller_installed: true,
trustedinstaller_running: false,
shells: vec![ShellStatus {
preset: ShellPreset::Pwsh,
available: true,
executable: Some("pwsh.exe".to_string()),
resolution_source: Some(ShellResolutionSource::Path),
error: None,
}],
},
true,
);
assert!(!rendered.contains("shell="));
}
#[test]
fn shell_preset_json_names_match_cli_names() {
for preset in ShellPreset::all() {
assert_eq!(
serde_json::to_string(&preset).expect("preset json"),
format!("\"{}\"", preset.name())
);
}
}
#[test]
fn shell_wrapping_for_pwsh_and_cmd_is_stable() {
let pwsh = prepare_shell_wrapped_command(
ShellPreset::Pwsh,
&[
"git".to_string(),
"status".to_string(),
"--short".to_string(),
],
)
.expect("pwsh shell command");
let normalized = pwsh.program.to_ascii_lowercase();
assert!(
shell_resolution_candidates(ShellPreset::Pwsh)
.iter()
.any(|candidate| { normalized.ends_with(&candidate.to_ascii_lowercase()) })
);
assert!(pwsh.args.contains(&"-Command".to_string()));
assert_eq!(
pwsh.args.last().expect("pwsh command"),
"& 'git' @('status', '--short')"
);
let cmd = prepare_shell_wrapped_command(
ShellPreset::Cmd,
&["tool.exe".to_string(), "two words".to_string()],
)
.expect("cmd shell command");
assert!(cmd.args.contains(&"/c".to_string()));
assert!(cmd.args.last().expect("last arg").contains("\"two words\""));
}
#[test]
fn shell_wrapping_rejects_or_quotes_shell_metacharacters() {
let cmd = prepare_shell_wrapped_command(
ShellPreset::Cmd,
&["tool.exe".to_string(), "%PATH%".to_string()],
)
.expect_err("cmd percent expansion should be rejected");
assert!(cmd.to_string().contains("unsafe shell metacharacter"));
let posix = quote_for_shell(
ShellPreset::GitBash,
&[
"tool".to_string(),
">".to_string(),
"<".to_string(),
"`whoami`".to_string(),
"two\nlines".to_string(),
"%PATH%".to_string(),
],
)
.expect("posix metacharacters should be quoted");
assert_eq!(posix, "tool '>' '<' '`whoami`' 'two\nlines' '%PATH%'");
}
#[test]
fn non_elevated_launch_relays_through_provider() {
let (_, cli) = parse_cli_from([
"msudo", "--user", "admin", "--", "cmd", "/d", "/c", "whoami",
])
.expect("cli should parse");
let provider = std::cell::RefCell::new(MockProvider::new());
provider.borrow_mut().status.is_elevated = false;
let code = run_with_provider(&cli, &provider).expect("run should succeed");
assert_eq!(code, ExitCode::Success.as_i32());
let elevated_arguments = provider.borrow().elevated_arguments.clone();
assert!(elevated_arguments.contains(&RELAY_SUBCOMMAND.to_string()));
assert!(elevated_arguments.contains(&"--exit-code-path".to_string()));
assert_eq!(provider.borrow().elevate_wait, Some(false));
}
#[test]
fn non_elevated_current_process_does_not_relay() {
let (_, cli) = parse_cli_from([
"msudo",
"--user",
"current-process",
"--",
"cmd",
"/d",
"/c",
"whoami",
])
.expect("cli should parse");
let provider = std::cell::RefCell::new(MockProvider::new());
provider.borrow_mut().status.is_elevated = false;
let code = run_with_provider(&cli, &provider).expect("run should succeed");
assert_eq!(code, ExitCode::Success.as_i32());
assert!(provider.borrow().elevated_arguments.is_empty());
assert_eq!(
provider.borrow().launched[0].identity,
LaunchIdentity::CurrentProcess
);
}
#[test]
fn non_elevated_wait_uses_relay_exit_code_file() {
let (_, cli) = parse_cli_from([
"msudo",
"--user",
"system",
"--dangerous",
"--wait",
"--",
"cmd",
"/d",
"/c",
"exit 9",
])
.expect("cli should parse");
let provider = std::cell::RefCell::new(MockProvider::new());
provider.borrow_mut().status.is_elevated = false;
provider.borrow_mut().relay_exit_code_to_write = Some(9);
let code = run_with_provider(&cli, &provider).expect("run should succeed");
assert_eq!(code, 9);
assert_eq!(provider.borrow().elevate_wait, Some(true));
}
#[test]
fn non_elevated_same_console_forces_waiting_relay() {
let (_, cli) = parse_cli_from([
"msudo",
"--same-console",
"--user",
"system",
"--dangerous",
"--",
"cmd",
"/d",
"/c",
"exit 0",
])
.expect("cli should parse");
let provider = std::cell::RefCell::new(MockProvider::new());
provider.borrow_mut().status.is_elevated = false;
provider.borrow_mut().relay_exit_code_to_write = Some(0);
let code = run_with_provider(&cli, &provider).expect("run should succeed");
assert_eq!(code, 0);
let elevated_arguments = provider.borrow().elevated_arguments.clone();
assert!(elevated_arguments.contains(&"--same-console".to_string()));
assert!(elevated_arguments.contains(&"--console-pid".to_string()));
assert_eq!(provider.borrow().elevate_wait, Some(true));
}
#[test]
fn same_console_relay_attaches_parent_console_before_launch() {
let relay_path = unique_exit_code_path();
let (_, cli) = parse_cli_from([
"msudo",
"__relay",
"--same-console",
"--console-pid",
"4242",
"--exit-code-path",
relay_path.to_string_lossy().as_ref(),
"--user",
"system",
"--dangerous",
"--wait",
"--",
"cmd",
"/d",
"/c",
"exit 0",
])
.expect("relay cli should parse");
let provider = std::cell::RefCell::new(MockProvider::new());
let code = run_with_provider(&cli, &provider).expect("relay should succeed");
assert_eq!(code, 0);
assert_eq!(provider.borrow().attached_console_pids, vec![4242]);
let _ = std::fs::remove_file(relay_path);
}
#[test]
fn validate_allows_interactive_system_and_trustedinstaller_shells() {
let mut run = sample_run();
run.command.clear();
run.user = LaunchIdentity::System;
run.dangerous = true;
assert!(validate_run_cli(&run).is_ok());
run.user = LaunchIdentity::TrustedInstaller;
assert!(validate_run_cli(&run).is_ok());
}
#[test]
fn elevated_launch_calls_provider_directly() {
let (_, cli) = parse_cli_from([
"msudo",
"--user",
"system",
"--dangerous",
"--",
"cmd",
"/d",
"/c",
"whoami",
])
.expect("cli should parse");
let provider = std::cell::RefCell::new(MockProvider::new());
let code = run_with_provider(&cli, &provider).expect("run should succeed");
assert_eq!(code, ExitCode::Success.as_i32());
assert_eq!(provider.borrow().launched.len(), 1);
assert_eq!(
provider.borrow().launched[0].identity,
LaunchIdentity::System
);
}
#[test]
fn prepare_launch_resolves_bare_direct_commands_from_path() {
let temp_root = std::env::temp_dir().join(format!(
"msudo-prepare-launch-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos()
));
std::fs::create_dir_all(&temp_root).expect("create temp root");
let tool = temp_root.join("tool.exe");
std::fs::write(&tool, []).expect("create tool stub");
let resolved = find_executable_in_path(
"tool",
temp_root.as_os_str(),
Some(std::ffi::OsStr::new(".EXE")),
)
.expect("tool should resolve");
let _ = std::fs::remove_file(&tool);
let _ = std::fs::remove_dir(&temp_root);
assert_eq!(resolved, tool.display().to_string());
}
#[test]
fn elevated_direct_commands_reject_path_resolved_bare_programs() {
let temp_root = std::env::temp_dir().join(format!(
"msudo-direct-path-policy-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos()
));
fs::create_dir_all(&temp_root).expect("create temp root");
let tool = temp_root.join("tool.exe");
fs::write(&tool, []).expect("create tool stub");
let error = prepare_direct_command(
"tool",
&["--version".to_string()],
LaunchIdentity::Admin,
Some(temp_root.as_os_str()),
Some(std::ffi::OsStr::new(".EXE")),
)
.expect_err("admin direct command should reject PATH-resolved bare programs");
let _ = fs::remove_file(&tool);
let _ = fs::remove_dir(&temp_root);
assert!(
error
.to_string()
.contains("refuses PATH-resolved bare command")
);
}
#[test]
fn current_process_direct_commands_keep_path_resolution() {
let temp_root = std::env::temp_dir().join(format!(
"msudo-current-process-path-policy-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos()
));
fs::create_dir_all(&temp_root).expect("create temp root");
let tool = temp_root.join("tool.exe");
fs::write(&tool, []).expect("create tool stub");
let prepared = prepare_direct_command(
"tool",
&["--version".to_string()],
LaunchIdentity::CurrentProcess,
Some(temp_root.as_os_str()),
Some(std::ffi::OsStr::new(".EXE")),
)
.expect("current-process direct command may use PATH");
let _ = fs::remove_file(&tool);
let _ = fs::remove_dir(&temp_root);
assert_eq!(prepared.program, tool.display().to_string());
assert_eq!(prepared.args, vec!["--version".to_string()]);
}
#[test]
fn prepare_launch_defaults_interactive_shells_to_new_window() {
let (_, cli) =
parse_cli_from(["msudo", "--shell", "powershell"]).expect("cli should parse");
let CommandMode::Run(run) = cli.command else {
panic!("expected run mode");
};
let prepared = prepare_launch(&run).expect("launch should prepare");
assert!(prepared.new_window);
}
#[test]
fn prepare_launch_same_console_keeps_interactive_shell_in_foreground() {
let (_, cli) = parse_cli_from(["msudo", "--same-console", "--shell", "powershell"])
.expect("cli should parse");
let CommandMode::Run(run) = cli.command else {
panic!("expected run mode");
};
let prepared = prepare_launch(&run).expect("launch should prepare");
assert!(prepared.same_console);
assert!(!prepared.new_window);
assert!(prepared.wait);
}
#[test]
fn prepare_launch_resolves_direct_program_to_executable_when_available() {
let resolved_cmd = resolve_direct_command_from_path(
"cmd",
std::env::var_os("PATH").as_deref(),
std::env::var_os("PATHEXT").as_deref(),
);
assert!(
resolved_cmd
.executable
.to_ascii_lowercase()
.ends_with("cmd.exe")
);
}
#[test]
fn shell_resolution_candidates_prefer_known_git_bash_locations() {
let candidates = shell_resolution_candidates(ShellPreset::GitBash);
assert_eq!(
candidates.first().copied(),
Some("C:\\Program Files\\Git\\usr\\bin\\bash.exe")
);
assert!(candidates.contains(&"git-bash.exe"));
}
#[cfg(windows)]
#[test]
fn shell_resolution_prefers_known_locations_over_path_hijack_and_reports_source() {
let temp_root = std::env::temp_dir().join(format!(
"msudo-shell-hijack-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos()
));
fs::create_dir_all(&temp_root).expect("create temp root");
let fake_cmd = temp_root.join("cmd.exe");
fs::write(&fake_cmd, []).expect("create fake cmd");
let resolved = resolve_shell_from_path(
ShellPreset::Cmd,
Some(temp_root.as_os_str()),
Some(std::ffi::OsStr::new(".EXE")),
)
.expect("cmd should resolve");
let _ = fs::remove_file(&fake_cmd);
let _ = fs::remove_dir(&temp_root);
assert_ne!(resolved.executable, fake_cmd.display().to_string());
assert_eq!(resolved.source, ShellResolutionSource::KnownLocation);
assert!(
resolved
.executable
.to_ascii_lowercase()
.ends_with("\\system32\\cmd.exe")
);
}
#[test]
fn msys_like_presets_do_not_fall_back_to_generic_bash() {
for preset in [ShellPreset::Mingw, ShellPreset::Msys2, ShellPreset::Cygwin] {
let candidates = shell_resolution_candidates(preset);
assert!(!candidates.contains(&"bash.exe"));
assert!(!candidates.contains(&"bash"));
}
}
#[test]
fn read_relay_exit_code_parses_and_removes_temp_file() {
let path = unique_exit_code_path();
assert!(!path.exists());
fs::write(&path, "17").expect("write exit code");
let code = read_relay_exit_code(&path).expect("read exit code");
assert_eq!(code, 17);
assert!(!path.exists());
}
#[test]
fn write_relay_exit_status_refuses_replaced_path() {
let path = unique_exit_code_path();
fs::write(&path, "attacker").expect("attacker replacement");
let error = write_relay_exit_status(
&path,
&RelayExitStatus {
ok: true,
exit_code: Some(0),
error: None,
},
)
.expect_err("relay writer should refuse existing replacement");
assert!(
error
.to_string()
.contains("failed to create relay exit status")
);
assert_eq!(
fs::read_to_string(&path).expect("replacement remains"),
"attacker"
);
let _ = fs::remove_file(path);
}
#[test]
fn read_relay_exit_code_surfaces_json_error_status() {
let path = unique_exit_code_path();
write_relay_exit_status(
&path,
&RelayExitStatus {
ok: false,
exit_code: None,
error: Some("CreateProcessWithTokenW failed with code 5".to_string()),
},
)
.expect("write relay status");
let error = read_relay_exit_code(&path).expect_err("relay should surface error");
assert!(
error
.to_string()
.contains("CreateProcessWithTokenW failed with code 5")
);
assert!(!path.exists());
}
fn sample_run() -> RunCli {
RunCli {
user: LaunchIdentity::Admin,
dangerous: false,
shell: Some(ShellPreset::Pwsh),
integrity: None,
privileges: PrivilegeMode::Default,
priority: ProcessPriority::Normal,
show_window: ShowWindowMode::Default,
session: None,
current_directory: None,
same_console: false,
new_window: false,
wait: false,
command: vec![
"cmd".to_string(),
"/d".to_string(),
"/c".to_string(),
"whoami".to_string(),
],
}
}
}