use std::sync::atomic::{AtomicBool, Ordering};
static SHELL_DISABLED: AtomicBool = AtomicBool::new(false);
pub fn disable_shell() {
SHELL_DISABLED.store(true, Ordering::Relaxed);
}
pub fn shell_disabled() -> bool {
SHELL_DISABLED.load(Ordering::Relaxed)
}
pub fn shell_command(command: &str) -> std::process::Command {
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
let shell = std::env::var_os("COMSPEC").unwrap_or_else(|| "cmd.exe".into());
let mut cmd = std::process::Command::new(shell);
cmd.arg("/S").arg("/C").raw_arg(format!("\"{command}\""));
cmd
}
#[cfg(not(windows))]
{
let mut cmd = std::process::Command::new("sh");
cmd.arg("-c").arg(command);
cmd
}
}
pub fn filter_output_rows(input: &str, stdout: &str) -> Vec<String> {
if stdout.is_empty() {
return Vec::new();
}
let input_has_cr = input.split('\n').any(|row| row.ends_with('\r'));
let body = stdout.strip_suffix('\n').unwrap_or(stdout);
body.split('\n')
.map(|row| {
if input_has_cr {
row
} else {
row.strip_suffix('\r').unwrap_or(row)
}
})
.map(String::from)
.collect()
}
static FS_RESTRICTED: AtomicBool = AtomicBool::new(false);
pub fn restrict_fs() {
FS_RESTRICTED.store(true, Ordering::Relaxed);
}
pub fn fs_restricted() -> bool {
FS_RESTRICTED.load(Ordering::Relaxed)
}
pub fn path_escapes(path: &std::path::Path) -> bool {
use std::path::Component;
path.components().any(|c| {
matches!(
c,
Component::ParentDir | Component::RootDir | Component::Prefix(_)
)
})
}
pub fn check_fs_path(path: &std::path::Path) -> Result<(), String> {
if fs_restricted() && path_escapes(path) {
return Err(format!(
"path {} is outside the working directory (blocked in RPC mode)",
path.display()
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn run(command: &str) -> String {
let out = shell_command(command)
.output()
.expect("the platform shell must spawn");
assert!(out.status.success(), "`{command}` failed: {out:?}");
String::from_utf8(out.stdout)
.unwrap()
.trim_end()
.to_string()
}
#[test]
fn shell_command_runs_a_command() {
assert_eq!(run("echo hjkl"), "hjkl");
}
#[test]
fn shell_command_passes_inner_quotes_verbatim() {
let expected = if cfg!(windows) { "\"a b\"" } else { "a b" };
assert_eq!(run(r#"echo "a b""#), expected);
}
fn rows(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn filter_output_rows_strips_cr_when_input_is_lf() {
assert_eq!(filter_output_rows("b\na", "a\r\nb\r\n"), rows(&["a", "b"]));
}
#[test]
fn filter_output_rows_keeps_cr_when_input_is_crlf() {
assert_eq!(
filter_output_rows("b\r\na\r", "a\r\nb\r\n"),
rows(&["a\r", "b\r"])
);
}
#[test]
fn filter_output_rows_trailing_newline_ends_the_last_row() {
assert_eq!(filter_output_rows("x", "a\n"), rows(&["a"]));
assert_eq!(filter_output_rows("x", "a"), rows(&["a"]));
assert_eq!(filter_output_rows("x", "a\n\n"), rows(&["a", ""]));
assert_eq!(filter_output_rows("x", "\n"), rows(&[""]));
}
#[test]
fn filter_output_rows_empty_output_is_no_rows() {
assert!(filter_output_rows("x", "").is_empty());
}
#[test]
fn shell_command_runs_pipelines() {
let command = if cfg!(windows) {
"echo ab| findstr b"
} else {
"echo ab | grep b"
};
assert_eq!(run(command), "ab");
}
}