pub mod limits;
pub mod loader;
pub mod values;
pub mod views;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use starlark::PrintHandler;
use starlark::environment::{Globals, GlobalsBuilder, LibraryExtension};
use starlark::eval::Evaluator;
use starlark::values::list::ListRef;
use starlark::values::{OwnedFrozenValue, Value, ValueLike};
use crate::bootstrap::{Bootstrap, Limits};
use crate::fs::ProjectDir;
use crate::paths::ProjectPath;
use crate::report::{Code, Diagnostic};
use loader::{Loader, Modules};
use values::{Finding, Output, Registry};
#[derive(Default)]
pub struct Printer(RefCell<Vec<String>>);
impl Printer {
fn drain(&self) -> Vec<String> {
std::mem::take(&mut *self.0.borrow_mut())
}
}
impl PrintHandler for Printer {
fn println(&self, text: &str) -> starlark::Result<()> {
self.0.borrow_mut().push(text.to_owned());
Ok(())
}
}
pub struct Schema {
pub shape: Option<ProjectPath>,
validate: Option<OwnedFrozenValue>,
}
pub struct Policy {
pub schemas: BTreeMap<String, Schema>,
pub checks: Vec<(String, OwnedFrozenValue)>,
pub generators: Vec<(String, OwnedFrozenValue)>,
pub entry: ProjectPath,
limits: Limits,
cancel: Arc<AtomicBool>,
pub max_ticks: std::cell::Cell<u64>,
pub max_heap_bytes: std::cell::Cell<u64>,
}
pub enum CallError {
Failure { message: String, line: Option<u32> },
Result(String),
}
pub struct CallOutcome<T> {
pub result: Result<T, CallError>,
pub printed: Vec<String>,
}
fn library_globals() -> Globals {
GlobalsBuilder::extended_by(&[
LibraryExtension::StructType,
LibraryExtension::RecordType,
LibraryExtension::EnumType,
LibraryExtension::Map,
LibraryExtension::Filter,
LibraryExtension::Partial,
LibraryExtension::Print,
LibraryExtension::Pprint,
LibraryExtension::Json,
LibraryExtension::Typing,
])
.with(values::library)
.build()
}
fn entry_globals() -> Globals {
GlobalsBuilder::extended_by(&[
LibraryExtension::StructType,
LibraryExtension::RecordType,
LibraryExtension::EnumType,
LibraryExtension::Map,
LibraryExtension::Filter,
LibraryExtension::Partial,
LibraryExtension::Print,
LibraryExtension::Pprint,
LibraryExtension::Json,
LibraryExtension::Typing,
])
.with(values::library)
.with(values::registration)
.build()
}
pub fn load(
fs: &ProjectDir,
bootstrap: &Bootstrap,
cancel: Arc<AtomicBool>,
diagnostics: &mut Vec<Diagnostic>,
) -> Option<Policy> {
let library = library_globals();
let entry_globals = entry_globals();
let loader = Loader {
fs,
rules_root: &bootstrap.rules_root,
library: &library,
limits: bootstrap.limits,
cancel: Arc::clone(&cancel),
};
let mut modules = Modules::default();
let mut chain = Vec::new();
let prepared = loader
.prepare(&mut modules, &bootstrap.entry, &mut chain, diagnostics)
.ok()?;
let registry = Registry::default();
let frozen = loader
.evaluate(
&mut modules,
prepared,
&entry_globals,
Some(®istry),
diagnostics,
)
.ok()?;
let fetch = |slot: &str| {
frozen
.get(slot)
.expect("registered slot exists in frozen entry module")
};
let mut schemas = BTreeMap::new();
for registration in registry.schemas.borrow().iter() {
let shape = registration.shape.as_deref().map(|shape| {
let relative = ProjectPath::parse(shape).expect("validated at registration");
bootstrap.rules_root.join(&relative)
});
schemas.insert(
registration.id.clone(),
Schema {
shape,
validate: registration.validate.as_deref().map(fetch),
},
);
}
let checks = registry
.checks
.borrow()
.iter()
.map(|(name, slot)| (name.clone(), fetch(slot)))
.collect();
let generators = registry
.generators
.borrow()
.iter()
.map(|(name, slot)| (name.clone(), fetch(slot)))
.collect();
Some(Policy {
schemas,
checks,
generators,
entry: bootstrap.entry.clone(),
limits: bootstrap.limits,
cancel,
max_ticks: std::cell::Cell::new(0),
max_heap_bytes: std::cell::Cell::new(0),
})
}
impl Policy {
pub fn validate(
&self,
schema: &str,
resource: &OwnedFrozenValue,
) -> Option<CallOutcome<Vec<Finding>>> {
let callback = self.schemas.get(schema)?.validate.as_ref()?;
Some(self.call(callback, resource, findings))
}
pub fn check(
&self,
callback: &OwnedFrozenValue,
project: &OwnedFrozenValue,
) -> CallOutcome<Vec<Finding>> {
self.call(callback, project, findings)
}
pub fn plan(
&self,
callback: &OwnedFrozenValue,
project: &OwnedFrozenValue,
) -> CallOutcome<Vec<Output>> {
self.call(callback, project, outputs)
}
fn call<T>(
&self,
callback: &OwnedFrozenValue,
argument: &OwnedFrozenValue,
interpret: fn(Value<'_>) -> Result<T, String>,
) -> CallOutcome<T> {
let printer = Printer::default();
let result = starlark::environment::Module::with_temp_heap(|module| {
let mut eval = Evaluator::new(&module);
eval.set_print_handler(&printer);
eval.enable_static_typechecking(true);
let result = if let Err(error) = apply(&mut eval, self.limits, &self.cancel) {
Err(CallError::Failure {
message: format!("cannot apply limits: {error}"),
line: None,
})
} else {
let function = module.heap().access_owned_frozen_value(callback);
let view = module.heap().access_owned_frozen_value(argument);
match eval.eval_function(function, &[view], &[]) {
Ok(value) => interpret(value).map_err(CallError::Result),
Err(error) => Err(CallError::Failure {
message: error.without_diagnostic().to_string(),
line: loader::error_line(&error),
}),
}
};
self.max_ticks
.set(self.max_ticks.get().max(eval.get_total_tick_count()));
let allocated = eval.heap().allocated_bytes() as u64;
self.max_heap_bytes
.set(self.max_heap_bytes.get().max(allocated));
result
});
CallOutcome {
result,
printed: printer.drain(),
}
}
}
fn apply(
eval: &mut Evaluator<'_, '_, '_>,
limits: Limits,
cancel: &Arc<AtomicBool>,
) -> anyhow::Result<()> {
limits::apply_limits(eval, limits, cancel)
}
fn findings(value: Value<'_>) -> Result<Vec<Finding>, String> {
let list = ListRef::from_value(value)
.ok_or_else(|| format!("must return a list of findings, found {}", value.get_type()))?;
list.iter()
.map(|item| {
item.downcast_ref::<Finding>().cloned().ok_or_else(|| {
format!(
"list item must be error() or warning(), found {}",
item.get_type()
)
})
})
.collect()
}
fn outputs(value: Value<'_>) -> Result<Vec<Output>, String> {
let list = ListRef::from_value(value)
.ok_or_else(|| format!("must return a list of outputs, found {}", value.get_type()))?;
list.iter()
.map(|item| {
item.downcast_ref::<Output>()
.cloned()
.ok_or_else(|| format!("list item must be output(), found {}", item.get_type()))
})
.collect()
}
pub fn failure_diagnostic(path: &str, label: &str, error: &CallError) -> Diagnostic {
match error {
CallError::Failure { message, line } => Diagnostic::new(
Code::ScriptFailure,
path,
format!("{label} failed: {message}"),
)
.at_line(*line),
CallError::Result(message) => {
Diagnostic::new(Code::ScriptResult, path, format!("{label} {message}"))
}
}
}