#![cfg_attr(not(unix), allow(dead_code))]
use crate::builtins::{Keep, TreeError};
use crate::safety;
use crate::value::Value;
use anyhow::Result;
use std::path::Path;
use std::time::Duration;
pub(crate) const MAX_OUTPUT: usize = 8 * 1024 * 1024;
const MAX_ARGV: usize = 64;
const MAX_ARG_BYTES: usize = 4096;
const SETTLE: Duration = Duration::from_secs(2);
pub(crate) struct Run {
pub(crate) code: Option<i32>,
pub(crate) timed_out: bool,
pub(crate) truncated: bool,
pub(crate) stdout: String,
pub(crate) stderr: String,
}
pub(crate) fn no_pipeline_input(builtin: &str, input: &Option<Value>) -> Result<()> {
if input.is_some() {
return Err(safety::bad_arg(
builtin,
"a direct call; pipeline input is not accepted",
"pipeline input",
));
}
Ok(())
}
pub(crate) fn interface_name(builtin: &str, name: &str) -> Result<()> {
let plain = !name.is_empty()
&& name.len() <= 15
&& !name.starts_with('-')
&& name != "."
&& name != ".."
&& name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'.' | b':' | b'@' | b'-'));
if plain {
Ok(())
} else {
Err(safety::bad_arg(
builtin,
"a network interface name of 1 to 15 letters, digits, '_', '.', ':', '@' or '-', not starting with '-'",
name,
))
}
}
pub(crate) fn device_path(builtin: &str, path: &str) -> Result<()> {
let plain = path.len() > 5
&& path.len() <= 256
&& path.starts_with("/dev/")
&& !path.split('/').any(|part| part == "..")
&& path
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"/._-:+@".contains(&b));
if plain {
Ok(())
} else {
Err(safety::bad_arg(
builtin,
"a device path under /dev/ (letters, digits and / . _ - : + @)",
path,
))
}
}
pub(crate) fn event_list(builtin: &str, events: &str) -> Result<()> {
let plain = !events.is_empty()
&& events.len() <= 512
&& !events.starts_with('-')
&& events.split(',').all(|event| {
!event.is_empty()
&& event.bytes().all(|b| {
b.is_ascii_alphanumeric()
|| matches!(b, b'-' | b'_' | b':' | b'.' | b'=' | b'/')
})
});
if plain {
Ok(())
} else {
Err(safety::bad_arg(
builtin,
"a comma-separated list of event names such as \"cycles,instructions\"",
events,
))
}
}
pub(crate) fn file_path(builtin: &str, path: &str) -> Result<()> {
if path.is_empty() || path.len() > 4096 || path.contains('\0') {
return Err(safety::arg_err(format!(
"{builtin}: a file path must be 1 to 4096 bytes without NUL"
)));
}
safety::reject_option_like(builtin, &[path.to_string()])
}
pub(crate) fn output_path(builtin: &str, path: &str) -> Result<()> {
file_path(builtin, path)?;
if let Some(parent) = Path::new(path)
.parent()
.filter(|p| !p.as_os_str().is_empty())
{
if !parent.is_dir() {
return Err(safety::not_found(
builtin,
"directory",
&parent.display().to_string(),
));
}
}
Ok(())
}
pub(crate) fn argv_arg(builtin: &str, value: &Value) -> Result<Vec<String>> {
let items = match value {
Value::Array(items) => items,
other => {
return Err(safety::bad_arg(
builtin,
"an argv Array of Strings, e.g. [\"/usr/bin/true\"]",
other.type_name(),
))
}
};
if items.is_empty() || items.len() > MAX_ARGV {
return Err(safety::arg_err(format!(
"{builtin}: argv must hold 1 to {MAX_ARGV} strings"
)));
}
let mut out = Vec::with_capacity(items.len());
for (i, item) in items.iter().enumerate() {
match item {
Value::Str(s) if !s.is_empty() && s.len() <= MAX_ARG_BYTES && !s.contains('\0') => {
out.push(s.clone())
}
Value::Str(_) => {
return Err(safety::arg_err(format!(
"{builtin}: argv[{i}] must be 1 to {MAX_ARG_BYTES} bytes without NUL"
)))
}
other => {
return Err(safety::bad_arg(
builtin,
&format!("a String at argv[{i}]"),
other.type_name(),
))
}
}
}
safety::reject_option_like(builtin, &out[..1])?;
Ok(out)
}
pub(crate) fn columns(line: &str, n: usize) -> Vec<&str> {
let mut out = Vec::with_capacity(n);
let mut rest = line.trim_start();
while out.len() + 1 < n {
match rest.find(char::is_whitespace) {
Some(i) => {
out.push(&rest[..i]);
rest = rest[i..].trim_start();
}
None => {
out.push(rest);
rest = "";
}
}
}
out.push(rest.trim_end());
out.resize(n, "");
out
}
pub(crate) fn require_success(builtin: &str, tool: &str, run: &Run, limit: Duration) -> Result<()> {
if run.timed_out {
return Err(safety::budget_exceeded(
builtin,
format!(
"`{tool}` was still running after {}s and was killed",
limit.as_secs()
),
"use a shorter command or a smaller request; the cap is fixed",
));
}
if run.truncated {
return Err(safety::budget_exceeded(
builtin,
format!("`{tool}` wrote more than {MAX_OUTPUT} bytes"),
"narrow what the request asks for",
));
}
if run.code != Some(0) {
return Err(safety::tool_failed(builtin, tool, run.code, &run.stderr));
}
Ok(())
}
pub(crate) fn run_bounded(
builtin: &str,
tool: &str,
program: &str,
args: &[String],
limit: Duration,
) -> Result<Run> {
run_bounded_capped(builtin, tool, program, args, limit, MAX_OUTPUT)
}
pub(crate) fn run_bounded_capped(
builtin: &str,
tool: &str,
program: &str,
args: &[String],
limit: Duration,
max_output: usize,
) -> Result<Run> {
let argv: Vec<&str> = args.iter().map(String::as_str).collect();
let run = crate::builtins::run_tree(
program,
&argv,
|_| {},
None,
limit,
Keep::Head(max_output),
Keep::Head(max_output),
)
.map_err(|e| match e {
TreeError::Spawn(e) => safety::spawn_error(builtin, tool, &e),
TreeError::Wait(e) => e.into(),
})?;
Ok(Run {
code: run.code,
timed_out: run.timed_out,
truncated: run.stdout_truncated || run.stderr_truncated || run.pipes_abandoned,
stdout: String::from_utf8_lossy(&run.stdout).into_owned(),
stderr: String::from_utf8_lossy(&run.stderr).into_owned(),
})
}