Gunnar

nordisk/heimdall

crates/heimdall-wasm/src/outcome.rs

// 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)
    }
}