use std::collections::BTreeSet;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Execution {
BuildScript,
ProceduralMacro,
Configure,
CompilerWrapper,
GeneratedSource,
}
pub const EXECUTION_CLASSES: [Execution; 5] = [
Execution::BuildScript,
Execution::ProceduralMacro,
Execution::Configure,
Execution::CompilerWrapper,
Execution::GeneratedSource,
];
impl Execution {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::BuildScript => "build-script",
Self::ProceduralMacro => "proc-macro",
Self::Configure => "configure",
Self::CompilerWrapper => "compiler-wrapper",
Self::GeneratedSource => "generated-source",
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
EXECUTION_CLASSES
.into_iter()
.find(|class| class.name() == name)
}
#[must_use]
pub const fn cost(self) -> &'static str {
match self {
Self::BuildScript => {
"types and items that only exist after a build script has generated them"
}
Self::ProceduralMacro => "the items a derive or attribute macro expands to",
Self::Configure => "the compile flags a configure step would have decided",
Self::CompilerWrapper => "whatever the project's own compiler wrapper adds",
Self::GeneratedSource => "source files that a command produces rather than a person",
}
}
#[must_use]
pub fn permission_argument(self) -> String {
format!("--allow-execution={}", self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Reading {
Source,
CargoMetadata,
CompilationDatabase,
ExistingArtifacts,
DebugInformation,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ExecutionPolicy {
allowed: BTreeSet<Execution>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Refusal {
pub execution: Execution,
pub cost: &'static str,
pub permission_argument: String,
}
impl Refusal {
#[must_use]
pub fn describe(&self) -> String {
format!(
"skipped {}: not permitted, so this run has no {}. Pass {} to allow it.",
self.execution.name(),
self.cost,
self.permission_argument
)
}
}
impl ExecutionPolicy {
#[must_use]
pub fn deny_all() -> Self {
Self::default()
}
#[must_use]
pub fn allowing(mut self, execution: Execution) -> Self {
self.allowed.insert(execution);
self
}
pub fn parse(names: &str) -> Result<Self, UnknownExecution> {
let mut policy = Self::deny_all();
for name in names.split(',').map(str::trim).filter(|n| !n.is_empty()) {
let execution = Execution::from_name(name).ok_or_else(|| UnknownExecution {
name: name.to_string(),
})?;
policy = policy.allowing(execution);
}
Ok(policy)
}
#[must_use]
pub fn permits(&self, execution: Execution) -> bool {
self.allowed.contains(&execution)
}
#[must_use]
pub const fn permits_reading(&self, _reading: Reading) -> bool {
true
}
#[must_use]
pub fn refusal(&self, execution: Execution) -> Option<Refusal> {
if self.permits(execution) {
return None;
}
Some(Refusal {
execution,
cost: execution.cost(),
permission_argument: execution.permission_argument(),
})
}
#[must_use]
pub fn permitted(&self) -> Vec<Execution> {
self.allowed.iter().copied().collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"no execution class is called `{name}`; the classes are \
build-script, proc-macro, configure, compiler-wrapper, generated-source"
)]
pub struct UnknownExecution {
pub name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Limits {
pub max_file_bytes: u64,
pub parse_timeout: Duration,
pub max_subprocess_bytes: Option<u64>,
pub max_candidates: usize,
pub posting_cap: usize,
pub max_component: usize,
pub verification_budget: usize,
pub max_alignment_cells: usize,
pub helper_timeout: Duration,
pub execution: ExecutionPolicy,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_file_bytes: crate::discovery::DEFAULT_MAX_FILE_BYTES,
parse_timeout: Duration::from_secs(30),
max_subprocess_bytes: None,
max_candidates: 5_000_000,
posting_cap: 256,
max_component: 1024,
verification_budget: 1_000_000,
max_alignment_cells: 4_000_000,
helper_timeout: Duration::from_secs(300),
execution: ExecutionPolicy::deny_all(),
}
}
}
impl Limits {
#[must_use]
pub fn untrusted() -> Self {
Self {
max_file_bytes: 512 * 1024,
parse_timeout: Duration::from_secs(5),
max_subprocess_bytes: Some(1024 * 1024 * 1024),
max_candidates: 500_000,
posting_cap: 32,
max_component: 128,
verification_budget: 100_000,
max_alignment_cells: 250_000,
helper_timeout: Duration::from_secs(30),
execution: ExecutionPolicy::deny_all(),
}
}
#[must_use]
pub fn is_at_most(&self, other: &Self) -> bool {
self.max_file_bytes <= other.max_file_bytes
&& self.parse_timeout <= other.parse_timeout
&& self.max_candidates <= other.max_candidates
&& self.posting_cap <= other.posting_cap
&& self.verification_budget <= other.verification_budget
&& self.max_alignment_cells <= other.max_alignment_cells
&& self.max_component <= other.max_component
&& self.helper_timeout <= other.helper_timeout
&& option_ceiling_at_most(self.max_subprocess_bytes, other.max_subprocess_bytes)
&& self
.execution
.permitted()
.iter()
.all(|class| other.execution.permits(*class))
}
}
const fn option_ceiling_at_most(left: Option<u64>, right: Option<u64>) -> bool {
match (left, right) {
(_, None) => true,
(Some(left), Some(right)) => left <= right,
(None, Some(_)) => false,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn nothing_runs_unless_it_was_asked_for() {
let policy = ExecutionPolicy::deny_all();
for class in EXECUTION_CLASSES {
assert!(!policy.permits(class), "{class:?}");
assert!(policy.refusal(class).is_some(), "{class:?}");
}
assert_eq!(ExecutionPolicy::default(), policy);
}
#[test]
fn permitting_one_class_permits_only_that_class() {
let policy = ExecutionPolicy::deny_all().allowing(Execution::ProceduralMacro);
assert!(policy.permits(Execution::ProceduralMacro));
for class in EXECUTION_CLASSES {
if class != Execution::ProceduralMacro {
assert!(!policy.permits(class), "{class:?}");
}
}
}
#[test]
fn the_argument_a_refusal_names_is_the_argument_that_permits_it() {
for class in EXECUTION_CLASSES {
let refusal = ExecutionPolicy::deny_all().refusal(class).unwrap();
let value = refusal
.permission_argument
.split_once('=')
.map(|(_, value)| value)
.unwrap();
let policy = ExecutionPolicy::parse(value).unwrap();
assert!(policy.permits(class), "{class:?}: {refusal:?}");
assert!(refusal.describe().contains(class.name()));
}
}
#[test]
fn several_permissions_can_be_given_at_once() {
let policy = ExecutionPolicy::parse("build-script, proc-macro").unwrap();
assert_eq!(
policy.permitted(),
vec![Execution::BuildScript, Execution::ProceduralMacro]
);
}
#[test]
fn a_permission_nobody_can_grant_is_an_error_rather_than_a_shrug() {
let error = ExecutionPolicy::parse("build-scripts").unwrap_err();
assert_eq!(error.name, "build-scripts");
assert!(error.to_string().contains("build-script"));
}
#[test]
fn every_class_has_a_name_that_maps_back() {
for class in EXECUTION_CLASSES {
assert_eq!(Execution::from_name(class.name()), Some(class), "{class:?}");
}
assert_eq!(Execution::from_name("run-everything"), None);
}
#[test]
fn reading_what_the_project_already_has_needs_no_permission() {
let policy = ExecutionPolicy::deny_all();
for reading in [
Reading::Source,
Reading::CargoMetadata,
Reading::CompilationDatabase,
Reading::ExistingArtifacts,
Reading::DebugInformation,
] {
assert!(policy.permits_reading(reading), "{reading:?}");
}
}
#[test]
fn the_untrusted_profile_is_stricter_in_every_dimension() {
let untrusted = Limits::untrusted();
let default = Limits::default();
assert!(untrusted.is_at_most(&default));
assert!(!default.is_at_most(&untrusted));
for class in EXECUTION_CLASSES {
assert!(!untrusted.execution.permits(class), "{class:?}");
}
}
#[test]
fn a_profile_that_trades_one_ceiling_for_another_is_not_stricter() {
let traded = Limits {
max_file_bytes: u64::MAX,
..Limits::untrusted()
};
assert!(!traded.is_at_most(&Limits::default()));
}
}