#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedCommand {
pub program: String,
pub args: Vec<String>,
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CommandResolutionError {
#[error(
"sandbox has no default command; configure an entrypoint or cmd, or execute a literal command"
)]
NoDefaultCommand,
#[error("{origin} executable must not be empty")]
EmptyExecutable {
origin: &'static str,
},
#[error("{origin} token at index {index} contains a NUL byte")]
NulByte {
origin: &'static str,
index: usize,
},
}
#[doc(hidden)]
pub fn resolve_default_command(
entrypoint: Option<&[String]>,
cmd: Option<&[String]>,
cmd_override: Option<&[String]>,
) -> Result<ResolvedCommand, CommandResolutionError> {
let entrypoint = entrypoint.filter(|tokens| !tokens.is_empty());
let (selected_cmd, cmd_source) = match cmd_override {
Some(tokens) => (Some(tokens), "cmd override"),
None => (cmd, "cmd"),
};
let selected_cmd = selected_cmd.filter(|tokens| !tokens.is_empty());
if let Some(tokens) = entrypoint {
validate_tokens("entrypoint", tokens)?;
if let Some(tokens) = selected_cmd {
validate_tokens(cmd_source, tokens)?;
}
return Ok(ResolvedCommand {
program: tokens[0].clone(),
args: tokens[1..]
.iter()
.chain(selected_cmd.into_iter().flatten())
.cloned()
.collect(),
});
}
let Some(tokens) = selected_cmd else {
return Err(CommandResolutionError::NoDefaultCommand);
};
validate_tokens(cmd_source, tokens)?;
Ok(ResolvedCommand {
program: tokens[0].clone(),
args: tokens[1..].to_vec(),
})
}
fn validate_tokens(origin: &'static str, tokens: &[String]) -> Result<(), CommandResolutionError> {
if tokens.first().is_some_and(String::is_empty) {
return Err(CommandResolutionError::EmptyExecutable { origin });
}
if let Some(index) = tokens.iter().position(|token| token.contains('\0')) {
return Err(CommandResolutionError::NulByte { origin, index });
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{CommandResolutionError, ResolvedCommand, resolve_default_command};
fn strings(values: &[&str]) -> Vec<String> {
values.iter().map(ToString::to_string).collect()
}
#[test]
fn resolves_required_oci_command_matrix() {
struct Case {
entrypoint: Option<Vec<String>>,
cmd: Option<Vec<String>>,
cmd_override: Option<Vec<String>>,
expected: Result<ResolvedCommand, CommandResolutionError>,
}
let cases = [
Case {
entrypoint: None,
cmd: None,
cmd_override: None,
expected: Err(CommandResolutionError::NoDefaultCommand),
},
Case {
entrypoint: None,
cmd: Some(strings(&["a", "b"])),
cmd_override: None,
expected: Ok(ResolvedCommand {
program: "a".into(),
args: strings(&["b"]),
}),
},
Case {
entrypoint: Some(strings(&["e", "x"])),
cmd: None,
cmd_override: None,
expected: Ok(ResolvedCommand {
program: "e".into(),
args: strings(&["x"]),
}),
},
Case {
entrypoint: Some(strings(&["e", "x"])),
cmd: Some(strings(&["c", "y"])),
cmd_override: None,
expected: Ok(ResolvedCommand {
program: "e".into(),
args: strings(&["x", "c", "y"]),
}),
},
Case {
entrypoint: Some(strings(&["e", "x"])),
cmd: Some(strings(&["c", "y"])),
cmd_override: Some(strings(&["u", "v"])),
expected: Ok(ResolvedCommand {
program: "e".into(),
args: strings(&["x", "u", "v"]),
}),
},
Case {
entrypoint: None,
cmd: Some(strings(&["c", "y"])),
cmd_override: Some(strings(&["u", "v"])),
expected: Ok(ResolvedCommand {
program: "u".into(),
args: strings(&["v"]),
}),
},
Case {
entrypoint: Some(strings(&["e", "x"])),
cmd: Some(strings(&["c", "y"])),
cmd_override: Some(Vec::new()),
expected: Ok(ResolvedCommand {
program: "e".into(),
args: strings(&["x"]),
}),
},
Case {
entrypoint: None,
cmd: Some(strings(&["c", "y"])),
cmd_override: Some(Vec::new()),
expected: Err(CommandResolutionError::NoDefaultCommand),
},
];
for case in cases {
assert_eq!(
resolve_default_command(
case.entrypoint.as_deref(),
case.cmd.as_deref(),
case.cmd_override.as_deref(),
),
case.expected
);
}
}
#[test]
fn rejects_empty_executable_and_nul_tokens() {
assert_eq!(
resolve_default_command(Some(&strings(&[""])), None, None),
Err(CommandResolutionError::EmptyExecutable {
origin: "entrypoint"
})
);
assert_eq!(
resolve_default_command(None, Some(&strings(&["echo", "bad\0arg"])), None),
Err(CommandResolutionError::NulByte {
origin: "cmd",
index: 1
})
);
}
}