// SPDX-License-Identifier: AGPL-3.0-only
//! What one guest run produced: a verdict and the files it emitted.
//!
//! Heimdall is a defensive sandbox. These are the wasm host's own result types;
//! heimdall-run maps them into heimdall-core's `JobResult` (spec §7). Kept small
//! on purpose, so the switch to the core types is one mapping function.
use std::fmt;
use heimdall_core::{Output, Sha256Digest};
/// The guest's answer, or the host's refusal of the guest.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
/// The guest returned `ok(output)` within every limit.
Accepted { note: String },
/// The guest returned `err(refusal)`, or it trapped, or a limit fired.
/// A trap or a limit reads `guest trapped (<reason>)`. `code` is a stable
/// `heimdall-wasm-<n>` from [`crate::codes`].
Refused { code: &'static str, reason: String },
}
impl Verdict {
pub fn is_accepted(&self) -> bool {
matches!(self, Verdict::Accepted { .. })
}
/// The refusal reason, or `None` for an accepted run.
pub fn reason(&self) -> Option<&str> {
match self {
Verdict::Refused { reason, .. } => Some(reason),
Verdict::Accepted { .. } => None,
}
}
/// The stable refusal code, or `None` for an accepted run.
pub fn code(&self) -> Option<&'static str> {
match self {
Verdict::Refused { code, .. } => Some(code),
Verdict::Accepted { .. } => None,
}
}
/// heimdall-core's verdict (the signed one carries no note).
pub fn to_core(&self) -> heimdall_core::Verdict {
match self {
Verdict::Accepted { .. } => heimdall_core::Verdict::Accepted,
Verdict::Refused { code, reason } => heimdall_core::Verdict::Refused {
code: code.to_string(),
reason: reason.clone(),
},
}
}
}
impl fmt::Display for Verdict {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Verdict::Accepted { note } => write!(f, "accepted: {note}"),
Verdict::Refused { code, reason } => write!(f, "refused: {reason} [{code}]"),
}
}
}
/// One file the guest handed out through `emit-file`, with its SHA-256.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmittedFile {
pub name: String,
pub bytes: Vec<u8>,
pub sha256: Sha256Digest,
}
impl EmittedFile {
/// heimdall-core's output (name, SHA-256, size) for this file.
pub fn entry(&self) -> Output {
Output {
name: self.name.clone(),
sha256: self.sha256,
bytes: self.bytes.len() as u64,
}
}
}
/// The whole outcome of one run.
///
/// `files` is empty whenever the guest trapped or a limit fired: a partial
/// output is never handed on. A guest that refuses on its own keeps the files
/// it emitted (a refusal report is a legitimate output).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GuestOutcome {
pub verdict: Verdict,
pub files: Vec<EmittedFile>,
/// Fuel the run consumed (deterministic for the same guest and input).
pub fuel_used: u64,
}
impl GuestOutcome {
/// A refusal with no files.
pub fn refused(code: &'static str, reason: impl Into<String>, fuel_used: u64) -> Self {
GuestOutcome {
verdict: Verdict::Refused {
code,
reason: reason.into(),
},
files: Vec::new(),
fuel_used,
}
}
/// A trap or a limit: `guest trapped (<why>)`, no files.
pub fn trapped(code: &'static str, why: impl fmt::Display, fuel_used: u64) -> Self {
Self::refused(code, format!("guest trapped ({why})"), fuel_used)
}
}