use super::*;
pub(crate) struct ShellInvocation {
pub(crate) program: PathBuf,
pub(crate) args: Vec<std::ffi::OsString>,
}
pub(crate) fn powershell_program() -> &'static str {
static PROGRAM: std::sync::LazyLock<&'static str> = std::sync::LazyLock::new(|| {
let has_pwsh = std::env::var_os("PATH").is_some_and(|path| {
std::env::split_paths(&path).any(|dir| dir.join("pwsh.exe").is_file())
});
if has_pwsh { "pwsh" } else { "powershell" }
});
&PROGRAM
}
pub(crate) fn powershell_wrap(command: &str) -> String {
format!(
"$ErrorActionPreference='Stop'\n{command}\nif ((Test-Path -LiteralPath variable:\\LASTEXITCODE)) {{ exit $LASTEXITCODE }}"
)
}
pub(crate) fn shell_invocation(
command: &str,
sandbox_network: bool,
confine_writes: Option<&[PathBuf]>,
) -> ShellInvocation {
if sandbox_network || confine_writes.is_some() {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("mermaid"));
let mut args: Vec<std::ffi::OsString> = vec!["__sandbox-exec".into()];
if sandbox_network {
args.push("--no-network".into());
}
for dir in confine_writes.unwrap_or_default() {
args.push("--confine-writes".into());
args.push(dir.into());
}
args.extend(["--".into(), "sh".into(), "-c".into(), command.into()]);
ShellInvocation { program: exe, args }
} else if cfg!(target_os = "windows") {
ShellInvocation {
program: PathBuf::from(powershell_program()),
args: vec![
"-NoProfile".into(),
"-NonInteractive".into(),
"-Command".into(),
powershell_wrap(command).into(),
],
}
} else {
ShellInvocation {
program: PathBuf::from("sh"),
args: vec!["-c".into(), command.into()],
}
}
}
pub(crate) fn build_sandboxed_shell(
command: &str,
sandbox_network: bool,
confine_writes: Option<&[PathBuf]>,
) -> Command {
let invocation = shell_invocation(command, sandbox_network, confine_writes);
let mut cmd = Command::new(&invocation.program);
cmd.args(&invocation.args);
cmd
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CwdContainment {
Project,
Scratchpad,
External,
}
pub(crate) fn classify_cwd(
within_project: bool,
effective_workdir: &Path,
scratchpad: Option<&Path>,
) -> CwdContainment {
if within_project {
return CwdContainment::Project;
}
match scratchpad.and_then(|s| std::fs::canonicalize(s).ok()) {
Some(scratch) if effective_workdir.starts_with(&scratch) => CwdContainment::Scratchpad,
_ => CwdContainment::External,
}
}
pub(crate) fn command_provably_in_scratch(command: &str, scratch: &Path) -> bool {
const OPAQUE: &[char] = &[
';', '|', '&', '<', '>', '$', '`', '~', '*', '?', '[', ']', '(', ')', '{', '}', '!', '\n',
'\r',
];
if command.contains(OPAQUE) {
return false;
}
let Ok(tokens) = shell_words::split(command) else {
return false;
};
if tokens.is_empty() {
return false;
}
tokens.iter().all(|t| token_provably_in_scratch(t, scratch))
}
pub(crate) fn token_provably_in_scratch(token: &str, scratch: &Path) -> bool {
if token.contains("..") || token.contains(":/") {
return false;
}
let bytes = token.as_bytes();
if bytes.len() >= 2 && bytes[1] == b':' && bytes[0].is_ascii_alphabetic() {
return false;
}
if !token.contains(['/', '\\']) {
return true;
}
if Path::new(token).has_root() {
return Path::new(token).starts_with(scratch);
}
!token.starts_with('-') && !token.contains('=')
}
pub(crate) const SCRATCHPAD_ENV_VAR: &str = "MERMAID_SCRATCHPAD";
pub(crate) fn export_scratchpad_env(cmd: &mut Command, scratchpad: Option<&Path>) {
if let Some(dir) = scratchpad {
cmd.env(SCRATCHPAD_ENV_VAR, dir);
}
}
pub(crate) fn tail_lines(text: &str, max_lines: usize) -> String {
let lines: Vec<&str> = text.lines().collect();
let start = lines.len().saturating_sub(max_lines);
lines[start..].join("\n")
}
pub(crate) fn first_url(text: &str) -> Option<String> {
text.split_whitespace()
.find(|part| part.starts_with("http://") || part.starts_with("https://"))
.map(|url| {
url.trim_matches(|c: char| matches!(c, ')' | ']' | '}' | ',' | ';' | '"' | '\''))
.to_string()
})
}
pub(crate) fn all_urls(text: &str) -> Vec<String> {
text.split_whitespace()
.filter(|part| part.starts_with("http://") || part.starts_with("https://"))
.map(|url| {
url.trim_matches(|c: char| matches!(c, ')' | ']' | '}' | ',' | ';' | '"' | '\''))
.to_string()
})
.collect()
}
pub(crate) async fn open_browser_url(url: &str) -> Result<(), String> {
super::super::web::require_http_scheme(url)?;
#[cfg(target_os = "macos")]
let mut command = {
let mut cmd = Command::new("open");
cmd.arg(url);
cmd
};
#[cfg(target_os = "linux")]
let mut command = {
let mut cmd = Command::new("xdg-open");
cmd.arg(url);
cmd
};
#[cfg(target_os = "windows")]
let mut command = {
let mut cmd = Command::new("rundll32");
cmd.args(["url.dll,FileProtocolHandler", url]);
cmd
};
command
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.kill_on_drop(false)
.spawn()
.map(|_| ())
.map_err(|e| e.to_string())
}