use std::collections::BTreeMap;
use serde_json::Value;
use crate::{
CapabilityCallResult, CapabilityInvoker, ExitCode,
limits::{Budget, LimitExceeded},
};
pub(crate) mod cap;
pub(crate) mod clock;
pub(crate) mod curl;
pub(crate) mod encode;
pub(crate) mod jq;
pub(crate) mod misc;
pub(crate) mod text;
pub(crate) mod xargs;
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct CommandResult {
pub value: Value,
pub status: ExitCode,
pub suppress_newline: bool,
}
impl CommandResult {
pub(crate) fn value(value: Value) -> Self {
Self {
value,
status: ExitCode::SUCCESS,
suppress_newline: false,
}
}
pub(crate) fn status(status: ExitCode) -> Self {
Self {
value: Value::Null,
status,
suppress_newline: false,
}
}
pub(crate) fn without_newline(mut self) -> Self {
self.suppress_newline = true;
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum CommandFailure {
Status {
message: String,
status: ExitCode,
},
Fatal(FatalError),
}
impl CommandFailure {
pub(crate) fn usage(message: impl Into<String>) -> Self {
Self::Status {
message: message.into(),
status: ExitCode::SYNTAX,
}
}
pub(crate) fn failed(message: impl Into<String>) -> Self {
Self::Status {
message: message.into(),
status: ExitCode::FAILURE,
}
}
}
impl From<LimitExceeded> for CommandFailure {
fn from(limit: LimitExceeded) -> Self {
Self::Fatal(FatalError::Limit(limit))
}
}
impl From<LimitExceeded> for FatalError {
fn from(limit: LimitExceeded) -> Self {
Self::Limit(limit)
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum FatalError {
Limit(LimitExceeded),
Unsupported(String),
}
pub(crate) struct BuiltinContext<'a> {
pub invoker: &'a dyn CapabilityInvoker,
pub budget: &'a mut Budget,
pub buffers: &'a mut BTreeMap<String, Value>,
pub curl_capability: Option<&'a str>,
pub allow_clock: bool,
}
impl BuiltinContext<'_> {
pub(crate) fn invoke_capability(
&mut self,
capability: &str,
input: Value,
) -> Result<CommandResult, CommandFailure> {
self.budget.charge_capability_call()?;
self.budget.check_deadline()?;
let result = self.invoker.invoke(capability, input);
self.budget.check_deadline()?;
let status = ExitCode::from_capability_result(&result);
Ok(match result {
CapabilityCallResult::Succeeded(output) => CommandResult {
value: output,
status,
suppress_newline: false,
},
CapabilityCallResult::Denied { reason } => {
return Err(CommandFailure::Status {
message: format!("{capability}: denied: {reason}"),
status,
});
}
CapabilityCallResult::Failed { error } => {
return Err(CommandFailure::Status {
message: format!("{capability}: failed: {error}"),
status,
});
}
CapabilityCallResult::NotFound => {
return Err(CommandFailure::Status {
message: format!("{capability}: capability not found"),
status,
});
}
})
}
}
pub(crate) trait Builtin {
fn name(&self) -> &'static str;
fn run(
&self,
context: &mut BuiltinContext<'_>,
arguments: &[String],
input: Option<Value>,
) -> Result<CommandResult, CommandFailure>;
}
#[derive(Clone, Copy)]
pub(crate) enum BuiltinKind {
Simple(&'static dyn Builtin),
Xargs,
}
const REGISTRY: &[&dyn Builtin] = &[
&jq::Jq,
&curl::Curl,
&clock::Date,
&misc::Sleep,
&text::Grep,
&text::Sed,
&text::Cut,
&text::Sort,
&text::Uniq,
&text::Wc,
&encode::Base64,
&misc::Echo,
&misc::Printf,
&misc::Test,
&misc::TestBracket,
&misc::True,
&misc::False,
&misc::Cat,
&cap::Cap,
];
pub(crate) fn lookup(name: &str) -> Option<BuiltinKind> {
if name == xargs::NAME {
return Some(BuiltinKind::Xargs);
}
REGISTRY
.iter()
.find(|builtin| builtin.name() == name)
.map(|builtin| BuiltinKind::Simple(*builtin))
}
#[cfg(test)]
pub(crate) fn names() -> Vec<&'static str> {
let mut names = REGISTRY
.iter()
.map(|builtin| builtin.name())
.collect::<Vec<_>>();
names.push(xargs::NAME);
names.sort_unstable();
names
}
pub(crate) fn unsupported_flag(command: &str, flag: &str) -> CommandFailure {
CommandFailure::usage(format!("{command}: option not yet supported: {flag}"))
}
#[cfg(test)]
pub(crate) mod test_support {
use std::collections::BTreeMap;
use serde_json::Value;
use crate::{
CapabilityCallResult, CapabilityInvoker,
limits::{Budget, Limits},
};
use super::{Builtin, BuiltinContext, CommandFailure, CommandResult};
pub(crate) struct NoCapabilities;
impl CapabilityInvoker for NoCapabilities {
fn granted(&self) -> Vec<String> {
Vec::new()
}
fn invoke(&self, _capability: &str, _input: Value) -> CapabilityCallResult {
CapabilityCallResult::NotFound
}
}
pub(crate) fn run_builtin(
builtin: &dyn Builtin,
arguments: &[&str],
input: Option<Value>,
) -> Result<CommandResult, CommandFailure> {
let mut buffers = BTreeMap::new();
run_builtin_with(
builtin,
arguments,
input,
Limits::default(),
None,
&mut buffers,
)
}
pub(crate) fn run_builtin_with_invoker(
builtin: &dyn Builtin,
arguments: &[&str],
invoker: &dyn CapabilityInvoker,
) -> Result<CommandResult, CommandFailure> {
let mut budget = Budget::start(Limits::default());
let mut buffers = BTreeMap::new();
let mut context = BuiltinContext {
invoker,
budget: &mut budget,
buffers: &mut buffers,
curl_capability: None,
allow_clock: false,
};
let arguments = arguments
.iter()
.map(|argument| (*argument).to_owned())
.collect::<Vec<_>>();
builtin.run(&mut context, &arguments, None)
}
pub(crate) fn run_builtin_with_clock(
builtin: &dyn Builtin,
arguments: &[&str],
) -> Result<CommandResult, CommandFailure> {
let invoker = NoCapabilities;
let mut budget = Budget::start(Limits::default());
let mut buffers = BTreeMap::new();
let mut context = BuiltinContext {
invoker: &invoker,
budget: &mut budget,
buffers: &mut buffers,
curl_capability: None,
allow_clock: true,
};
let arguments = arguments
.iter()
.map(|argument| (*argument).to_owned())
.collect::<Vec<_>>();
builtin.run(&mut context, &arguments, None)
}
pub(crate) fn run_builtin_with(
builtin: &dyn Builtin,
arguments: &[&str],
input: Option<Value>,
limits: Limits,
curl_capability: Option<&str>,
buffers: &mut BTreeMap<String, Value>,
) -> Result<CommandResult, CommandFailure> {
let invoker = NoCapabilities;
let mut budget = Budget::start(limits);
let mut context = BuiltinContext {
invoker: &invoker,
budget: &mut budget,
buffers,
curl_capability,
allow_clock: false,
};
let arguments = arguments
.iter()
.map(|argument| (*argument).to_owned())
.collect::<Vec<_>>();
builtin.run(&mut context, &arguments, input)
}
}
#[cfg(test)]
mod tests {
use super::{lookup, names, xargs};
#[test]
fn builtin_names_can_never_collide_with_capabilities() {
for name in names() {
assert!(
!name.contains(['.', '-', '_']),
"builtin {name:?} contains a capability-identifier separator"
);
}
}
#[test]
fn the_registry_covers_every_documented_builtin() {
let expected = [
"[",
"base64",
"cap",
"cat",
"curl",
"cut",
"date",
"echo",
"false",
"grep",
"jq",
"printf",
"sed",
"sleep",
"sort",
"test",
"true",
"uniq",
"wc",
xargs::NAME,
];
let mut expected = expected.to_vec();
expected.sort_unstable();
assert_eq!(names(), expected);
for name in expected {
assert!(lookup(name).is_some(), "{name} must resolve");
}
assert!(lookup("definitely-not-a-builtin").is_none());
}
}