use crate::cap::{Auth, Cap};
use crate::effect::InputIdentity;
use crate::error::{BotError, DispatchCertainty};
use crate::inspect::{Budgets, InspectRequest, Inspection, RuleSet, Scope as InspectScope};
use crate::verb;
pub const DOMAIN: &str = "inspect::subject";
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct InspectionJob {
artifact: String,
subject: String,
language: Option<lgwks_ast::Language>,
language_version: Option<String>,
rules: RuleSet,
scope: InspectScope,
budgets: Budgets,
}
impl InspectionJob {
#[must_use]
pub fn new(artifact: impl Into<String>, subject: impl Into<String>) -> Self {
Self {
artifact: artifact.into(),
subject: subject.into(),
language: None,
language_version: None,
rules: RuleSet::STRUCTURAL_V1,
scope: InspectScope::Structural,
budgets: Budgets::default_budgets(),
}
}
#[must_use]
pub fn with_language(mut self, language: lgwks_ast::Language) -> Self {
self.language = Some(language);
self
}
#[must_use]
pub fn with_language_version(mut self, version: impl Into<String>) -> Self {
self.language_version = Some(version.into());
self
}
#[must_use]
pub fn with_rules(mut self, rules: RuleSet) -> Self {
self.rules = rules;
self
}
#[must_use]
pub fn with_scope(mut self, scope: InspectScope) -> Self {
self.scope = scope;
self
}
#[must_use]
pub fn with_budgets(mut self, budgets: Budgets) -> Self {
self.budgets = budgets;
self
}
#[must_use]
pub fn artifact(&self) -> &str {
&self.artifact
}
#[must_use]
pub fn subject(&self) -> &str {
&self.subject
}
#[must_use]
pub fn inspect(&self) -> Inspection {
let mut request = InspectRequest::new(&self.artifact, &self.subject)
.rules(self.rules)
.scope(self.scope)
.budgets(self.budgets);
if let Some(language) = self.language {
request = request.language(language);
}
if let Some(version) = self.language_version.as_deref() {
request = request.language_version(version);
}
crate::inspect::inspect(&request)
}
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct Inspector;
impl Inspector {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl verb::Query for Inspector {
type Input = InspectionJob;
type Output = Inspection;
fn required_caps(&self) -> &[Cap] {
&[]
}
async fn query(&self, call: (Auth, &InspectionJob)) -> Result<Inspection, BotError> {
let (auth, job) = call;
auth.check(verb::Query::required_caps(self))?;
Ok(job.inspect())
}
fn domain_id(&self) -> &str {
DOMAIN
}
}
#[derive(Debug, Clone)]
pub struct Subject {
artifact: String,
caps: Vec<Cap>,
budgets: Budgets,
}
impl Subject {
#[must_use]
pub fn at(path: impl Into<String>) -> Self {
Self {
artifact: path.into(),
caps: vec![Cap::fs()],
budgets: Budgets::default_budgets(),
}
}
pub fn from_target(target: &str) -> Result<crate::Source, BotError> {
if target.is_empty() {
let refusal = Err(BotError::IncompleteSpec {
field: "target",
cause: String::from("the spec names no artifact path"),
});
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "from_target: returning an error to the caller");
return refusal;
}
Ok(crate::Source::new(Self::at(target)))
}
#[must_use]
pub fn artifact(&self) -> &str {
&self.artifact
}
#[must_use]
pub fn with_budgets(mut self, budgets: Budgets) -> Self {
self.budgets = budgets;
self
}
}
impl verb::Observe for Subject {
type Output = Inspection;
fn required_caps(&self) -> &[Cap] {
&self.caps
}
async fn poll(&self, call: (Auth, ())) -> Result<Inspection, BotError> {
call.0.check(self.required_caps())?;
let path = self.artifact.clone();
let limit = self.budgets.max_source_bytes;
let subject = lgwks_std::task::spawn_blocking(move || read_subject(&path, limit)).await?;
Ok(crate::inspect::inspect(
&InspectRequest::new(&self.artifact, &subject).budgets(self.budgets),
))
}
fn domain_id(&self) -> &str {
DOMAIN
}
}
fn read_subject(path: &str, limit: usize) -> Result<String, BotError> {
let metadata = std::fs::metadata(path).map_err(|error| io_error(path, error))?;
let length = match usize::try_from(metadata.len()) {
Ok(addressable) => addressable,
Err(_wider_than_this_host_addresses) => usize::MAX,
};
if length > limit {
let refusal = Err(BotError::DomainError {
domain: DOMAIN.into(),
certainty: DispatchCertainty::Refused,
cause: format!("{path} is {length} bytes; the inspection source budget is {limit}"),
});
lgwks_std::trace::debug!(error = ?refusal.as_ref().err(), "read_subject: returning an error to the caller");
return refusal;
}
std::fs::read_to_string(path).map_err(|error| io_error(path, error))
}
fn io_error(path: &str, error: std::io::Error) -> BotError {
BotError::DomainError {
domain: DOMAIN.into(),
certainty: DispatchCertainty::NotDelivered,
cause: format!("could not read artifact {path}: {error}"),
}
}
impl InputIdentity for Inspection {
const SCHEMA_ID: &'static [u8] = b"lgwks.bot.schema.v1.inspection";
fn write_identity(&self, hasher: &mut lgwks_std::hash::Hasher) {
hasher.write_framed(self.subject_digest().as_bytes());
}
}
#[cfg(feature = "script")]
pub type InspectionBody =
fn(
crate::script::Scope,
InspectionJob,
) -> crate::BoxFuture<'static, Result<Inspection, crate::script::FlowError>>;
#[cfg(feature = "script")]
pub fn inspection_task(
name: &str,
) -> Result<crate::task::Task<InspectionBody>, crate::script::FlowError> {
crate::task::task(name, inspection_body)
}
#[cfg(feature = "script")]
fn inspection_body(
_scope: crate::script::Scope,
job: InspectionJob,
) -> crate::BoxFuture<'static, Result<Inspection, crate::script::FlowError>> {
Box::pin(async move { Ok(job.inspect()) })
}